feat: 优化储存卡能量格组件EnergyBar(更紧凑、可调尺寸、充能特效);修复样式命名冲突;微调色带进度条底色与不换行显示;清理调试信息

This commit is contained in:
24kycj
2025-09-15 21:47:42 +08:00
parent 859a8a985e
commit 2e1a1864aa
8 changed files with 2636 additions and 2296 deletions
+148
View File
@@ -0,0 +1,148 @@
<template>
<div class="energy-bar compact energy-dash" :title="tooltip">
<div class="energy-cells" :style="{ gap: gap + 'px' }">
<div
v-for="n in segments"
:key="n"
:class="cellClass(n)"
:style="{ width: cellWidth + 'px', height: cellHeight + 'px', borderRadius: borderRadius + 'px' }" />
</div>
</div>
</template>
<script>
export default {
name: 'EnergyBar',
props: {
// 百分比 0-100
percent: { type: Number, default: 0 },
// 段数 6 或 8
segments: { type: Number, default: 8 },
// 显示文本
showText: { type: Boolean, default: true },
// 自定义文本
text: { type: String, default: '' },
// 尺寸控制(像素)
cellWidth: { type: Number, default: 4 },
cellHeight: { type: Number, default: 10 },
gap: { type: Number, default: 1 },
borderRadius: { type: Number, default: 1 },
},
computed: {
litCells() {
const clamped = Math.max(0, Math.min(100, Math.round(this.percent || 0)))
// 一个能量格代表10(进1制向上取整)
const cells = Math.ceil(clamped / 10)
return Math.max(0, Math.min(this.segments, cells))
},
displayText() {
return this.text || `${Math.max(0, Math.min(100, Math.round(this.percent || 0)))}%`
},
tooltip() {
return this.displayText
},
tone() {
// 低电量红 / 中电量橙 / 高电量绿
const p = Math.max(0, Math.min(100, this.percent || 0))
if (p <= 20) return 'danger'
if (p <= 50) return 'warning'
return 'success'
},
chargingIndex() {
// 若不是整十,最后一格视为“充能中”
const p = Math.max(0, Math.min(100, this.percent || 0))
if (p % 10 === 0 || this.litCells === 0) return -1
return this.litCells
}
},
methods: {
cellClass(n) {
const lit = n <= this.litCells
// 仿数码表样式:前2格为红色,其余为绿色(只在被点亮时着色)
const zone = n <= 2 ? 'low' : 'high'
return [
'energy-cell',
lit ? (zone === 'low' ? 'is-red' : 'is-green') : 'is-off',
(this.chargingIndex === n && lit) ? 'is-charging' : ''
]
}
}
}
</script>
<style lang="less" scoped>
.energy-bar {
display: inline-flex;
}
.energy-dash {
/* 取消黑色底,采用透明背景以突出分隔与未点亮格 */
background: transparent;
padding: 1px 3px;
border-radius: 2px;
}
.compact .energy-cells {
display: flex;
align-items: center;
}
.energy-cell {
background: transparent;
box-sizing: border-box;
display: inline-block;
}
.energy-cell.is-red {
background: linear-gradient(180deg, #ff6b6b, #ff3f3f);
box-shadow: 0 0 2px rgba(255,58,58,.45);
position: relative;
overflow: hidden;
animation: pulse 2.8s ease-in-out infinite;
}
.energy-cell.is-green {
background: linear-gradient(180deg, #6dff55, #30cc42);
box-shadow: 0 0 2px rgba(103,255,77,.35);
position: relative;
overflow: hidden;
animation: pulse 2.8s ease-in-out infinite;
}
.energy-cell.is-off {
/* 提高对比度:更亮的灰底+更明显边框,便于在深色背景下辨识格数 */
background: #2f3643;
border: 1px solid #556179;
box-shadow: inset 0 1px 1px rgba(255,255,255,.06);
}
.energy-cell.is-charging {
position: relative;
overflow: hidden;
}
.energy-cell.is-charging::after {
content: '';
position: absolute;
left: -120%;
top: 0;
width: 120%;
height: 100%;
background: linear-gradient(90deg, rgba(255,255,255,0), rgba(255,255,255,.28), rgba(255,255,255,0));
animation: sweep 2.4s linear infinite;
}
/* 所有点亮的格子也有较弱的扫光效果与更高亮度 */
.energy-cell.is-green::before,
.energy-cell.is-red::before {
content: '';
position: absolute;
left: -120%;
top: 0;
width: 120%;
height: 100%;
background: linear-gradient(90deg, rgba(255,255,255,0), rgba(255,255,255,.18), rgba(255,255,255,0));
animation: sweep 3s linear infinite;
}
@keyframes pulse {
0% { filter: brightness(0.98); }
50% { filter: brightness(1.08); }
100% { filter: brightness(0.98); }
}
@keyframes sweep {
0% { left: -120%; }
100% { left: 120%; }
}
</style>
+1 -1
View File
@@ -1 +1 @@
{"files":["E:\\TortoiseGit License.txt"]}
{"files":["\\\\NAS\\study\\research_reports\\2025\\数据资产价值释放之行业应用场景解析与合规框架——科学研究和技术服务业.pdf"]}
+125 -61
View File
@@ -223,7 +223,8 @@
:class="{ guide_body: beginStep && currentStep == 7 }">
<el-button
type="primary"
@click="newWork">
@click="newWork"
:disabled="infoData.length == 0 && state == false">
{{ $t('index.newWork') }}
</el-button>
</div>
@@ -408,7 +409,7 @@
prop="status"
:label="$t('main.tableState')"
min-width="80">
<template slot-scope="scope">{{ $t('main.tableOnline') }}</template>
<template slot-scope="{}">{{ $t('main.tableOnline') }}</template>
</el-table-column>
<el-table-column
prop="SerialNo"
@@ -540,7 +541,9 @@
style="width: 100%"
size="mini"
:empty-text="$t('index.nodata')"
row-class-name="rowclass">
row-class-name="rowclass"
:row-key="row => row.JobID || row.AssUuid"
:tree-props="{ children: 'children' }">
<el-table-column
label="#"
width="50">
@@ -552,6 +555,10 @@
prop="jId"
:label="$t('index.wordID')"
min-width="85">
<template slot-scope="scope">
<span v-if="!scope.row.AssUuid">{{ scope.row.jId }}</span>
<span v-else>{{ (scope.row.ParentJobID || '') | shortJobId }}</span>
</template>
</el-table-column>
<el-table-column
prop="DataSource"
@@ -562,20 +569,23 @@
:label="$t('index.wordSpace')"
min-width="45">
<template slot-scope="scope">
{{ getPrintName(scope.row.PrinterID, true) }}
<span v-if="!scope.row.AssUuid">{{ getPrintName(scope.row.PrinterID, true) }}</span>
<span v-else>{{ getPrintName(scope.row.ParentPrinterID || scope.row.PrinterID, true) }}</span>
</template>
</el-table-column>
<el-table-column
:label="$t('main.USBType')"
min-width="55">
<template slot-scope="scope">
{{ scope.row.PrinterType }}
{{ !scope.row.AssUuid ? scope.row.PrinterType : (scope.row.ParentPrinterType || scope.row.PrinterType) }}
</template>
</el-table-column>
<el-table-column
min-width="60"
prop="TaskCapacity"
:label="$t('main.TaskCapacity')">
<template slot-scope="scope">
{{ !scope.row.AssUuid ? scope.row.TaskCapacity : (scope.row.ParentTaskCapacity || '') }}
</template>
</el-table-column>
<el-table-column
v-if="false"
@@ -583,9 +593,11 @@
:label="$t('main.taskFile')">
</el-table-column>
<el-table-column
prop="CreateTime"
:label="$t('index.createTime')"
min-width="75">
<template slot-scope="scope">
{{ !scope.row.AssUuid ? scope.row.CreateTime : (scope.row.StartTime || '') }}
</template>
</el-table-column>
<el-table-column
v-if="false"
@@ -598,48 +610,64 @@
:filter-method="filterHandler">
</el-table-column>
<el-table-column
prop="FinishTime"
:label="$t('index.finishedTime')"
min-width="75">
<template slot-scope="scope">
{{ !scope.row.AssUuid ? scope.row.FinishTime : (scope.row.FinishTime || '') }}
</template>
</el-table-column>
<el-table-column
prop="JobCompletion"
min-width="40"
:label="$t('index.finishedNumber')">
<template slot-scope="scope">
<span v-if="!scope.row.AssUuid">{{ scope.row.JobCompletion }}</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column
:label="$t('index.state')"
prop="JobStatus"
width="135">
<template slot-scope="scope">
<el-dropdown
trigger="click"
@command="(e) => handleTaskCommand(e, scope.row.JobID)"
:disabled="scope.row.JobStatus != 'Fail'">
<span
class="el-dropdown-link"
style="color: #000; font-size: 12px">
{{ jobState(scope.row.JobStatus) }}<b v-if="scope.row.JobStatus === 'Copying'" style="margin-left: 3px;">{{ scope.row.CopySpeed ? Math.ceil(parseFloat(scope.row.CopySpeed)*10)/10 : '0.0' }}MB/s</b>
<i
class="el-icon-arrow-down el-icon--right"
v-if="scope.row.JobStatus == 'Fail'"></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="refresh">
<span style="color: #000; font-size: 12px">{{ $t('main.commandRefresh') }}</span>
</el-dropdown-item>
<el-dropdown-item command="delete">
<span style="color: #000; font-size: 12px">{{ $t('main.commandDelete') }}</span>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<el-progress
:color="customColors"
:width="45"
define-back-color="#ebeef5"
:stroke-width="3"
:percentage="parseFloat(scope.row.TaskPercentage.replace(`%`, ``))"
v-if="scope.row.JobStatus == `Copying`"></el-progress>
<template v-if="!scope.row.AssUuid">
<el-dropdown
trigger="click"
@command="(e) => handleTaskCommand(e, scope.row.JobID)"
:disabled="scope.row.JobStatus != 'Fail'">
<span
class="el-dropdown-link"
style="color: #000; font-size: 12px">
{{ jobState(scope.row.JobStatus) }}<b v-if="scope.row.JobStatus === 'Copying'" style="margin-left: 3px;">{{ scope.row.CopySpeed ? Math.ceil(parseFloat(scope.row.CopySpeed)*10)/10 : '0.0' }}MB/s</b>
<i
class="el-icon-arrow-down el-icon--right"
v-if="scope.row.JobStatus == 'Fail'"></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="refresh">
<span style="color: #000; font-size: 12px">{{ $t('main.commandRefresh') }}</span>
</el-dropdown-item>
<el-dropdown-item command="delete">
<span style="color: #000; font-size: 12px">{{ $t('main.commandDelete') }}</span>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<el-progress
:color="customColors"
:width="45"
define-back-color="#ebeef5"
:stroke-width="3"
:percentage="parseFloat((scope.row.TaskPercentage || '0').replace(`%`, ``))"
v-if="scope.row.JobStatus == `Copying`"></el-progress>
</template>
<template v-else>
{{ jobState(scope.row.TaskStatus) }}
<el-progress
:color="customColors"
:width="45"
define-back-color="#ebeef5"
:stroke-width="3"
:percentage="parseFloat((scope.row.TaskPercentage || '0').replace(`%`, ``))"
v-if="scope.row.TaskStatus == `Copying`"></el-progress>
</template>
</template>
</el-table-column>
</el-table>
@@ -823,6 +851,12 @@ export default {
warnCard,
sFooter
},
filters: {
shortJobId(v) {
if (!v) return ''
return String(v).split('-')[0]
}
},
data() {
return {
guideStep: null,
@@ -1085,12 +1119,12 @@ export default {
this.getSystemInfo()
this.selectLan = localStorage.getItem('lang')
this.isAdmin = localStorage.getItem('loginrole') == 'administrator'
// const tempTimer = setInterval(() => {
// if (JSON.stringify(this.ribbonList) != '{}') {
// this.getData(1) //首次触发需要先获取到Ribbon数据
// clearInterval(tempTimer)
// }
// }, 200)
const tempTimer = setInterval(() => {
if (JSON.stringify(this.ribbonList) != '{}') {
this.getData(1) //首次触发需要先获取到Ribbon数据
clearInterval(tempTimer)
}
}, 200)
ipcRenderer.on('open-program-callback', (event, data) => {
console.log(data)
this.openFile(data)
@@ -1105,13 +1139,12 @@ export default {
ipcRenderer.send('get-root')
ipcRenderer.send('open-program')
this.guideStep = JSON.parse(localStorage.getItem('guideStep'))
console.log(this.guideStep)
this.currentStep = localStorage.getItem('currentStep')
if ((!localStorage.getItem('hideStep') || localStorage.getItem('hideStep') == 0)&&this.currentStep>-1) this.showStep = true
setTimeout(() => {
this.doingWorkStatus = false
}, 10000)
// this.timer2 = setInterval(this.getPrintData, this.refreshTime2)
this.timer2 = setInterval(this.getPrintData, this.refreshTime2)
},
beforeDestroy() {
this.stopTimer()
@@ -1265,13 +1298,31 @@ export default {
this.state = true
let tasks = [...this.tasks]
this.tasks = []
for (let item of res.data.unfinishedTasks) {
item.jId = item.JobID.split('-')[0] //+ "...";
this.tasks.push(item)
const normalize = (task) => {
const jId = (task.JobID || '').split('-')[0]
const parentCommon = {
jId,
ParentJobID: task.JobID,
ParentPrinterID: task.PrinterID,
ParentPrinterType: task.PrinterType,
ParentTaskCapacity: task.TaskCapacity
}
const children = Array.isArray(task.AssTask) ? task.AssTask.map((child) => ({
...child,
...parentCommon
})) : []
return {
...task,
jId,
children
}
}
for (let item of res.data.finishedTasks) {
item.jId = item.JobID.split('-')[0] //+ "...";
this.tasks.push(item)
for (let item of (res.data.unfinishedTasks || [])) {
this.tasks.push(normalize(item))
}
for (let item of (res.data.finishedTasks || [])) {
this.tasks.push(normalize(item))
}
let total1 = 0
let total2 = 0
@@ -1681,6 +1732,19 @@ export default {
}
this.isNew = true
this.dialogVisible = true
// 新建任务时重置文件列表与网络认证状态,避免上一次残留导致卡顿
this.$nextTick(() => {
const workRef = this.$refs.work
if (workRef && workRef.$refs && workRef.$refs.files && workRef.$refs.files.reset) {
workRef.$refs.files.reset()
}
if (workRef) {
workRef.networkAuthVisible = false
workRef.networkAuthPaths = []
workRef.networkCredentials = {}
workRef.submitLoading = false
}
})
},
newNetWork() {
//this.isNew = true;
@@ -1869,15 +1933,15 @@ export default {
watch: {
hasDoingWork: {
handler() {
// if (this.hasDoingWork) {
// this.intervalTime = parseInt(this.refreshTime1)
// clearInterval(this.timer1)
// this.timer1 = setInterval(this.initData, this.intervalTime)
// } else {
// this.intervalTime = parseInt(this.refreshTime2)
// clearInterval(this.timer1)
// this.timer1 = setInterval(this.initData, this.intervalTime)
// }
if (this.hasDoingWork) {
this.intervalTime = parseInt(this.refreshTime1)
clearInterval(this.timer1)
this.timer1 = setInterval(this.initData, this.intervalTime)
} else {
this.intervalTime = parseInt(this.refreshTime2)
clearInterval(this.timer1)
this.timer1 = setInterval(this.initData, this.intervalTime)
}
},
immediate: true
},
File diff suppressed because it is too large Load Diff
+60 -6
View File
@@ -1209,6 +1209,10 @@ export default {
this.loadNetworkCredentials()
this.getTemplates()
// 加载已保存的网络认证,支持二次新建直接使用
if (this.loadNetworkCredentials) {
this.loadNetworkCredentials()
}
},
updated() {},
methods: {
@@ -1244,14 +1248,50 @@ export default {
// 检查是否有网络路径需要认证
checkNetworkPaths() {
if (this.$refs.files && this.$refs.files.hasNetworkPaths()) {
const networkPaths = this.$refs.files.getNetworkPaths()
if (networkPaths.length > 0) {
this.networkAuthPaths = networkPaths
this.networkAuthVisible = true
return true
if (!(this.$refs.files && this.$refs.files.hasNetworkPaths())) {
return false
}
const networkPaths = this.$refs.files.getNetworkPaths() || []
if (networkPaths.length === 0) return false
// 从本地存储的 credentials 中匹配已有主机
let stored = {}
try {
stored = JSON.parse(localStorage.getItem('networkCredentials') || '{}')
} catch (e) {
stored = {}
}
const missing = []
const prepared = []
for (const item of networkPaths) {
const host = item.hostName || this.getHostFromPath(item.path) || ''
if (host && stored[host] && stored[host].userName && stored[host].password) {
prepared.push({
host_name: host,
user_name: stored[host].userName,
password: stored[host].password,
})
} else {
missing.push({ ...item, hostName: host })
}
}
if (missing.length > 0) {
// 仅对缺失凭据的主机弹窗
this.networkAuthPaths = missing.map(m => ({
path: m.path,
hostName: m.hostName,
userName: '',
password: ''
}))
this.networkAuthVisible = true
// 先保存已准备好的,等用户补齐再一起提交
this.networkCredentials = prepared
return true
}
// 全部已有凭据,直接使用
this.networkCredentials = prepared
return false
},
@@ -2426,6 +2466,20 @@ export default {
},
help() {
ipcRenderer.send('open-help-file')
},
// 从路径提取主机名(兼容 \\host\path 与 //host/path
getHostFromPath(p) {
if (!p || typeof p !== 'string') return ''
if (p.indexOf('\\\\') === 0) {
const parts = p.slice(2).split('\\')
return parts[0] || ''
}
if (p.indexOf('//') === 0) {
const parts = p.slice(2).split('/')
return parts[0] || ''
}
const m = p.match(/^([^\\\/:]+)/)
return (m && m[1]) ? m[1] : ''
}
},
computed: {