优化界面

This commit is contained in:
eafonyang
2026-07-16 09:50:32 +08:00
parent f48f1cce70
commit 1a9003261d
22 changed files with 241 additions and 124 deletions
+5 -1
View File
@@ -455,7 +455,11 @@ router.put('/api/models/:model_id', upload.single('measurement_file'), async (re
if (newBrandName !== dbModel.brand_name || newName !== dbModel.name) {
const existing = await Model.findOne({
where: { brand_name: newBrandName, name: newName },
where: {
brand_name: newBrandName,
name: newName,
id: { [Op.ne]: modelId },
},
});
if (existing) {
logger.warn(`Model already exists: brand_name=${newBrandName}, name=${newName}`);
+1 -1
View File
@@ -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",
+12 -3
View File
@@ -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
+8 -4
View File
@@ -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) {
+14 -14
View File
@@ -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() {
+4 -4
View File
@@ -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' }
}
]
}
+44 -8
View File
@@ -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 {
+10 -4
View File
@@ -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'
+10 -3
View File
@@ -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)
+7 -7
View File
@@ -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) {
+2 -2
View File
@@ -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 },
+31 -20
View File
@@ -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"
@keyup.enter="handleSearch"
/>
</el-select>
</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)
+10 -4
View File
@@ -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,
+15 -9
View File
@@ -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'
+10 -3
View File
@@ -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'
+10 -4
View File
@@ -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' })
+1 -1
View File
@@ -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'],
}
}
}