Commit 2912c668 authored by 陈冲's avatar 陈冲

commit

parent 8a5923a5
DROP TABLE IF EXISTS tb_game; DROP TABLE IF EXISTS tb_game;
CREATE TABLE tb_game CREATE TABLE tb_game
( (
wechat_id VARCHAR(50), wechat_id text,
nickname VARCHAR(50), nickname VARCHAR(50),
avatar VARCHAR(200), avatar text,
game_rank INT NOT NULL, game_rank INT NOT NULL,
item_num INT NOT NULL, item_num INT NOT NULL,
score INT NOT NULL DEFAULT 0, score INT NOT NULL DEFAULT 0,
......
...@@ -5,8 +5,8 @@ use async_trait::async_trait; ...@@ -5,8 +5,8 @@ use async_trait::async_trait;
use serde_json::Value; use serde_json::Value;
use tokio::sync::OnceCell; use tokio::sync::OnceCell;
pub mod mysql;
pub mod models; pub mod models;
pub mod mysql;
pub mod pg; pub mod pg;
#[async_trait] #[async_trait]
......
...@@ -3,7 +3,7 @@ use crate::db::DbDriver; ...@@ -3,7 +3,7 @@ use crate::db::DbDriver;
use async_trait::async_trait; use async_trait::async_trait;
use serde_json::{Value, json}; use serde_json::{Value, json};
use sqlx::mysql::{MySqlArguments, MySqlPoolOptions, MySqlRow}; use sqlx::mysql::{MySqlArguments, MySqlPoolOptions, MySqlRow};
use sqlx::{Column, MySql, MySqlPool, Row, TypeInfo}; use sqlx::{Column, MySql, MySqlConnection, MySqlPool, Row, TypeInfo};
pub struct MysqlDb { pub struct MysqlDb {
pool: MySqlPool, pool: MySqlPool,
...@@ -12,11 +12,18 @@ pub struct MysqlDb { ...@@ -12,11 +12,18 @@ pub struct MysqlDb {
impl MysqlDb { impl MysqlDb {
pub async fn new() -> Result<Self, String> { pub async fn new() -> Result<Self, String> {
let pool = MySqlPoolOptions::new() let pool = MySqlPoolOptions::new()
.after_connect(|conn: &mut MySqlConnection, _| {
Box::pin(async move {
sqlx::query("SET time_zone = '+08:00'")
.execute(conn)
.await?;
Ok(())
})
})
.max_connections(16) .max_connections(16)
.connect(&config.db.uri) .connect(&config.db.uri)
.await .await
.map_err(|err| err.to_string())?; .map_err(|err| err.to_string())?;
Ok(Self { pool }) Ok(Self { pool })
} }
} }
......
...@@ -9,12 +9,12 @@ use serde_json::{Value, json}; ...@@ -9,12 +9,12 @@ use serde_json::{Value, json};
use socketioxide::SocketIo; use socketioxide::SocketIo;
use socketioxide::extract::{Data, SocketRef}; use socketioxide::extract::{Data, SocketRef};
use socketioxide::socket::DisconnectReason; use socketioxide::socket::DisconnectReason;
use sqlx::encode::IsNull::No;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration; use std::time::Duration;
lazy_static::lazy_static! { lazy_static::lazy_static! {
/// 可以有多个管理员,但不要用同一个账户登录,否则后面的没效果
static ref CLIENT_ADMIN_MAP: DashMap<String, SocketRef> = DashMap::new(); static ref CLIENT_ADMIN_MAP: DashMap<String, SocketRef> = DashMap::new();
static ref CLIENT_PLAYER_MAP: DashMap<String, (i32,SocketRef)> = DashMap::new(); // 1: 正在游戏中 static ref CLIENT_PLAYER_MAP: DashMap<String, (i32,SocketRef)> = DashMap::new(); // 1: 正在游戏中
static ref ROOM_STATE: std::sync::Mutex<RoomState> = std::sync::Mutex::new(RoomState::default()); static ref ROOM_STATE: std::sync::Mutex<RoomState> = std::sync::Mutex::new(RoomState::default());
...@@ -297,7 +297,7 @@ fn spawn_game_timeout(round_id: u64, secs: u64) { ...@@ -297,7 +297,7 @@ fn spawn_game_timeout(round_id: u64, secs: u64) {
if let Ok(mut state) = ROOM_STATE.lock() { if let Ok(mut state) = ROOM_STATE.lock() {
state.status = RoomStatus::Submit; state.status = RoomStatus::Submit;
dbg!("submit"); // dbg!("submit");
drop(state); drop(state);
} }
}); });
...@@ -322,25 +322,30 @@ async fn handle_room_command( ...@@ -322,25 +322,30 @@ async fn handle_room_command(
) { ) {
let evt = &format!("{cmd}_result"); let evt = &format!("{cmd}_result");
match cmd { match cmd {
"room_create" => { "room_create" | "room_back" => {
//room_back返回当前游戏的Loading状态
// 只有管理员可以创建房间 // 只有管理员可以创建房间
if !is_admin { if !is_admin {
return; return;
} }
if let Some(index) = data.as_i64() { if let Some(index) = data.as_i64() {
if let Ok(mut state) = ROOM_STATE.lock() { if let Ok(mut state) = ROOM_STATE.lock() {
if state.status != RoomStatus::Closed { if cmd == "room_create" {
drop(state); if state.status != RoomStatus::Closed {
emit_msgpack(&socket, evt, &err_resp("不能重复创建房间", None)); drop(state);
return; emit_msgpack(&socket, evt, &err_resp("不能重复创建房间", None));
return;
}
} }
state.reset(); state.reset();
state.status = RoomStatus::Start; state.status = RoomStatus::Start;
// let snapshot = state.snapshot(); // let snapshot = state.snapshot();
drop(state); drop(state);
emit_msgpack(&socket, evt, &ok_resp(Some(json!(index)))); emit_msgpack(&socket, evt, &ok_resp(Some(json!(index))));
if cmd == "room_back" {
broadcast_msgpack_all_player(evt, &ok_resp(Some(json!(index))));
}
} }
} }
} }
...@@ -504,7 +509,8 @@ async fn handle_room_command( ...@@ -504,7 +509,8 @@ async fn handle_room_command(
return; return;
} }
if let Ok(state) = ROOM_STATE.lock() { if let Ok(state) = ROOM_STATE.lock() {
if !matches!(state.status, RoomStatus::Submit) { //防止客户端提交的状态还在Running中
if !matches!(state.status, RoomStatus::Submit | RoomStatus::Running) {
drop(state); drop(state);
return; return;
} }
...@@ -616,6 +622,11 @@ pub async fn on_connect(_io: SocketIo, socket: SocketRef, Data(data): Data<Value ...@@ -616,6 +622,11 @@ pub async fn on_connect(_io: SocketIo, socket: SocketRef, Data(data): Data<Value
if is_admin { if is_admin {
match CLIENT_ADMIN_MAP.entry(userid.clone()) { match CLIENT_ADMIN_MAP.entry(userid.clone()) {
Entry::Occupied(_) => { Entry::Occupied(_) => {
emit_msgpack(
&socket,
"login_result",
&err_resp("当前管理员已登录,不能重复登录", None),
);
_ = socket.disconnect(); _ = socket.disconnect();
return; return;
} }
......
/// <reference types="vite/client" /> /// <reference types="vite/client" />
declare module 'crypto-js'; declare module 'crypto-js';
declare module 'crypto-js/sha256';
declare const __APP_DOMAIN__: string; declare const __APP_DOMAIN__: string;
......
...@@ -19,6 +19,7 @@ type GameSocketOptions = { ...@@ -19,6 +19,7 @@ type GameSocketOptions = {
onRescoreSubmitted?: (recount: number) => void onRescoreSubmitted?: (recount: number) => void
onRoomClosed?: (data: any) => void onRoomClosed?: (data: any) => void
onEvent?: (evt: string, payload: SocketPayload) => void onEvent?: (evt: string, payload: SocketPayload) => void
onRoomBack?: (data: any) => void
} }
function getGameIndex(data: any) { function getGameIndex(data: any) {
...@@ -122,11 +123,15 @@ export function useGameSocket(options: GameSocketOptions) { ...@@ -122,11 +123,15 @@ export function useGameSocket(options: GameSocketOptions) {
options.onScoreSubmitted?.(true, data) options.onScoreSubmitted?.(true, data)
break break
} }
case 'room_close': { case 'room_close_result': {
userJoinStatus.value = false userJoinStatus.value = false
options.onRoomClosed?.(data) options.onRoomClosed?.(data)
break break
} }
case 'room_back_result': {
options.onRoomBack?.(data);
break;
}
} }
options.onEvent?.(evt, payload) options.onEvent?.(evt, payload)
......
...@@ -107,6 +107,22 @@ function showLoadingView() { ...@@ -107,6 +107,22 @@ function showLoadingView() {
setDivDescVisible(false) setDivDescVisible(false)
} }
function resetToLoadingView() {
stopGameCountdown()
if (guFrameTimer) {
window.clearTimeout(guFrameTimer)
guFrameTimer = undefined
}
tick.value = 0
rank.value = 0
countdownInterval.value = 60
currentGu.value = guFrames[0]
isGuPlaying.value = false
userJoinStatus.value = false
currentView.value = 'loading'
setDivDescVisible(false)
}
function startGameView() { function startGameView() {
stopGameCountdown() stopGameCountdown()
tick.value = 0 tick.value = 0
...@@ -126,10 +142,17 @@ function replayGame() { ...@@ -126,10 +142,17 @@ function replayGame() {
} }
function backToWaiting() { function backToWaiting() {
showLoadingView() resetToLoadingView()
joinSharedRoom(nickname.value, token.value, avatar.value) joinSharedRoom(nickname.value, token.value, avatar.value)
} }
function handleRoomBack() {
resetToLoadingView()
window.setTimeout(() => {
joinSharedRoom(nickname.value, token.value, avatar.value)
}, 200)
}
const touchHandler = () => { const touchHandler = () => {
if (isGuPlaying.value) { if (isGuPlaying.value) {
return return
...@@ -186,6 +209,7 @@ if (wechat) { ...@@ -186,6 +209,7 @@ if (wechat) {
submitScore(true); submitScore(true);
}, },
onRoomClosed: showLoadingView, onRoomClosed: showLoadingView,
onRoomBack: handleRoomBack,
}) })
_offSocketMessage.value = offSocketMessage _offSocketMessage.value = offSocketMessage
} }
......
...@@ -120,6 +120,8 @@ defineEmits<{ ...@@ -120,6 +120,8 @@ defineEmits<{
.play-content { .play-content {
margin-top: 240px; margin-top: 240px;
margin-left: 40px;
margin-right: 40px;
padding: 30px 0; padding: 30px 0;
border-radius: 25px; border-radius: 25px;
background: rgba(40, 12, 8, 0.52); background: rgba(40, 12, 8, 0.52);
......
<script setup lang="ts"> <script setup lang="ts">
import { $getWechat } from '@/commons/utils';
import { onMounted, ref } from 'vue';
defineProps<{ defineProps<{
imageUrls: Record<string, string> imageUrls: Record<string, string>
tick: number tick: number
...@@ -9,6 +12,13 @@ defineEmits<{ ...@@ -9,6 +12,13 @@ defineEmits<{
replay: [] replay: []
back: [] back: []
}>() }>()
const nickname = ref('');
const avatar = ref('');
onMounted(() => {
const wechat = $getWechat();
nickname.value = wechat.nickname;
avatar.value = wechat.avatar;
});
</script> </script>
<template> <template>
...@@ -16,9 +26,10 @@ defineEmits<{ ...@@ -16,9 +26,10 @@ defineEmits<{
<div class="img-logo"></div> <div class="img-logo"></div>
<div class="bg-bottom"></div> <div class="bg-bottom"></div>
<div class="img-score"> <div class="img-score">
<div class="score-avatar"></div> <div class="score-avatar"
:style="`background: url(${avatar});border-radius:66px;width:132px;height:132px;`"></div>
<div class="score-content"> <div class="score-content">
<div>张小明</div> <div>{{ nickname }}</div>
<div>您的成绩</div> <div>您的成绩</div>
<div>击鼓次数(次)</div> <div>击鼓次数(次)</div>
<div>{{ tick }}</div> <div>{{ tick }}</div>
...@@ -73,12 +84,12 @@ defineEmits<{ ...@@ -73,12 +84,12 @@ defineEmits<{
.score-avatar { .score-avatar {
position: absolute; position: absolute;
top: -20px; top: -50px;
left: 50%; left: 50%;
width: 60px; width: 60px;
height: 60px; height: 60px;
background: v-bind('imageUrls.avatar') center/cover; background: v-bind('imageUrls.avatar') center/cover;
transform: translateX(-50%) scale(2); transform: translateX(-50%);
} }
.score-content { .score-content {
...@@ -91,41 +102,41 @@ defineEmits<{ ...@@ -91,41 +102,41 @@ defineEmits<{
text-align: center; text-align: center;
} }
.score-content > div:nth-child(1) { .score-content>div:nth-child(1) {
color: #E00000; color: #E00000;
font-size: 32pt; font-size: 32pt;
font-weight: bold; font-weight: bold;
} }
.score-content > div:nth-child(2) { .score-content>div:nth-child(2) {
margin-top: 20px; margin-top: 20px;
font-size: 32pt; font-size: 32pt;
} }
.score-content > div:nth-child(3) { .score-content>div:nth-child(3) {
margin-top: 40px; margin-top: 40px;
} }
.score-content > div:nth-child(4) { .score-content>div:nth-child(4) {
margin-top: 30px; margin-top: 30px;
color: #E00000; color: #E00000;
font-size: 32pt; font-size: 32pt;
font-weight: bold; /* font-weight: bold; */
transform: scaleY(2.4); transform: scale(1.5);
} }
.score-content > div:nth-child(5) { .score-content>div:nth-child(5) {
margin-top: 40px; margin-top: 40px;
} }
.score-content > div:nth-child(6) { .score-content>div:nth-child(6) {
margin-top: 20px; margin-top: 20px;
color: #E00000; color: #E00000;
font-size: 32pt; font-size: 32pt;
font-weight: bold; font-weight: bold;
} }
.score-content > div:nth-child(6) label { .score-content>div:nth-child(6) label {
color: #E00000; color: #E00000;
font-size: 42pt; font-size: 42pt;
margin: 0 10px; margin: 0 10px;
......
...@@ -53,14 +53,6 @@ export function $read<T>(key: string | StorageRecord, defaultValue?: T): T | Sto ...@@ -53,14 +53,6 @@ export function $read<T>(key: string | StorageRecord, defaultValue?: T): T | Sto
return defaultValue ?? null; return defaultValue ?? null;
} }
} }
export function $goto_login(evt: string, router: any, path: string): boolean {
if (evt == "login_result") {
$remove_socket_storage();
router.replace(path).then(() => { });
return true;
}
return false;
}
export function $remove_socket_storage(): void { export function $remove_socket_storage(): void {
localStorage.removeItem('passphrase'); localStorage.removeItem('passphrase');
......
import CryptoJS from 'crypto-js' import CryptoJS from 'crypto-js'
import { onBeforeUnmount, onMounted } from 'vue' import { onBeforeUnmount, onMounted } from 'vue'
import { useRouter } from 'vue-router' import { $read_socket_storge } from '@/commons/utils'
import { $goto_login, $read_socket_storge } from '@/commons/utils'
import { initSocket, onSocketMessage, sendSocketMessage } from '@/commons/ws' import { initSocket, onSocketMessage, sendSocketMessage } from '@/commons/ws'
type SocketPayload = { type SocketPayload = {
...@@ -22,7 +21,6 @@ type AdminGameSocketOptions = { ...@@ -22,7 +21,6 @@ type AdminGameSocketOptions = {
} }
export function useAdminGameSocket(options: AdminGameSocketOptions) { export function useAdminGameSocket(options: AdminGameSocketOptions) {
const router = useRouter()
let offSocketMessage: (() => void) | undefined let offSocketMessage: (() => void) | undefined
function requestRoomState() { function requestRoomState() {
...@@ -32,6 +30,9 @@ export function useAdminGameSocket(options: AdminGameSocketOptions) { ...@@ -32,6 +30,9 @@ export function useAdminGameSocket(options: AdminGameSocketOptions) {
function createRoom(index: number) { function createRoom(index: number) {
return sendSocketMessage('room_create', index) return sendSocketMessage('room_create', index)
} }
function backRoom(index: number) {
return sendSocketMessage('room_back', index)
}
function startGame() { function startGame() {
return sendSocketMessage('game_start', options.gameId) return sendSocketMessage('game_start', options.gameId)
...@@ -45,7 +46,7 @@ export function useAdminGameSocket(options: AdminGameSocketOptions) { ...@@ -45,7 +46,7 @@ export function useAdminGameSocket(options: AdminGameSocketOptions) {
const { passphrase, token, socket_path, client_ns, domain } = $read_socket_storge() const { passphrase, token, socket_path, client_ns, domain } = $read_socket_storge()
initSocket({ initSocket({
socket_path: `${domain}${socket_path}`, socket_path: `${domain}${socket_path}`,
client_ns:"/ws", client_ns: "/ws",
auth: { auth: {
userid: token, userid: token,
validuser: CryptoJS.AES.encrypt(token, passphrase).toString(), validuser: CryptoJS.AES.encrypt(token, passphrase).toString(),
...@@ -58,16 +59,11 @@ export function useAdminGameSocket(options: AdminGameSocketOptions) { ...@@ -58,16 +59,11 @@ export function useAdminGameSocket(options: AdminGameSocketOptions) {
offSocketMessage = onSocketMessage(async (evt, payload: SocketPayload) => { offSocketMessage = onSocketMessage(async (evt, payload: SocketPayload) => {
const { msg, state, data } = payload const { msg, state, data } = payload
// if ($goto_login(evt, router, options.loginRedirectPath ?? '/game1')) {
// return
// }
if (state === 0) { if (state === 0) {
if (evt === 'game_start_result' && data === 1) { if (evt === 'game_start_result' && data === 1) {
options.onGameStartRejected?.(data) options.onGameStartRejected?.(data)
return return
} }
alert(msg) alert(msg)
return return
} }
...@@ -104,6 +100,7 @@ export function useAdminGameSocket(options: AdminGameSocketOptions) { ...@@ -104,6 +100,7 @@ export function useAdminGameSocket(options: AdminGameSocketOptions) {
return { return {
requestRoomState, requestRoomState,
createRoom, createRoom,
backRoom,
startGame, startGame,
sendAdminMessage, sendAdminMessage,
} }
......
...@@ -59,7 +59,43 @@ const updateRankPlayers = (data: any) => { ...@@ -59,7 +59,43 @@ const updateRankPlayers = (data: any) => {
) )
} }
const gotoLoading = () => { const playerToRankPlayer = (player: any, index: number): Player => ({
active: player?.active ?? false,
telephone: player?.telephone ?? '',
userid: player?.userid ?? '',
wechat_id: player?.wechat_id || `empty-${index}`,
nickname: player?.nickname || '铏氫綅浠ュ緟',
avatar: player?.avatar || '',
score: Number(player?.score ?? 0),
})
const seedRankPlayersFromLoading = () => {
rankPlayers.value = Array.from(
{ length: RANK_LIMIT },
(_, index) => {
const player = loadingPlayers.value[index]
if (!player?.userid && !player?.wechat_id) {
return emptyRankPlayer(index)
}
return playerToRankPlayer(player, index)
},
)
}
const resetRoundState = () => {
playerCount.value = 0
loadingPlayers.value = Array.from({ length: PLAYER_LIMIT }, emptyLoadingPlayer)
rankPlayers.value = Array.from({ length: RANK_LIMIT }, (_, index) => emptyRankPlayer(index))
isPlayingStartAnimation = false
}
const gotoLoading = (reset = false) => {
if (reset) {
resetRoundState()
}
screen.value = 'loading' screen.value = 'loading'
} }
...@@ -67,6 +103,7 @@ const gotoRank1WithIntro = async () => { ...@@ -67,6 +103,7 @@ const gotoRank1WithIntro = async () => {
if (isPlayingStartAnimation) return if (isPlayingStartAnimation) return
isPlayingStartAnimation = true isPlayingStartAnimation = true
seedRankPlayersFromLoading()
screen.value = 'rank1' screen.value = 'rank1'
await nextTick() await nextTick()
await rank1Ref.value?.startIntro() await rank1Ref.value?.startIntro()
...@@ -81,13 +118,14 @@ function handleRoomState(data: any) { ...@@ -81,13 +118,14 @@ function handleRoomState(data: any) {
switch (data?.status) { switch (data?.status) {
case 0: case 0:
gotoLoading() gotoLoading(true)
createRoom(1) createRoom(1)
break break
case 1: case 1:
gotoLoading() gotoLoading()
break break
case 2: case 2:
seedRankPlayersFromLoading()
screen.value = 'rank1' screen.value = 'rank1'
break break
case 3: case 3:
...@@ -96,7 +134,7 @@ function handleRoomState(data: any) { ...@@ -96,7 +134,7 @@ function handleRoomState(data: any) {
} }
} }
const { startGame, createRoom, requestRoomState } = useAdminGameSocket({ const { startGame, createRoom, backRoom } = useAdminGameSocket({
gameId: 'game1', gameId: 'game1',
// loginRedirectPath: '/game1', // loginRedirectPath: '/game1',
onGameStartRejected: gotoLoading, onGameStartRejected: gotoLoading,
...@@ -115,11 +153,9 @@ const startGameWithMusic = () => { ...@@ -115,11 +153,9 @@ const startGameWithMusic = () => {
} }
const nextRound = () => { const nextRound = () => {
gotoLoading() gotoLoading(true)
createRoom(1) //重开房间
setTimeout(() => { backRoom(1)
requestRoomState()
}, 200)
} }
const nextRoundWithMusic = () => { const nextRoundWithMusic = () => {
...@@ -138,7 +174,8 @@ onUnmounted(() => { ...@@ -138,7 +174,8 @@ onUnmounted(() => {
</script> </script>
<template> <template>
<Loading v-if="screen === 'loading'" :players="loadingPlayers" :player-count="playerCount" @start="startGameWithMusic" /> <Loading v-if="screen === 'loading'" :players="loadingPlayers" :player-count="playerCount"
@start="startGameWithMusic" />
<Rank1 v-else-if="screen === 'rank1'" ref="rank1Ref" :rank-list="rankPlayers" @start="startGameWithMusic" /> <Rank1 v-else-if="screen === 'rank1'" ref="rank1Ref" :rank-list="rankPlayers" @start="startGameWithMusic" />
<Rank2 v-else :rank-list="rankPlayers" @next="nextRoundWithMusic" /> <Rank2 v-else :rank-list="rankPlayers" @next="nextRoundWithMusic" />
</template> </template>
...@@ -50,13 +50,13 @@ const imageUrls = { ...@@ -50,13 +50,13 @@ const imageUrls = {
rankNo1Icon: cssAssetUrl('game1/no1-icon.png'), rankNo1Icon: cssAssetUrl('game1/no1-icon.png'),
rankNo2Icon: cssAssetUrl('game1/no2-icon.png'), rankNo2Icon: cssAssetUrl('game1/no2-icon.png'),
rankNo3Icon: cssAssetUrl('game1/no3-icon.png'), rankNo3Icon: cssAssetUrl('game1/no3-icon.png'),
avatar: cssAssetUrl('avatar.png'), avatar: assetUrl('avatar.png'),
}; };
const emptyPlayer = (index: number): Player => ({ const emptyPlayer = (index: number): Player => ({
wechat_id: `empty-${index}`, wechat_id: `empty-${index}`,
nickname: '-', nickname: '-',
avatar: "", avatar: '',
score: 0, score: 0,
}); });
...@@ -232,6 +232,15 @@ const startIntro = async () => { ...@@ -232,6 +232,15 @@ const startIntro = async () => {
defineExpose({ defineExpose({
startIntro, startIntro,
}); });
const avatarStyle = (item: Player) => {
const avatar = item.avatar || imageUrls.avatar
return {
backgroundImage: `url("${avatar}")`,
}
}
</script> </script>
<template> <template>
...@@ -259,7 +268,7 @@ defineExpose({ ...@@ -259,7 +268,7 @@ defineExpose({
<div v-for="(item, index) in list" :key="playerKey(item, index)" class="rank-item" <div v-for="(item, index) in list" :key="playerKey(item, index)" class="rank-item"
:class="[rankEffects[playerKey(item, index)], index < 3 ? `rank-top-${index + 1}` : '']"> :class="[rankEffects[playerKey(item, index)], index < 3 ? `rank-top-${index + 1}` : '']">
<div class="rank-icon" :key="`rank-icon-${index}`">{{ index > 2 ? index + 1 : '' }}</div> <div class="rank-icon" :key="`rank-icon-${index}`">{{ index > 2 ? index + 1 : '' }}</div>
<div class="rank-avatar"></div> <div class="rank-avatar" :style="avatarStyle(item)"></div>
<div class="rank-nickname">{{ item.nickname }}</div> <div class="rank-nickname">{{ item.nickname }}</div>
<div class="rank-score">{{ item.score ?? 0 }}</div> <div class="rank-score">{{ item.score ?? 0 }}</div>
</div> </div>
...@@ -431,7 +440,9 @@ defineExpose({ ...@@ -431,7 +440,9 @@ defineExpose({
} }
.rank-avatar { .rank-avatar {
background: v-bind('imageUrls.avatar') center / cover no-repeat; background-position: center;
background-repeat: no-repeat;
background-size: cover;
width: 48px; width: 48px;
height: 48px; height: 48px;
border-radius: 24px; border-radius: 24px;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment