feat(后台首页): 后台首页改成看板——判题机、今天提交、在线、正在上课一排,全站的数一排,最近 30 天每天交了几次一张图;抽签改成选班级
拿掉要手输班级前缀的班级排名表(前台排名页有)。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1d406a50e4
commit
56071c71bd
1 file changed
+332
-182
+332
-182
@@ -1,224 +1,367 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { h, onMounted, reactive, ref, watch } from "vue"
|
import type { DashboardInfo } from "@oj2/contract"
|
||||||
import { useRouter } from "vue-router"
|
import { BarElement, CategoryScale, Chart as ChartJS, LinearScale, Tooltip } from "chart.js"
|
||||||
import { NButton } from "naive-ui"
|
import { useThemeVars } from "naive-ui"
|
||||||
|
import { Bar } from "vue-chartjs"
|
||||||
|
import { useChartTheme } from "shared/composables/chartTheme"
|
||||||
|
import { rgba } from "oj/submission/composables/tone"
|
||||||
import PageHeader from "admin/components/PageHeader.vue"
|
import PageHeader from "admin/components/PageHeader.vue"
|
||||||
import Pagination from "shared/components/Pagination.vue"
|
import { classLabel, groupClassOptions } from "oj/submission/utils"
|
||||||
|
import { useConfigStore } from "shared/store/config"
|
||||||
import { useUserStore } from "shared/store/user"
|
import { useUserStore } from "shared/store/user"
|
||||||
import { getACRate } from "utils/functions"
|
import { SubmissionStatus } from "utils/constants"
|
||||||
import type { Rank } from "utils/types"
|
import { getBaseInfo, randomUser10 } from "../api"
|
||||||
import { getAdminUserRank, getBaseInfo, randomUser10 } from "../api"
|
|
||||||
|
|
||||||
const userCount = ref(0)
|
/**
|
||||||
const submissionCount = ref(0)
|
* 后台首页(设计稿「后台首页」AdminHome):一块看板。上面一排看「现在」—— 判题机在不在、
|
||||||
const contestCount = ref(0)
|
* 今天交了多少其中有没有系统错误、现在几个人在线、哪个班在上课;中间一排全站的数(用户、题目、
|
||||||
const judgeServerCount = ref(0)
|
* 提交、比赛、题单、自学、这周做题的人,用户要的「类似一个看板」)和最近 30 天每天交了几次;
|
||||||
const userStore = useUserStore()
|
* 最下面是常用的四个入口和「抽一个人」。
|
||||||
|
*
|
||||||
|
* 原来是总用户数 / 今日提交 / 近期比赛 / 判题机四个数,加一张要手输班级前缀的班级排名表 ——
|
||||||
|
* 总用户数、近期比赛没人看,排名前台排名页有(还更好看)。「随机抽签」上课点名用,留着,
|
||||||
|
* 班级改成下拉选,默认就是正在上课的那个班
|
||||||
|
*/
|
||||||
|
ChartJS.register(BarElement, CategoryScale, LinearScale, Tooltip)
|
||||||
|
|
||||||
|
const theme = useThemeVars()
|
||||||
|
const { chartKey } = useChartTheme()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const showModal = ref(false)
|
const userStore = useUserStore()
|
||||||
|
const configStore = useConfigStore()
|
||||||
|
|
||||||
|
const info = ref<DashboardInfo | null>(null)
|
||||||
|
onMounted(async () => {
|
||||||
|
info.value = await getBaseInfo()
|
||||||
|
if (!luckyClass.value && info.value.activeClass) luckyClass.value = info.value.activeClass
|
||||||
|
})
|
||||||
|
|
||||||
|
const siteTiles = computed(() => {
|
||||||
|
const site = info.value?.site
|
||||||
|
if (!site) return []
|
||||||
|
const n = (value: number) => value.toLocaleString("zh-CN")
|
||||||
|
return [
|
||||||
|
{ label: "学生", value: n(site.students), note: `老师 ${site.teachers} 人` },
|
||||||
|
{
|
||||||
|
label: "题目",
|
||||||
|
value: n(site.problems),
|
||||||
|
note: `学生看得到 ${n(site.visibleProblems)} 道`,
|
||||||
|
to: "/admin/problem/list",
|
||||||
|
},
|
||||||
|
{ label: "提交", value: n(site.submissions), note: `这学期 ${n(site.termSubmissions)} 次` },
|
||||||
|
{ label: "这周做题的人", value: n(site.weekActive), note: "周一起交过题的" },
|
||||||
|
{ label: "比赛", value: n(site.contests), note: "", to: "/admin/contest/list" },
|
||||||
|
{ label: "题单", value: n(site.problemSets), note: "", to: "/admin/problemset/list" },
|
||||||
|
{ label: "自学", value: n(site.tutorials), note: "课", to: "/admin/tutorial/list" },
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 最近 30 天每天交了几次。今天那根用主色,其余淡一点 */
|
||||||
|
const dailyData = computed(() => {
|
||||||
|
const daily = info.value?.daily ?? []
|
||||||
|
return {
|
||||||
|
labels: daily.map((d) => d.day.slice(5)),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
data: daily.map((d) => d.count),
|
||||||
|
backgroundColor: daily.map((_, i) =>
|
||||||
|
i === daily.length - 1 ? theme.value.primaryColor : rgba(theme.value.primaryColor, 0.4),
|
||||||
|
),
|
||||||
|
borderRadius: 3,
|
||||||
|
maxBarThickness: 22,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const dailyOptions = {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: { display: false },
|
||||||
|
tooltip: {
|
||||||
|
callbacks: { label: (ctx: { parsed: { y: number | null } }) => `${ctx.parsed.y ?? 0} 次` },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: { grid: { display: false }, ticks: { maxRotation: 0, autoSkipPadding: 10 } },
|
||||||
|
y: { beginAtZero: true, ticks: { precision: 0 } },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
function openSystemErrors() {
|
||||||
|
router.push({ name: "submissions", query: { result: String(SubmissionStatus.system_error) } })
|
||||||
|
}
|
||||||
|
|
||||||
|
const shortcuts = [
|
||||||
|
{ label: "新建题目", to: "/admin/problem/create" },
|
||||||
|
{ label: "新建比赛", to: "/admin/contest/create" },
|
||||||
|
{ label: "批量建号", to: "/admin/user/generate" },
|
||||||
|
{ label: "课堂看板", to: "/classroom" },
|
||||||
|
]
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- 抽一个人
|
||||||
|
|
||||||
|
const classOptions = computed(() => groupClassOptions(configStore.config?.classList ?? []))
|
||||||
|
const luckyClass = ref<string | null>(null)
|
||||||
const luckyGuy = ref("")
|
const luckyGuy = ref("")
|
||||||
const isRolling = ref(false)
|
const isRolling = ref(false)
|
||||||
const rollingNames = ref<string[]>([])
|
|
||||||
const pulseKey = ref(0)
|
const pulseKey = ref(0)
|
||||||
let rollingTimer: ReturnType<typeof setInterval> | null = null
|
let rollingTimer: ReturnType<typeof setInterval> | null = null
|
||||||
let rollingStopper: ReturnType<typeof setTimeout> | null = null
|
let rollingStopper: ReturnType<typeof setTimeout> | null = null
|
||||||
const data = ref<Rank[]>([])
|
|
||||||
const total = ref(0)
|
|
||||||
const query = reactive({
|
|
||||||
limit: 10,
|
|
||||||
page: 1,
|
|
||||||
classroom: "",
|
|
||||||
})
|
|
||||||
|
|
||||||
const columns: DataTableColumn<Rank>[] = [
|
|
||||||
{
|
|
||||||
title: "排名",
|
|
||||||
key: "index",
|
|
||||||
width: 80,
|
|
||||||
align: "center",
|
|
||||||
render: (_, index) => index + (query.page - 1) * query.limit + 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "用户",
|
|
||||||
key: "username",
|
|
||||||
width: 200,
|
|
||||||
render: (row) =>
|
|
||||||
h(
|
|
||||||
NButton,
|
|
||||||
{
|
|
||||||
text: true,
|
|
||||||
type: "info",
|
|
||||||
onClick: () => router.push("/user?name=" + row.user.username),
|
|
||||||
},
|
|
||||||
() => row.user.username,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{ title: "个性签名", key: "mood" },
|
|
||||||
{ title: "已解决", key: "acceptedNumber", width: 100 },
|
|
||||||
{ title: "提交数", key: "submissionNumber", width: 100 },
|
|
||||||
{
|
|
||||||
title: "正确率",
|
|
||||||
key: "rate",
|
|
||||||
width: 100,
|
|
||||||
render: (row) => getACRate(row.acceptedNumber, row.submissionNumber),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
const res = await getBaseInfo()
|
|
||||||
userCount.value = res.userCount
|
|
||||||
submissionCount.value = res.todaySubmissionCount
|
|
||||||
contestCount.value = res.recentContestCount
|
|
||||||
judgeServerCount.value = res.judgeServerCount
|
|
||||||
})
|
|
||||||
|
|
||||||
async function listRanks() {
|
|
||||||
const offset = (query.page - 1) * query.limit
|
|
||||||
const res = await getAdminUserRank(offset, query.limit, query.classroom)
|
|
||||||
data.value = res.results
|
|
||||||
total.value = res.total
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopRolling() {
|
function stopRolling() {
|
||||||
if (rollingTimer) {
|
if (rollingTimer) clearInterval(rollingTimer)
|
||||||
clearInterval(rollingTimer)
|
if (rollingStopper) clearTimeout(rollingStopper)
|
||||||
rollingTimer = null
|
rollingTimer = null
|
||||||
}
|
rollingStopper = null
|
||||||
if (rollingStopper) {
|
|
||||||
clearTimeout(rollingStopper)
|
|
||||||
rollingStopper = null
|
|
||||||
}
|
|
||||||
isRolling.value = false
|
isRolling.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
function startRolling(finalName: string) {
|
/** 名字滚两秒停下:接口一次给 10 个随机的,最后一个就是抽中的 */
|
||||||
|
async function draw() {
|
||||||
|
if (!luckyClass.value) return
|
||||||
|
const prefix = `ks${luckyClass.value}`
|
||||||
|
const names = (await randomUser10(prefix)).map((name) => name.slice(prefix.length) || name)
|
||||||
|
if (!names.length) {
|
||||||
|
luckyGuy.value = "这个班没有人"
|
||||||
|
return
|
||||||
|
}
|
||||||
stopRolling()
|
stopRolling()
|
||||||
if (!rollingNames.value.length) return
|
|
||||||
isRolling.value = true
|
isRolling.value = true
|
||||||
const interval = 80
|
|
||||||
const duration = 2000
|
|
||||||
let index = 0
|
let index = 0
|
||||||
rollingTimer = setInterval(() => {
|
rollingTimer = setInterval(() => {
|
||||||
luckyGuy.value = rollingNames.value[index % rollingNames.value.length]
|
luckyGuy.value = names[index % names.length]!
|
||||||
index += 1
|
index += 1
|
||||||
}, interval)
|
}, 80)
|
||||||
rollingStopper = setTimeout(() => {
|
rollingStopper = setTimeout(() => {
|
||||||
stopRolling()
|
stopRolling()
|
||||||
luckyGuy.value = finalName
|
luckyGuy.value = names[names.length - 1]!
|
||||||
pulseKey.value += 1
|
pulseKey.value += 1
|
||||||
}, duration)
|
}, 2000)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getRandom() {
|
watch(luckyClass, () => {
|
||||||
const res = await randomUser10(query.classroom)
|
|
||||||
const names = (res as string[]).map((name) => name.split(query.classroom)[1])
|
|
||||||
rollingNames.value = names
|
|
||||||
const finalName = names[names.length - 1]
|
|
||||||
startRolling(finalName)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getRandomModal() {
|
|
||||||
showModal.value = true
|
|
||||||
stopRolling()
|
stopRolling()
|
||||||
luckyGuy.value = ""
|
luckyGuy.value = ""
|
||||||
}
|
|
||||||
|
|
||||||
watch(() => query.page, listRanks)
|
|
||||||
watch(
|
|
||||||
() => query.limit,
|
|
||||||
() => {
|
|
||||||
query.page = 1
|
|
||||||
listRanks()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => query.classroom,
|
|
||||||
(v) => {
|
|
||||||
query.page = 1
|
|
||||||
if (!v) {
|
|
||||||
data.value = []
|
|
||||||
total.value = 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
watch(showModal, (v) => {
|
|
||||||
if (!v) {
|
|
||||||
stopRolling()
|
|
||||||
luckyGuy.value = ""
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
onUnmounted(stopRolling)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<PageHeader :title="`你好,${userStore.user?.username ?? '管理员'}`">
|
<PageHeader :title="`你好,${userStore.user?.username ?? '管理员'}`" />
|
||||||
<template #actions>
|
|
||||||
<n-button @click="router.push('/admin/contest/create')">新建比赛</n-button>
|
|
||||||
<n-button type="primary" @click="router.push('/admin/problem/create')">新建题目</n-button>
|
|
||||||
</template>
|
|
||||||
</PageHeader>
|
|
||||||
|
|
||||||
<n-grid cols="2 m:4" :x-gap="12" :y-gap="12" responsive="screen" class="stats">
|
<div v-if="info" class="tiles">
|
||||||
<n-gi>
|
<div class="tile">
|
||||||
<n-card size="small"><n-statistic label="总用户数" :value="userCount" /></n-card>
|
<span class="label">判题机</span>
|
||||||
</n-gi>
|
<b :class="info.judgeServerCount ? 'ok' : 'bad'">{{ info.judgeServerCount }} 台在线</b>
|
||||||
<n-gi>
|
<span v-if="!info.judgeServerCount" class="bad-note">学生交了会一直判不出来</span>
|
||||||
<n-card size="small"><n-statistic label="今日提交" :value="submissionCount" /></n-card>
|
</div>
|
||||||
</n-gi>
|
<div class="tile">
|
||||||
<n-gi>
|
<span class="label">今天提交</span>
|
||||||
<n-card size="small"><n-statistic label="近期比赛" :value="contestCount" /></n-card>
|
<b>{{ info.todaySubmissionCount }} 次</b>
|
||||||
</n-gi>
|
<button
|
||||||
<n-gi>
|
v-if="info.todaySystemErrorCount"
|
||||||
<!-- 判题机数量后端一直在下发,这里以前没显示 —— 判题机全掉线的时候,
|
type="button"
|
||||||
这一栏是 0,比学生喊「交了没反应」早得多 -->
|
class="link bad-note"
|
||||||
<n-card size="small">
|
@click="openSystemErrors"
|
||||||
<n-statistic label="在线判题机">
|
>
|
||||||
<n-text :type="judgeServerCount > 0 ? 'success' : 'error'">
|
系统错误 {{ info.todaySystemErrorCount }} 次 →
|
||||||
{{ judgeServerCount }}
|
</button>
|
||||||
</n-text>
|
<span v-else class="note">系统错误 0 次</span>
|
||||||
</n-statistic>
|
</div>
|
||||||
</n-card>
|
<div class="tile">
|
||||||
</n-gi>
|
<span class="label">现在在线</span>
|
||||||
</n-grid>
|
<b>{{ info.onlineCount }} 人</b>
|
||||||
|
<span class="note">5 分钟内有动静的</span>
|
||||||
|
</div>
|
||||||
|
<div class="tile">
|
||||||
|
<span class="label">正在上课</span>
|
||||||
|
<b>{{ info.activeClass ? classLabel(info.activeClass) : "没有" }}</b>
|
||||||
|
<router-link
|
||||||
|
v-if="info.activeClass"
|
||||||
|
class="link"
|
||||||
|
:to="{ name: 'classroom board', query: { className: info.activeClass } }"
|
||||||
|
>
|
||||||
|
去课堂看板 →
|
||||||
|
</router-link>
|
||||||
|
<span v-else class="note">最近两小时没有哪个班在交题</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<n-card size="small" title="班级排名">
|
<div v-if="info" class="site">
|
||||||
<template #header-extra>
|
<component
|
||||||
<n-flex align="center" :size="8">
|
:is="tile.to ? 'router-link' : 'div'"
|
||||||
<n-input
|
v-for="tile in siteTiles"
|
||||||
style="width: 160px"
|
:key="tile.label"
|
||||||
clearable
|
:to="tile.to"
|
||||||
v-model:value="query.classroom"
|
class="site-tile"
|
||||||
placeholder="班级前缀"
|
:class="{ clickable: tile.to }"
|
||||||
@keyup.enter="listRanks"
|
>
|
||||||
/>
|
<span class="label">{{ tile.label }}</span>
|
||||||
<n-button secondary type="primary" @click="listRanks">查询</n-button>
|
<b>{{ tile.value }}</b>
|
||||||
<n-button v-if="query.classroom" @click="getRandomModal">随机抽签</n-button>
|
<span class="note">{{ tile.note }}</span>
|
||||||
</n-flex>
|
</component>
|
||||||
</template>
|
</div>
|
||||||
<template v-if="data.length">
|
|
||||||
<n-data-table striped :data="data" :columns="columns" />
|
<div v-if="info" class="chart-card">
|
||||||
<Pagination :total="total" v-model:page="query.page" v-model:limit="query.limit" />
|
<div class="chart-head">
|
||||||
</template>
|
<b>最近 30 天每天交了几次</b>
|
||||||
<n-empty v-else description="输入班级前缀后查询" style="padding: 24px 0" />
|
</div>
|
||||||
</n-card>
|
<div class="chart">
|
||||||
<n-modal preset="card" title="猜猜看幸运儿是谁?" v-model:show="showModal" style="width: 400px">
|
<Bar :key="chartKey" :data="dailyData" :options="dailyOptions" />
|
||||||
<n-flex vertical justify="center" align="center">
|
</div>
|
||||||
<n-h1 :key="pulseKey" class="lucky pulse">{{ luckyGuy }}</n-h1>
|
</div>
|
||||||
<n-button block :disabled="isRolling" @click="getRandom">
|
|
||||||
{{ luckyGuy ? "再来一次" : "开始抽签" }}
|
<div class="shortcuts">
|
||||||
</n-button>
|
<n-button v-for="it in shortcuts" :key="it.to" size="large" @click="router.push(it.to)">
|
||||||
</n-flex>
|
{{ it.label }}
|
||||||
</n-modal>
|
</n-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="lucky">
|
||||||
|
<b class="lucky-title">抽一个人</b>
|
||||||
|
<n-select
|
||||||
|
v-model:value="luckyClass"
|
||||||
|
:options="classOptions"
|
||||||
|
filterable
|
||||||
|
placeholder="选班级"
|
||||||
|
aria-label="班级"
|
||||||
|
style="width: 160px"
|
||||||
|
/>
|
||||||
|
<span :key="pulseKey" class="lucky-name" :class="{ pulse: !isRolling && luckyGuy }">
|
||||||
|
{{ luckyGuy }}
|
||||||
|
</span>
|
||||||
|
<n-button type="primary" :disabled="!luckyClass || isRolling" @click="draw">
|
||||||
|
{{ luckyGuy ? "再抽一个" : "抽" }}
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.stats {
|
.tiles {
|
||||||
margin-bottom: 16px;
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid v-bind("theme.dividerColor");
|
||||||
|
}
|
||||||
|
|
||||||
|
.label,
|
||||||
|
.note {
|
||||||
|
font-size: 13px;
|
||||||
|
color: v-bind("theme.textColor3");
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile b {
|
||||||
|
font-size: 24px;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile b.ok {
|
||||||
|
color: v-bind("theme.successColor");
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile b.bad,
|
||||||
|
.bad-note {
|
||||||
|
color: v-bind("theme.errorColor");
|
||||||
|
}
|
||||||
|
|
||||||
|
.bad-note {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
align-self: flex-start;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
background: none;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 13px;
|
||||||
|
color: v-bind("theme.primaryColor");
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(7, minmax(0, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-tile {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: v-bind("theme.actionColor");
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-tile b {
|
||||||
|
font-size: 22px;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-tile.clickable:hover {
|
||||||
|
background: v-bind("theme.hoverColor");
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-card {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid v-bind("theme.dividerColor");
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-head {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart {
|
||||||
|
height: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shortcuts {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lucky {
|
.lucky {
|
||||||
height: 48px;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 16px 18px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid v-bind("theme.dividerColor");
|
||||||
|
}
|
||||||
|
|
||||||
|
.lucky-title {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lucky-name {
|
||||||
|
min-width: 120px;
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pulse {
|
.pulse {
|
||||||
@@ -236,4 +379,11 @@ watch(showModal, (v) => {
|
|||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.tiles,
|
||||||
|
.site {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
Reference in new issue
Block a user