添加设备信息和教学视频目录,完成基础ui

This commit is contained in:
2026-03-31 17:56:52 +08:00
parent 9baaff77ad
commit d8520da894
5 changed files with 918 additions and 0 deletions

18
src/api/system/info.js Normal file
View File

@@ -0,0 +1,18 @@
import request from '@/utils/request'
// 查询设备基本信息列表
export function listInfo(query) {
return request({
url: '/system/info/list',
method: 'get',
params: query
})
}
// 查询设备基本信息详细
export function getInfo(deviceId) {
return request({
url: '/system/info/' + deviceId,
method: 'get'
})
}

18
src/api/system/video.js Normal file
View File

@@ -0,0 +1,18 @@
import request from '@/utils/request'
// 查询教学视频列表
export function listVideo(query) {
return request({
url: '/system/video/list',
method: 'get',
params: query
})
}
// 查询教学视频详细
export function getVideo(videoId) {
return request({
url: '/system/video/' + videoId,
method: 'get'
})
}

View File

@@ -58,6 +58,14 @@ const usePermissionStore = defineStore(
// 遍历后台传来的路由字符串,转换为组件对象
function filterAsyncRouter(asyncRouterMap, lastRouter = false, type = false) {
return asyncRouterMap.filter(route => {
// 兼容:后端把顶级菜单配置成了具体页面组件(非Layout)时,会导致进入后丢失整体布局(侧边栏/顶部栏)。
// 这里将该类顶级叶子路由自动包一层 Layout子路由使用空 path 以保持原 URL 不变。
if (!lastRouter && route.component && !route.children && !['Layout', 'ParentView', 'InnerLink'].includes(route.component) && typeof route.path === 'string' && route.path.startsWith('/')) {
const inner = { ...route }
route.component = 'Layout'
route.name = inner.name ? `${inner.name}Wrapper` : route.name
route.children = [Object.assign(inner, { path: '' })]
}
if (type && route.children) {
route.children = filterChildren(route.children)
}

View File

@@ -0,0 +1,545 @@
<template>
<div class="app-container">
<div class="m-page">
<div v-if="!detailOpen" class="m-header">
<div class="m-title">设备信息</div>
<div class="m-subtitle">查看您公司拥有的焊接机器人设备信息</div>
</div>
<div v-if="!detailOpen" class="m-list">
<div v-if="loading && infoList.length === 0" class="m-hint">正在加载...</div>
<div v-else-if="!loading && infoList.length === 0" class="m-hint">暂无设备</div>
<div
v-for="item in infoList"
:key="item.deviceId"
class="m-device-card"
@click="handleDetail(item)"
>
<div class="m-device-row">
<div class="m-device-name">{{ item.deviceName }}</div>
<div class="m-device-status" :class="statusClass(item.deviceStatus)">
<span class="m-dot" />
<span>{{ formatDeviceStatus(item.deviceStatus) }}</span>
</div>
</div>
<div class="m-device-meta">
<div class="m-meta-item"><span class="m-meta-label">设备ID</span><span class="m-meta-value">{{ item.deviceId }}</span></div>
<div class="m-meta-item"><span class="m-meta-label">设备编号</span><span class="m-meta-value">{{ item.deviceCode }}</span></div>
</div>
<div class="m-device-action">
<span class="m-action-text">查看详情</span>
<el-icon class="m-arrow"><ArrowRight /></el-icon>
</div>
</div>
<div ref="loadMoreRef" class="m-load-more">
<span v-if="loadingMore">加载中...</span>
<span v-else-if="finished && infoList.length > 0">已到底</span>
<span v-else-if="infoList.length > 0">上滑加载更多</span>
</div>
</div>
<div v-else class="m-detail" v-loading="detailLoading" element-loading-text="加载详情中...">
<div class="m-detail-top">
<el-button class="m-back" plain @click="detailOpen = false">
<el-icon><ArrowLeft /></el-icon>
返回
</el-button>
<div class="m-title m-center">设备信息</div>
</div>
<div class="m-card m-hero-card">
<div class="m-hero">
<img v-if="detail.deviceImage" :src="detail.deviceImage" alt="device" />
<div v-else class="m-hero-placeholder"></div>
</div>
<div class="m-hero-body">
<div class="m-hero-title">{{ detail.deviceName || '设备详情' }}</div>
<div class="m-hero-sub">设备编号{{ detail.deviceCode || '-' }}</div>
<div class="m-badges">
<div class="m-badge" :class="statusClass(detail.deviceStatus)">
<span class="m-dot" />
<span>{{ formatDeviceStatus(detail.deviceStatus) }}</span>
</div>
<div class="m-badge" :class="warrantyStatus().cls">
<span class="m-dot" />
<span>质保{{ warrantyStatus().text }}</span>
</div>
</div>
</div>
</div>
<div class="m-section">
<div class="m-section-title">基础信息</div>
<div class="m-kv"><span class="m-kv-key">型号规格</span><span class="m-kv-val">{{ detail.modelSpec || '-' }}</span></div>
<div class="m-kv"><span class="m-kv-key">购买日期</span><span class="m-kv-val">{{ detail.purchaseDate || '-' }}</span></div>
<div class="m-kv"><span class="m-kv-key">设备序列号</span><span class="m-kv-val">{{ detail.serialNumber || '-' }}</span></div>
<div class="m-kv"><span class="m-kv-key">安装地址</span><span class="m-kv-val">{{ detail.installAddress || '-' }}</span></div>
<div class="m-kv"><span class="m-kv-key">负责人</span><span class="m-kv-val">{{ detail.responsiblePerson || '-' }}</span></div>
</div>
<div class="m-section">
<div class="m-section-title">质保信息</div>
<div class="m-kv">
<span class="m-kv-key">质保状态</span>
<span class="m-kv-val">
<span class="m-chip" :class="warrantyStatus().cls"> {{ warrantyStatus().text }} </span>
</span>
</div>
<div class="m-kv"><span class="m-kv-key">质保开始日期</span><span class="m-kv-val">{{ detail.warrantyStartDate || '-' }}</span></div>
<div class="m-kv"><span class="m-kv-key">质保结束日期</span><span class="m-kv-val">{{ detail.warrantyEndDate || '-' }}</span></div>
<div class="m-kv">
<span class="m-kv-key">质保{{ warrantyStatus().leftLabel }}</span>
<span class="m-kv-val">{{ leftDaysText(warrantyStatus().leftDays) }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup name="Info">
import { listInfo, getInfo } from "@/api/system/info"
import { ArrowLeft, ArrowRight } from "@element-plus/icons-vue"
const { proxy } = getCurrentInstance()
const infoList = ref([])
const detailOpen = ref(false)
const detail = ref({})
const detailLoading = ref(false)
const detailLock = ref(false)
const loading = ref(true)
const loadingMore = ref(false)
const finished = ref(false)
const loadMoreRef = ref(null)
let observer = null
const loadingLock = ref(false)
const loadedIds = ref(new Set())
const data = reactive({
queryParams: {
pageNum: 1,
pageSize: 10,
}
})
const { queryParams } = toRefs(data)
/** 查询设备基本信息列表 */
function getList(reset = false) {
if (reset) {
queryParams.value.pageNum = 1
finished.value = false
infoList.value = []
loadedIds.value = new Set()
}
if (finished.value) return
if (loadingLock.value) return
const isFirstPage = queryParams.value.pageNum === 1
if (isFirstPage) loading.value = true
else loadingMore.value = true
loadingLock.value = true
listInfo(queryParams.value).then(response => {
const rows = response.rows || []
const next = []
for (const r of rows) {
const id = r?.deviceId
if (id == null) continue
if (loadedIds.value.has(id)) continue
loadedIds.value.add(id)
next.push(r)
}
infoList.value = infoList.value.concat(next)
if (rows.length < queryParams.value.pageSize) {
finished.value = true
} else {
queryParams.value.pageNum += 1
}
}).finally(() => {
loading.value = false
loadingMore.value = false
loadingLock.value = false
})
}
/** 详情按钮操作 */
function handleDetail(row) {
if (detailLock.value) return
detailLock.value = true
detailLoading.value = true
detail.value = {}
detailOpen.value = true
const _deviceId = row.deviceId
getInfo(_deviceId).then(response => {
detail.value = response.data
}).finally(() => {
detailLoading.value = false
detailLock.value = false
})
}
function formatDeviceStatus(val) {
const map = {
"0": "运行中",
"1": "故障",
"2": "停机",
"3": "维护"
}
return map[val] || "-"
}
function statusClass(val) {
const map = {
"0": "is-ok",
"1": "is-bad",
"2": "is-warn",
"3": "is-info"
}
return map[val] || "is-info"
}
function warrantyStatus() {
const start = parseYmd(detail.value?.warrantyStartDate)
const end = parseYmd(detail.value?.warrantyEndDate)
if (!start || !end) return { text: "未知", cls: "is-info", leftDays: null, leftLabel: "质保剩余" }
const today = startOfToday()
if (today.getTime() < start.getTime()) {
return { text: "未开始", cls: "is-info", leftDays: diffDays(today, start), leftLabel: "距开始" }
}
if (today.getTime() <= end.getTime()) {
return { text: "在保", cls: "is-ok", leftDays: diffDays(today, end), leftLabel: "剩余" }
}
return { text: "已过保", cls: "is-bad", leftDays: diffDays(end, today), leftLabel: "已过期" }
}
function startOfToday() {
const d = new Date()
d.setHours(0, 0, 0, 0)
return d
}
function parseYmd(val) {
if (!val) return null
// 支持 "YYYY-MM-DD" 或 Date 可解析字符串
const s = String(val)
const m = s.match(/^(\d{4})-(\d{2})-(\d{2})/)
if (m) {
const d = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]))
d.setHours(0, 0, 0, 0)
return isNaN(d.getTime()) ? null : d
}
const d = new Date(s)
d.setHours(0, 0, 0, 0)
return isNaN(d.getTime()) ? null : d
}
function diffDays(a, b) {
const ms = b.getTime() - a.getTime()
return Math.floor(ms / 86400000)
}
function leftDaysText(days) {
if (days === null || days === undefined) return "-"
const d = Number(days)
if (!Number.isFinite(d)) return "-"
return `${Math.max(0, Math.floor(d))}`
}
function setupObserver() {
if (observer) observer.disconnect()
if (!loadMoreRef.value) return
observer = new IntersectionObserver((entries) => {
if (detailOpen.value) return
const entry = entries[0]
if (entry && entry.isIntersecting) {
getList(false)
}
}, { root: null, rootMargin: "200px", threshold: 0 })
observer.observe(loadMoreRef.value)
}
onMounted(() => {
getList(true)
nextTick(setupObserver)
})
watch(detailOpen, (v) => {
if (!v) nextTick(setupObserver)
})
</script>
<style scoped>
.m-page {
max-width: 480px;
margin: 0 auto;
padding: 12px;
min-height: calc(100vh - 84px);
background: linear-gradient(180deg, rgba(37, 99, 235, 0.06), rgba(37, 99, 235, 0) 140px);
}
.m-header {
padding: 4px 4px 12px 4px;
}
.m-title {
font-size: 18px;
font-weight: 700;
line-height: 24px;
}
.m-subtitle {
margin-top: 6px;
font-size: 12px;
color: var(--el-text-color-secondary);
}
.m-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.m-device-card {
background: var(--el-bg-color);
border: 1px solid var(--el-border-color-lighter);
border-radius: 12px;
padding: 12px;
box-shadow: 0 2px 10px rgba(15, 23, 42, 0.06);
transition: transform 0.08s ease, box-shadow 0.12s ease;
}
.m-device-card:active {
transform: scale(0.99);
box-shadow: 0 1px 6px rgba(15, 23, 42, 0.05);
}
.m-device-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.m-device-name {
font-weight: 700;
font-size: 15px;
line-height: 20px;
}
.m-device-status {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12px;
padding: 4px 10px;
border-radius: 999px;
background: rgba(16, 185, 129, 0.10);
color: var(--el-color-success);
}
.m-device-status.is-bad {
background: rgba(239, 68, 68, 0.10);
color: var(--el-color-danger);
}
.m-device-status.is-warn {
background: rgba(245, 158, 11, 0.12);
color: var(--el-color-warning);
}
.m-device-status.is-info {
background: rgba(100, 116, 139, 0.12);
color: var(--el-text-color-regular);
}
.m-dot {
width: 8px;
height: 8px;
border-radius: 999px;
background: currentColor;
display: inline-block;
}
.m-device-meta {
margin-top: 10px;
display: grid;
grid-template-columns: 1fr;
gap: 6px;
font-size: 13px;
}
.m-meta-item {
display: flex;
justify-content: space-between;
gap: 12px;
}
.m-meta-label {
color: var(--el-text-color-secondary);
}
.m-device-action {
margin-top: 10px;
display: flex;
justify-content: space-between;
align-items: center;
color: var(--el-text-color-secondary);
font-size: 12px;
padding-top: 10px;
border-top: 1px solid var(--el-border-color-lighter);
}
.m-action-text {
font-weight: 600;
}
.m-arrow {
font-size: 14px;
}
.m-hint {
padding: 12px;
text-align: center;
color: var(--el-text-color-secondary);
font-size: 13px;
}
.m-load-more {
padding: 12px;
text-align: center;
color: var(--el-text-color-secondary);
font-size: 12px;
}
.m-detail-top {
display: grid;
grid-template-columns: 92px 1fr 92px;
align-items: center;
margin-bottom: 10px;
}
.m-center {
text-align: center;
}
.m-back {
justify-self: start;
border-radius: 999px;
padding: 6px 10px;
}
.m-detail-top .m-title {
text-align: center;
}
.m-card {
background: var(--el-bg-color);
border: 1px solid var(--el-border-color-lighter);
border-radius: 12px;
padding: 12px;
}
.m-hero-card {
padding: 10px;
}
.m-hero {
margin-top: 0;
border-radius: 12px;
overflow: hidden;
border: 1px solid var(--el-border-color-lighter);
background: #0f172a;
}
.m-hero img {
width: 100%;
height: 180px;
object-fit: cover;
display: block;
}
.m-hero-placeholder {
width: 100%;
height: 180px;
background: linear-gradient(135deg, #0f172a, #334155);
}
.m-hero-body {
padding: 10px 4px 2px 4px;
}
.m-hero-title {
font-size: 16px;
font-weight: 800;
line-height: 22px;
}
.m-hero-sub {
margin-top: 6px;
font-size: 12px;
color: var(--el-text-color-secondary);
}
.m-badges {
margin-top: 10px;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.m-badge {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12px;
padding: 4px 10px;
border-radius: 999px;
background: rgba(100, 116, 139, 0.12);
color: var(--el-text-color-regular);
}
.m-badge.is-ok {
background: rgba(16, 185, 129, 0.10);
color: var(--el-color-success);
}
.m-badge.is-bad {
background: rgba(239, 68, 68, 0.10);
color: var(--el-color-danger);
}
.m-badge.is-warn {
background: rgba(245, 158, 11, 0.12);
color: var(--el-color-warning);
}
.m-badge.is-info {
background: rgba(100, 116, 139, 0.12);
color: var(--el-text-color-regular);
}
.m-kv-key {
color: var(--el-text-color-secondary);
}
.m-kv-val {
font-weight: 600;
color: var(--el-text-color-regular);
}
.m-chip {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: 999px;
font-size: 12px;
font-weight: 700;
background: rgba(100, 116, 139, 0.12);
color: var(--el-text-color-regular);
}
.m-chip.is-ok {
background: rgba(16, 185, 129, 0.10);
color: var(--el-color-success);
}
.m-chip.is-bad {
background: rgba(239, 68, 68, 0.10);
color: var(--el-color-danger);
}
.m-chip.is-info {
background: rgba(100, 116, 139, 0.12);
color: var(--el-text-color-regular);
}
.m-device-code {
margin-top: 6px;
font-size: 12px;
color: var(--el-text-color-secondary);
}
.m-section {
margin-top: 12px;
background: var(--el-bg-color);
border: 1px solid var(--el-border-color-lighter);
border-radius: 12px;
padding: 12px;
}
.m-section-title {
font-weight: 700;
margin-bottom: 8px;
}
.m-kv {
display: flex;
justify-content: space-between;
gap: 12px;
padding: 10px 0;
border-top: 1px solid var(--el-border-color-lighter);
font-size: 13px;
}
.m-kv:first-of-type {
border-top: none;
padding-top: 0;
}
.m-kv span:first-child {
color: var(--el-text-color-secondary);
}
</style>

View File

@@ -0,0 +1,329 @@
<template>
<div class="app-container">
<div class="m-page">
<div class="m-header">
<div class="m-title">焊接机器人教学视频</div>
<div class="m-subtitle">这里提供焊接机器人使用教程与常见问题解决方案</div>
</div>
<div class="m-tabs">
<el-segmented v-model="activeTab" :options="tabOptions" size="small" />
</div>
<div class="m-list">
<div v-if="loading && videoList.length === 0" class="m-hint">正在加载...</div>
<div v-else-if="!loading && videoList.length === 0" class="m-hint">暂无视频</div>
<div
v-for="item in videoList"
:key="item.videoId"
class="m-video-card"
@click="openVideo(item)"
>
<div class="m-cover">
<img v-if="item.coverUrl" :src="biliCoverProxy(item.coverUrl)" alt="cover" />
<div v-else class="m-cover-placeholder" />
<div class="m-play"></div>
<div class="m-duration">{{ formatDuration(item.durationSeconds) }}</div>
</div>
<div class="m-content">
<div class="m-video-title">{{ item.title }}</div>
<div class="m-video-desc">{{ item.summary }}</div>
<div class="m-meta">
<span>{{ formatViews(item.viewCount) }}</span>
<span>·</span>
<span>{{ item.publishDate || '-' }}</span>
</div>
</div>
</div>
<div ref="loadMoreRef" class="m-load-more">
<span v-if="loadingMore">加载中...</span>
<span v-else-if="finished && videoList.length > 0">已到底</span>
<span v-else-if="videoList.length > 0">上滑加载更多</span>
</div>
</div>
</div>
<el-dialog title="播放视频" v-model="playerOpen" width="720px" append-to-body>
<div class="m-player">
<iframe
v-if="isBiliEmbed(currentVideo)"
:src="currentVideo.videoUrl"
frameborder="0"
allowfullscreen="true"
referrerpolicy="no-referrer"
style="width: 100%; height: 420px"
/>
<video v-else-if="currentVideo?.videoUrl" controls playsinline style="width: 100%">
<source :src="currentVideo.videoUrl" />
</video>
<div v-else class="m-empty">
暂无视频地址
<div v-if="currentVideo?.sourceUrl" style="margin-top: 10px">
<el-button type="primary" plain @click="openSourceUrl(currentVideo.sourceUrl)">打开B站</el-button>
</div>
</div>
</div>
<div class="m-player-title">{{ currentVideo?.title }}</div>
<div class="m-player-desc">{{ currentVideo?.summary }}</div>
<template #footer>
<div class="dialog-footer">
<el-button v-if="currentVideo?.sourceUrl" plain @click="openSourceUrl(currentVideo.sourceUrl)">打开B站</el-button>
<el-button @click="playerOpen = false"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="Video">
import { listVideo, getVideo } from "@/api/system/video"
const videoList = ref([])
const playerOpen = ref(false)
const currentVideo = ref(null)
const loading = ref(true)
const loadingMore = ref(false)
const finished = ref(false)
const loadMoreRef = ref(null)
let observer = null
const activeTab = ref("ALL")
const tabOptions = [
{ label: "全部视频", value: "ALL" },
{ label: "基础教学", value: "BASIC" },
{ label: "常见问题", value: "FAQ" },
{ label: "故障排除", value: "TROUBLE" }
]
const data = reactive({
queryParams: {
pageNum: 1,
pageSize: 10,
categoryCode: null,
}
})
const { queryParams } = toRefs(data)
/** 查询教学视频列表 */
function getList(reset = false) {
if (reset) {
queryParams.value.pageNum = 1
finished.value = false
videoList.value = []
}
if (finished.value) return
queryParams.value.categoryCode = activeTab.value === "ALL" ? null : activeTab.value
const isFirstPage = queryParams.value.pageNum === 1
if (isFirstPage) loading.value = true
else loadingMore.value = true
listVideo(queryParams.value).then(response => {
const rows = response.rows || []
videoList.value = videoList.value.concat(rows)
if (rows.length < queryParams.value.pageSize) {
finished.value = true
} else {
queryParams.value.pageNum += 1
}
}).finally(() => {
loading.value = false
loadingMore.value = false
})
}
watch(activeTab, () => {
getList(true)
})
function openVideo(row) {
getVideo(row.videoId).then(res => {
currentVideo.value = res.data
playerOpen.value = true
})
}
function setupObserver() {
if (observer) observer.disconnect()
if (!loadMoreRef.value) return
observer = new IntersectionObserver((entries) => {
const entry = entries[0]
if (entry && entry.isIntersecting) {
getList(false)
}
}, { root: null, rootMargin: "200px", threshold: 0 })
observer.observe(loadMoreRef.value)
}
function formatDuration(sec) {
const s = Number(sec || 0)
const m = Math.floor(s / 60)
const r = s % 60
return `${String(m).padStart(2, "0")}:${String(r).padStart(2, "0")}`
}
function formatViews(v) {
const n = Number(v || 0)
if (n >= 10000) return `${(n / 10000).toFixed(1)}w 次播放`
return `${n} 次播放`
}
function biliCoverProxy(url) {
if (!url) return ""
const u = String(url).replace(/^http:\/\//i, "https://")
return `${import.meta.env.VITE_APP_BASE_API}/system/bili/cover?url=${encodeURIComponent(u)}`
}
function isBiliEmbed(v) {
const url = v?.videoUrl || ""
return typeof url === "string" && url.includes("player.bilibili.com/player.html")
}
function openSourceUrl(url) {
if (!url) return
window.open(url, "_blank")
}
onMounted(() => {
getList(true)
nextTick(setupObserver)
})
</script>
<style scoped>
.m-page {
max-width: 480px;
margin: 0 auto;
padding: 12px;
min-height: calc(100vh - 84px);
background: linear-gradient(180deg, rgba(37, 99, 235, 0.06), rgba(37, 99, 235, 0) 140px);
}
.m-header {
padding: 4px 4px 12px 4px;
}
.m-title {
font-size: 18px;
font-weight: 700;
line-height: 24px;
}
.m-subtitle {
margin-top: 6px;
font-size: 12px;
color: var(--el-text-color-secondary);
}
.m-tabs {
margin-bottom: 12px;
}
.m-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.m-video-card {
background: var(--el-bg-color);
border: 1px solid var(--el-border-color-lighter);
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 10px rgba(15, 23, 42, 0.06);
transition: transform 0.08s ease, box-shadow 0.12s ease;
}
.m-video-card:active {
transform: scale(0.99);
box-shadow: 0 1px 6px rgba(15, 23, 42, 0.05);
}
.m-cover {
position: relative;
width: 100%;
height: 180px;
background: #0f172a;
}
.m-cover img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.m-cover-placeholder {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #0f172a, #334155);
}
.m-play {
position: absolute;
inset: 0;
display: grid;
place-items: center;
font-size: 42px;
color: rgba(255, 255, 255, 0.92);
text-shadow: 0 2px 16px rgba(0, 0, 0, 0.35);
}
.m-duration {
position: absolute;
right: 10px;
bottom: 10px;
font-size: 12px;
color: #fff;
padding: 4px 8px;
border-radius: 999px;
background: rgba(0, 0, 0, 0.45);
}
.m-content {
padding: 12px;
}
.m-video-title {
font-weight: 700;
font-size: 15px;
line-height: 20px;
}
.m-video-desc {
margin-top: 6px;
font-size: 12px;
color: var(--el-text-color-secondary);
line-height: 18px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.m-meta {
margin-top: 8px;
font-size: 12px;
color: var(--el-text-color-secondary);
display: inline-flex;
gap: 8px;
align-items: center;
}
.m-hint {
padding: 12px;
text-align: center;
color: var(--el-text-color-secondary);
font-size: 13px;
}
.m-load-more {
padding: 12px;
text-align: center;
color: var(--el-text-color-secondary);
font-size: 12px;
}
.m-player {
border-radius: 12px;
overflow: hidden;
border: 1px solid var(--el-border-color-lighter);
background: #000;
}
.m-empty {
padding: 24px;
text-align: center;
color: #fff;
}
.m-player-title {
margin-top: 10px;
font-weight: 700;
}
.m-player-desc {
margin-top: 6px;
font-size: 12px;
color: var(--el-text-color-secondary);
}
</style>