持续优化
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 获取今日新增数据(型号 + OTA)
|
||||
*/
|
||||
export function getTodayStats() {
|
||||
return request({
|
||||
url: '/dashboard/today',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
@@ -12,6 +12,7 @@
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 快捷入口 -->
|
||||
<div class="home-links">
|
||||
<el-card
|
||||
v-for="item in quickLinks"
|
||||
@@ -29,20 +30,86 @@
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 今日新增数据 -->
|
||||
<el-card class="home-today-card-wrapper">
|
||||
<template #header>
|
||||
<h2 class="home-today-title">今日新增</h2>
|
||||
</template>
|
||||
|
||||
<div class="home-today-grid">
|
||||
<!-- 今日新增型号 -->
|
||||
<el-card class="home-today-card">
|
||||
<template #header>
|
||||
<div class="home-card-header">
|
||||
<span class="home-card-title">
|
||||
<el-icon><List /></el-icon>
|
||||
新增型号
|
||||
</span>
|
||||
<el-tag size="small" type="primary">{{ todayModels.length }}</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="todayLoading" class="home-today-loading">
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
</div>
|
||||
<div v-else-if="todayModels.length === 0" class="home-today-empty">
|
||||
暂无新增
|
||||
</div>
|
||||
<ul v-else class="home-today-list">
|
||||
<li v-for="item in todayModels" :key="item.id" class="home-today-item">
|
||||
<span class="home-today-item-text">{{ item.brand_name }} {{ item.name }}</span>
|
||||
<span class="home-today-item-time">{{ formatTime(item.create_at) }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</el-card>
|
||||
|
||||
<!-- 今日新增 OTA -->
|
||||
<el-card class="home-today-card">
|
||||
<template #header>
|
||||
<div class="home-card-header">
|
||||
<span class="home-card-title">
|
||||
<el-icon><Upload /></el-icon>
|
||||
新增 OTA
|
||||
</span>
|
||||
<el-tag size="small" type="success">{{ todayOtas.length }}</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="todayLoading" class="home-today-loading">
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
</div>
|
||||
<div v-else-if="todayOtas.length === 0" class="home-today-empty">
|
||||
暂无新增
|
||||
</div>
|
||||
<ul v-else class="home-today-list">
|
||||
<li v-for="item in todayOtas" :key="item.id" class="home-today-item">
|
||||
<span class="home-today-item-text">{{ item.verName }} · {{ item.model }}</span>
|
||||
<span class="home-today-item-time">{{ formatTime(item.create_at) }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</el-card>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { House, Collection, List, Document, Share } from '@element-plus/icons-vue'
|
||||
import { House, Collection, List, Document, Share, Upload, Loading } from '@element-plus/icons-vue'
|
||||
import { getUser } from '@/utils/auth'
|
||||
import { getTodayStats } from '@/api/dashboard'
|
||||
|
||||
defineOptions({ name: 'Home' })
|
||||
|
||||
const router = useRouter()
|
||||
const username = computed(() => getUser()?.username || '')
|
||||
|
||||
const todayLoading = ref(false)
|
||||
const todayModels = ref([])
|
||||
const todayOtas = ref([])
|
||||
|
||||
const quickLinks = [
|
||||
{ path: '/brand', title: '耳机品牌管理', desc: '维护耳机品牌信息', icon: Collection },
|
||||
{ path: '/model', title: '耳机型号管理', desc: '维护耳机型号与配置', icon: List },
|
||||
@@ -50,9 +117,35 @@ const quickLinks = [
|
||||
{ path: '/share-code/log', title: '分享日志', desc: '查看分享码使用记录', icon: Share }
|
||||
]
|
||||
|
||||
function formatTime(isoStr) {
|
||||
if (!isoStr) return ''
|
||||
const d = new Date(isoStr)
|
||||
const pad = (n) => String(n).padStart(2, '0')
|
||||
return `${pad(d.getHours())}:${pad(d.getMinutes())}`
|
||||
}
|
||||
|
||||
async function loadTodayStats() {
|
||||
todayLoading.value = true
|
||||
try {
|
||||
const res = await getTodayStats()
|
||||
if (res.code === 1 && res.data) {
|
||||
todayModels.value = res.data.models || []
|
||||
todayOtas.value = res.data.otas || []
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取今日数据失败:', e)
|
||||
} finally {
|
||||
todayLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function go(path) {
|
||||
router.push(path)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadTodayStats()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -98,6 +191,99 @@ function go(path) {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
/* 今日新增 */
|
||||
.home-today-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.home-today-card-wrapper {
|
||||
max-width: 760px;
|
||||
}
|
||||
|
||||
.home-today-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.home-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.home-card-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.home-card-title .el-icon {
|
||||
color: #7dd3fc;
|
||||
}
|
||||
|
||||
.home-today-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 32px 0;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.home-today-loading .el-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.home-today-empty {
|
||||
text-align: center;
|
||||
padding: 24px 0;
|
||||
color: #64748b;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.home-today-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
max-height: 280px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.home-today-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
transition: background-color 0.15s;
|
||||
}
|
||||
|
||||
.home-today-item:hover {
|
||||
background: rgba(148, 163, 184, 0.08);
|
||||
}
|
||||
|
||||
.home-today-item-text {
|
||||
font-size: 14px;
|
||||
color: #cbd5e1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.home-today-item-time {
|
||||
flex-shrink: 0;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
/* 快捷入口 */
|
||||
.home-links {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||
|
||||
@@ -49,12 +49,20 @@
|
||||
@click="handlePushToSearch"
|
||||
>
|
||||
<el-icon><Upload /></el-icon>
|
||||
推送到搜索 ({{ selectedIds.length }})
|
||||
推送搜索 「{{ selectedIds.length }}」
|
||||
</el-button>
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>
|
||||
新型号
|
||||
</el-button>
|
||||
<el-button
|
||||
type="success"
|
||||
:disabled="selectedIds.length === 0"
|
||||
@click="handleCopySelectedNames"
|
||||
>
|
||||
<el-icon><CopyDocument /></el-icon>
|
||||
复制 「{{ selectedIds.length }}」
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -71,8 +79,34 @@
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column prop="id" label="ID" width="80" sortable="custom" />
|
||||
<el-table-column prop="brand_name" label="品牌名称" width="150" />
|
||||
<el-table-column prop="name" label="型号名称" />
|
||||
<el-table-column prop="brand_name" label="品牌名称" width="150">
|
||||
<template #default="scope">
|
||||
<div class="brand-name-cell">
|
||||
<span>{{ scope.row.brand_name }}</span>
|
||||
<el-icon
|
||||
class="brand-name-copy-icon"
|
||||
title="复制品牌名称"
|
||||
@click.stop="handleCopyBrand(scope.row)"
|
||||
>
|
||||
<CopyDocument />
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="name" label="型号名称">
|
||||
<template #default="scope">
|
||||
<div class="model-name-cell">
|
||||
<span>{{ scope.row.name }}</span>
|
||||
<el-icon
|
||||
class="model-name-copy-icon"
|
||||
title="复制品牌+型号"
|
||||
@click.stop="handleCopyName(scope.row)"
|
||||
>
|
||||
<CopyDocument />
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="form" label="佩戴方式" width="120">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.form === 'in-ear'" type="success">入耳式</el-tag>
|
||||
@@ -435,7 +469,8 @@ import {
|
||||
Loading,
|
||||
CircleCheck,
|
||||
CircleClose,
|
||||
ArrowDown
|
||||
ArrowDown,
|
||||
CopyDocument
|
||||
} from '@element-plus/icons-vue'
|
||||
import {
|
||||
getModels,
|
||||
@@ -738,6 +773,33 @@ function formatFormLabel(form) {
|
||||
return form || '-'
|
||||
}
|
||||
|
||||
const handleCopyName = (row) => {
|
||||
const text = `${row.brand_name} ${row.name}`
|
||||
navigator.clipboard.writeText(text).then(
|
||||
() => ElMessage.success(`已复制: ${text}`),
|
||||
() => ElMessage.error('复制失败')
|
||||
)
|
||||
}
|
||||
|
||||
const handleCopyBrand = (row) => {
|
||||
const text = row.brand_name
|
||||
navigator.clipboard.writeText(text).then(
|
||||
() => ElMessage.success(`已复制: ${text}`),
|
||||
() => ElMessage.error('复制失败')
|
||||
)
|
||||
}
|
||||
|
||||
const handleCopySelectedNames = () => {
|
||||
if (selectedRows.value.length === 0) return
|
||||
const text = selectedRows.value
|
||||
.map((row) => `${row.brand_name} ${row.name}`)
|
||||
.join('\n')
|
||||
navigator.clipboard.writeText(text).then(
|
||||
() => ElMessage.success(`已复制 ${selectedRows.value.length} 条`),
|
||||
() => ElMessage.error('复制失败')
|
||||
)
|
||||
}
|
||||
|
||||
function resetPushViewDialog() {
|
||||
pushViewLoading.value = false
|
||||
pushViewPushed.value = false
|
||||
@@ -1459,6 +1521,52 @@ onMounted(() => {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.model-name-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.model-name-copy-icon {
|
||||
flex-shrink: 0;
|
||||
font-size: 14px;
|
||||
color: #94a3b8;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s, color 0.2s;
|
||||
}
|
||||
|
||||
.model-name-copy-icon:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.model-name-cell:hover .model-name-copy-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.brand-name-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.brand-name-copy-icon {
|
||||
flex-shrink: 0;
|
||||
font-size: 14px;
|
||||
color: #94a3b8;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s, color 0.2s;
|
||||
}
|
||||
|
||||
.brand-name-copy-icon:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.brand-name-cell:hover .brand-name-copy-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.row-actions-more {
|
||||
flex-shrink: 0;
|
||||
margin-left: 4px;
|
||||
|
||||
@@ -252,8 +252,6 @@ import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search, Refresh, Plus } from '@element-plus/icons-vue'
|
||||
import { getOtaList, createOta, updateOta, deleteOta, uploadOtaPackage } from '@/api/ota'
|
||||
|
||||
const PAW_MD5_PLACEHOLDER = '0'.repeat(32)
|
||||
|
||||
const PACKAGE_UPLOAD_MODELS = ['Luxsin-X8', 'Luxsin-X9']
|
||||
|
||||
const loading = ref(false)
|
||||
@@ -294,10 +292,6 @@ function emptyForm() {
|
||||
hw: 6,
|
||||
target: 0,
|
||||
beta: 0,
|
||||
pawVerCode: 0,
|
||||
pawVerName: '',
|
||||
pawUrl: '',
|
||||
pawMd5: '',
|
||||
status: 1
|
||||
}
|
||||
}
|
||||
@@ -344,10 +338,6 @@ const formRules = {
|
||||
}
|
||||
|
||||
function buildPayload() {
|
||||
let pawMd5 = (formData.pawMd5 || '').trim()
|
||||
if (pawMd5.length !== 32) {
|
||||
pawMd5 = PAW_MD5_PLACEHOLDER
|
||||
}
|
||||
const model = (formData.model == null || formData.model === '') ? '' : String(formData.model).trim()
|
||||
return {
|
||||
verCode: Number(formData.verCode),
|
||||
@@ -360,10 +350,6 @@ function buildPayload() {
|
||||
hw: Number(formData.hw ?? 0),
|
||||
target: Number(formData.target ?? 0),
|
||||
beta: Number(formData.beta ?? 0),
|
||||
pawVerCode: Number(formData.pawVerCode ?? 0),
|
||||
pawVerName: (formData.pawVerName || '').trim(),
|
||||
pawUrl: (formData.pawUrl || '').trim(),
|
||||
pawMd5,
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
status: Number(formData.status ?? 1)
|
||||
@@ -497,10 +483,6 @@ function applyOtaRowToForm(row, isCopy) {
|
||||
hw: row.hw ?? 0,
|
||||
target: row.target ?? 0,
|
||||
beta: row.beta ?? 0,
|
||||
pawVerCode: row.pawVerCode ?? 0,
|
||||
pawVerName: row.pawVerName ?? '',
|
||||
pawUrl: row.pawUrl ?? '',
|
||||
pawMd5: row.pawMd5 || '',
|
||||
status: row.status ?? 1
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user