11<template >
22 <div class =" settings-container" >
3- <div style = " flex : 1 ; max-width : 700 px ; margin : auto ; " >
3+ <div class = " settings-container-internal " >
44 <n-space :justify =" 'space-between'" >
55 <n-space :align =" 'baseline'" >
66 <n-h2 >全局设置</n-h2 >
77 <span >高级设置</span >
88 <n-switch v-model:value =" showAdvanced" ></n-switch >
99 </n-space >
10- <n-button @click =" saveConfig" >保存</n-button >
1110 </n-space >
1211 <div id =" danmaku-record" class =" setting-box" >
1312 <n-h3 >弹幕录制</n-h3 >
1413 <optional-input type =" boolean" label =" 保存弹幕" v-model:value =" newConfig['optionalRecordDanmaku']"
15- :same-as-default =" true" />
14+ :same-as-default =" true" @changed = " onChanged " />
1615 <p >本设置同时是所有“弹幕录制”的总开关,当本设置为 false 时其他所有“弹幕录制”设置无效,不会写入弹幕XML文件。</p >
1716 <n-collapse-transition :show =" newConfig['optionalRecordDanmaku']?.value || false" >
1817 <optional-input type =" boolean" label =" 保存 SuperChat" v-model:value =" newConfig['optionalRecordDanmakuSuperChat']"
19- :same-as-default =" true" />
18+ :same-as-default =" true" @changed = " onChanged " />
2019 <optional-input type =" boolean" label =" 保存 舰长购买" v-model:value =" newConfig['optionalRecordDanmakuGuard']"
21- :same-as-default =" true" />
20+ :same-as-default =" true" @changed = " onChanged " />
2221 <optional-input type =" boolean" label =" 保存 送礼信息" v-model:value =" newConfig['optionalRecordDanmakuGift']"
23- :same-as-default =" true" />
22+ :same-as-default =" true" @changed = " onChanged " />
2423 <optional-input type =" boolean" label =" 保存 弹幕原始数据" v-model:value =" newConfig['optionalRecordDanmakuRaw']"
25- :same-as-default =" true" />
24+ :same-as-default =" true" @changed = " onChanged " />
2625 <n-collapse-transition :show =" showAdvanced" >
2726 <optional-input type =" number" label =" 触发写硬盘所需的弹幕数量"
2827 v-model:value =" newConfig['optionalRecordDanmakuFlushInterval']" :same-as-default =" true" unit =" 个"
29- max-input-width =" 150px" />
28+ max-input-width =" 150px" @changed = " onChanged " />
3029 </n-collapse-transition >
3130 </n-collapse-transition >
3231 </div >
3332 <div id =" record-mode" class =" setting-box" >
3433 <n-h3 >录制模式</n-h3 >
3534 <optional-input type =" enum" v-model:value =" newConfig['optionalRecordMode']" :enums =" RecordModes"
36- :same-as-default =" true" />
35+ :same-as-default =" true" @changed = " onChanged " />
3736 <n-collapse-transition :show =" newConfig['optionalRecordMode']?.value == 0" >
3837 <n-h3 >标准模式录制修复设置</n-h3 >
3938 <optional-input type =" boolean" label =" 检测到可能缺少数据时分段"
40- v-model:value =" newConfig['optionalFlvProcessorSplitOnScriptTag']" :same-as-default =" true" />
39+ v-model:value =" newConfig['optionalFlvProcessorSplitOnScriptTag']" :same-as-default =" true" @changed = " onChanged " />
4140 </n-collapse-transition >
4241 </div >
4342 <div id =" auto-split" class =" setting-box" >
4443 <n-h3 >自动分段</n-h3 >
4544 <optional-input type =" enum" v-model:value =" newConfig['optionalCuttingMode']" :enums =" CuttingModes"
46- :same-as-default =" true" />
45+ :same-as-default =" true" @changed = " onChanged " />
4746 <n-collapse-transition :show =" newConfig['optionalCuttingMode']?.value == 1" >
4847 <optional-input type =" number" prefix =" 每" suffix =" 保存为一个文件" v-model:value =" newConfig['optionalCuttingNumber']"
49- :same-as-default =" true" unit =" 分" max-input-width =" 150px" />
48+ :same-as-default =" true" unit =" 分" max-input-width =" 150px" @changed = " onChanged " />
5049 </n-collapse-transition >
5150 <n-collapse-transition :show =" newConfig['optionalCuttingMode']?.value == 2" >
5251 <optional-input type =" number" prefix =" 每" suffix =" 保存为一个文件" v-model:value =" newConfig['optionalCuttingNumber']"
53- :same-as-default =" true" unit =" MiB" max-input-width =" 150px" />
52+ :same-as-default =" true" unit =" MiB" max-input-width =" 150px" @changed = " onChanged " />
5453 </n-collapse-transition >
5554
5655 </div >
5756 <div id =" storage" class =" setting-box" >
5857 <n-h3 >文件写入</n-h3 >
5958 <optional-input style =" max-width : 700px ;" label =" 文件名" type =" text"
60- v-model:value =" newConfig['optionalFileNameRecordTemplate']" :same-as-default =" false" />
59+ v-model:value =" newConfig['optionalFileNameRecordTemplate']" :same-as-default =" false" @changed = " onChanged " />
6160 <n-button @click =" toggleFileNamePreviewModal" >预览文件名</n-button >
6261 <optional-input type =" boolean" label =" 保存直播封面" v-model:value =" newConfig['optionalSaveStreamCover']"
63- :same-as-default =" true" />
62+ :same-as-default =" true" @changed = " onChanged " />
6463 <optional-input type =" boolean" label =" 在flv中写入直播信息" v-model:value =" newConfig['optionalFlvWriteMetadata']"
65- :same-as-default =" true" />
64+ :same-as-default =" true" @changed = " onChanged " />
6665 </div >
6766 <div id =" record-quality" class =" setting-box" >
6867 <n-h3 >录制画质</n-h3 >
6968 <optional-input style =" max-width : 700px ;" type =" text" v-model:value =" newConfig['optionalRecordingQuality']"
70- :same-as-default =" false" />
69+ :same-as-default =" false" @changed = " onChanged " />
7170 </div >
7271 <div id =" webhook" class =" setting-box" >
7372 <n-h3 >Webhook</n-h3 >
7776 </p >
7877 <p >Webhook V1</p >
7978 <optional-input type =" textarea" :max-input-width =" '700px'" v-model:value =" newConfig['optionalWebHookUrls']"
80- :same-as-default =" true" />
79+ :same-as-default =" true" @changed = " onChanged " />
8180 <p >Webhook V2</p >
8281 <optional-input type =" textarea" :max-input-width =" '700px'" v-model:value =" newConfig['optionalWebHookUrlsV2']"
83- :same-as-default =" true" />
82+ :same-as-default =" true" @changed = " onChanged " />
8483 </div >
8584 <n-collapse-transition :show =" showAdvanced" >
8685 <div id =" live-api-host" >
8786 <n-h3 >请求的 API Host</n-h3 >
8887 <optional-input style =" max-width : 700px ;" type =" text" v-model:value =" newConfig['optionalLiveApiHost']"
89- :same-as-default =" false" />
88+ :same-as-default =" false" @changed = " onChanged " />
9089 </div >
9190 <div id =" cookie" class =" setting-box" >
9291 <n-h3 >Cookie</n-h3 >
9392 <p >Cookie 会用于获取直播间信息、获取直播流地址、连接弹幕服务器以及未来开发者更新增加功能所需要的操作。</p >
9493 <p >软件开发者不对账号发生的任何事情负责,包括并不限于被标记为<b >机器人账号、大会员被冻结、无法参与各种抽奖和活动等</b >。<b style =" color :red " >建议使用小号。</b ></p >
9594 <p >如您知晓您的账号会因以上所列出来的部分原因所导致无法使用或权益受损等情况,并愿意承担由此所会带来的一系列后果,请继续以下的操作,软件开发者不会对您账号所发生的任何后果承担责任。 </p >
9695 <optional-input type =" text" :max-input-width =" '700px'" v-model:value =" newConfig['optionalCookie']"
97- :same-as-default =" true" />
96+ :same-as-default =" true" @changed = " onChanged " />
9897 </div >
9998 <div id =" network" class =" setting-box" >
10099 <n-h3 >网络设置</n-h3 >
101100 <optional-input type =" boolean" label =" 使用系统代理"
102- v-model:value =" newConfig['optionalNetworkTransportUseSystemProxy']" :same-as-default =" true" />
101+ v-model:value =" newConfig['optionalNetworkTransportUseSystemProxy']" :same-as-default =" true" @changed = " onChanged " />
103102 <optional-input type =" enum" label =" 允许使用的网络类型"
104103 v-model:value =" newConfig['optionalNetworkTransportAllowedAddressFamily']" :enums =" IPFamilies"
105- :same-as-default =" true" />
104+ :same-as-default =" true" @changed = " onChanged " />
106105 <optional-input type =" enum" label =" 弹幕链接协议" v-model:value =" newConfig['optionalDanmakuTransport']"
107- :enums =" DanmakuTransport" :same-as-default =" true" />
106+ :enums =" DanmakuTransport" :same-as-default =" true" @changed = " onChanged " />
108107 <optional-input type =" boolean" label =" 使用直播间主播的uid进行弹幕服务器认证"
109- v-model:value =" newConfig['optionalDanmakuAuthenticateWithStreamerUid']" :same-as-default =" true" />
108+ v-model:value =" newConfig['optionalDanmakuAuthenticateWithStreamerUid']" :same-as-default =" true" @changed = " onChanged " />
110109 </div >
111110 <div id =" timing" class =" setting-box" >
112111 <n-h3 >时间间隔</n-h3 >
113112 <optional-input style =" max-width : 700px ;" type =" number" label =" HTTP API 检查时间间隔"
114- v-model:value =" newConfig['optionalTimingCheckInterval']" unit =" 秒" max-input-width =" 150px" />
113+ v-model:value =" newConfig['optionalTimingCheckInterval']" unit =" 秒" max-input-width =" 150px" @changed = " onChanged " />
115114 <optional-input style =" max-width : 700px ;" type =" number" label =" API请求超时时间"
116- v-model:value =" newConfig['optionalTimingApiTimeout']" unit =" 毫秒" max-input-width =" 150px" />
115+ v-model:value =" newConfig['optionalTimingApiTimeout']" unit =" 毫秒" max-input-width =" 150px" @changed = " onChanged " />
117116 <optional-input style =" max-width : 700px ;" type =" number" label =" 录制断开重连时间间隔"
118- v-model:value =" newConfig['optionalTimingStreamRetry']" unit =" 毫秒" max-input-width =" 150px" />
117+ v-model:value =" newConfig['optionalTimingStreamRetry']" unit =" 毫秒" max-input-width =" 150px" @changed = " onChanged " />
119118 <optional-input style =" max-width : 700px ;" type =" number" label =" 录制无指定画质重连时间间隔"
120- v-model:value =" newConfig['optionalTimingStreamRetryNoQn']" unit =" 秒" max-input-width =" 150px" />
119+ v-model:value =" newConfig['optionalTimingStreamRetryNoQn']" unit =" 秒" max-input-width =" 150px" @changed = " onChanged " />
121120 <optional-input style =" max-width : 700px ;" type =" number" label =" 最大允许未收到直播数据时间"
122- v-model:value =" newConfig['optionalTimingWatchdogTimeout']" unit =" 毫秒" max-input-width =" 150px" />
121+ v-model:value =" newConfig['optionalTimingWatchdogTimeout']" unit =" 毫秒" max-input-width =" 150px" @changed = " onChanged " />
123122 <optional-input style =" max-width : 700px ;" type =" number" label =" 连接直播服务器超时时间"
124- v-model:value =" newConfig['optionalTimingStreamConnect']" unit =" 毫秒" max-input-width =" 150px" />
123+ v-model:value =" newConfig['optionalTimingStreamConnect']" unit =" 毫秒" max-input-width =" 150px" @changed = " onChanged " />
125124 <optional-input style =" max-width : 700px ;" type =" number" label =" 弹幕服务器重连时间间隔"
126- v-model:value =" newConfig['optionalTimingDanmakuRetry']" unit =" 毫秒" max-input-width =" 150px" />
125+ v-model:value =" newConfig['optionalTimingDanmakuRetry']" unit =" 毫秒" max-input-width =" 150px" @changed = " onChanged " />
127126 </div >
128127 <div id =" userscript" class =" setting-box" >
129128 <n-h3 >用户脚本</n-h3 >
130129 <optional-input type =" textarea" :max-input-width =" '700px'" v-model:value =" newConfig['optionalUserScript']"
131- :same-as-default =" true" />
130+ :same-as-default =" true" @changed = " onChanged " />
132131 </div >
133132 </n-collapse-transition >
133+ <n-affix v-if =" isChanged"
134+ :bottom =" 16"
135+ :trigger-bottom =" 128"
136+ :listen-to =" () => containerRef"
137+ style =" max-width : 700px ; width : 100% ;" >
138+ <n-card size =" small" >注意!你尚未保存修改!<n-button @click =" saveConfig" type =" primary" style =" float : right ;" >保存</n-button ></n-card >
139+ </n-affix >
134140 </div >
135141 <div class =" anchor" >
136142 <n-anchor :show-rail =" false" offset-target =" #app-layout" position =" fix" ignore-gap z-index =" 1" type =" block"
156162</template >
157163
158164<script setup lang="ts">
159- import { NH2 , NH3 , NCollapseTransition , NAnchor , NAnchorLink , NSpace , NSwitch , NA , NButton , useLoadingBar , useMessage } from ' naive-ui' ;
165+ import { NH2 , NH3 , NCollapseTransition , NAnchor , NAnchorLink , NSpace , NSwitch , NA , NButton , NAffix , NCard , useLoadingBar , useMessage } from ' naive-ui' ;
160166import { onMounted , ref } from ' vue' ;
161167import { Recorder , Optional } from ' ../../utils/api' ;
162168import OptionalInput from ' ../../components/OptionalInput.vue' ;
@@ -357,6 +363,7 @@ async function saveConfig() {
357363 loadingbar .finish ();
358364 loadMessage .destroy ();
359365 message .success (' 保存成功' );
366+ isChanged .value = false ;
360367 } catch (error : any ) {
361368 loadMessage .destroy ();
362369 message .error (' 保存设置时出错:' + (error ?.message || error .toString ()), {
@@ -385,8 +392,16 @@ function toggleFileNamePreviewModal() {
385392function handleFileNamePreviewModalClose(newTemplate : string ) {
386393 newConfig .value [' optionalFileNameRecordTemplate' ].value = newTemplate ;
387394 newConfig .value [' optionalFileNameRecordTemplate' ].hasValue = true ;
395+ isChanged .value = true ;
388396}
389397
398+ const isChanged = ref (false );
399+ function onChanged() {
400+ isChanged .value = true ;
401+ }
402+
403+ const containerRef = ref (document .getElementById (' content-scrollbar' )?.children [0 ] as HTMLElement );
404+
390405 </script >
391406<style scoped lang="scss">
392407.settings-container {
@@ -395,6 +410,13 @@ function handleFileNamePreviewModalClose(newTemplate: string) {
395410 flex-direction : row
396411}
397412
413+ .settings-container-internal {
414+ flex :1 ;
415+ max-width : 700px ;
416+ margin : auto ;
417+ padding-bottom : 64px ;
418+ }
419+
398420.setting-box {
399421 margin-bottom : 24px
400422}
0 commit comments