🎨 优化扩展模块
This commit is contained in:
@@ -1,103 +1,174 @@
|
||||
import request from '@/utils/request'
|
||||
import type {
|
||||
Extension,
|
||||
ExtensionManifest,
|
||||
CreateExtensionRequest,
|
||||
UpdateExtensionRequest,
|
||||
ExtensionListParams,
|
||||
ExtensionListResponse,
|
||||
ToggleExtensionRequest,
|
||||
UpdateExtensionSettingsRequest,
|
||||
ExtensionStatsRequest,
|
||||
Extension,
|
||||
ExtensionListParams,
|
||||
ExtensionListResponse,
|
||||
CreateExtensionRequest,
|
||||
UpdateExtensionRequest,
|
||||
ToggleExtensionRequest,
|
||||
UpdateExtensionSettingsRequest,
|
||||
ExtensionStatsRequest,
|
||||
} from '@/types/extension'
|
||||
|
||||
// 创建/安装扩展
|
||||
// 创建扩展
|
||||
export function createExtension(data: CreateExtensionRequest) {
|
||||
return request.post<Extension>('/app/extension', data)
|
||||
return request<Extension>({
|
||||
url: '/app/extension',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
// 更新扩展
|
||||
export function updateExtension(id: number, data: UpdateExtensionRequest) {
|
||||
return request.put(`/app/extension/${id}`, data)
|
||||
return request({
|
||||
url: `/app/extension/${id}`,
|
||||
method: 'put',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
// 删除/卸载扩展
|
||||
// 删除扩展
|
||||
export function deleteExtension(id: number, deleteFiles = false) {
|
||||
return request.delete(`/app/extension/${id}`, { params: { deleteFiles } })
|
||||
return request({
|
||||
url: `/app/extension/${id}`,
|
||||
method: 'delete',
|
||||
params: deleteFiles ? { deleteFiles: true } : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
// 获取扩展详情
|
||||
export function getExtension(id: number) {
|
||||
return request.get<Extension>(`/app/extension/${id}`)
|
||||
return request<Extension>({
|
||||
url: `/app/extension/${id}`,
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
// 获取扩展列表
|
||||
export function getExtensionList(params?: ExtensionListParams) {
|
||||
return request.get<ExtensionListResponse>('/app/extension/list', { params })
|
||||
return request<ExtensionListResponse>({
|
||||
url: '/app/extension',
|
||||
method: 'get',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
// 获取启用的扩展列表
|
||||
export function getEnabledExtensions() {
|
||||
return request.get<Extension[]>('/app/extension/enabled')
|
||||
return request<Extension[]>({
|
||||
url: '/app/extension/enabled',
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
// 启用/禁用扩展
|
||||
export function toggleExtension(id: number, data: ToggleExtensionRequest) {
|
||||
return request.post(`/app/extension/${id}/toggle`, data)
|
||||
return request({
|
||||
url: `/app/extension/${id}/toggle`,
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
// 获取扩展配置
|
||||
// 获取扩展设置
|
||||
export function getExtensionSettings(id: number) {
|
||||
return request.get<Record<string, any>>(`/app/extension/${id}/settings`)
|
||||
return request<Record<string, any>>({
|
||||
url: `/app/extension/${id}/settings`,
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
// 更新扩展配置
|
||||
// 更新扩展设置
|
||||
export function updateExtensionSettings(id: number, data: UpdateExtensionSettingsRequest) {
|
||||
return request.put(`/app/extension/${id}/settings`, data)
|
||||
return request({
|
||||
url: `/app/extension/${id}/settings`,
|
||||
method: 'put',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
// 获取扩展 manifest
|
||||
export function getExtensionManifest(id: number) {
|
||||
return request.get<ExtensionManifest>(`/app/extension/${id}/manifest`)
|
||||
return request<Record<string, any>>({
|
||||
url: `/app/extension/${id}/manifest`,
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
// 导入扩展
|
||||
export function importExtension(file: File) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
return request.post<Extension>('/app/extension/import', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 导出扩展
|
||||
export function exportExtension(id: number) {
|
||||
return request.get(`/app/extension/${id}/export`, {
|
||||
responseType: 'blob',
|
||||
})
|
||||
}
|
||||
|
||||
// 更新扩展统计
|
||||
export function updateExtensionStats(data: ExtensionStatsRequest) {
|
||||
return request.post('/app/extension/stats', data)
|
||||
}
|
||||
|
||||
// 智能安装扩展(自动识别 Git URL 或 Manifest URL,兼容 SillyTavern)
|
||||
// 从 URL 安装扩展(智能识别 Git URL 或 Manifest URL)
|
||||
export function installExtensionFromUrl(url: string, branch = 'main') {
|
||||
return request.post<Extension>('/app/extension/install/url', { url, branch })
|
||||
return request<Extension>({
|
||||
url: '/app/extension/install/url',
|
||||
method: 'post',
|
||||
data: { url, branch },
|
||||
})
|
||||
}
|
||||
|
||||
// 从 Git URL 安装扩展
|
||||
export function installExtensionFromGit(gitUrl: string, branch = 'main') {
|
||||
return request.post<Extension>('/app/extension/install/git', {
|
||||
gitUrl,
|
||||
branch,
|
||||
})
|
||||
return request<Extension>({
|
||||
url: '/app/extension/install/git',
|
||||
method: 'post',
|
||||
data: { url: gitUrl, branch },
|
||||
})
|
||||
}
|
||||
|
||||
// 升级扩展版本(从源重新安装)
|
||||
// 升级扩展
|
||||
export function upgradeExtension(id: number, force = false) {
|
||||
return request.post<Extension>(`/app/extension/${id}/update`, { force })
|
||||
return request<Extension>({
|
||||
url: `/app/extension/${id}/upgrade`,
|
||||
method: 'post',
|
||||
data: { force },
|
||||
})
|
||||
}
|
||||
|
||||
// 导入扩展(上传 zip 压缩包或文件夹)
|
||||
export function importExtension(file: File) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
return request<Extension>({
|
||||
url: '/app/extension/import',
|
||||
method: 'post',
|
||||
data: formData,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 导出扩展
|
||||
export function exportExtension(id: number) {
|
||||
return request<Extension>({
|
||||
url: `/app/extension/${id}/export`,
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
// 更新扩展统计
|
||||
export function updateExtensionStats(data: ExtensionStatsRequest) {
|
||||
return request({
|
||||
url: `/app/extension/${data.extensionId}/stats`,
|
||||
method: 'post',
|
||||
data: {
|
||||
action: data.action,
|
||||
value: data.value || 1,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 下载扩展 JSON 文件
|
||||
export function downloadExtensionJSON(id: number, name: string) {
|
||||
return exportExtension(id).then((data) => {
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `${name || 'extension'}_${Date.now()}.json`
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(url)
|
||||
})
|
||||
}
|
||||
|
||||
2
web-app-vue/src/components.d.ts
vendored
2
web-app-vue/src/components.d.ts
vendored
@@ -17,6 +17,7 @@ declare module 'vue' {
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
ElCard: typeof import('element-plus/es')['ElCard']
|
||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||
ElCol: typeof import('element-plus/es')['ElCol']
|
||||
ElCollapse: typeof import('element-plus/es')['ElCollapse']
|
||||
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
|
||||
ElContainer: typeof import('element-plus/es')['ElContainer']
|
||||
@@ -45,6 +46,7 @@ declare module 'vue' {
|
||||
ElPopconfirm: typeof import('element-plus/es')['ElPopconfirm']
|
||||
ElRadio: typeof import('element-plus/es')['ElRadio']
|
||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||
ElRow: typeof import('element-plus/es')['ElRow']
|
||||
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||
ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
|
||||
|
||||
@@ -204,12 +204,18 @@
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<!-- 没有配置项 -->
|
||||
<!-- 没有配置项(仅当: 无 manifest schema 配置 且 无扩展注入的自定义 UI 且 无全局设置面板) -->
|
||||
<el-empty
|
||||
v-if="Object.keys(getSettingsSchema(ext)).length === 0"
|
||||
v-if="Object.keys(getSettingsSchema(ext)).length === 0 && !extensionsWithCustomUI.has(ext.name) && !hasExtensionSettingsUI"
|
||||
description="此扩展没有可配置项"
|
||||
:image-size="60"
|
||||
/>
|
||||
<div
|
||||
v-else-if="Object.keys(getSettingsSchema(ext)).length === 0 && hasExtensionSettingsUI"
|
||||
class="setting-hint" style="text-align: center; padding: 8px 0; color: var(--el-text-color-secondary);"
|
||||
>
|
||||
此扩展的设置面板已显示在上方「扩展设置面板」区域
|
||||
</div>
|
||||
</el-form>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
@@ -263,11 +269,11 @@
|
||||
:on-change="handleFileChange"
|
||||
:show-file-list="true"
|
||||
:limit="1"
|
||||
accept=".json"
|
||||
accept=".zip,.json"
|
||||
>
|
||||
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
|
||||
<div class="el-upload__text">
|
||||
将 manifest.json 拖到此处,或<em>点击上传</em>
|
||||
将扩展包(.zip)或 manifest.json 拖到此处,或<em>点击上传</em>
|
||||
</div>
|
||||
</el-upload>
|
||||
</el-tab-pane>
|
||||
@@ -377,6 +383,10 @@ const installForm = reactive({
|
||||
const settingsMountRef = ref<HTMLElement | null>(null)
|
||||
const hasExtensionSettingsUI = ref(false)
|
||||
|
||||
// 跟踪哪些扩展已经注入了自定义 UI
|
||||
const extensionsWithCustomUI = reactive(new Set<string>())
|
||||
let settingsObserver: MutationObserver | null = null
|
||||
|
||||
/**
|
||||
* 将全局 #extensions_settings 容器中的扩展 UI 搬入 Drawer 中显示
|
||||
* 原版 SillyTavern 扩展通过 jQuery 向 #extensions_settings 追加自定义 HTML,
|
||||
@@ -424,9 +434,63 @@ const unmountExtensionSettings = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 组件卸载时归还容器
|
||||
/**
|
||||
* 检查每个已注册扩展的 per-extension 容器是否有内容
|
||||
* 并监听全局 #extensions_settings 容器变化
|
||||
*/
|
||||
const scanCustomSettingsUI = () => {
|
||||
// 检查每个扩展的专属容器
|
||||
for (const ext of extensionStore.extensions) {
|
||||
const container = document.getElementById(`extension-settings-${ext.name}`)
|
||||
if (container && container.children.length > 0) {
|
||||
extensionsWithCustomUI.add(ext.name)
|
||||
}
|
||||
}
|
||||
|
||||
// 重新检测全局容器
|
||||
const globalContainer = document.getElementById('extensions_settings')
|
||||
const globalContainer2 = document.getElementById('extensions_settings2')
|
||||
const hasContent = (globalContainer && globalContainer.children.length > 0) ||
|
||||
(globalContainer2 && globalContainer2.children.length > 0)
|
||||
hasExtensionSettingsUI.value = hasContent
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动 MutationObserver 监听全局 #extensions_settings 容器变化
|
||||
* 扩展可能在加载完成后异步注入设置 UI,需要动态检测
|
||||
*/
|
||||
const startSettingsObserver = () => {
|
||||
if (settingsObserver) settingsObserver.disconnect()
|
||||
|
||||
settingsObserver = new MutationObserver(() => {
|
||||
scanCustomSettingsUI()
|
||||
// 如果 Drawer 已打开,重新尝试挂载
|
||||
if (drawerVisible.value) {
|
||||
mountExtensionSettings()
|
||||
}
|
||||
})
|
||||
|
||||
// 监听全局容器
|
||||
const targets = [
|
||||
document.getElementById('extensions_settings'),
|
||||
document.getElementById('extensions_settings2'),
|
||||
].filter(Boolean) as HTMLElement[]
|
||||
|
||||
for (const target of targets) {
|
||||
settingsObserver.observe(target, { childList: true, subtree: true })
|
||||
}
|
||||
|
||||
// 也监听 body 上的 per-extension 容器变化(扩展可能通过 id 定位并注入)
|
||||
settingsObserver.observe(document.body, { childList: true, subtree: false })
|
||||
}
|
||||
|
||||
// 组件卸载时归还容器并清理 Observer
|
||||
onBeforeUnmount(() => {
|
||||
unmountExtensionSettings()
|
||||
if (settingsObserver) {
|
||||
settingsObserver.disconnect()
|
||||
settingsObserver = null
|
||||
}
|
||||
})
|
||||
|
||||
// Computed
|
||||
@@ -648,7 +712,7 @@ const handleInstall = async () => {
|
||||
|
||||
if (installTab.value === 'file') {
|
||||
if (!selectedFile.value) {
|
||||
ElMessage.warning('请选择 manifest.json 文件')
|
||||
ElMessage.warning('请选择扩展文件(.zip 或 .json)')
|
||||
return
|
||||
}
|
||||
await extensionStore.importExtension(selectedFile.value)
|
||||
@@ -727,9 +791,20 @@ watch(drawerVisible, async (visible) => {
|
||||
handleRefresh()
|
||||
// 等待 DOM 更新后再挂载扩展设置面板
|
||||
await nextTick()
|
||||
setTimeout(mountExtensionSettings, 100)
|
||||
// 启动 MutationObserver 以检测扩展动态注入的 UI
|
||||
startSettingsObserver()
|
||||
// 扫描已有的自定义 settings UI
|
||||
scanCustomSettingsUI()
|
||||
setTimeout(() => {
|
||||
mountExtensionSettings()
|
||||
// 再次扫描(给扩展一点时间注入 UI)
|
||||
scanCustomSettingsUI()
|
||||
}, 200)
|
||||
} else {
|
||||
unmountExtensionSettings()
|
||||
if (settingsObserver) {
|
||||
settingsObserver.disconnect()
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,522 +0,0 @@
|
||||
<template>
|
||||
<div class="extension-list-page">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<h2>扩展管理</h2>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="showInstallDialog = true">
|
||||
<el-icon><Plus /></el-icon>
|
||||
安装扩展
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 过滤器 -->
|
||||
<div class="filters">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索扩展名称..."
|
||||
clearable
|
||||
@change="handleSearch"
|
||||
style="width: 300px; margin-right: 10px"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
|
||||
<el-select
|
||||
v-model="filterType"
|
||||
placeholder="扩展类型"
|
||||
clearable
|
||||
@change="handleFilterChange"
|
||||
style="width: 150px; margin-right: 10px"
|
||||
>
|
||||
<el-option label="UI 扩展" value="ui" />
|
||||
<el-option label="服务端扩展" value="server" />
|
||||
<el-option label="混合扩展" value="hybrid" />
|
||||
</el-select>
|
||||
|
||||
<el-select
|
||||
v-model="filterCategory"
|
||||
placeholder="分类"
|
||||
clearable
|
||||
@change="handleFilterChange"
|
||||
style="width: 150px; margin-right: 10px"
|
||||
>
|
||||
<el-option label="工具" value="utilities" />
|
||||
<el-option label="主题" value="themes" />
|
||||
<el-option label="集成" value="integrations" />
|
||||
<el-option label="其他" value="tools" />
|
||||
</el-select>
|
||||
|
||||
<el-select
|
||||
v-model="filterEnabled"
|
||||
placeholder="状态"
|
||||
clearable
|
||||
@change="handleFilterChange"
|
||||
style="width: 120px; margin-right: 10px"
|
||||
>
|
||||
<el-option label="已启用" :value="true" />
|
||||
<el-option label="已禁用" :value="false" />
|
||||
</el-select>
|
||||
|
||||
<el-button @click="handleResetFilters">重置</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 扩展列表 -->
|
||||
<el-table
|
||||
v-loading="extensionStore.loading"
|
||||
:data="extensionStore.extensions"
|
||||
style="width: 100%; margin-top: 20px"
|
||||
>
|
||||
<el-table-column prop="displayName" label="扩展名称" width="200">
|
||||
<template #default="{ row }">
|
||||
<div class="extension-name">
|
||||
<strong>{{ row.displayName || row.name }}</strong>
|
||||
<el-tag
|
||||
v-if="row.isSystemExt"
|
||||
size="small"
|
||||
type="info"
|
||||
style="margin-left: 8px"
|
||||
>
|
||||
系统
|
||||
</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="description" label="描述" show-overflow-tooltip />
|
||||
|
||||
<el-table-column prop="version" label="版本" width="100" />
|
||||
|
||||
<el-table-column prop="author" label="作者" width="120" />
|
||||
|
||||
<el-table-column prop="extensionType" label="类型" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getTypeTagType(row.extensionType)" size="small">
|
||||
{{ getTypeLabel(row.extensionType) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="category" label="分类" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ getCategoryLabel(row.category) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="isEnabled" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-switch
|
||||
v-model="row.isEnabled"
|
||||
:disabled="row.isSystemExt"
|
||||
@change="handleToggle(row)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="280" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
size="small"
|
||||
@click="handleViewDetail(row)"
|
||||
>
|
||||
详情
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
@click="handleSettings(row)"
|
||||
>
|
||||
配置
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
@click="handleExport(row)"
|
||||
>
|
||||
导出
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="!row.isSystemExt"
|
||||
size="small"
|
||||
type="danger"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
卸载
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-model:current-page="extensionStore.currentPage"
|
||||
v-model:page-size="extensionStore.pageSize"
|
||||
:total="extensionStore.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handlePageChange"
|
||||
style="margin-top: 20px; justify-content: flex-end"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<!-- 安装扩展对话框 -->
|
||||
<el-dialog
|
||||
v-model="showInstallDialog"
|
||||
title="安装扩展"
|
||||
width="600px"
|
||||
>
|
||||
<el-tabs v-model="installTab">
|
||||
<el-tab-pane label="从文件导入" name="file">
|
||||
<el-upload
|
||||
ref="uploadRef"
|
||||
:auto-upload="false"
|
||||
:limit="1"
|
||||
accept=".json"
|
||||
:on-change="handleFileSelect"
|
||||
drag
|
||||
>
|
||||
<el-icon class="el-icon--upload"><UploadFilled /></el-icon>
|
||||
<div class="el-upload__text">
|
||||
将 manifest.json 拖到此处,或<em>点击上传</em>
|
||||
</div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">
|
||||
只能上传 manifest.json 文件
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="从 Git 安装" name="git">
|
||||
<el-form :model="installForm" label-width="120px">
|
||||
<el-form-item label="Git 仓库 URL">
|
||||
<el-input
|
||||
v-model="installForm.gitUrl"
|
||||
placeholder="https://github.com/username/extension-name.git"
|
||||
/>
|
||||
<template #extra>
|
||||
<div style="font-size: 12px; color: #909399; margin-top: 5px">
|
||||
支持 GitHub、GitLab、Gitee 等 Git 仓库<br />
|
||||
示例:https://github.com/SillyTavern/Extension-Example.git
|
||||
</div>
|
||||
</template>
|
||||
</el-form-item>
|
||||
<el-form-item label="分支(可选)">
|
||||
<el-input
|
||||
v-model="installForm.branch"
|
||||
placeholder="main"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="从 URL 安装" name="url">
|
||||
<el-form :model="installForm" label-width="120px">
|
||||
<el-form-item label="Manifest URL">
|
||||
<el-input
|
||||
v-model="installForm.url"
|
||||
placeholder="https://example.com/manifest.json"
|
||||
/>
|
||||
<template #extra>
|
||||
<div style="font-size: 12px; color: #909399; margin-top: 5px">
|
||||
直接指向 manifest.json 文件的 URL
|
||||
</div>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="showInstallDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleInstall" :loading="extensionStore.loading">
|
||||
安装
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 扩展详情对话框 -->
|
||||
<el-dialog
|
||||
v-model="showDetailDialog"
|
||||
:title="currentExtension?.displayName || currentExtension?.name"
|
||||
width="800px"
|
||||
>
|
||||
<el-descriptions v-if="currentExtension" :column="2" border>
|
||||
<el-descriptions-item label="扩展名称">
|
||||
{{ currentExtension.displayName || currentExtension.name }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="版本">
|
||||
{{ currentExtension.version }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="作者">
|
||||
{{ currentExtension.author }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="许可证">
|
||||
{{ currentExtension.license || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="类型">
|
||||
{{ getTypeLabel(currentExtension.extensionType) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="分类">
|
||||
{{ getCategoryLabel(currentExtension.category) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="主页" :span="2">
|
||||
<a v-if="currentExtension.homepage" :href="currentExtension.homepage" target="_blank">
|
||||
{{ currentExtension.homepage }}
|
||||
</a>
|
||||
<span v-else>-</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="仓库" :span="2">
|
||||
<a v-if="currentExtension.repository" :href="currentExtension.repository" target="_blank">
|
||||
{{ currentExtension.repository }}
|
||||
</a>
|
||||
<span v-else>-</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="描述" :span="2">
|
||||
{{ currentExtension.description }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="标签" :span="2">
|
||||
<el-tag
|
||||
v-for="tag in currentExtension.tags"
|
||||
:key="tag"
|
||||
size="small"
|
||||
style="margin-right: 5px"
|
||||
>
|
||||
{{ tag }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="安装日期">
|
||||
{{ formatDate(currentExtension.installDate) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="使用次数">
|
||||
{{ currentExtension.usageCount }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessageBox, ElMessage } from 'element-plus'
|
||||
import { Plus, Search, UploadFilled } from '@element-plus/icons-vue'
|
||||
import { useExtensionStore } from '@/stores/extension'
|
||||
import type { Extension } from '@/types/extension'
|
||||
|
||||
const router = useRouter()
|
||||
const extensionStore = useExtensionStore()
|
||||
|
||||
// 搜索和过滤
|
||||
const searchKeyword = ref('')
|
||||
const filterType = ref('')
|
||||
const filterCategory = ref('')
|
||||
const filterEnabled = ref<boolean | undefined>(undefined)
|
||||
|
||||
// 对话框
|
||||
const showInstallDialog = ref(false)
|
||||
const showDetailDialog = ref(false)
|
||||
const installTab = ref('git')
|
||||
const installForm = ref({
|
||||
gitUrl: '',
|
||||
branch: 'main',
|
||||
url: '',
|
||||
})
|
||||
const uploadRef = ref()
|
||||
const selectedFile = ref<File | null>(null)
|
||||
const currentExtension = ref<Extension | null>(null)
|
||||
|
||||
onMounted(() => {
|
||||
extensionStore.fetchExtensionList()
|
||||
})
|
||||
|
||||
const handleSearch = () => {
|
||||
extensionStore.fetchExtensionList({ keyword: searchKeyword.value })
|
||||
}
|
||||
|
||||
const handleFilterChange = () => {
|
||||
extensionStore.setFilters({
|
||||
extensionType: filterType.value,
|
||||
category: filterCategory.value,
|
||||
isEnabled: filterEnabled.value,
|
||||
})
|
||||
}
|
||||
|
||||
const handleResetFilters = () => {
|
||||
searchKeyword.value = ''
|
||||
filterType.value = ''
|
||||
filterCategory.value = ''
|
||||
filterEnabled.value = undefined
|
||||
extensionStore.resetFilters()
|
||||
}
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
extensionStore.currentPage = page
|
||||
extensionStore.fetchExtensionList()
|
||||
}
|
||||
|
||||
const handleSizeChange = (size: number) => {
|
||||
extensionStore.pageSize = size
|
||||
extensionStore.fetchExtensionList()
|
||||
}
|
||||
|
||||
const handleToggle = async (row: Extension) => {
|
||||
try {
|
||||
await extensionStore.toggleExtension(row.id, row.isEnabled)
|
||||
} catch (error) {
|
||||
// 切换失败,还原状态
|
||||
row.isEnabled = !row.isEnabled
|
||||
}
|
||||
}
|
||||
|
||||
const handleViewDetail = (row: Extension) => {
|
||||
currentExtension.value = row
|
||||
showDetailDialog.value = true
|
||||
}
|
||||
|
||||
const handleSettings = (row: Extension) => {
|
||||
router.push(`/extension/settings/${row.id}`)
|
||||
}
|
||||
|
||||
const handleExport = async (row: Extension) => {
|
||||
try {
|
||||
await extensionStore.exportExtension(row.id, row.name)
|
||||
} catch (error) {
|
||||
console.error('导出失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (row: Extension) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要卸载扩展 "${row.displayName || row.name}" 吗?`,
|
||||
'确认卸载',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}
|
||||
)
|
||||
await extensionStore.deleteExtension(row.id, false)
|
||||
} catch (error: any) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('卸载失败:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleFileSelect = (file: any) => {
|
||||
selectedFile.value = file.raw
|
||||
}
|
||||
|
||||
const handleInstall = async () => {
|
||||
try {
|
||||
if (installTab.value === 'file') {
|
||||
if (!selectedFile.value) {
|
||||
ElMessage.warning('请选择要上传的文件')
|
||||
return
|
||||
}
|
||||
await extensionStore.importExtension(selectedFile.value)
|
||||
} else if (installTab.value === 'git') {
|
||||
if (!installForm.value.gitUrl) {
|
||||
ElMessage.warning('请输入 Git 仓库 URL')
|
||||
return
|
||||
}
|
||||
await extensionStore.installExtensionFromGit(
|
||||
installForm.value.gitUrl,
|
||||
installForm.value.branch || 'main'
|
||||
)
|
||||
} else {
|
||||
if (!installForm.value.url) {
|
||||
ElMessage.warning('请输入 Manifest URL')
|
||||
return
|
||||
}
|
||||
await extensionStore.installExtensionFromUrl(installForm.value.url)
|
||||
}
|
||||
|
||||
showInstallDialog.value = false
|
||||
selectedFile.value = null
|
||||
installForm.value = { gitUrl: '', branch: 'main', url: '' }
|
||||
} catch (error) {
|
||||
console.error('安装失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const getTypeLabel = (type: string) => {
|
||||
const labels: Record<string, string> = {
|
||||
ui: 'UI',
|
||||
server: '服务端',
|
||||
hybrid: '混合',
|
||||
}
|
||||
return labels[type] || type
|
||||
}
|
||||
|
||||
const getTypeTagType = (type: string) => {
|
||||
const types: Record<string, any> = {
|
||||
ui: 'primary',
|
||||
server: 'success',
|
||||
hybrid: 'warning',
|
||||
}
|
||||
return types[type] || ''
|
||||
}
|
||||
|
||||
const getCategoryLabel = (category: string) => {
|
||||
const labels: Record<string, string> = {
|
||||
utilities: '工具',
|
||||
themes: '主题',
|
||||
integrations: '集成',
|
||||
tools: '其他',
|
||||
}
|
||||
return labels[category] || category || '-'
|
||||
}
|
||||
|
||||
const formatDate = (date: string) => {
|
||||
if (!date) return '-'
|
||||
return new Date(date).toLocaleString('zh-CN')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.extension-list-page {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-header h2 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.extension-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.el-icon--upload {
|
||||
font-size: 67px;
|
||||
color: #c0c4cc;
|
||||
margin: 40px 0 16px;
|
||||
line-height: 50px;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,378 +1,291 @@
|
||||
<template>
|
||||
<div class="extension-settings-page">
|
||||
<el-card v-loading="loading">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<h2>{{ extension?.displayName || extension?.name }} - 配置</h2>
|
||||
<p class="subtitle">{{ extension?.description }}</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button @click="handleBack">返回</el-button>
|
||||
<el-button
|
||||
v-if="hasSettings"
|
||||
type="primary"
|
||||
@click="handleSave"
|
||||
:loading="saving"
|
||||
>
|
||||
保存配置
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="extension-settings">
|
||||
<el-page-header @back="goBack" style="margin-bottom: 20px">
|
||||
<template #content>
|
||||
<span class="page-title">{{ extensionName }} - 扩展配置</span>
|
||||
</template>
|
||||
</el-page-header>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<!-- 扩展信息 -->
|
||||
<el-descriptions :column="3" border style="margin-bottom: 20px">
|
||||
<el-descriptions-item label="版本">
|
||||
{{ extension?.version }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="作者">
|
||||
{{ extension?.author }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="类型">
|
||||
{{ getTypeLabel(extension?.extensionType) }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-col :span="8">
|
||||
<el-card shadow="never" v-loading="extensionStore.loading">
|
||||
<template #header>
|
||||
<span>扩展信息</span>
|
||||
</template>
|
||||
<el-descriptions :column="1" border size="small">
|
||||
<el-descriptions-item label="名称">{{ extensionStore.currentExtension?.displayName || extensionStore.currentExtension?.name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="版本">{{ extensionStore.currentExtension?.version }}</el-descriptions-item>
|
||||
<el-descriptions-item label="作者">{{ extensionStore.currentExtension?.author || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="类型">
|
||||
<el-tag size="small">{{ typeLabel(extensionStore.currentExtension?.extensionType || '') }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="分类">{{ extensionStore.currentExtension?.category || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="extensionStore.currentExtension?.isEnabled ? 'success' : 'info'" size="small">
|
||||
{{ extensionStore.currentExtension?.isEnabled ? '已启用' : '已禁用' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="安装来源">{{ extensionStore.currentExtension?.installSource || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="许可证">{{ extensionStore.currentExtension?.license || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<!-- 配置表单 -->
|
||||
<el-form
|
||||
v-if="extension"
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
label-width="200px"
|
||||
style="max-width: 800px"
|
||||
>
|
||||
<el-divider content-position="left">扩展配置</el-divider>
|
||||
<p v-if="extensionStore.currentExtension?.description" class="ext-description">
|
||||
{{ extensionStore.currentExtension.description }}
|
||||
</p>
|
||||
|
||||
<!-- 动态生成配置项 -->
|
||||
<template v-if="hasSettings">
|
||||
<el-form-item
|
||||
v-for="(value, key) in settings"
|
||||
:key="key"
|
||||
:label="getSettingLabel(key)"
|
||||
>
|
||||
<!-- 布尔值 -->
|
||||
<el-switch
|
||||
v-if="typeof value === 'boolean'"
|
||||
v-model="formData[key]"
|
||||
/>
|
||||
|
||||
<!-- 数字 -->
|
||||
<el-input-number
|
||||
v-else-if="typeof value === 'number'"
|
||||
v-model="formData[key]"
|
||||
:min="0"
|
||||
/>
|
||||
|
||||
<!-- 选择器(如果有 options) -->
|
||||
<el-select
|
||||
v-else-if="isSelectField(key)"
|
||||
v-model="formData[key]"
|
||||
placeholder="请选择"
|
||||
<div v-if="extensionStore.currentExtension?.homepage || extensionStore.currentExtension?.repository" class="ext-links">
|
||||
<el-link
|
||||
v-if="extensionStore.currentExtension?.homepage"
|
||||
type="primary"
|
||||
:href="extensionStore.currentExtension.homepage"
|
||||
target="_blank"
|
||||
>
|
||||
<el-option
|
||||
v-for="option in getSelectOptions(key)"
|
||||
:key="option.value"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
主页
|
||||
</el-link>
|
||||
<el-link
|
||||
v-if="extensionStore.currentExtension?.repository"
|
||||
type="primary"
|
||||
:href="extensionStore.currentExtension.repository"
|
||||
target="_blank"
|
||||
>
|
||||
仓库
|
||||
</el-link>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- 设置编辑 -->
|
||||
<el-col :span="16">
|
||||
<el-card shadow="never" v-loading="settingsLoading">
|
||||
<template #header>
|
||||
<div class="settings-header">
|
||||
<span>扩展设置</span>
|
||||
<div>
|
||||
<el-button size="small" @click="resetSettings">重置</el-button>
|
||||
<el-button type="primary" size="small" :loading="saving" @click="saveSettings">
|
||||
保存设置
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-empty v-if="!settingsKeys.length && !settingsLoading" description="该扩展暂无可配置项" />
|
||||
|
||||
<el-form v-else label-width="160px" label-position="left">
|
||||
<el-form-item
|
||||
v-for="key in settingsKeys"
|
||||
:key="key"
|
||||
:label="key"
|
||||
>
|
||||
<!-- 布尔值 -->
|
||||
<el-switch
|
||||
v-if="typeof settingsData[key] === 'boolean'"
|
||||
v-model="settingsData[key]"
|
||||
/>
|
||||
</el-select>
|
||||
<!-- 数字 -->
|
||||
<el-input-number
|
||||
v-else-if="typeof settingsData[key] === 'number'"
|
||||
v-model="settingsData[key]"
|
||||
:controls="true"
|
||||
/>
|
||||
<!-- 字符串 -->
|
||||
<el-input
|
||||
v-else-if="typeof settingsData[key] === 'string'"
|
||||
v-model="settingsData[key]"
|
||||
:autosize="{ minRows: 1, maxRows: 4 }"
|
||||
type="textarea"
|
||||
/>
|
||||
<!-- 对象/数组 - JSON 编辑 -->
|
||||
<el-input
|
||||
v-else
|
||||
v-model="settingsJsonStrings[key]"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 2, maxRows: 8 }"
|
||||
@blur="parseJsonField(key)"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- 多行文本 -->
|
||||
<el-input
|
||||
v-else-if="isTextareaField(key)"
|
||||
v-model="formData[key]"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
/>
|
||||
|
||||
<!-- 默认文本输入 -->
|
||||
<el-input
|
||||
v-else
|
||||
v-model="formData[key]"
|
||||
:placeholder="getSettingPlaceholder(key)"
|
||||
/>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<!-- 没有配置项:SillyTavern 扩展的配置由扩展自身管理 -->
|
||||
<template v-else>
|
||||
<el-alert
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
style="margin-bottom: 20px"
|
||||
>
|
||||
<template #title>
|
||||
此扩展的配置由扩展自身管理
|
||||
</template>
|
||||
<p style="margin: 5px 0 0 0">
|
||||
SillyTavern 扩展通过内置的 extension_settings API 管理配置,
|
||||
启用扩展后可在聊天界面中进行配置。
|
||||
</p>
|
||||
</el-alert>
|
||||
</template>
|
||||
|
||||
<!-- 高级选项 -->
|
||||
<template v-if="extension.options && Object.keys(extension.options).length > 0">
|
||||
<el-divider content-position="left">高级选项</el-divider>
|
||||
|
||||
<el-form-item
|
||||
v-for="(value, key) in extension.options"
|
||||
:key="'option_' + key"
|
||||
:label="getSettingLabel(key)"
|
||||
>
|
||||
<el-input
|
||||
v-model="optionsData[key]"
|
||||
:placeholder="getSettingPlaceholder(key)"
|
||||
/>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-form>
|
||||
|
||||
<!-- Manifest 信息 -->
|
||||
<el-divider content-position="left">Manifest 信息</el-divider>
|
||||
<el-descriptions v-if="extension" :column="2" border>
|
||||
<el-descriptions-item label="扩展名称">
|
||||
{{ extension.name || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="显示名称">
|
||||
{{ extension.displayName || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="主脚本">
|
||||
{{ extension.scriptPath || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="样式文件">
|
||||
{{ extension.stylePath || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="安装来源">
|
||||
{{ getInstallSourceLabel(extension.installSource) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="源地址">
|
||||
<el-link
|
||||
v-if="extension.sourceUrl"
|
||||
:href="extension.sourceUrl"
|
||||
type="primary"
|
||||
target="_blank"
|
||||
>
|
||||
{{ extension.sourceUrl }}
|
||||
</el-link>
|
||||
<span v-else>-</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="依赖扩展" :span="2">
|
||||
<template v-if="extension.dependencies && Object.keys(extension.dependencies).length > 0">
|
||||
<el-tag
|
||||
v-for="(version, name) in extension.dependencies"
|
||||
:key="name"
|
||||
size="small"
|
||||
style="margin-right: 5px"
|
||||
>
|
||||
{{ name }} {{ version }}
|
||||
</el-tag>
|
||||
<!-- Manifest 数据 -->
|
||||
<el-card shadow="never" style="margin-top: 20px" v-loading="extensionStore.loading">
|
||||
<template #header>
|
||||
<span>Manifest 数据</span>
|
||||
</template>
|
||||
<span v-else>无</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="冲突扩展" :span="2">
|
||||
<template v-if="extension.conflicts && extension.conflicts.length > 0">
|
||||
<el-tag
|
||||
v-for="conflict in extension.conflicts"
|
||||
:key="conflict"
|
||||
size="small"
|
||||
type="danger"
|
||||
style="margin-right: 5px"
|
||||
>
|
||||
{{ conflict }}
|
||||
</el-tag>
|
||||
</template>
|
||||
<span v-else>无</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<!-- Manifest 原始数据 -->
|
||||
<template v-if="extension?.manifestData && Object.keys(extension.manifestData).length > 0">
|
||||
<el-divider content-position="left">Manifest 原始数据</el-divider>
|
||||
<el-input
|
||||
type="textarea"
|
||||
:model-value="JSON.stringify(extension.manifestData, null, 2)"
|
||||
:rows="10"
|
||||
readonly
|
||||
style="font-family: monospace"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<el-divider content-position="left">使用统计</el-divider>
|
||||
<el-descriptions v-if="extension" :column="3" border>
|
||||
<el-descriptions-item label="使用次数">
|
||||
{{ extension.usageCount }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="错误次数">
|
||||
{{ extension.errorCount }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="平均加载时间">
|
||||
{{ extension.loadTime }}ms
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="安装日期">
|
||||
{{ formatDate(extension.installDate) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="最后启用时间">
|
||||
{{ formatDate(extension.lastEnabled) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="自动更新">
|
||||
{{ extension.autoUpdate ? '是' : '否' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
<pre v-if="manifest" class="manifest-json">{{ JSON.stringify(manifest, null, 2) }}</pre>
|
||||
<el-empty v-else description="无 Manifest 数据" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useExtensionStore } from '@/stores/extension'
|
||||
import type { Extension } from '@/types/extension'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const extensionStore = useExtensionStore()
|
||||
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const extension = ref<Extension | null>(null)
|
||||
const settings = ref<Record<string, any> | null>(null)
|
||||
const formData = ref<Record<string, any>>({})
|
||||
const optionsData = ref<Record<string, any>>({})
|
||||
const formRef = ref()
|
||||
|
||||
const extensionId = computed(() => Number(route.params.id))
|
||||
|
||||
const hasSettings = computed(() => {
|
||||
return settings.value && Object.keys(settings.value).length > 0
|
||||
})
|
||||
// 状态
|
||||
const settingsLoading = ref(false)
|
||||
const saving = ref(false)
|
||||
const settingsData = ref<Record<string, any>>({})
|
||||
const settingsJsonStrings = ref<Record<string, string>>({})
|
||||
const originalSettings = ref<Record<string, any>>({})
|
||||
const manifest = ref<Record<string, any> | null>(null)
|
||||
|
||||
onMounted(async () => {
|
||||
await loadExtension()
|
||||
await loadSettings()
|
||||
})
|
||||
const extensionName = computed(() =>
|
||||
extensionStore.currentExtension?.displayName || extensionStore.currentExtension?.name || '扩展',
|
||||
)
|
||||
|
||||
const loadExtension = async () => {
|
||||
const settingsKeys = computed(() => Object.keys(settingsData.value))
|
||||
|
||||
// 类型标签
|
||||
const typeLabel = (type: string) => {
|
||||
const map: Record<string, string> = {
|
||||
ui: 'UI 扩展',
|
||||
server: '服务端扩展',
|
||||
hybrid: '混合扩展',
|
||||
}
|
||||
return map[type] || type
|
||||
}
|
||||
|
||||
// 获取扩展详情
|
||||
const fetchExtension = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
extension.value = await extensionStore.fetchExtension(extensionId.value)
|
||||
await extensionStore.fetchExtension(extensionId.value)
|
||||
} catch (error) {
|
||||
console.error('获取扩展详情失败:', error)
|
||||
ElMessage.error('获取扩展详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化 options 数据
|
||||
if (extension.value.options) {
|
||||
optionsData.value = { ...extension.value.options }
|
||||
// 获取扩展设置
|
||||
const fetchSettings = async () => {
|
||||
settingsLoading.value = true
|
||||
try {
|
||||
const data = await extensionStore.getExtensionSettings(extensionId.value)
|
||||
if (data) {
|
||||
settingsData.value = { ...data }
|
||||
originalSettings.value = JSON.parse(JSON.stringify(data))
|
||||
|
||||
// 为复杂类型生成 JSON 字符串
|
||||
for (const key of Object.keys(data)) {
|
||||
if (typeof data[key] === 'object' && data[key] !== null) {
|
||||
settingsJsonStrings.value[key] = JSON.stringify(data[key], null, 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载扩展失败:', error)
|
||||
ElMessage.error('加载扩展失败')
|
||||
router.back()
|
||||
console.error('获取设置失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
settingsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const loadSettings = async () => {
|
||||
// 获取 manifest
|
||||
const fetchManifest = async () => {
|
||||
try {
|
||||
settings.value = await extensionStore.getExtensionSettings(extensionId.value)
|
||||
formData.value = { ...settings.value }
|
||||
const data = await extensionStore.getExtensionManifest(extensionId.value)
|
||||
if (data) {
|
||||
manifest.value = data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载配置失败:', error)
|
||||
console.error('获取 manifest 失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
// 解析 JSON 字段
|
||||
const parseJsonField = (key: string) => {
|
||||
try {
|
||||
saving.value = true
|
||||
await extensionStore.updateExtensionSettings(extensionId.value, formData.value)
|
||||
ElMessage.success('配置保存成功')
|
||||
settingsData.value[key] = JSON.parse(settingsJsonStrings.value[key])
|
||||
} catch {
|
||||
ElMessage.warning(`字段 "${key}" 的 JSON 格式无效`)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存设置
|
||||
const saveSettings = async () => {
|
||||
saving.value = true
|
||||
try {
|
||||
await extensionStore.updateExtensionSettings(extensionId.value, settingsData.value)
|
||||
originalSettings.value = JSON.parse(JSON.stringify(settingsData.value))
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error)
|
||||
console.error('保存设置失败:', error)
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleBack = () => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
const getTypeLabel = (type?: string) => {
|
||||
if (!type) return '-'
|
||||
const labels: Record<string, string> = {
|
||||
ui: 'UI 扩展',
|
||||
server: '服务端扩展',
|
||||
hybrid: '混合扩展',
|
||||
// 重置设置
|
||||
const resetSettings = () => {
|
||||
settingsData.value = JSON.parse(JSON.stringify(originalSettings.value))
|
||||
for (const key of Object.keys(settingsData.value)) {
|
||||
if (typeof settingsData.value[key] === 'object' && settingsData.value[key] !== null) {
|
||||
settingsJsonStrings.value[key] = JSON.stringify(settingsData.value[key], null, 2)
|
||||
}
|
||||
}
|
||||
return labels[type] || type
|
||||
ElMessage.info('已重置')
|
||||
}
|
||||
|
||||
const getSettingLabel = (key: string) => {
|
||||
return key
|
||||
.replace(/([A-Z])/g, ' $1')
|
||||
.replace(/^./, (str) => str.toUpperCase())
|
||||
.replace(/_/g, ' ')
|
||||
// 返回
|
||||
const goBack = () => {
|
||||
router.push('/extension')
|
||||
}
|
||||
|
||||
const getSettingPlaceholder = (key: string) => {
|
||||
return `请输入 ${getSettingLabel(key)}`
|
||||
}
|
||||
|
||||
const isSelectField = (_key: string) => {
|
||||
return false
|
||||
}
|
||||
|
||||
const getSelectOptions = (_key: string) => {
|
||||
return []
|
||||
}
|
||||
|
||||
const isTextareaField = (key: string) => {
|
||||
const textareaFields = ['description', 'content', 'notes', 'script', 'code']
|
||||
return textareaFields.some(field => key.toLowerCase().includes(field))
|
||||
}
|
||||
|
||||
const getInstallSourceLabel = (source: string) => {
|
||||
const labels: Record<string, string> = {
|
||||
url: 'URL 下载',
|
||||
git: 'Git 仓库',
|
||||
file: '文件导入',
|
||||
marketplace: '应用市场',
|
||||
}
|
||||
return labels[source] || source
|
||||
}
|
||||
|
||||
const formatDate = (date: string) => {
|
||||
if (!date) return '-'
|
||||
return new Date(date).toLocaleString('zh-CN')
|
||||
}
|
||||
onMounted(() => {
|
||||
fetchExtension()
|
||||
fetchSettings()
|
||||
fetchManifest()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.extension-settings-page {
|
||||
<style scoped lang="scss">
|
||||
.extension-settings {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.page-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.card-header h2 {
|
||||
margin: 0 0 5px 0;
|
||||
font-size: 20px;
|
||||
}
|
||||
.settings-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
}
|
||||
.ext-description {
|
||||
margin-top: 16px;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-regular);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
.ext-links {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.manifest-json {
|
||||
background: var(--el-fill-color-lighter);
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
overflow-x: auto;
|
||||
max-height: 400px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user