优化界面
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.1.0",
|
||||
"@lucide/vue": "^1.24.0",
|
||||
"axios": "^1.5.0",
|
||||
"element-plus": "^2.3.14",
|
||||
"vue": "^3.3.4",
|
||||
|
||||
Generated
+12
-3
@@ -8,9 +8,9 @@ importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@element-plus/icons-vue':
|
||||
specifier: ^2.1.0
|
||||
version: 2.3.2(vue@3.5.35)
|
||||
'@lucide/vue':
|
||||
specifier: ^1.24.0
|
||||
version: 1.24.0(vue@3.5.35)
|
||||
axios:
|
||||
specifier: ^1.5.0
|
||||
version: 1.17.0
|
||||
@@ -224,6 +224,11 @@ packages:
|
||||
'@jridgewell/sourcemap-codec@1.5.5':
|
||||
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
|
||||
|
||||
'@lucide/vue@1.24.0':
|
||||
resolution: {integrity: sha512-5bNPX0G2YEWdUlBYk7pE8SgDg/f1mkIFpJ9vtE44pW/cwRz7Ioc0tOTESoVJAPvxIELSmYekX+XXIJMjsswNIg==}
|
||||
peerDependencies:
|
||||
vue: '>=3.0.1'
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.62.2':
|
||||
resolution: {integrity: sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==}
|
||||
cpu: [arm]
|
||||
@@ -805,6 +810,10 @@ snapshots:
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.5': {}
|
||||
|
||||
'@lucide/vue@1.24.0(vue@3.5.35)':
|
||||
dependencies:
|
||||
vue: 3.5.35
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.62.2':
|
||||
optional: true
|
||||
|
||||
|
||||
@@ -55,20 +55,24 @@
|
||||
<script setup>
|
||||
import { computed, markRaw, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { Document, List, Upload, Collection, House, User, Tools } from '@element-plus/icons-vue'
|
||||
import { FileText, List, Upload, Layers, House, User, ToolCase, UserX, Crosshair, SlidersHorizontal } from '@lucide/vue'
|
||||
import { TABS_STORAGE_KEY, HOME_PATH } from '@/utils/tabs'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const iconMap = markRaw({
|
||||
document: Document,
|
||||
document: FileText,
|
||||
'file-text': FileText,
|
||||
list: List,
|
||||
upload: Upload,
|
||||
collection: Collection,
|
||||
collection: Layers,
|
||||
home: House,
|
||||
user: User,
|
||||
tools: Tools
|
||||
'user-x': UserX,
|
||||
crosshair: Crosshair,
|
||||
tools: ToolCase,
|
||||
'sliders-horizontal': SlidersHorizontal
|
||||
})
|
||||
|
||||
function resolveTabIcon(iconKey) {
|
||||
|
||||
@@ -49,11 +49,11 @@
|
||||
>
|
||||
<el-sub-menu index="1">
|
||||
<template #title>
|
||||
<el-icon><Headset /></el-icon>
|
||||
<el-icon><Headphones /></el-icon>
|
||||
<span>耳机管理</span>
|
||||
</template>
|
||||
<el-menu-item index="/brand">
|
||||
<el-icon><Collection /></el-icon>
|
||||
<el-icon><Layers /></el-icon>
|
||||
<span>品牌管理</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/model">
|
||||
@@ -67,35 +67,35 @@
|
||||
<span>升级管理</span>
|
||||
</template>
|
||||
<el-menu-item index="/ota">
|
||||
<el-icon><Document /></el-icon>
|
||||
<el-icon><FileText /></el-icon>
|
||||
<span>OTA 管理</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/blacklist">
|
||||
<el-icon><Warning /></el-icon>
|
||||
<span>黑名单管理</span>
|
||||
<el-icon><UserX /></el-icon>
|
||||
<span>黑名单</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/ota-target-device">
|
||||
<el-icon><Aim /></el-icon>
|
||||
<el-icon><Crosshair /></el-icon>
|
||||
<span>定向升级</span>
|
||||
</el-menu-item>
|
||||
</el-sub-menu>
|
||||
<el-sub-menu index="share-code">
|
||||
<template #title>
|
||||
<el-icon><Share /></el-icon>
|
||||
<el-icon><Share2 /></el-icon>
|
||||
<span>分享码</span>
|
||||
</template>
|
||||
<el-menu-item index="/share-code/log">
|
||||
<el-icon><Document /></el-icon>
|
||||
<el-icon><FileText /></el-icon>
|
||||
<span>分享日志</span>
|
||||
</el-menu-item>
|
||||
</el-sub-menu>
|
||||
<el-sub-menu index="toolbox">
|
||||
<template #title>
|
||||
<el-icon><Tools /></el-icon>
|
||||
<el-icon><ToolCase /></el-icon>
|
||||
<span>工具箱</span>
|
||||
</template>
|
||||
<el-menu-item index="/toolbox/luxsin-controller">
|
||||
<el-icon><Operation /></el-icon>
|
||||
<el-icon><SlidersHorizontal /></el-icon>
|
||||
<span>luxsin-controller</span>
|
||||
</el-menu-item>
|
||||
</el-sub-menu>
|
||||
@@ -164,7 +164,7 @@
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Headset, Collection, List, Upload, Document, Share, User, Warning, Aim, Tools, Operation } from '@element-plus/icons-vue'
|
||||
import { Headphones, Layers, List, Upload, FileText, Share2, User, UserX, Crosshair, ToolCase, SlidersHorizontal } from '@lucide/vue'
|
||||
import { clearAuth, getUser, isSuperAdmin } from '@/utils/auth'
|
||||
import TabsView from '@/components/TabsView.vue'
|
||||
import SidebarLogo from '@/components/SidebarLogo.vue'
|
||||
@@ -199,10 +199,10 @@ watch(
|
||||
)
|
||||
|
||||
const railNavItems = [
|
||||
{ title: '耳机管理', icon: Headset },
|
||||
{ title: '耳机管理', icon: Headphones },
|
||||
{ title: '升级管理', icon: Upload },
|
||||
{ title: '分享码', icon: Share },
|
||||
{ title: '工具箱', icon: Tools }
|
||||
{ title: '分享码', icon: Share2 },
|
||||
{ title: '工具箱', icon: ToolCase }
|
||||
]
|
||||
|
||||
function handleRailClick() {
|
||||
|
||||
@@ -38,19 +38,19 @@ const routes = [
|
||||
path: '/ota',
|
||||
name: 'Ota',
|
||||
component: () => import('@/views/ota/index.vue'),
|
||||
meta: { title: 'OTA 管理', icon: 'upload' }
|
||||
meta: { title: 'OTA 管理', icon: 'file-text' }
|
||||
},
|
||||
{
|
||||
path: '/blacklist',
|
||||
name: 'BlackList',
|
||||
component: () => import('@/views/blacklist/index.vue'),
|
||||
meta: { title: '黑名单管理', icon: 'document' }
|
||||
meta: { title: '黑名单管理', icon: 'user-x' }
|
||||
},
|
||||
{
|
||||
path: '/ota-target-device',
|
||||
name: 'OtaTargetDevice',
|
||||
component: () => import('@/views/ota-target-device/index.vue'),
|
||||
meta: { title: '定向升级', icon: 'document' }
|
||||
meta: { title: '定向升级', icon: 'crosshair' }
|
||||
},
|
||||
{
|
||||
path: '/share-code/log',
|
||||
@@ -68,7 +68,7 @@ const routes = [
|
||||
path: '/toolbox/luxsin-controller',
|
||||
name: 'LuxsinController',
|
||||
component: () => import('@/views/toolbox/luxsin-controller/index.vue'),
|
||||
meta: { title: 'luxsin-controller', icon: 'tools' }
|
||||
meta: { title: 'luxsin 控制器', icon: 'sliders-horizontal' }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -103,34 +103,34 @@ body {
|
||||
.lux-orb {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(80px);
|
||||
opacity: 0.34;
|
||||
contain: layout style paint;
|
||||
}
|
||||
|
||||
.lux-orb-1 {
|
||||
width: min(52vw, 400px);
|
||||
height: min(52vw, 400px);
|
||||
background: #22d3ee;
|
||||
top: -12%;
|
||||
left: -8%;
|
||||
background: radial-gradient(circle at center, #22d3ee 0%, rgba(34, 211, 238, 0.4) 35%, transparent 70%);
|
||||
}
|
||||
|
||||
.lux-orb-2 {
|
||||
width: min(48vw, 360px);
|
||||
height: min(48vw, 360px);
|
||||
background: #a78bfa;
|
||||
top: 40%;
|
||||
right: -12%;
|
||||
opacity: 0.28;
|
||||
background: radial-gradient(circle at center, #a78bfa 0%, rgba(167, 139, 250, 0.4) 35%, transparent 70%);
|
||||
}
|
||||
|
||||
.lux-orb-3 {
|
||||
width: min(42vw, 320px);
|
||||
height: min(42vw, 320px);
|
||||
background: #fb7185;
|
||||
bottom: -8%;
|
||||
left: 20%;
|
||||
opacity: 0.18;
|
||||
background: radial-gradient(circle at center, #fb7185 0%, rgba(251, 113, 133, 0.4) 35%, transparent 70%);
|
||||
}
|
||||
|
||||
.lux-shell-body {
|
||||
@@ -175,6 +175,8 @@ body {
|
||||
border-right: 1px solid var(--lux-glass-border);
|
||||
box-shadow: inset -1px 0 0 rgba(255, 255, 255, 0.04);
|
||||
transition: width 0.30s ease, box-shadow 0.30s ease;
|
||||
transform: translateZ(0);
|
||||
will-change: width;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
@@ -313,6 +315,32 @@ body {
|
||||
z-index: 8 !important;
|
||||
}
|
||||
|
||||
/* 骨架屏深色主题配色(冷调蓝灰,融入深蓝黑背景) */
|
||||
.lux-shell .el-skeleton {
|
||||
--el-fill-color-lighter: rgba(100, 116, 139, 0.16);
|
||||
--el-skeleton-color: rgba(100, 116, 139, 0.08);
|
||||
}
|
||||
|
||||
.lux-shell .el-skeleton .el-skeleton__item {
|
||||
background: rgba(100, 116, 139, 0.14);
|
||||
}
|
||||
|
||||
.lux-shell .el-skeleton.is-animated .el-skeleton__item {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(100, 116, 139, 0.10) 25%,
|
||||
rgba(120, 140, 160, 0.22) 50%,
|
||||
rgba(100, 116, 139, 0.10) 75%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
animation: lux-skeleton-pulse 1.6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes lux-skeleton-pulse {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
/* ──────────────────────────────────────────
|
||||
* 底栏
|
||||
* ────────────────────────────────────────── */
|
||||
@@ -598,6 +626,8 @@ body {
|
||||
0 0 0 1px rgba(255, 255, 255, 0.06) inset,
|
||||
0 1px 0 rgba(255, 255, 255, 0.1) inset;
|
||||
position: relative;
|
||||
transform: translateZ(0);
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
/* 光照渐变叠层 (登录框 ::before) */
|
||||
@@ -608,6 +638,7 @@ body {
|
||||
background: linear-gradient(145deg, rgba(255, 255, 255, 0.14) 0%, transparent 45%);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
contain: strict;
|
||||
}
|
||||
|
||||
/* 顶部青色辉光 (登录框 ::after) */
|
||||
@@ -621,6 +652,7 @@ body {
|
||||
background: linear-gradient(90deg, transparent, rgba(56, 189, 248, 0.4), transparent);
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
contain: strict;
|
||||
}
|
||||
|
||||
.el-overlay .el-dialog > * {
|
||||
@@ -907,34 +939,34 @@ body {
|
||||
.lux-orb {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(80px);
|
||||
opacity: 0.34;
|
||||
contain: layout style paint;
|
||||
}
|
||||
|
||||
.lux-orb-1 {
|
||||
width: min(52vw, 400px);
|
||||
height: min(52vw, 400px);
|
||||
background: #22d3ee;
|
||||
top: -12%;
|
||||
left: -8%;
|
||||
background: radial-gradient(circle at center, #22d3ee 0%, rgba(34, 211, 238, 0.4) 35%, transparent 70%);
|
||||
}
|
||||
|
||||
.lux-orb-2 {
|
||||
width: min(48vw, 360px);
|
||||
height: min(48vw, 360px);
|
||||
background: #a78bfa;
|
||||
top: 40%;
|
||||
right: -12%;
|
||||
opacity: 0.28;
|
||||
background: radial-gradient(circle at center, #a78bfa 0%, rgba(167, 139, 250, 0.4) 35%, transparent 70%);
|
||||
}
|
||||
|
||||
.lux-orb-3 {
|
||||
width: min(42vw, 320px);
|
||||
height: min(42vw, 320px);
|
||||
background: #fb7185;
|
||||
bottom: -8%;
|
||||
left: 20%;
|
||||
opacity: 0.18;
|
||||
background: radial-gradient(circle at center, #fb7185 0%, rgba(251, 113, 133, 0.4) 35%, transparent 70%);
|
||||
}
|
||||
|
||||
.lux-shell-body {
|
||||
@@ -977,6 +1009,8 @@ body {
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.08);
|
||||
box-shadow: inset -1px 0 0 rgba(255, 255, 255, 0.04);
|
||||
transition: width 0.30s ease, box-shadow 0.30s ease;
|
||||
transform: translateZ(0);
|
||||
will-change: width;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
@@ -1221,6 +1255,8 @@ body {
|
||||
0 25px 50px -12px rgba(0, 0, 0, 0.5),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.06) inset,
|
||||
0 1px 0 rgba(255, 255, 255, 0.1) inset;
|
||||
transform: translateZ(0);
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
.el-overlay .el-dialog__header {
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" circle :icon="Search" title="搜索" @click="handleSearch" />
|
||||
<el-button circle :icon="Refresh" title="重置" @click="handleReset" />
|
||||
<el-button circle :icon="RefreshCw" title="重置" @click="handleReset" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
@@ -50,8 +50,14 @@
|
||||
<el-button type="primary" circle :icon="Plus" title="新增黑名单" @click="handleAdd" />
|
||||
</div>
|
||||
|
||||
<div class="table-loading-host" v-loading="loading">
|
||||
<el-table :data="tableData" border stripe style="width: 100%">
|
||||
<div class="table-loading-host">
|
||||
<el-skeleton v-if="loading" animated>
|
||||
<template #template>
|
||||
<el-skeleton-item variant="rect" style="height: 40px; margin-bottom: 4px; border-radius: 4px" />
|
||||
<el-skeleton-item v-for="i in 6" :key="i" variant="rect" style="height: 48px; margin-bottom: 4px; border-radius: 4px" />
|
||||
</template>
|
||||
</el-skeleton>
|
||||
<el-table v-else :data="tableData" border stripe style="width: 100%">
|
||||
<el-table-column prop="id" label="ID" width="70" />
|
||||
<el-table-column label="OTA 版本" min-width="200" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
@@ -135,7 +141,7 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search, Refresh, Plus } from '@element-plus/icons-vue'
|
||||
import { Search, RefreshCw, Plus } from '@lucide/vue'
|
||||
import { getBlackList, createBlackList, updateBlackList, deleteBlackList } from '@/api/blacklist'
|
||||
import { getOtaList } from '@/api/ota'
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" circle :icon="Search" title="搜索" @click="handleSearch" />
|
||||
<el-button circle :icon="Refresh" title="重置" @click="handleReset" />
|
||||
<el-button circle :icon="RefreshCw" title="重置" @click="handleReset" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
@@ -23,8 +23,15 @@
|
||||
<el-button type="primary" circle :icon="Plus" title="新增品牌" @click="handleAdd" />
|
||||
</div>
|
||||
|
||||
<div class="table-loading-host" v-loading="loading">
|
||||
<div class="table-loading-host">
|
||||
<el-skeleton v-if="loading" animated>
|
||||
<template #template>
|
||||
<el-skeleton-item variant="rect" style="height: 40px; margin-bottom: 4px; border-radius: 4px" />
|
||||
<el-skeleton-item v-for="i in 6" :key="i" variant="rect" style="height: 48px; margin-bottom: 4px; border-radius: 4px" />
|
||||
</template>
|
||||
</el-skeleton>
|
||||
<el-table
|
||||
v-else
|
||||
:data="tableData"
|
||||
border
|
||||
stripe
|
||||
@@ -100,7 +107,7 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search, Refresh, Plus } from '@element-plus/icons-vue'
|
||||
import { Search, RefreshCw, Plus } from '@lucide/vue'
|
||||
import { getBrands, createBrand, updateBrand, deleteBrand } from '@/api/brand'
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="todayLoading" class="home-today-loading">
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
<el-icon class="is-loading"><LoaderCircle /></el-icon>
|
||||
</div>
|
||||
<div v-else-if="todayModels.length === 0" class="home-today-empty">
|
||||
暂无新增
|
||||
@@ -75,7 +75,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="todayLoading" class="home-today-loading">
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
<el-icon class="is-loading"><LoaderCircle /></el-icon>
|
||||
</div>
|
||||
<div v-else-if="todayOtas.length === 0" class="home-today-empty">
|
||||
暂无新增
|
||||
@@ -97,7 +97,7 @@
|
||||
<script setup>
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { House, Collection, List, Document, Share, Upload, Loading, Tools } from '@element-plus/icons-vue'
|
||||
import { House, Layers, List, FileText, Share2, Upload, LoaderCircle, ToolCase } from '@lucide/vue'
|
||||
import { getUser } from '@/utils/auth'
|
||||
import { getTodayStats } from '@/api/dashboard'
|
||||
|
||||
@@ -111,11 +111,11 @@ const todayModels = ref([])
|
||||
const todayOtas = ref([])
|
||||
|
||||
const quickLinks = [
|
||||
{ path: '/brand', title: '耳机品牌管理', desc: '维护耳机品牌信息', icon: Collection, colorClass: 'icon-cyan' },
|
||||
{ path: '/brand', title: '耳机品牌管理', desc: '维护耳机品牌信息', icon: Layers, colorClass: 'icon-cyan' },
|
||||
{ path: '/model', title: '耳机型号管理', desc: '维护耳机型号与配置', icon: List, colorClass: 'icon-coral' },
|
||||
{ path: '/ota', title: 'OTA 管理', desc: '固件升级包管理', icon: Document, colorClass: 'icon-cyan' },
|
||||
{ path: '/share-code/log', title: '分享日志', desc: '查看分享码使用记录', icon: Share, colorClass: 'icon-coral' },
|
||||
{ path: '/toolbox/luxsin-controller', title: 'Luxsin 控制器', desc: '局域网设备数据同步', icon: Tools, colorClass: 'icon-cyan' }
|
||||
{ path: '/ota', title: 'OTA 管理', desc: '固件升级包管理', icon: FileText, colorClass: 'icon-cyan' },
|
||||
{ path: '/share-code/log', title: '分享日志', desc: '查看分享码使用记录', icon: Share2, colorClass: 'icon-coral' },
|
||||
{ path: '/toolbox/luxsin-controller', title: 'Luxsin 控制器', desc: '局域网设备数据同步', icon: ToolCase, colorClass: 'icon-cyan' }
|
||||
]
|
||||
|
||||
function formatTime(isoStr) {
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
show-password
|
||||
autocomplete="current-password"
|
||||
placeholder="请输入密码"
|
||||
:prefix-icon="Key"
|
||||
:prefix-icon="KeyRound"
|
||||
@keyup.enter="handleSubmit"
|
||||
/>
|
||||
</el-form-item>
|
||||
@@ -101,7 +101,7 @@
|
||||
import { reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { User, Lock, Headset, Key } from '@element-plus/icons-vue'
|
||||
import { User, Lock, KeyRound } from '@lucide/vue'
|
||||
import { login } from '@/api/auth'
|
||||
import { setToken, setUser } from '@/utils/auth'
|
||||
import { clearTabsStorage, HOME_PATH } from '@/utils/tabs'
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
@closed="resetEqCacheDialog"
|
||||
>
|
||||
<div v-if="eqCacheView === 'loading'" class="push-view-loading">
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
<el-icon class="is-loading"><LoaderCircle /></el-icon>
|
||||
加载中…
|
||||
</div>
|
||||
<div v-else-if="eqCacheView === 'empty'">
|
||||
@@ -34,10 +34,10 @@
|
||||
v-if="eqCacheFieldLoadingKey === key"
|
||||
class="is-loading eq-cache-key-icon"
|
||||
>
|
||||
<Loading />
|
||||
<LoaderCircle />
|
||||
</el-icon>
|
||||
<el-icon v-else class="eq-cache-key-icon">
|
||||
<ArrowDown />
|
||||
<ChevronDown />
|
||||
</el-icon>
|
||||
</button>
|
||||
<div
|
||||
@@ -66,7 +66,7 @@
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Loading, ArrowDown } from '@element-plus/icons-vue'
|
||||
import { LoaderCircle, ChevronDown } from '@lucide/vue'
|
||||
import { getModelEqCache, getModelEqCacheField } from '@/api/model'
|
||||
import VueJsonPretty from 'vue-json-pretty'
|
||||
import 'vue-json-pretty/lib/styles.css'
|
||||
|
||||
@@ -46,21 +46,13 @@
|
||||
</div>
|
||||
|
||||
<el-form-item label="品牌名称" prop="brand_name">
|
||||
<el-select
|
||||
<el-autocomplete
|
||||
v-model="formData.brand_name"
|
||||
placeholder="请选择品牌"
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
placeholder="请输入或选择品牌"
|
||||
clearable
|
||||
:fetch-suggestions="queryBrandSuggestions"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="brand in brandOptions"
|
||||
:key="brand.id"
|
||||
:label="brand.name"
|
||||
:value="brand.name"
|
||||
/>
|
||||
</el-select>
|
||||
/>
|
||||
<div v-if="showBrandNotInDbTip" class="brand-not-in-db-tip">
|
||||
品牌「{{ (formData.brand_name || '').trim() }}」尚未在品牌库中,可继续填写并保存;保存新型号时将自动创建该品牌。
|
||||
</div>
|
||||
@@ -108,7 +100,7 @@
|
||||
accept=".csv,.txt,.json"
|
||||
drag
|
||||
>
|
||||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||
<el-icon class="el-icon--upload"><FileUp /></el-icon>
|
||||
<div class="el-upload__text">
|
||||
拖拽文件到此处或<em>点击上传</em>
|
||||
</div>
|
||||
@@ -161,8 +153,8 @@ import { ElMessage } from 'element-plus'
|
||||
import {
|
||||
Link,
|
||||
CircleCheck,
|
||||
UploadFilled
|
||||
} from '@element-plus/icons-vue'
|
||||
FileUp
|
||||
} from '@lucide/vue'
|
||||
import {
|
||||
createModel,
|
||||
updateModel,
|
||||
@@ -205,7 +197,7 @@ const formData = reactive({
|
||||
|
||||
const formRules = {
|
||||
brand_name: [
|
||||
{ required: true, message: '请选择品牌名称', trigger: 'change' }
|
||||
{ required: true, message: '请输入品牌名称', trigger: 'blur' }
|
||||
],
|
||||
name: [
|
||||
{ required: true, message: '请输入型号名称', trigger: 'blur' },
|
||||
@@ -213,6 +205,29 @@ const formRules = {
|
||||
]
|
||||
}
|
||||
|
||||
// ---- 品牌搜索建议 ----
|
||||
|
||||
function queryBrandSuggestions(queryString, cb) {
|
||||
const q = queryString.trim().toLowerCase()
|
||||
const results = q
|
||||
? props.brandOptions.filter(b => b.name.toLowerCase().includes(q)).map(b => ({ value: b.name }))
|
||||
: props.brandOptions.map(b => ({ value: b.name }))
|
||||
cb(results)
|
||||
}
|
||||
|
||||
// ---- 品牌大小写对齐 ----
|
||||
|
||||
/**
|
||||
* 将品牌名对齐到数据库中已有品牌的大小写形式。
|
||||
* 如果找到不区分大小写的匹配,返回 DB 版本;否则返回原始输入。
|
||||
*/
|
||||
function normalizeBrandName(name) {
|
||||
const n = (name || '').trim().toLowerCase()
|
||||
if (!n) return name
|
||||
const match = props.brandOptions.find(b => String(b.name || '').trim().toLowerCase() === n)
|
||||
return match ? match.name : name
|
||||
}
|
||||
|
||||
// ---- 品牌检测 ----
|
||||
|
||||
function brandNameExistsInOptions(name) {
|
||||
@@ -310,7 +325,7 @@ const handleSquigLinkFetch = async (selectedFile) => {
|
||||
squigLinkCsv.value = data.csv_content
|
||||
squigLinkDataUrl.value = data.data_url || ''
|
||||
if (!formData.id) {
|
||||
if (data.brand_name) formData.brand_name = data.brand_name
|
||||
if (data.brand_name) formData.brand_name = normalizeBrandName(data.brand_name)
|
||||
if (data.model_name) formData.name = data.model_name
|
||||
formData.source = 'Eafonyoung'
|
||||
formData.form = data.form || formData.form || 'in-ear'
|
||||
@@ -357,7 +372,7 @@ const handleSubmit = async (continueAfter = false) => {
|
||||
if (valid) {
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const brandTrimmed = formData.brand_name.trim()
|
||||
const brandTrimmed = normalizeBrandName(formData.brand_name.trim())
|
||||
if (!formData.id) {
|
||||
const brandOk = await ensureBrandExistsForNewModel(brandTrimmed)
|
||||
if (!brandOk) return
|
||||
|
||||
@@ -25,13 +25,13 @@
|
||||
</div>
|
||||
<span class="step-status">
|
||||
<el-icon v-if="pushSteps.validate === 'loading'" class="is-loading step-icon-loading">
|
||||
<Loading />
|
||||
<LoaderCircle />
|
||||
</el-icon>
|
||||
<el-icon v-else-if="pushSteps.validate === 'success'" class="step-icon-success">
|
||||
<CircleCheck />
|
||||
</el-icon>
|
||||
<el-icon v-else-if="pushSteps.validate === 'error'" class="step-icon-error">
|
||||
<CircleClose />
|
||||
<CircleX />
|
||||
</el-icon>
|
||||
</span>
|
||||
</li>
|
||||
@@ -47,13 +47,13 @@
|
||||
</div>
|
||||
<span class="step-status">
|
||||
<el-icon v-if="pushSteps.push === 'loading'" class="is-loading step-icon-loading">
|
||||
<Loading />
|
||||
<LoaderCircle />
|
||||
</el-icon>
|
||||
<el-icon v-else-if="pushSteps.push === 'success'" class="step-icon-success">
|
||||
<CircleCheck />
|
||||
</el-icon>
|
||||
<el-icon v-else-if="pushSteps.push === 'error'" class="step-icon-error">
|
||||
<CircleClose />
|
||||
<CircleX />
|
||||
</el-icon>
|
||||
</span>
|
||||
</li>
|
||||
@@ -77,7 +77,7 @@
|
||||
<script setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Loading, CircleCheck, CircleClose } from '@element-plus/icons-vue'
|
||||
import { LoaderCircle, CircleCheck, CircleX } from '@lucide/vue'
|
||||
import { validatePushToMeilisearch, pushToMeilisearch } from '@/api/model'
|
||||
|
||||
const props = defineProps({
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
@update:model-value="$emit('update:modelValue', $event)"
|
||||
>
|
||||
<div v-if="loading" class="push-view-loading">
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
<el-icon class="is-loading"><LoaderCircle /></el-icon>
|
||||
加载中…
|
||||
</div>
|
||||
<template v-else-if="!pushed">
|
||||
@@ -27,7 +27,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Loading } from '@element-plus/icons-vue'
|
||||
import { LoaderCircle } from '@lucide/vue'
|
||||
|
||||
defineProps({
|
||||
modelValue: { type: Boolean, default: false },
|
||||
|
||||
@@ -3,20 +3,14 @@
|
||||
<el-card class="search-card">
|
||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||
<el-form-item label="品牌名称">
|
||||
<el-select
|
||||
<el-autocomplete
|
||||
v-model="searchForm.brand_name"
|
||||
placeholder="请选择品牌"
|
||||
filterable
|
||||
placeholder="请输入或选择品牌"
|
||||
clearable
|
||||
:fetch-suggestions="queryBrandSuggestions"
|
||||
style="width: 200px"
|
||||
>
|
||||
<el-option
|
||||
v-for="brand in brandOptions"
|
||||
:key="brand.id"
|
||||
:label="brand.name"
|
||||
:value="brand.name"
|
||||
/>
|
||||
</el-select>
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="型号名称">
|
||||
<el-input
|
||||
@@ -28,7 +22,7 @@
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" circle :icon="Search" title="搜索" @click="handleSearch" />
|
||||
<el-button circle :icon="Refresh" title="重置" @click="handleReset" />
|
||||
<el-button circle :icon="RefreshCw" title="重置" @click="handleReset" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
@@ -51,14 +45,21 @@
|
||||
:disabled="selectedIds.length === 0"
|
||||
@click="handleCopySelectedNames"
|
||||
>
|
||||
<el-icon><CopyDocument /></el-icon>
|
||||
<el-icon><Copy /></el-icon>
|
||||
复制 「{{ selectedIds.length }}」
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-loading-host" v-loading="loading">
|
||||
<div class="table-loading-host">
|
||||
<el-skeleton v-if="loading" animated>
|
||||
<template #template>
|
||||
<el-skeleton-item variant="rect" style="height: 40px; margin-bottom: 4px; border-radius: 4px" />
|
||||
<el-skeleton-item v-for="i in 6" :key="i" variant="rect" style="height: 48px; margin-bottom: 4px; border-radius: 4px" />
|
||||
</template>
|
||||
</el-skeleton>
|
||||
<el-table
|
||||
v-else
|
||||
ref="tableRef"
|
||||
:data="tableData"
|
||||
border
|
||||
@@ -79,7 +80,7 @@
|
||||
title="复制品牌名称"
|
||||
@click.stop="handleCopyBrand(scope.row)"
|
||||
>
|
||||
<CopyDocument />
|
||||
<Copy />
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
@@ -93,7 +94,7 @@
|
||||
title="复制品牌+型号"
|
||||
@click.stop="handleCopyName(scope.row)"
|
||||
>
|
||||
<CopyDocument />
|
||||
<Copy />
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
@@ -140,7 +141,7 @@
|
||||
:loading="moreActionLoadingId === scope.row.id"
|
||||
>
|
||||
更多操作
|
||||
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
|
||||
<el-icon class="el-icon--right"><ChevronDown /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
@@ -220,12 +221,12 @@ import { ref, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
Search,
|
||||
Refresh,
|
||||
RefreshCw,
|
||||
Plus,
|
||||
Upload,
|
||||
ArrowDown,
|
||||
CopyDocument
|
||||
} from '@element-plus/icons-vue'
|
||||
ChevronDown,
|
||||
Copy
|
||||
} from '@lucide/vue'
|
||||
import {
|
||||
deleteModel,
|
||||
getModelMeasurement,
|
||||
@@ -253,6 +254,16 @@ const {
|
||||
formatDateTime
|
||||
} = useModelList(tableRef)
|
||||
|
||||
// ---- 品牌搜索建议 ----
|
||||
const queryBrandSuggestions = (queryString, cb) => {
|
||||
const list = Array.isArray(brandOptions.value) ? brandOptions.value : []
|
||||
const q = (queryString || '').trim().toLowerCase()
|
||||
const results = q
|
||||
? list.filter(b => b.name.toLowerCase().includes(q)).map(b => ({ value: b.name }))
|
||||
: list.map(b => ({ value: b.name }))
|
||||
cb(results)
|
||||
}
|
||||
|
||||
// ---- 对话框状态 ----
|
||||
const formDialogVisible = ref(false)
|
||||
const moreActionLoadingId = ref(null)
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" circle :icon="Search" title="搜索" @click="handleSearch" />
|
||||
<el-button circle :icon="Refresh" title="重置" @click="handleReset" />
|
||||
<el-button circle :icon="RefreshCw" title="重置" @click="handleReset" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
@@ -50,8 +50,14 @@
|
||||
<el-button type="primary" circle :icon="Plus" title="新增定向设备" @click="handleAdd" />
|
||||
</div>
|
||||
|
||||
<div class="table-loading-host" v-loading="loading">
|
||||
<el-table :data="tableData" border stripe style="width: 100%">
|
||||
<div class="table-loading-host">
|
||||
<el-skeleton v-if="loading" animated>
|
||||
<template #template>
|
||||
<el-skeleton-item variant="rect" style="height: 40px; margin-bottom: 4px; border-radius: 4px" />
|
||||
<el-skeleton-item v-for="i in 6" :key="i" variant="rect" style="height: 48px; margin-bottom: 4px; border-radius: 4px" />
|
||||
</template>
|
||||
</el-skeleton>
|
||||
<el-table v-else :data="tableData" border stripe style="width: 100%">
|
||||
<el-table-column prop="id" label="ID" width="70" />
|
||||
<el-table-column label="OTA 版本" min-width="220" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
@@ -135,7 +141,7 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search, Refresh, Plus } from '@element-plus/icons-vue'
|
||||
import { Search, RefreshCw, Plus } from '@lucide/vue'
|
||||
import {
|
||||
getOtaTargetDeviceList,
|
||||
createOtaTargetDevice,
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" circle :icon="Search" title="搜索" @click="handleSearch" />
|
||||
<el-button circle :icon="Refresh" title="重置" @click="handleReset" />
|
||||
<el-button circle :icon="RefreshCw" title="重置" @click="handleReset" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
@@ -66,8 +66,14 @@
|
||||
<el-button type="primary" circle :icon="Plus" title="新增 OTA" @click="handleAdd" />
|
||||
</div>
|
||||
|
||||
<div class="table-loading-host" v-loading="loading">
|
||||
<el-table :data="tableData" border stripe style="width: 100%">
|
||||
<div class="table-loading-host">
|
||||
<el-skeleton v-if="loading" animated>
|
||||
<template #template>
|
||||
<el-skeleton-item variant="rect" style="height: 40px; margin-bottom: 4px; border-radius: 4px" />
|
||||
<el-skeleton-item v-for="i in 6" :key="i" variant="rect" style="height: 48px; margin-bottom: 4px; border-radius: 4px" />
|
||||
</template>
|
||||
</el-skeleton>
|
||||
<el-table v-else :data="tableData" border stripe style="width: 100%">
|
||||
<el-table-column prop="id" label="ID" width="70" />
|
||||
<el-table-column prop="verCode" label="版本号" width="90" />
|
||||
<el-table-column prop="verName" label="版本名" width="200">
|
||||
@@ -91,7 +97,7 @@
|
||||
title="复制包地址"
|
||||
@click.stop="handleCopyUrl(row)"
|
||||
>
|
||||
<CopyDocument />
|
||||
<Copy />
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
@@ -129,18 +135,18 @@
|
||||
>
|
||||
<el-button type="info" size="small">
|
||||
更多操作
|
||||
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
|
||||
<el-icon class="el-icon--right"><ChevronDown /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="blacklist">
|
||||
<el-icon style="margin-right:4px"><Warning /></el-icon>黑名单
|
||||
<el-icon style="margin-right:4px"><UserX /></el-icon>黑名单
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="target">
|
||||
<el-icon style="margin-right:4px"><Aim /></el-icon>定向升级
|
||||
<el-icon style="margin-right:4px"><Crosshair /></el-icon>定向升级
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="delete" divided>
|
||||
<el-icon style="margin-right:4px;color:#f56c6c"><Delete /></el-icon>删除
|
||||
<el-icon style="margin-right:4px;color:#f56c6c"><Trash2 /></el-icon>删除
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
@@ -382,7 +388,7 @@
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted, watch, nextTick } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search, Refresh, Plus, CopyDocument, Warning, Aim, ArrowDown, Delete } from '@element-plus/icons-vue'
|
||||
import { Search, RefreshCw, Plus, Copy, UserX, Crosshair, ChevronDown, Trash2 } from '@lucide/vue'
|
||||
import { getOtaList, createOta, updateOta, deleteOta, uploadOtaPackage } from '@/api/ota'
|
||||
import { getBlackList, createBlackList, deleteBlackList } from '@/api/blacklist'
|
||||
import { getOtaTargetDeviceList, createOtaTargetDevice, deleteOtaTargetDevice } from '@/api/otaTargetDevice'
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" circle :icon="Search" title="搜索" @click="handleSearch" />
|
||||
<el-button circle :icon="Refresh" title="重置" @click="handleReset" />
|
||||
<el-button circle :icon="RefreshCw" title="重置" @click="handleReset" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
@@ -62,8 +62,15 @@
|
||||
<span class="title">分享日志</span>
|
||||
</div>
|
||||
|
||||
<div class="table-loading-host" v-loading="loading">
|
||||
<div class="table-loading-host">
|
||||
<el-skeleton v-if="loading" animated>
|
||||
<template #template>
|
||||
<el-skeleton-item variant="rect" style="height: 40px; margin-bottom: 4px; border-radius: 4px" />
|
||||
<el-skeleton-item v-for="i in 6" :key="i" variant="rect" style="height: 48px; margin-bottom: 4px; border-radius: 4px" />
|
||||
</template>
|
||||
</el-skeleton>
|
||||
<el-table
|
||||
v-else
|
||||
:data="tableData"
|
||||
border
|
||||
stripe
|
||||
@@ -142,7 +149,7 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Search, Refresh } from '@element-plus/icons-vue'
|
||||
import { Search, RefreshCw } from '@lucide/vue'
|
||||
import VueJsonPretty from 'vue-json-pretty'
|
||||
import 'vue-json-pretty/lib/styles.css'
|
||||
import { getShareCodeLogs } from '@/api/shareCodeLog'
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" circle :icon="Search" title="搜索" @click="handleSearch" />
|
||||
<el-button circle :icon="Refresh" title="重置" @click="handleReset" />
|
||||
<el-button circle :icon="RefreshCw" title="重置" @click="handleReset" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
@@ -23,8 +23,14 @@
|
||||
<el-button type="primary" circle :icon="Plus" title="新建账号" @click="handleAdd" />
|
||||
</div>
|
||||
|
||||
<div class="table-loading-host" v-loading="loading">
|
||||
<el-table :data="tableData" border stripe style="width: 100%">
|
||||
<div class="table-loading-host">
|
||||
<el-skeleton v-if="loading" animated>
|
||||
<template #template>
|
||||
<el-skeleton-item variant="rect" style="height: 40px; margin-bottom: 4px; border-radius: 4px" />
|
||||
<el-skeleton-item v-for="i in 6" :key="i" variant="rect" style="height: 48px; margin-bottom: 4px; border-radius: 4px" />
|
||||
</template>
|
||||
</el-skeleton>
|
||||
<el-table v-else :data="tableData" border stripe style="width: 100%">
|
||||
<el-table-column prop="id" label="ID" width="70" />
|
||||
<el-table-column prop="username" label="用户名" min-width="140" />
|
||||
<el-table-column prop="is_super_admin" label="超级管理员" width="120">
|
||||
@@ -160,7 +166,7 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search, Refresh, Plus } from '@element-plus/icons-vue'
|
||||
import { Search, RefreshCw, Plus } from '@lucide/vue'
|
||||
import { getUsers, createUser, updateUser, deleteUser } from '@/api/user'
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { ArrowRight } from '@element-plus/icons-vue'
|
||||
import { ArrowRight } from '@lucide/vue'
|
||||
|
||||
defineOptions({ name: 'LuxsinController' })
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ export default defineConfig({
|
||||
manualChunks: {
|
||||
'vendor-vue': ['vue', 'vue-router'],
|
||||
'vendor-element-plus': ['element-plus'],
|
||||
'vendor-element-icons': ['@element-plus/icons-vue'],
|
||||
'vendor-lucide': ['@lucide/vue'],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user