更新若干功能
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
import { computed, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { Database, Plus, Trash2, Upload, BarChart3, Folder, Languages, CheckCircle2, Info, Copy, Timer, Rocket, Sparkles, CloudUpload, Wifi } from 'lucide-vue-next'
|
||||
import { Database, Plus, Trash2, Upload, BarChart3, Folder, Languages, CheckCircle2, Info, Copy, Timer, Rocket, Sparkles, CloudUpload, Wifi, Image, ImageUp, X } from 'lucide-vue-next'
|
||||
import { call, isNative } from '../api'
|
||||
import { useAppStore } from '../store'
|
||||
import AIScopeDrawer from '../components/AIScopeDrawer.vue'
|
||||
@@ -10,8 +10,8 @@ import AIScopeDrawer from '../components/AIScopeDrawer.vue'
|
||||
const route=useRoute()
|
||||
// tab 记忆:优先 URL 深链,其次上次停留的 tab(sync 已迁往个人主页,做合法性回退)
|
||||
// 分区切换入口在侧边栏“设置”一级导航的二级菜单,页内不再有 tabbar。
|
||||
const TABS=['rules','appearance','ai','filestorage','database']
|
||||
const TAB_TITLE={rules:'tabRules',appearance:'tabAppearance',ai:'aiAnalysis',filestorage:'tabFileStorage',database:'tabDatabase'}
|
||||
const TABS=['rules','appearance','ai','filestorage','kindicons','database']
|
||||
const TAB_TITLE={rules:'tabRules',appearance:'tabAppearance',ai:'aiAnalysis',filestorage:'tabFileStorage',kindicons:'tabKindIcons',database:'tabDatabase'}
|
||||
const pickTab=v=>TABS.includes(String(v))?String(v):''
|
||||
const tab=ref(pickTab(route.query.tab)||pickTab(localStorage.getItem('cc-settings-tab'))||'rules'),rules=ref([]),form=reactive({pattern:'',category:'custom'})
|
||||
const aiOpen=ref(false)
|
||||
@@ -47,12 +47,12 @@ async function toggleAutostart(){
|
||||
}
|
||||
// ---- 全局文件存储(仅管理员 id=1;权威配置存远端 MySQL,保存即全员生效) ----
|
||||
const isAdmin=computed(()=>store.syncStatus.userId===1)
|
||||
const fsCfg=reactive({mode:'local',baseUrl:'',apiKey:''})
|
||||
const fsCfg=reactive({mode:'local',baseUrl:''})
|
||||
const fsBusy=ref(''),fsLoaded=ref(false)
|
||||
async function loadFileStorage(){
|
||||
try{
|
||||
const c=await call('GetFileStorageConfig')
|
||||
Object.assign(fsCfg,{mode:c.mode||'local',baseUrl:c.baseUrl||'',apiKey:c.apiKey||''})
|
||||
Object.assign(fsCfg,{mode:c.mode||'local',baseUrl:c.baseUrl||''})
|
||||
}catch{}
|
||||
fsLoaded.value=true
|
||||
}
|
||||
@@ -60,9 +60,28 @@ async function saveFileStorage(){
|
||||
if(fsBusy.value)return
|
||||
fsBusy.value='save'
|
||||
try{
|
||||
const has=await call('AdminHasStepUp')
|
||||
if(!has){
|
||||
const code=window.prompt(t('adminStepupHint'),'')
|
||||
if(!code){fsBusy.value='';return}
|
||||
await call('AdminStepUp',String(code).trim())
|
||||
}
|
||||
await call('SaveFileStorageConfig',{...fsCfg})
|
||||
store.showToast({type:'success',key:'fsSavedToast'})
|
||||
}catch(e){store.showToast({type:'error',text:errText(e)})}
|
||||
}catch(e){
|
||||
const code=String(e).split(':')[0].trim()
|
||||
if(code==='ADMIN_STEPUP_REQUIRED'||code==='ADMIN_IP_CHANGED'){
|
||||
const tip=code==='ADMIN_IP_CHANGED'?t('adminIpChangedRisk'):t('adminStepupHint')
|
||||
const c=window.prompt(tip,'')
|
||||
if(c){
|
||||
try{
|
||||
await call('AdminStepUp',String(c).trim())
|
||||
await call('SaveFileStorageConfig',{...fsCfg})
|
||||
store.showToast({type:'success',key:'fsSavedToast'})
|
||||
}catch(e2){store.showToast({type:'error',text:errText(e2)})}
|
||||
}
|
||||
}else store.showToast({type:'error',text:errText(e)})
|
||||
}
|
||||
finally{fsBusy.value=''}
|
||||
}
|
||||
async function testFileStorage(){
|
||||
@@ -75,7 +94,42 @@ async function testFileStorage(){
|
||||
finally{fsBusy.value=''}
|
||||
}
|
||||
const errText=e=>{const code=String(e).split(':')[0].trim();return t('errors.'+code)!=='errors.'+code?t('errors.'+code):String(e)}
|
||||
watch(tab,v=>{if(v==='filestorage')loadFileStorage()})
|
||||
async function checkSoftwareUpdate(){
|
||||
try{
|
||||
const r=await call('CheckAppUpdate',true)
|
||||
if(r?.upToDate) store.showToast({type:'success',text:t('aboutUpToDate',{version:r.current||r.latest||'—'})})
|
||||
else store.showToast({type:'info',text:t('aboutUpdateAvailable',{latest:r.latest,current:r.current})})
|
||||
}catch(e){store.showToast({type:'error',text:errText(e)})}
|
||||
}
|
||||
watch(tab,v=>{if(v==='filestorage')loadFileStorage();if(v==='kindicons')loadKindIcons()})
|
||||
|
||||
const knownKinds=ref([])
|
||||
const kindIcons=ref({})
|
||||
const kindBusy=ref('')
|
||||
async function loadKindIcons(){
|
||||
try{
|
||||
knownKinds.value=await call('ListKnownLaunchKinds')||[]
|
||||
kindIcons.value=await call('ListKindIcons')||{}
|
||||
}catch{knownKinds.value=[];kindIcons.value={}}
|
||||
}
|
||||
async function pickKindIcon(kind){
|
||||
if(kindBusy.value)return
|
||||
kindBusy.value=kind
|
||||
try{
|
||||
const u=await call('PickKindIcon',kind)
|
||||
if(u)kindIcons.value={...kindIcons.value,[kind]:u}
|
||||
}catch(e){store.showToast({type:'error',text:errText(e)})}
|
||||
finally{kindBusy.value=''}
|
||||
}
|
||||
async function clearKindIcon(kind){
|
||||
if(kindBusy.value)return
|
||||
kindBusy.value=kind
|
||||
try{
|
||||
await call('ClearKindIcon',kind)
|
||||
const next={...kindIcons.value};delete next[kind];kindIcons.value=next
|
||||
}catch(e){store.showToast({type:'error',text:errText(e)})}
|
||||
finally{kindBusy.value=''}
|
||||
}
|
||||
|
||||
async function add(){if(!form.pattern)return;await call('AddRule',form.pattern,form.category);form.pattern='';await load()}
|
||||
async function remove(r){if(!r.builtin){await call('DeleteRule',r.id);await load()}}
|
||||
@@ -99,7 +153,7 @@ watch(()=>[settings.theme,settings.locale,settings.glassOpacity,settings.loading
|
||||
let aiKeyTimer=null
|
||||
watch(()=>[settings.sparkKey,settings.deepSeekKey],()=>{clearTimeout(aiKeyTimer);aiKeyTimer=setTimeout(save,600)})
|
||||
watch(()=>route.query.tab,v=>{const n=pickTab(v);if(n)tab.value=n})
|
||||
onMounted(async()=>{await load();apply();if(tab.value==='filestorage')loadFileStorage()})
|
||||
onMounted(async()=>{await load();apply();if(tab.value==='filestorage')loadFileStorage();if(tab.value==='kindicons')loadKindIcons()})
|
||||
onUnmounted(()=>{clearTimeout(aiKeyTimer)})
|
||||
</script>
|
||||
|
||||
@@ -121,6 +175,7 @@ onUnmounted(()=>{clearTimeout(aiKeyTimer)})
|
||||
<label v-if="settings.autoUpdateMode!=='everyNHours'">{{t('triggerTime')}}<input v-model="settings.autoUpdateTime" type="time" class="interval-input"/></label>
|
||||
<p class="auto-update-hint">{{t('autoUpdateHint')}}</p>
|
||||
</template>
|
||||
<button type="button" class="btn secondary" style="margin-top:.5rem" @click="checkSoftwareUpdate">{{t('checkAppUpdate')}}</button>
|
||||
</section></template>
|
||||
<template v-else-if="tab==='ai'">
|
||||
<section class="panel form-panel"><h2><Sparkles/>{{t('aiProviderTitle')}}</h2>
|
||||
@@ -138,8 +193,7 @@ onUnmounted(()=>{clearTimeout(aiKeyTimer)})
|
||||
<template v-if="isAdmin">
|
||||
<label>{{t('fsMode')}}<select v-model="fsCfg.mode"><option value="local">{{t('fsModeLocal')}}</option><option value="server">{{t('fsModeServer')}}</option></select></label>
|
||||
<template v-if="fsCfg.mode==='server'">
|
||||
<label>{{t('fsBaseUrl')}}<input v-model.trim="fsCfg.baseUrl" placeholder="http://192.168.1.10:8788"/></label>
|
||||
<label>{{t('fsApiKey')}}<input v-model.trim="fsCfg.apiKey" type="password" :placeholder="t('fsApiKeyPh')"/></label>
|
||||
<label>{{t('fsBaseUrl')}}<input v-model.trim="fsCfg.baseUrl" placeholder="https://o-api.nailaoyun.cn/pms-api"/></label>
|
||||
</template>
|
||||
<p class="auto-update-hint">{{t('fsPageHint')}}</p>
|
||||
<div class="fs-page-actions">
|
||||
@@ -150,6 +204,28 @@ onUnmounted(()=>{clearTimeout(aiKeyTimer)})
|
||||
<div v-else class="preview-notice"><Info/><div><b>{{t('fsAdminOnlyTitle')}}</b><small>{{t('fsAdminOnlyDesc')}}</small></div></div>
|
||||
</section>
|
||||
</template>
|
||||
<template v-else-if="tab==='kindicons'">
|
||||
<section class="panel form-panel">
|
||||
<h2><Image/>{{t('kindIconsTitle')}}</h2>
|
||||
<p class="auto-update-hint">{{t('kindIconsHint')}}</p>
|
||||
<template v-if="isAdmin">
|
||||
<div class="kind-icons-grid">
|
||||
<div v-for="k in knownKinds" :key="k" class="kind-icon-card">
|
||||
<span class="kind-icon-preview">
|
||||
<img v-if="kindIcons[k]" :src="kindIcons[k]" alt="" />
|
||||
<Image v-else />
|
||||
</span>
|
||||
<b>{{k}}</b>
|
||||
<div class="kind-icon-ops">
|
||||
<button type="button" class="btn secondary" :disabled="!!kindBusy" @click="pickKindIcon(k)"><ImageUp/>{{t('avatarPick')}}</button>
|
||||
<button v-if="kindIcons[k]" type="button" class="btn secondary" :disabled="!!kindBusy" @click="clearKindIcon(k)"><X/>{{t('lpClearIcon')}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="preview-notice"><Info/><div><b>{{t('fsAdminOnlyTitle')}}</b><small>{{t('kindIconsAdminOnly')}}</small></div></div>
|
||||
</section>
|
||||
</template>
|
||||
<template v-else><section class="panel database-panel"><div class="db-title"><h2><Database/>{{t('dbLocation')}}</h2><span class="db-connected"><CheckCircle2/>{{t('dbConnected')}}</span></div><div v-if="!native" class="preview-notice"><Info/><div><b>{{t('previewModeTitle')}}</b><small>{{t('previewModeDb')}}</small></div></div><div class="db-path"><span>{{t('dbCurrentLoc')}}</span><code :title="settings.databasePath">{{settings.databasePath||t('dbNoPath')}}</code><button :title="t('copyPathTitle')" :disabled="!settings.databasePath" @click="copyPath"><Copy/></button></div><p class="db-relocate-hint">{{t('dbRelocateHint')}}</p><p v-if="dbMessage" class="db-message">{{dbMessage}}</p><button class="btn secondary migrate" :disabled="!native" @click="migrate"><Upload/>{{t('migrateBtn')}}</button></section>
|
||||
<section class="panel danger-zone"><h2><Trash2/>{{t('dangerZone')}}</h2><p>{{t('dangerDesc')}}</p><div><button @click="clear('stats')"><BarChart3/><span><b>{{t('clearStats')}}</b><small>{{t('clearStatsDesc')}}</small></span></button><button @click="clear('project')"><Folder/><span><b>{{t('clearProject')}}</b><small>{{t('clearProjectDesc')}}</small></span></button><button class="danger" @click="clear('all')"><Trash2/><span><b>{{t('clearAllData')}}</b><small>{{t('clearAllDesc')}}</small></span></button></div></section></template>
|
||||
<AIScopeDrawer v-if="aiOpen" kind="config" :title="t('settings')" @close="aiOpen=false"/>
|
||||
|
||||
Reference in New Issue
Block a user