功能完善

UI改进
This commit is contained in:
sansen
2026-07-30 14:30:21 +08:00
parent 7e391d9381
commit f5dce6efe5
18 changed files with 533 additions and 230 deletions
+16 -2
View File
@@ -3,8 +3,11 @@
<Header
:currentView="currentView"
:theme="theme"
:activeTaskCount="activeTaskCount"
:showTaskDrawer="showTaskDrawer"
@navigate="navigate"
@toggleTheme="toggleTheme"
@toggleTaskDrawer="showTaskDrawer = !showTaskDrawer"
/>
<div class="main-area">
@@ -24,15 +27,15 @@
</KeepAlive>
<LogPage v-show="currentView === 'logs'" ref="logPageRef" />
</main>
<TaskDrawer :tasks="tasks" @cancel="handleCancel" @remove="handleRemove" />
</div>
<TaskDrawer :tasks="tasks" :show="showTaskDrawer" @cancel="handleCancel" @remove="handleRemove" @close="showTaskDrawer = false" />
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ref, computed, onMounted } from 'vue'
import Header from './components/Header.vue'
import Sidebar from './components/Sidebar.vue'
import TaskDrawer from './components/TaskDrawer.vue'
@@ -58,12 +61,23 @@ async function restoreTheme() {
document.documentElement.setAttribute('data-theme', theme.value)
}
const tasks = ref<Task[]>([])
const showTaskDrawer = ref(false)
const activeTaskCount = computed(() => tasks.value.filter(t => t.status === 'running' || t.status === 'pending').length)
const gpuInfo = ref<any[]>([])
const logPageRef = ref<InstanceType<typeof LogPage> | null>(null)
onMounted(() => {
// Suppress browser-native behaviours in the desktop webview
document.addEventListener('contextmenu', e => e.preventDefault())
document.addEventListener('wheel', e => { if (e.ctrlKey) e.preventDefault() }, { passive: false })
document.addEventListener('dragover', e => e.preventDefault())
document.addEventListener('drop', e => e.preventDefault())
document.addEventListener('dragstart', e => e.preventDefault())
document.addEventListener('keydown', (e: KeyboardEvent) => {
// Block refresh: F5 / Ctrl+R / Ctrl+Shift+R
if (e.key === 'F5' || ((e.ctrlKey || e.metaKey) && e.key === 'r')) e.preventDefault()
})
restoreTheme()
loadTasks()
loadGPUInfo()
+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1785380202265" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="14723" xmlns:xlink="http://www.w3.org/1999/xlink"><path fill="currentColor" d="M509.184 84.992a427.008 427.008 0 0 1 85.888 8.704 38.4 38.4 0 0 1 1.92 74.496 207.104 207.104 0 1 0 252.864 252.864 38.4 38.4 0 0 1 74.752 1.92 424.256 424.256 0 1 1-415.424-337.984z m140.288 567.808a283.968 283.968 0 0 1-199.68-485.952 347.392 347.392 0 1 0 401.6 401.6 283.904 283.904 0 0 1-201.92 84.352z" p-id="14724"></path></svg>

After

Width:  |  Height:  |  Size: 665 B

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1785380229847" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="16726" xmlns:xlink="http://www.w3.org/1999/xlink"><path fill="currentColor" d="M533.333333 853.333333a21.333333 21.333333 0 0 1 21.333334 21.333334v106.666666a21.333333 21.333333 0 0 1-21.333334 21.333334h-42.666666a21.333333 21.333333 0 0 1-21.333334-21.333334v-106.666666a21.333333 21.333333 0 0 1 21.333334-21.333334h42.666666z m-282.282666-100.949333l30.272 30.08a21.333333 21.333333 0 0 1 0.085333 30.165333L207.637333 886.826667a21.333333 21.333333 0 0 1-30.165333 0.085333l-30.250667-30.08a21.333333 21.333333 0 0 1-0.085333-30.165333l73.749333-74.176a21.333333 21.333333 0 0 1 30.165334-0.085334z m564.714666-8.682667l82.816 82.816a21.333333 21.333333 0 0 1 0 30.165334l-30.165333 30.165333a21.333333 21.333333 0 0 1-30.165333 0L755.413333 804.053333a21.333333 21.333333 0 0 1 0-30.165333l30.165334-30.165333a21.333333 21.333333 0 0 1 30.165333 0zM512.426667 256c153.173333 0 277.333333 124.16 277.333333 277.333333s-124.16 277.333333-277.333333 277.333334-277.333333-124.16-277.333334-277.333334 124.16-277.333333 277.333334-277.333333z m0 85.333333a192 192 0 1 0 0 384 192 192 0 0 0 0-384zM192 490.666667a21.333333 21.333333 0 0 1 21.333333 21.333333v42.666667a21.333333 21.333333 0 0 1-21.333333 21.333333H64a21.333333 21.333333 0 0 1-21.333333-21.333333v-42.666667a21.333333 21.333333 0 0 1 21.333333-21.333333h128z m768-21.333334a21.333333 21.333333 0 0 1 21.333333 21.333334v42.666666a21.333333 21.333333 0 0 1-21.333333 21.333334h-128a21.333333 21.333333 0 0 1-21.333333-21.333334v-42.666666a21.333333 21.333333 0 0 1 21.333333-21.333334h128zM846.08 160.704l30.250667 30.08a21.333333 21.333333 0 0 1 0.085333 30.165333l-82.56 83.050667a21.333333 21.333333 0 0 1-30.165333 0.085333l-30.250667-30.08a21.333333 21.333333 0 0 1-0.106667-30.186666l82.56-83.029334a21.333333 21.333333 0 0 1 30.186667-0.085333z m-616.746667 0.042667l73.962667 73.962666a21.333333 21.333333 0 0 1 0 30.165334l-30.165333 30.165333a21.333333 21.333333 0 0 1-30.165334 0l-73.962666-73.962667a21.333333 21.333333 0 0 1 0-30.165333l30.165333-30.165333a21.333333 21.333333 0 0 1 30.165333 0zM533.333333 42.666667a21.333333 21.333333 0 0 1 21.333334 21.333333v128a21.333333 21.333333 0 0 1-21.333334 21.333333h-42.666666a21.333333 21.333333 0 0 1-21.333334-21.333333V64a21.333333 21.333333 0 0 1 21.333334-21.333333h42.666666z" p-id="16727"></path></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1785378916571" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2496" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M812.3 959.4H213.7c-81.6 0-148-66.4-148-148V212.9c0-81.6 66.4-148 148-148h598.5c81.6 0 148 66.4 148 148v598.5C960.3 893 893.9 959.4 812.3 959.4zM213.7 120.9c-50.7 0-92 41.3-92 92v598.5c0 50.7 41.3 92 92 92h598.5c50.7 0 92-41.3 92-92V212.9c0-50.7-41.3-92-92-92H213.7z" fill="currentColor" p-id="2497"></path></svg>

After

Width:  |  Height:  |  Size: 621 B

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1785378927584" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2651" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M929.8 528.1H93.5c-15.5 0-28-12.5-28-28s12.5-28 28-28h836.3c15.5 0 28 12.5 28 28s-12.5 28-28 28z" fill="currentColor" p-id="2652"></path></svg>

After

Width:  |  Height:  |  Size: 451 B

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1785379007596" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3117" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M812.2 65H351.6c-78.3 0-142.5 61.1-147.7 138.1-77 5.1-138.1 69.4-138.1 147.7v460.6c0 81.6 66.4 148 148 148h460.6c78.3 0 142.5-61.1 147.7-138.1 77-5.1 138.1-69.4 138.1-147.7V213c0-81.6-66.4-148-148-148z m-45.8 746.3c0 50.7-41.3 92-92 92H213.8c-50.7 0-92-41.3-92-92V350.7c0-50.7 41.3-92 92-92h460.6c50.7 0 92 41.3 92 92v460.6z m137.8-137.7c0 47.3-35.8 86.3-81.8 91.4V350.7c0-81.6-66.4-148-148-148H260.2c5.1-45.9 44.2-81.8 91.4-81.8h460.6c50.7 0 92 41.3 92 92v460.7z" fill="currentColor" p-id="3118"></path></svg>

After

Width:  |  Height:  |  Size: 818 B

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1785378895955" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2340" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M512.7 664.3c-82.9 0-150.4-67.4-150.4-150.4 0-82.9 67.4-150.4 150.4-150.4 82.9 0 150.4 67.4 150.4 150.4-0.1 83-67.5 150.4-150.4 150.4z m0-244.7c-52 0-94.4 42.3-94.4 94.4 0 52 42.3 94.4 94.4 94.4S607 566 607 514c0-52-42.3-94.4-94.3-94.4z" fill="currentColor" p-id="2341"></path><path d="M631.2 940.5c-15.2 0-30.1-6-41.2-17.3l-63.5-64.8c-4.1-4.2-9.5-6.5-15.4-6.5-5.8 0-11.3 2.3-15.3 6.4l-63.5 64.4c-17.4 17.6-44 22.2-66.2 11.4l-94.5-45.7c-22.2-10.8-35.2-34.5-32.2-59l11-90.1c0.7-5.8-0.9-11.5-4.5-16-3.6-4.6-8.8-7.4-14.6-8l-89.9-9.5c-24.6-2.6-44.8-20.5-50.2-44.6L67.7 558.8c-5.5-24.1 5-49 26-62l77.3-47.6c5-3.1 8.4-7.9 9.7-13.5 1.3-5.7 0.3-11.5-2.8-16.4L129.2 343c-13.3-20.8-11.9-47.8 3.5-67.1l65.5-82c15.4-19.3 41.4-26.7 64.7-18.3l85.4 30.7c5.5 2 11.4 1.7 16.6-0.9 5.2-2.5 9.2-7 11.1-12.5l29.2-85.6c8-23.4 29.9-39.1 54.6-39.1h105c24.7 0 46.7 15.7 54.6 39.1l29.6 86.8c1.9 5.5 5.8 9.9 11 12.5s11.1 2.8 16.6 0.9l86.1-30.6c23.3-8.3 49.2-0.8 64.6 18.5l65.2 82.3c15.3 19.4 16.7 46.3 3.3 67.1l-49.1 76.3c-3.2 4.9-4.2 10.7-2.9 16.4 1.3 5.7 4.7 10.5 9.7 13.6l76.8 47.7c21 13 31.4 38 25.8 62l-23.6 102.3a57.67 57.67 0 0 1-50.4 44.4l-90.3 9.2c-5.8 0.6-11 3.4-14.6 8-3.6 4.5-5.3 10.2-4.6 16l10.7 89.8c2.9 24.5-10.1 48.2-32.4 58.9l-94.7 45.4c-8.1 3.9-16.6 5.7-25 5.7zM511 795.9h0.1c21 0 40.6 8.3 55.3 23.3l63.5 64.8c0.5 0.5 1.3 0.7 2 0.4l94.7-45.4c0.7-0.3 1.1-1 1-1.8l-10.7-89.8c-2.5-20.8 3.4-41.3 16.5-57.6s31.8-26.5 52.7-28.7l90.3-9.2c0.7-0.1 1.3-0.6 1.5-1.3l23.6-102.3c0.2-0.7-0.1-1.5-0.8-1.9l-76.8-47.7c-17.8-11.1-30.2-28.4-34.8-48.8-4.6-20.4-0.9-41.4 10.5-59l49.1-76.3c0.4-0.6 0.4-1.4-0.1-2l-65.2-82.3c-0.5-0.6-1.2-0.8-1.9-0.6l-86.1 30.6c-19.7 7-40.9 5.9-59.7-3.2-18.8-9.1-32.9-25-39.7-44.8l-29.6-86.8c-0.2-0.7-0.9-1.2-1.6-1.2h-105c-0.7 0-1.4 0.5-1.6 1.2L429 211c-6.8 19.8-20.9 35.8-39.8 44.9-18.9 9.1-40.1 10.2-59.9 3.1l-85.4-30.7c-0.7-0.2-1.5 0-1.9 0.5l-65.5 82c-0.5 0.6-0.5 1.4-0.1 2l48.7 76.2c11.3 17.7 14.9 38.6 10.2 59.1-4.7 20.4-17.1 37.7-34.9 48.7l-77.3 47.6c-0.6 0.4-0.9 1.1-0.8 1.9l23.3 102.4c0.2 0.7 0.8 1.3 1.5 1.3l89.9 9.5c20.8 2.2 39.5 12.4 52.6 28.8 13 16.4 18.8 36.9 16.3 57.7l-11 90.1c-0.1 0.7 0.3 1.4 1 1.8l94.5 45.7c0.7 0.3 1.5 0.2 2-0.3l63.5-64.4c14.6-14.8 34.2-23 55.1-23z" fill="currentColor" p-id="2342"></path></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1785378934771" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2806" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M96.6 915c-10.9-10.9-10.9-28.7 0-39.6L887 85c10.9-10.9 28.7-10.9 39.6 0 10.9 10.9 10.9 28.7 0 39.6L136.2 915c-10.9 10.9-28.6 10.9-39.6 0z" fill="currentColor" p-id="2807"></path><path d="M887 915L96.6 124.6c-10.9-10.9-10.9-28.7 0-39.6 10.9-10.9 28.7-10.9 39.6 0l790.4 790.4c10.9 10.9 10.9 28.7 0 39.6-10.9 10.9-28.6 10.9-39.6 0z" fill="currentColor" p-id="2808"></path></svg>

After

Width:  |  Height:  |  Size: 683 B

+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1785380061683" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9491" xmlns:xlink="http://www.w3.org/1999/xlink"><path fill="currentColor" d="M832 64H192c-70.4 0-128 57.6-128 128v640c0 70.4 57.6 128 128 128h640c70.4 0 128-57.6 128-128V192c0-70.4-57.6-128-128-128z m64 768c0 38.4-25.6 64-64 64H192c-38.4 0-64-25.6-64-64V192c0-38.4 25.6-64 64-64h640c38.4 0 64 25.6 64 64v640z" p-id="9492"></path><path fill="currentColor" d="M332.8 537.6c-51.2 0-96 44.8-96 96s44.8 96 96 96 96-44.8 96-96S384 537.6 332.8 537.6z m0 128c-19.2 0-32-12.8-32-32s12.8-32 32-32 32 12.8 32 32-12.8 32-32 32zM403.2 281.6L307.2 377.6l-44.8-38.4c-12.8-12.8-32-12.8-44.8 0s-12.8 32 0 44.8l64 64c6.4 6.4 12.8 6.4 25.6 6.4s12.8 0 19.2-6.4L448 326.4c12.8-12.8 12.8-32 0-44.8s-32-12.8-44.8 0zM787.2 332.8h-256c-19.2 0-32 12.8-32 32s12.8 32 32 32h256c19.2 0 32-12.8 32-32s-12.8-32-32-32zM787.2 601.6h-256c-19.2 0-32 12.8-32 32s12.8 32 32 32h256c19.2 0 32-12.8 32-32s-12.8-32-32-32z" p-id="9493"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+1 -1
View File
@@ -1 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1785291728333" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1480" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M861.257143 54.857143c27.428571 0 51.2 21.942857 53.028571 49.371428v446.171429h-69.485714V124.342857H142.628571v791.771429h316.342858V987.428571H126.171429c-27.428571 0-51.2-21.942857-53.028572-49.371428V107.885714C73.142857 78.628571 95.085714 56.685714 122.514286 54.857143H861.257143zM592.457143 499.2c3.657143 0 7.314286 1.828571 9.142857 3.657143L950.857143 725.942857c9.142857 5.485714 10.971429 16.457143 5.485714 25.6l-5.485714 5.485714-347.428572 221.257143c-9.142857 5.485714-20.114286 3.657143-25.6-5.485714-1.828571-3.657143-3.657143-5.485714-3.657142-9.142857V517.485714c0-10.971429 9.142857-18.285714 18.285714-18.285714z m54.857143 117.028571v245.028572L841.142857 738.742857l-193.828571-122.514286zM457.142857 548.571429c10.971429 0 18.285714 7.314286 18.285714 18.285714v36.571428c0 10.971429-7.314286 18.285714-18.285714 18.285715H274.285714c-10.971429 0-18.285714-7.314286-18.285714-18.285715v-36.571428c0-10.971429 7.314286-18.285714 18.285714-18.285714h182.857143z m292.571429-256c10.971429 0 18.285714 7.314286 18.285714 18.285714v36.571428c0 10.971429-7.314286 18.285714-18.285714 18.285715H274.285714c-10.971429 0-18.285714-7.314286-18.285714-18.285715v-36.571428c0-10.971429 7.314286-18.285714 18.285714-18.285714h475.428572z" fill="#1296db" p-id="1481"></path></svg>
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1785291728333" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1480" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M861.257143 54.857143c27.428571 0 51.2 21.942857 53.028571 49.371428v446.171429h-69.485714V124.342857H142.628571v791.771429h316.342858V987.428571H126.171429c-27.428571 0-51.2-21.942857-53.028572-49.371428V107.885714C73.142857 78.628571 95.085714 56.685714 122.514286 54.857143H861.257143zM592.457143 499.2c3.657143 0 7.314286 1.828571 9.142857 3.657143L950.857143 725.942857c9.142857 5.485714 10.971429 16.457143 5.485714 25.6l-5.485714 5.485714-347.428572 221.257143c-9.142857 5.485714-20.114286 3.657143-25.6-5.485714-1.828571-3.657143-3.657143-5.485714-3.657142-9.142857V517.485714c0-10.971429 9.142857-18.285714 18.285714-18.285714z m54.857143 117.028571v245.028572L841.142857 738.742857l-193.828571-122.514286zM457.142857 548.571429c10.971429 0 18.285714 7.314286 18.285714 18.285714v36.571428c0 10.971429-7.314286 18.285714-18.285714 18.285715H274.285714c-10.971429 0-18.285714-7.314286-18.285714-18.285715v-36.571428c0-10.971429 7.314286-18.285714 18.285714-18.285714h182.857143z m292.571429-256c10.971429 0 18.285714 7.314286 18.285714 18.285714v36.571428c0 10.971429-7.314286 18.285714-18.285714 18.285715H274.285714c-10.971429 0-18.285714-7.314286-18.285714-18.285715v-36.571428c0-10.971429 7.314286-18.285714 18.285714-18.285714h475.428572z" fill="#1296db" p-id="1481"></path></svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

+72 -21
View File
@@ -1,48 +1,49 @@
<template>
<header class="app-header" @dblclick="toggleMaximize">
<div class="header-left" style="--wails-draggable: drag">
<img :src="logoSrc" class="app-logo" width="22" height="22" alt="" />
<span class="hdr-icon app-logo" v-html="logoSrc"></span>
<span class="app-name">FFmpeg GUI</span>
</div>
<div class="header-center" style="--wails-draggable: drag"></div>
<div class="header-right" style="--wails-draggable: no-drag">
<button
class="btn-ghost header-btn task-btn"
:class="{ active: showTaskDrawer }"
title="任务列表"
@click="$emit('toggleTaskDrawer')"
>
<span class="hdr-icon" v-html="taskListIcon"></span>
<span v-if="activeTaskCount" class="task-badge">{{ activeTaskCount }}</span>
</button>
<button
class="btn-ghost header-btn"
:class="{ active: currentView === 'settings' }"
title="设置"
@click="$emit('navigate', 'settings')"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
</svg>
<span class="hdr-icon" v-html="settingsIcon"></span>
</button>
<button
class="btn-ghost header-btn"
:title="theme === 'dark' ? '亮色模式' : '暗色模式'"
@click="$emit('toggleTheme')"
>
<svg v-if="theme === 'dark'" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<circle cx="12" cy="12" r="5"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>
</svg>
<svg v-else width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
</svg>
<span v-if="theme === 'dark'" class="hdr-icon" v-html="lightModeIcon"></span>
<span v-else class="hdr-icon" v-html="darkModeIcon"></span>
</button>
<div class="win-controls">
<button class="win-btn" title="最小化" @click="minimize">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="5" y1="12" x2="19" y2="12"/></svg>
<span class="hdr-icon" v-html="minimizeIcon"></span>
</button>
<button class="win-btn" :title="maximised ? '还原' : '最大化'" @click="toggleMaximize">
<svg v-if="maximised" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="8" y="4" width="12" height="12" rx="1"/><path d="M5 16V7a1 1 0 0 1 1-1h8"/>
</svg>
<svg v-else width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="4" width="16" height="16" rx="2"/></svg>
<span v-if="maximised" class="hdr-icon" v-html="restoreIcon"></span>
<span v-else class="hdr-icon" v-html="maximizeIcon"></span>
</button>
<button class="win-btn win-btn-close" title="关闭" @click="closeWindow">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
<span class="hdr-icon" v-html="closeIcon"></span>
</button>
</div>
</div>
@@ -50,22 +51,40 @@
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import logoSrc from '../assets/icon/app-logo.svg'
import { ref, onMounted, onUnmounted } from 'vue'
import logoSrc from '../assets/icon/app-logo.svg?raw'
import settingsIcon from '../assets/icon/Settings.svg?raw'
import taskListIcon from '../assets/icon/TaskList.svg?raw'
import minimizeIcon from '../assets/icon/Minimize.svg?raw'
import maximizeIcon from '../assets/icon/Maximize.svg?raw'
import restoreIcon from '../assets/icon/Normalmize.svg?raw'
import closeIcon from '../assets/icon/Shut down.svg?raw'
import darkModeIcon from '../assets/icon/DarkMode.svg?raw'
import lightModeIcon from '../assets/icon/LightMode.svg?raw'
defineProps<{ currentView: string; theme: string }>()
defineProps<{ currentView: string; theme: string; activeTaskCount: number; showTaskDrawer: boolean }>()
defineEmits<{
navigate: [view: string]
toggleTheme: []
toggleTaskDrawer: []
}>()
const maximised = ref(false)
onMounted(async () => {
async function syncMaximisedState() {
try {
const app = (window as any).go?.main?.App
if (app?.IsMaximised) maximised.value = await app.IsMaximised()
} catch {}
}
onMounted(() => {
syncMaximisedState()
window.addEventListener('resize', syncMaximisedState)
})
onUnmounted(() => {
window.removeEventListener('resize', syncMaximisedState)
})
function minimize() {
@@ -108,7 +127,7 @@ function closeWindow() {
height: 100%;
}
.app-logo { flex-shrink: 0; }
.app-logo { width: 22px; height: 22px; flex-shrink: 0; }
.app-name {
font-size: 15px;
font-weight: 600;
@@ -123,6 +142,20 @@ function closeWindow() {
padding-right: 2px;
}
/* Icon — inline SVG injected via v-html inherits button text color */
.hdr-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
}
.hdr-icon :deep(svg) {
width: 100%;
height: 100%;
display: block;
}
.header-btn {
color: var(--header-text-dim) !important;
width: 32px;
@@ -136,6 +169,24 @@ function closeWindow() {
.header-btn:hover { color: var(--header-text) !important; background: var(--bg-hover) !important; }
.header-btn.active { color: var(--accent) !important; background: var(--accent-light) !important; }
.task-btn { position: relative; }
.task-badge {
position: absolute;
top: -2px;
right: -2px;
min-width: 16px;
height: 16px;
padding: 0 4px;
font-size: 10px;
font-weight: 700;
line-height: 16px;
text-align: center;
color: #fff;
background: var(--danger, #e81123);
border-radius: 8px;
pointer-events: none;
}
/* Window controls */
.win-controls {
display: flex;
+188 -162
View File
@@ -1,145 +1,149 @@
<template>
<div class="task-drawer" :class="{ expanded }">
<!-- Collapsed bar -->
<div class="drawer-bar" @click="expanded = !expanded">
<div class="bar-left">
<span class="bar-label">任务进度</span>
<span v-if="runningTasks.length" class="bar-status running">
{{ runningTasks.length }} 个运行中 速度{{ fmtFps(runningTasks[0].progress.fps) }} FPS
</span>
<span v-else class="bar-status idle">空闲</span>
</div>
<div class="bar-right">
<span class="bar-count" v-if="taskCount">{{ taskCount }}</span>
<svg
class="bar-arrow"
:class="{ flipped: expanded }"
width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
>
<polyline points="18 15 12 9 6 15"/>
</svg>
</div>
</div>
<!-- Expanded panel -->
<div class="drawer-body" v-show="expanded">
<!-- Running tasks -->
<div v-for="t in runningTasks" :key="t.id" class="current-task">
<div class="current-task-header">
<span class="task-filename">{{ basename(t.inputFile) }}</span>
<div class="header-badges">
<span class="task-type-tag">{{ typeLabel(t) }}</span>
<span class="task-codec">{{ codecLabel(t) }}</span>
</div>
</div>
<div class="progress-section">
<div class="progress-bar">
<div class="progress-fill" :style="{ width: t.progress.percent + '%' }"></div>
</div>
<div class="progress-pct">{{ fmtPct(t.progress.percent) }}%</div>
</div>
<div class="task-stats">
<div class="stat">
<span class="stat-label">速度</span>
<span class="stat-value">{{ fmtFps(t.progress.fps) }} FPS</span>
</div>
<div class="stat">
<span class="stat-label">剩余时间</span>
<span class="stat-value">{{ t.progress.eta || speedLabel(t.progress.speed) || '—' }}</span>
</div>
<div class="stat">
<span class="stat-label">码率</span>
<span class="stat-value">{{ t.progress.bitrate || '—' }}</span>
</div>
</div>
<div class="current-task-actions">
<button class="btn-danger btn-sm" @click="$emit('cancel', t.id)">停止</button>
</div>
<Transition name="slide-right">
<div v-if="show" class="task-overlay" @click.self="$emit('close')">
<div class="task-panel">
<!-- Title bar -->
<div class="panel-header">
<span class="panel-title">任务列表</span>
<button class="btn-ghost panel-close" title="关闭" @click="$emit('close')">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<!-- Queued tasks -->
<div v-if="queuedTasks.length" class="queued-section">
<div class="queued-header">排队中 ({{ queuedTasks.length }})</div>
<div v-for="t in queuedTasks" :key="t.id" class="queued-item">
<span class="queued-name">{{ basename(t.inputFile) }}</span>
<span class="queued-type">{{ typeLabel(t) }}</span>
<button class="btn-ghost btn-sm" @click="$emit('cancel', t.id)" title="移除">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
</div>
<!-- Done tasks -->
<div v-if="doneTasks.length" class="done-section">
<div class="queued-header">已完成 ({{ doneTasks.length }})</div>
<template v-for="t in doneTasks.slice(-10)" :key="t.id">
<div class="queued-item done" @click="toggleDoneDetail(t.id)">
<span class="queued-name">{{ basename(t.inputFile) }}</span>
<span class="queued-type">{{ typeLabel(t) }}</span>
<span :class="['done-badge', t.status]">{{ statusLabel(t) }}</span>
<svg
class="expand-arrow"
:class="{ rotated: expandedDone === t.id }"
width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
>
<polyline points="6 9 12 15 18 9"/>
</svg>
</div>
<div v-if="expandedDone === t.id" class="current-task done-detail">
<div class="current-task-header">
<span class="task-filename">{{ basename(t.inputFile) }}</span>
<!-- Body -->
<div class="panel-body">
<!-- Running tasks -->
<div v-for="t in runningTasks" :key="t.id" class="current-task">
<div class="current-task-header">
<span class="task-filename">{{ basename(t.inputFile) }}</span>
<div class="header-badges">
<span class="task-type-tag">{{ typeLabel(t) }}</span>
<span class="task-codec">{{ codecLabel(t) }}</span>
</div>
<div class="task-stats">
<div class="stat">
<span class="stat-label">输出</span>
<span class="stat-value output-name">{{ basename(t.outputFile) }}</span>
</div>
<div class="stat">
<span class="stat-label">类型</span>
<span class="stat-value">{{ typeLabel(t) }}</span>
</div>
<div class="stat">
<span class="stat-label">状态</span>
<span :class="['stat-value', 'status-' + t.status]">{{ statusLabel(t) }}</span>
</div>
</div>
<div class="progress-section">
<div class="progress-bar">
<div class="progress-fill" :style="{ width: t.progress.percent + '%' }"></div>
</div>
<div v-if="t.error" class="task-error">
<span class="error-label">错误信息</span>
<pre class="error-msg">{{ t.error }}</pre>
<div class="progress-pct">{{ fmtPct(t.progress.percent) }}%</div>
</div>
<div class="task-stats">
<div class="stat">
<span class="stat-label">速度</span>
<span class="stat-value">{{ fmtFps(t.progress.fps) }} FPS</span>
</div>
<div v-if="t.args?.length" class="task-args">
<span class="error-label">命令行</span>
<pre class="error-msg">ffmpeg {{ t.args.join(' ') }}</pre>
<div class="stat">
<span class="stat-label">剩余时间</span>
<span class="stat-value">{{ t.progress.eta || speedLabel(t.progress.speed) || '' }}</span>
</div>
<div class="current-task-actions">
<button class="btn-ghost btn-sm" style="color: var(--text-dim)" @click.stop="$emit('remove', t.id)">移除记录</button>
</div>
<div class="task-time">
<span>{{ fmtTime(t.createdAt) }}</span>
<span v-if="t.completedAt"> {{ fmtTime(t.completedAt) }}</span>
<div class="stat">
<span class="stat-label">码率</span>
<span class="stat-value">{{ t.progress.bitrate || '—' }}</span>
</div>
</div>
</template>
<div class="current-task-actions">
<button class="btn-danger btn-sm" @click="$emit('cancel', t.id)">停止</button>
</div>
</div>
<!-- Queued tasks -->
<div v-if="queuedTasks.length" class="queued-section">
<div class="queued-header">排队中 ({{ queuedTasks.length }})</div>
<div v-for="t in queuedTasks" :key="t.id" class="queued-item">
<span class="queued-name">{{ basename(t.inputFile) }}</span>
<span class="queued-type">{{ typeLabel(t) }}</span>
<button class="btn-ghost btn-sm" @click="$emit('cancel', t.id)" title="移除">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
</div>
<!-- Done tasks -->
<div v-if="doneTasks.length" class="done-section">
<div class="queued-header">已完成 ({{ doneTasks.length }})</div>
<template v-for="t in doneTasks.slice(-20)" :key="t.id">
<div class="queued-item done" @click="toggleDoneDetail(t.id)">
<span class="queued-name">{{ basename(t.inputFile) }}</span>
<span class="queued-type">{{ typeLabel(t) }}</span>
<span :class="['done-badge', t.status]">{{ statusLabel(t) }}</span>
<svg
class="expand-arrow"
:class="{ rotated: expandedDone === t.id }"
width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
>
<polyline points="6 9 12 15 18 9"/>
</svg>
</div>
<div v-if="expandedDone === t.id" class="current-task done-detail">
<div class="current-task-header">
<span class="task-filename">{{ basename(t.inputFile) }}</span>
<span class="task-codec">{{ codecLabel(t) }}</span>
</div>
<div class="task-stats">
<div class="stat">
<span class="stat-label">输出</span>
<span class="stat-value output-name">{{ basename(t.outputFile) }}</span>
</div>
<div class="stat">
<span class="stat-label">类型</span>
<span class="stat-value">{{ typeLabel(t) }}</span>
</div>
<div class="stat">
<span class="stat-label">状态</span>
<span :class="['stat-value', 'status-' + t.status]">{{ statusLabel(t) }}</span>
</div>
</div>
<div v-if="t.error" class="task-error">
<span class="error-label">错误信息</span>
<pre class="error-msg">{{ t.error }}</pre>
</div>
<div v-if="t.args?.length" class="task-args">
<span class="error-label">命令行</span>
<pre class="error-msg">ffmpeg {{ t.args.join(' ') }}</pre>
</div>
<div class="current-task-actions">
<button class="btn-ghost btn-sm" style="color: var(--text-dim)" @click.stop="$emit('remove', t.id)">移除记录</button>
</div>
<div class="task-time">
<span>{{ fmtTime(t.createdAt) }}</span>
<span v-if="t.completedAt"> {{ fmtTime(t.completedAt) }}</span>
</div>
</div>
</template>
</div>
<div v-if="!taskCount" class="empty-hint">
暂无任务
</div>
</div>
</div>
</div>
</Transition>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ref, computed, watch } from 'vue'
import type { Task } from '../types'
const props = defineProps<{ tasks: Task[] }>()
defineEmits<{
const props = defineProps<{ tasks: Task[]; show: boolean }>()
const emit = defineEmits<{
cancel: [id: string]
remove: [id: string]
close: []
}>()
const expanded = ref(false)
watch(() => props.show, (val) => {
if (val) {
const onEsc = (e: KeyboardEvent) => {
if (e.key === 'Escape') { emit('close'); document.removeEventListener('keydown', onEsc) }
}
document.addEventListener('keydown', onEsc)
}
})
const expandedDone = ref<string | null>(null)
function toggleDoneDetail(id: string) {
@@ -204,59 +208,80 @@ function fmtTime(iso?: string): string {
</script>
<style scoped>
.task-drawer {
flex-shrink: 0;
border-top: 1px solid var(--border);
background: var(--bg-card);
transition: height 0.25s ease;
height: var(--drawer-collapsed);
}
.task-drawer.expanded {
height: var(--drawer-expanded);
/* Overlay — catches clicks outside the panel */
.task-overlay {
position: fixed;
inset: 0;
z-index: 20;
}
/* Collapsed bar */
.drawer-bar {
height: var(--drawer-collapsed);
/* Panel */
.task-panel {
position: fixed;
top: var(--header-height);
right: 0;
bottom: 0;
width: 380px;
max-width: calc(100vw - 200px);
background: var(--bg-card);
border-left: 1px solid var(--border);
display: flex;
flex-direction: column;
box-shadow: -4px 0 20px rgba(0, 0, 0, 0.1);
}
/* Title bar */
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
cursor: pointer;
user-select: none;
padding: 0 16px;
height: 44px;
flex-shrink: 0;
border-bottom: 1px solid var(--border);
}
.drawer-bar:hover { background: var(--bg-hover); }
.bar-left { display: flex; align-items: center; gap: 16px; }
.bar-label { font-size: 13px; font-weight: 600; color: var(--text-primary); }
.bar-status { font-size: 12px; color: var(--text-dim); }
.bar-status.running { color: var(--accent); font-weight: 500; }
.bar-right { display: flex; align-items: center; gap: 8px; }
.bar-count {
font-size: 11px;
font-weight: 600;
background: var(--accent);
color: white;
min-width: 20px;
height: 18px;
border-radius: 9px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 6px;
.panel-title { font-size: 14px; font-weight: 600; color: var(--text-primary); }
.panel-close {
width: 28px; height: 28px; padding: 0;
display: flex; align-items: center; justify-content: center;
border-radius: 6px;
color: var(--text-dim);
}
.bar-arrow { color: var(--text-dim); transition: transform 0.2s; }
.bar-arrow.flipped { transform: rotate(180deg); }
.panel-close:hover { background: var(--bg-hover); color: var(--text-primary); }
/* Body */
.drawer-body {
padding: 0 20px 16px;
.panel-body {
flex: 1;
overflow-y: auto;
padding: 16px;
display: flex;
flex-direction: column;
gap: 16px;
max-height: calc(var(--drawer-expanded) - var(--drawer-collapsed));
overflow-y: auto;
gap: 14px;
}
.empty-hint {
font-size: 13px;
color: var(--text-dim);
text-align: center;
padding: 40px 20px;
}
/* Slide transition */
.slide-right-enter-active,
.slide-right-leave-active {
transition: opacity 0.25s ease;
}
.slide-right-enter-active .task-panel,
.slide-right-leave-active .task-panel {
transition: transform 0.25s ease;
}
.slide-right-enter-from,
.slide-right-leave-to {
opacity: 0;
}
.slide-right-enter-from .task-panel,
.slide-right-leave-to .task-panel {
transform: translateX(100%);
}
/* Current task */
@@ -379,6 +404,7 @@ function fmtTime(iso?: string): string {
color: var(--text-dim);
flex-shrink: 0;
transition: transform 0.2s;
cursor: pointer;
}
.expand-arrow.rotated { transform: rotate(180deg); }
+72 -2
View File
@@ -110,7 +110,22 @@
<div class="card-header"><h3>编码参数</h3></div>
<div class="fields-row">
<div class="field">
<label class="field-label">质量 / CRF (越小质量越高)</label>
<label class="field-label">
质量 / CRF (越小质量越高)
<span class="tip-wrap">
<span class="tip-icon">?</span>
<span class="tip-popup">
<em class="tip-title">CRF / CQ 参考</em>
<span class="tip-row"><b>110</b> 视觉无损文件极大</span>
<span class="tip-row"><b>1517</b> 非常清晰</span>
<span class="tip-row"><b>1820</b> 优秀推荐</span>
<span class="tip-row"><b>2123</b> 良好默认 23</span>
<span class="tip-row"><b>2428</b> 可接受体积小</span>
<span class="tip-row"><b>30+</b> 明显损失</span>
<span class="tip-note">H.265/HEVC CQ 约省 3050% 体积</span>
</span>
</span>
</label>
<div class="crf-group">
<input type="range" min="1" max="51" v-model.number="encodeSettings.crf" />
<span class="crf-value">{{ encodeSettings.crf }}</span>
@@ -128,6 +143,22 @@
<option value="p6">P6</option>
<option value="p7">P7 最慢高画质</option>
</template>
<template v-else-if="isSvtAv1">
<option value="13">13 最快低画质</option>
<option value="12">12</option>
<option value="11">11</option>
<option value="10">10</option>
<option value="9">9</option>
<option value="8">8</option>
<option value="7">7 中等</option>
<option value="6">6</option>
<option value="5">5</option>
<option value="4">4</option>
<option value="3">3</option>
<option value="2">2</option>
<option value="1">1</option>
<option value="0">0 最慢高画质</option>
</template>
<template v-else>
<option value="ultrafast">ultrafast</option>
<option value="superfast">superfast</option>
@@ -262,6 +293,10 @@ const isHardwareCodec = computed(() => {
const c = encodeSettings.value.videoCodec
return c.includes('nvenc') || c.includes('qsv') || c.includes('amf')
})
const isSvtAv1 = computed(() => {
const c = encodeSettings.value.videoCodec
return c.includes('svtav1') || c === 'libsvtav1'
})
// When switching between HW/software encoder, reset preset and hwaccel
watch(() => encodeSettings.value.videoCodec, (codec) => {
@@ -274,8 +309,10 @@ watch(() => encodeSettings.value.videoCodec, (codec) => {
} else if (codec.includes('amf')) {
if (!encodeSettings.value.preset.startsWith('p')) encodeSettings.value.preset = 'p4'
hwAccel.value = 'd3d11va'
} else if (codec.includes('svtav1') || codec === 'libsvtav1') {
if (!/^\d+$/.test(encodeSettings.value.preset)) encodeSettings.value.preset = '10'
} else {
if (encodeSettings.value.preset.startsWith('p')) encodeSettings.value.preset = 'medium'
if (encodeSettings.value.preset.startsWith('p') || /^\d+$/.test(encodeSettings.value.preset)) encodeSettings.value.preset = 'medium'
}
})
@@ -389,6 +426,39 @@ function subTagLabel(s: StreamInfo): string {
.submit-area { display: flex; justify-content: flex-end; padding-top: 4px; }
/* Tooltip */
.tip-wrap { position: relative; display: inline-flex; vertical-align: middle; }
.tip-icon {
display: inline-flex; align-items: center; justify-content: center;
width: 15px; height: 15px; border-radius: 50%;
background: var(--text-dim); color: #fff;
font-size: 10px; font-weight: 700; cursor: help;
margin-left: 3px; opacity: 0.6; transition: opacity 0.15s;
}
.tip-wrap:hover .tip-icon { opacity: 1; }
.tip-popup {
display: none; position: absolute; bottom: calc(100% + 8px); left: 50%;
transform: translateX(-50%); z-index: 50;
background: var(--bg-card); border: 1px solid var(--border);
border-radius: var(--radius-sm); padding: 10px 14px;
min-width: 260px; box-shadow: 0 4px 16px rgba(0,0,0,0.15);
flex-direction: column; gap: 4px; white-space: nowrap;
}
.tip-wrap:hover .tip-popup { display: flex; }
.tip-title {
font-style: normal; font-size: 13px; color: var(--text-primary);
margin-bottom: 6px; font-weight: 600;
}
.tip-row {
font-style: normal; font-size: 11px; color: var(--text-secondary);
display: flex; gap: 6px;
}
.tip-row b { color: var(--accent); min-width: 36px; font-weight: 600; }
.tip-note {
font-style: normal; font-size: 11px; color: var(--text-dim);
margin-top: 4px; padding-top: 4px; border-top: 1px solid var(--border-light);
}
.crf-group { display: flex; align-items: center; gap: 10px; }
.crf-value { font-size: 16px; font-weight: 700; color: var(--accent); min-width: 28px; }
input[type="range"] { accent-color: var(--accent); height: auto; padding: 0; box-shadow: none; flex: 1; }
+73 -2
View File
@@ -56,6 +56,22 @@
<option value="p6">P6</option>
<option value="p7">P7 最慢高画质</option>
</template>
<template v-else-if="isSvtAv1">
<option value="13">13 最快低画质</option>
<option value="12">12</option>
<option value="11">11</option>
<option value="10">10</option>
<option value="9">9</option>
<option value="8">8</option>
<option value="7">7 中等</option>
<option value="6">6</option>
<option value="5">5</option>
<option value="4">4</option>
<option value="3">3</option>
<option value="2">2</option>
<option value="1">1</option>
<option value="0">0 最慢高画质</option>
</template>
<template v-else>
<option value="ultrafast">ultrafast</option>
<option value="superfast">superfast</option>
@@ -84,7 +100,22 @@
</select>
</div>
<div class="field" v-if="rateControl === 'crf'">
<label class="field-label">CRF / CQ (越小质量越高)</label>
<label class="field-label">
CRF / CQ (越小质量越高)
<span class="tip-wrap">
<span class="tip-icon">?</span>
<span class="tip-popup">
<em class="tip-title">CRF / CQ 参考</em>
<span class="tip-row"><b>110</b> 视觉无损文件极大</span>
<span class="tip-row"><b>1517</b> 非常清晰</span>
<span class="tip-row"><b>1820</b> 优秀推荐</span>
<span class="tip-row"><b>2123</b> 良好默认 23</span>
<span class="tip-row"><b>2428</b> 可接受体积小</span>
<span class="tip-row"><b>30+</b> 明显损失</span>
<span class="tip-note">H.265/HEVC CQ 约省 3050% 体积</span>
</span>
</span>
</label>
<div class="crf-group">
<input type="range" min="1" max="51" v-model.number="encodeSettings.crf"
style="height:auto;padding:0;box-shadow:none;flex:1" />
@@ -251,6 +282,10 @@ const isHardwareCodec = computed(() => {
const c = encodeSettings.value.videoCodec
return c.includes('nvenc') || c.includes('qsv') || c.includes('amf')
})
const isSvtAv1 = computed(() => {
const c = encodeSettings.value.videoCodec
return c.includes('svtav1') || c === 'libsvtav1'
})
// When switching rate control mode, clear the unused field
watch(rateControl, (mode) => {
@@ -312,8 +347,11 @@ watch(() => encodeSettings.value.videoCodec, (codec) => {
} else if (codec.includes('amf')) {
if (!encodeSettings.value.preset.startsWith('p')) encodeSettings.value.preset = 'p4'
hwAccel.value = 'd3d11va'
} else if (codec.includes('svtav1') || codec === 'libsvtav1') {
// SVT-AV1 uses numeric presets 013, not named presets
if (!/^\d+$/.test(encodeSettings.value.preset)) encodeSettings.value.preset = '10'
} else {
if (encodeSettings.value.preset.startsWith('p')) encodeSettings.value.preset = 'medium'
if (encodeSettings.value.preset.startsWith('p') || /^\d+$/.test(encodeSettings.value.preset)) encodeSettings.value.preset = 'medium'
// For software encoders, keep hwaccel as-is (user's choice for decode only)
}
})
@@ -382,6 +420,39 @@ function resetForm() {
}
.stream-tag.dur { font-weight: 500; }
/* Tooltip */
.tip-wrap { position: relative; display: inline-flex; vertical-align: middle; }
.tip-icon {
display: inline-flex; align-items: center; justify-content: center;
width: 15px; height: 15px; border-radius: 50%;
background: var(--text-dim); color: #fff;
font-size: 10px; font-weight: 700; cursor: help;
margin-left: 3px; opacity: 0.6; transition: opacity 0.15s;
}
.tip-wrap:hover .tip-icon { opacity: 1; }
.tip-popup {
display: none; position: absolute; bottom: calc(100% + 8px); left: 50%;
transform: translateX(-50%); z-index: 50;
background: var(--bg-card); border: 1px solid var(--border);
border-radius: var(--radius-sm); padding: 10px 14px;
min-width: 260px; box-shadow: 0 4px 16px rgba(0,0,0,0.15);
flex-direction: column; gap: 4px; white-space: nowrap;
}
.tip-wrap:hover .tip-popup { display: flex; }
.tip-title {
font-style: normal; font-size: 13px; color: var(--text-primary);
margin-bottom: 6px; font-weight: 600;
}
.tip-row {
font-style: normal; font-size: 11px; color: var(--text-secondary);
display: flex; gap: 6px;
}
.tip-row b { color: var(--accent); min-width: 36px; font-weight: 600; }
.tip-note {
font-style: normal; font-size: 11px; color: var(--text-dim);
margin-top: 4px; padding-top: 4px; border-top: 1px solid var(--border-light);
}
.crf-group { display: flex; align-items: center; gap: 10px; }
.crf-value { font-size: 16px; font-weight: 700; color: var(--accent); min-width: 28px; }
.range-hint { display: flex; justify-content: space-between; font-size: 11px; color: var(--text-dim); }
+30 -17
View File
@@ -21,7 +21,7 @@
</div>
<!-- Log content -->
<div v-if="activeTab" class="log-body" ref="logBody">
<div v-if="activeTab" class="log-body" ref="logBody" @scroll="onLogScroll">
<div class="log-head">
<span class="log-cmd">Command: ffmpeg {{ activeTask?.args?.join(' ') || '' }}</span>
<button class="btn-ghost btn-sm" @click="copyLogs" :title="copied ? '已复制' : '复制日志'">
@@ -71,8 +71,20 @@ async function copyLogs() {
} catch {}
}
// Auto-scroll on new lines
// Auto-scroll: only when user is near the bottom
let autoScroll = true
function onLogScroll() {
const el = logBody.value
if (!el) return
// If user scrolled away from bottom (>50px), stop auto-following
autoScroll = el.scrollTop + el.clientHeight >= el.scrollHeight - 50
}
// Reset auto-scroll when switching tabs
watch(activeTab, () => { autoScroll = true })
watch(activeLogs, () => {
if (!autoScroll) return
nextTick(() => {
const el = logBody.value
if (el) el.scrollTop = el.scrollHeight
@@ -116,17 +128,17 @@ function lineClass(line: string): string {
// Exposed for App.vue to push log events
function upsertTask(t: Partial<TaskInfo> & { id: string }) {
const existing = tasks.value.find(x => x.id === t.id)
if (existing) {
// Protect logs/args — they come via appendLog and the first task:updated
const idx = tasks.value.findIndex(x => x.id === t.id)
if (idx !== -1) {
const existing = tasks.value[idx]
// Replace to trigger reactivity; protect logs/args from being overwritten
const keepLogs = existing.logs
const keepArgs = existing.args
Object.assign(existing, t)
if (!t.logs || (Array.isArray(t.logs) && t.logs.length === 0)) {
existing.logs = keepLogs
}
if (!t.args || (Array.isArray(t.args) && t.args.length === 0)) {
existing.args = keepArgs
tasks.value[idx] = {
...existing,
...t,
logs: (!t.logs || t.logs.length === 0) ? keepLogs : t.logs,
args: (!t.args || t.args.length === 0) ? keepArgs : t.args,
}
} else {
tasks.value.push({
@@ -144,14 +156,15 @@ function upsertTask(t: Partial<TaskInfo> & { id: string }) {
}
function appendLog(taskId: string, line: string) {
let t = tasks.value.find(x => x.id === taskId)
if (!t) {
// Log arrived before task:updated event — create entry now
t = { id: taskId, inputFile: '', type: '', status: 'running', args: [], logs: [] }
tasks.value.push(t)
const idx = tasks.value.findIndex(x => x.id === taskId)
if (idx === -1) {
tasks.value.push({ id: taskId, inputFile: '', type: '', status: 'running', args: [], logs: [line] })
if (!activeTab.value) activeTab.value = taskId
return
}
t.logs.push(line)
// Replace object to trigger shallow-ref reactivity
const t = tasks.value[idx]
tasks.value[idx] = { ...t, logs: [...t.logs, line] }
}
// Allow parent to call these