mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-06-29 03:21:39 +08:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14aa75dfae | ||
|
|
348aa4757b | ||
|
|
6e6819acc1 | ||
|
|
51a58aaae0 | ||
|
|
fbde99389e | ||
|
|
5a4e345529 | ||
|
|
b446afb6d8 | ||
|
|
8580af36d1 | ||
|
|
95ca092117 | ||
|
|
ba200cae5c | ||
|
|
87c73e0253 | ||
|
|
d4d7f635f5 | ||
|
|
729db1510e | ||
|
|
8a12ecf918 | ||
|
|
cacc2602df | ||
|
|
8c6cfa7fc5 | ||
|
|
0113f28d8c | ||
|
|
d870b788bc |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "moviepilot",
|
"name": "moviepilot",
|
||||||
"version": "2.13.0",
|
"version": "2.13.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"bin": "dist/service.js",
|
"bin": "dist/service.js",
|
||||||
|
|||||||
@@ -1292,7 +1292,7 @@ export interface TransferForm {
|
|||||||
// 目标存储
|
// 目标存储
|
||||||
target_storage: string
|
target_storage: string
|
||||||
// 目标路径
|
// 目标路径
|
||||||
target_path: string
|
target_path: string | null
|
||||||
// TMDB ID
|
// TMDB ID
|
||||||
tmdbid?: number
|
tmdbid?: number
|
||||||
// 豆瓣 ID
|
// 豆瓣 ID
|
||||||
@@ -1335,6 +1335,22 @@ export interface ManualTransferPayload extends Omit<TransferForm, 'fileitem'> {
|
|||||||
fileitems?: FileItem[]
|
fileitems?: FileItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 手动整理目的路径匹配结果
|
||||||
|
export interface ManualTransferTargetPathData {
|
||||||
|
// 目标存储
|
||||||
|
target_storage?: string | null
|
||||||
|
// 目标路径
|
||||||
|
target_path?: string | null
|
||||||
|
// 整理方式
|
||||||
|
transfer_type?: string | null
|
||||||
|
// 刮削
|
||||||
|
scrape?: boolean
|
||||||
|
// 媒体库类型子目录
|
||||||
|
library_type_folder?: boolean
|
||||||
|
// 媒体库类别子目录
|
||||||
|
library_category_folder?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
// 手动整理预览统计
|
// 手动整理预览统计
|
||||||
export interface ManualTransferPreviewSummary {
|
export interface ManualTransferPreviewSummary {
|
||||||
// 总数
|
// 总数
|
||||||
@@ -1369,6 +1385,14 @@ export interface ManualTransferPreviewItem {
|
|||||||
episode_end?: number | string
|
episode_end?: number | string
|
||||||
// Part
|
// Part
|
||||||
part?: string
|
part?: string
|
||||||
|
// 原始识别字符串
|
||||||
|
org_string?: string
|
||||||
|
// 应用的自定义识别词
|
||||||
|
apply_words?: string[]
|
||||||
|
// 制作组/字幕组
|
||||||
|
resource_team?: string
|
||||||
|
// 自定义占位符
|
||||||
|
customization?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// 手动整理预览数据
|
// 手动整理预览数据
|
||||||
|
|||||||
@@ -84,6 +84,33 @@ const releaseDialogTitle = ref('')
|
|||||||
// 变更日志对话框内容
|
// 变更日志对话框内容
|
||||||
const releaseDialogBody = ref('')
|
const releaseDialogBody = ref('')
|
||||||
|
|
||||||
|
// 版本统计对话框
|
||||||
|
const versionStatisticDialog = ref(false)
|
||||||
|
|
||||||
|
// 版本统计加载状态
|
||||||
|
const versionStatisticLoading = ref(false)
|
||||||
|
|
||||||
|
// 版本统计数据
|
||||||
|
const versionStatistic = ref<any>({})
|
||||||
|
|
||||||
|
// 后端版本统计
|
||||||
|
const backendVersionStatistics = computed(() => versionStatistic.value?.backend_versions ?? [])
|
||||||
|
|
||||||
|
// 前端版本统计
|
||||||
|
const frontendVersionStatistics = computed(() => versionStatistic.value?.frontend_versions ?? [])
|
||||||
|
|
||||||
|
// 活跃用户统计
|
||||||
|
const activeUsers = computed(() => versionStatistic.value?.active_users ?? {})
|
||||||
|
|
||||||
|
/** 格式化版本安装统计数字为千分位展示。 */
|
||||||
|
function formatVersionStatisticNumber(value: unknown) {
|
||||||
|
const numberValue = Number(value ?? 0)
|
||||||
|
|
||||||
|
if (!Number.isFinite(numberValue)) return '0'
|
||||||
|
|
||||||
|
return numberValue.toLocaleString()
|
||||||
|
}
|
||||||
|
|
||||||
// 打开日志对话框
|
// 打开日志对话框
|
||||||
function showReleaseDialog(title: string, body: string) {
|
function showReleaseDialog(title: string, body: string) {
|
||||||
releaseDialogTitle.value = title
|
releaseDialogTitle.value = title
|
||||||
@@ -91,6 +118,28 @@ function showReleaseDialog(title: string, body: string) {
|
|||||||
releaseDialog.value = true
|
releaseDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 查询版本统计
|
||||||
|
async function queryVersionStatistic() {
|
||||||
|
if (!systemEnv.value.USAGE_STATISTIC_SHARE) return
|
||||||
|
versionStatisticLoading.value = true
|
||||||
|
try {
|
||||||
|
const result: { [key: string]: any } = await api.get('system/usage/statistic')
|
||||||
|
|
||||||
|
versionStatistic.value = result.data ?? {}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
versionStatistic.value = {}
|
||||||
|
} finally {
|
||||||
|
versionStatisticLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开版本统计对话框
|
||||||
|
async function showVersionStatisticDialog() {
|
||||||
|
versionStatisticDialog.value = true
|
||||||
|
await queryVersionStatistic()
|
||||||
|
}
|
||||||
|
|
||||||
// 查询系统环境变量
|
// 查询系统环境变量
|
||||||
async function querySystemEnv() {
|
async function querySystemEnv() {
|
||||||
try {
|
try {
|
||||||
@@ -182,6 +231,18 @@ onMounted(() => {
|
|||||||
{{ t('setting.about.latest') }}
|
{{ t('setting.about.latest') }}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
|
<VTooltip v-if="systemEnv.USAGE_STATISTIC_SHARE" :text="t('setting.about.versionStatistic')">
|
||||||
|
<template #activator="{ props }">
|
||||||
|
<VBtn
|
||||||
|
v-bind="props"
|
||||||
|
icon="mdi-chart-bar"
|
||||||
|
size="x-small"
|
||||||
|
variant="text"
|
||||||
|
class="ms-2 flex-shrink-0"
|
||||||
|
@click="showVersionStatisticDialog"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</VTooltip>
|
||||||
</span>
|
</span>
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
@@ -406,6 +467,86 @@ onMounted(() => {
|
|||||||
<VCardText class="markdown-body" v-html="releaseDialogBody" />
|
<VCardText class="markdown-body" v-html="releaseDialogBody" />
|
||||||
</VCard>
|
</VCard>
|
||||||
</VDialog>
|
</VDialog>
|
||||||
|
<VDialog v-if="versionStatisticDialog" v-model="versionStatisticDialog" width="680" scrollable max-height="85vh">
|
||||||
|
<VCard>
|
||||||
|
<VCardItem>
|
||||||
|
<VDialogCloseBtn @click="versionStatisticDialog = false" />
|
||||||
|
<VCardTitle>
|
||||||
|
<VIcon icon="mdi-chart-bar" class="me-2" />
|
||||||
|
{{ t('setting.about.versionStatisticTitle') }}
|
||||||
|
</VCardTitle>
|
||||||
|
</VCardItem>
|
||||||
|
<VDivider />
|
||||||
|
<VProgressLinear v-if="versionStatisticLoading" indeterminate color="primary" />
|
||||||
|
<VCardText>
|
||||||
|
<div class="version-stat-summary">
|
||||||
|
<div>
|
||||||
|
<div class="text-caption text-medium-emphasis">{{ t('setting.about.totalInstallUsers') }}</div>
|
||||||
|
<div class="version-stat-number">{{ formatVersionStatisticNumber(versionStatistic.total_users) }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-caption text-medium-emphasis">{{ t('setting.about.activeToday') }}</div>
|
||||||
|
<div class="version-stat-number">{{ formatVersionStatisticNumber(activeUsers.today) }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-caption text-medium-emphasis">{{ t('setting.about.active7Days') }}</div>
|
||||||
|
<div class="version-stat-number">{{ formatVersionStatisticNumber(activeUsers.last_7_days) }}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-caption text-medium-emphasis">{{ t('setting.about.active30Days') }}</div>
|
||||||
|
<div class="version-stat-number">{{ formatVersionStatisticNumber(activeUsers.last_30_days) }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-5">
|
||||||
|
<div class="text-subtitle-2 mb-2">{{ t('setting.about.backendVersionStatistic') }}</div>
|
||||||
|
<VTable density="compact">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{ t('setting.about.version') }}</th>
|
||||||
|
<th class="text-end">{{ t('setting.about.users') }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="item in backendVersionStatistics" :key="`backend-${item.version}`">
|
||||||
|
<td>
|
||||||
|
<code>{{ item.version }}</code>
|
||||||
|
</td>
|
||||||
|
<td class="text-end">{{ formatVersionStatisticNumber(item.count) }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="!backendVersionStatistics.length">
|
||||||
|
<td colspan="2" class="text-medium-emphasis">{{ t('setting.about.noVersionStatisticData') }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</VTable>
|
||||||
|
</div>
|
||||||
|
<div class="mt-5">
|
||||||
|
<div class="text-subtitle-2 mb-2">{{ t('setting.about.frontendVersionStatistic') }}</div>
|
||||||
|
<VTable density="compact">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{ t('setting.about.version') }}</th>
|
||||||
|
<th class="text-end">{{ t('setting.about.users') }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="item in frontendVersionStatistics" :key="`frontend-${item.version}`">
|
||||||
|
<td>
|
||||||
|
<code>{{ item.version }}</code>
|
||||||
|
</td>
|
||||||
|
<td class="text-end">{{ formatVersionStatisticNumber(item.count) }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="!frontendVersionStatistics.length">
|
||||||
|
<td colspan="2" class="text-medium-emphasis">{{ t('setting.about.noVersionStatisticData') }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</VTable>
|
||||||
|
</div>
|
||||||
|
<div v-if="versionStatistic.updated_at" class="mt-4 text-caption text-medium-emphasis">
|
||||||
|
{{ t('setting.about.lastUpdated') }}: {{ versionStatistic.updated_at }}
|
||||||
|
</div>
|
||||||
|
</VCardText>
|
||||||
|
</VCard>
|
||||||
|
</VDialog>
|
||||||
</VDialog>
|
</VDialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -422,6 +563,18 @@ onMounted(() => {
|
|||||||
margin-block: 0.5rem 2.5rem;
|
margin-block: 0.5rem 2.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.version-stat-summary {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-stat-number {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
.markdown-body :deep(h1),
|
.markdown-body :deep(h1),
|
||||||
.markdown-body :deep(h2),
|
.markdown-body :deep(h2),
|
||||||
.markdown-body :deep(h3) {
|
.markdown-body :deep(h3) {
|
||||||
|
|||||||
@@ -41,42 +41,69 @@ const otpPassword = ref('')
|
|||||||
|
|
||||||
const allowPasskeyWithoutOtp = computed(() => globalSettingsStore.get('PASSKEY_ALLOW_REGISTER_WITHOUT_OTP') === true)
|
const allowPasskeyWithoutOtp = computed(() => globalSettingsStore.get('PASSKEY_ALLOW_REGISTER_WITHOUT_OTP') === true)
|
||||||
|
|
||||||
|
// OTP 初始化加载状态
|
||||||
|
const otpLoading = ref(false)
|
||||||
|
|
||||||
|
// OTP 初始化失败信息
|
||||||
|
const otpGenerateError = ref('')
|
||||||
|
|
||||||
// 二维码图片 base64
|
// 二维码图片 base64
|
||||||
const qrCodeImage = ref('')
|
const qrCodeImage = ref('')
|
||||||
|
|
||||||
// 二维码信息
|
// 二维码信息
|
||||||
const qrCode = ref('')
|
const qrCode = ref('')
|
||||||
|
|
||||||
// 为当前用户获取Otp Uri
|
// 清空当前 OTP 设置流程的临时数据。
|
||||||
|
function resetOtpSetupState() {
|
||||||
|
qrCodeImage.value = ''
|
||||||
|
qrCode.value = ''
|
||||||
|
otpUri.value = ''
|
||||||
|
secret.value = ''
|
||||||
|
otpGenerateError.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标记 OTP 初始化失败,并向用户显示明确错误。
|
||||||
|
function setOtpGenerateError(message?: string) {
|
||||||
|
const errorMessage = message || t('common.error')
|
||||||
|
otpGenerateError.value = t('profile.otpGenerateFailed', { message: errorMessage })
|
||||||
|
$toast.error(otpGenerateError.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为当前用户获取 OTP URI 并生成二维码图片。
|
||||||
async function getOtpUri() {
|
async function getOtpUri() {
|
||||||
|
resetOtpSetupState()
|
||||||
// 如果已经启用OTP,只打开对话框,不生成新的二维码
|
// 如果已经启用OTP,只打开对话框,不生成新的二维码
|
||||||
if (props.isOtp) {
|
if (props.isOtp) {
|
||||||
qrCode.value = '' // 清空二维码,这样对话框会显示清除界面
|
|
||||||
qrCodeImage.value = ''
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 未启用OTP,生成新的二维码
|
// 未启用OTP,生成新的二维码
|
||||||
|
otpLoading.value = true
|
||||||
try {
|
try {
|
||||||
const result = (await api.post('mfa/otp/generate')) as ApiResponse<{
|
const result = (await api.post('mfa/otp/generate')) as ApiResponse<{
|
||||||
uri: string
|
uri: string
|
||||||
secret: string
|
secret: string
|
||||||
}>
|
}>
|
||||||
if (result.success) {
|
const uri = result.data?.uri?.trim()
|
||||||
otpUri.value = result.data.uri
|
const otpSecret = result.data?.secret?.trim()
|
||||||
secret.value = result.data.secret
|
|
||||||
qrCode.value = result.data.uri
|
if (result.success && uri) {
|
||||||
|
otpUri.value = uri
|
||||||
|
secret.value = otpSecret || ''
|
||||||
|
qrCode.value = uri
|
||||||
// 生成二维码图片
|
// 生成二维码图片
|
||||||
qrCodeImage.value = await QRCode.toDataURL(result.data.uri, {
|
qrCodeImage.value = await QRCode.toDataURL(uri, {
|
||||||
width: 200,
|
width: 200,
|
||||||
margin: 1,
|
margin: 1,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
$toast.error(t('profile.otpGenerateFailed', { message: result.message }))
|
setOtpGenerateError(result.message || 'empty otp uri')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
$toast.error(t('profile.otpGenerateFailed', { message: error instanceof Error ? error.message : String(error) }))
|
setOtpGenerateError(error instanceof Error ? error.message : String(error))
|
||||||
|
} finally {
|
||||||
|
otpLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,13 +172,12 @@ watch(
|
|||||||
otpPassword.value = ''
|
otpPassword.value = ''
|
||||||
} else {
|
} else {
|
||||||
// 弹窗关闭时,清空数据
|
// 弹窗关闭时,清空数据
|
||||||
qrCodeImage.value = ''
|
resetOtpSetupState()
|
||||||
qrCode.value = ''
|
otpLoading.value = false
|
||||||
otpUri.value = ''
|
|
||||||
secret.value = ''
|
|
||||||
otpPassword.value = ''
|
otpPassword.value = ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -193,16 +219,29 @@ watch(
|
|||||||
|
|
||||||
<!-- 设置新的OTP -->
|
<!-- 设置新的OTP -->
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="my-6 rounded text-center p-3 border" style="width: fit-content; margin: 0 auto">
|
<div
|
||||||
<VImg class="mx-auto" :src="qrCodeImage" width="200" height="200">
|
class="my-6 rounded text-center p-3 border d-flex align-center justify-center"
|
||||||
<template #placeholder>
|
style="width: 226px; height: 226px; margin: 0 auto"
|
||||||
<div class="w-full h-full">
|
>
|
||||||
<VSkeletonLoader class="object-cover aspect-w-1 aspect-h-1" />
|
<img
|
||||||
</div>
|
v-if="qrCodeImage"
|
||||||
</template>
|
class="mx-auto d-block otp-qrcode-image"
|
||||||
</VImg>
|
:src="qrCodeImage"
|
||||||
|
:alt="t('profile.setupAuthenticator')"
|
||||||
|
width="200"
|
||||||
|
height="200"
|
||||||
|
/>
|
||||||
|
<VProgressCircular v-else-if="otpLoading" indeterminate color="primary" />
|
||||||
|
<div v-else class="w-100">
|
||||||
|
<VAlert type="error" variant="tonal" density="compact" class="mb-3">
|
||||||
|
{{ otpGenerateError || t('profile.otpGenerateFailed', { message: t('common.error') }) }}
|
||||||
|
</VAlert>
|
||||||
|
<VBtn size="small" variant="tonal" prepend-icon="mdi-refresh" @click="getOtpUri">
|
||||||
|
{{ t('common.retry') }}
|
||||||
|
</VBtn>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<VAlert :title="secret" variant="tonal" type="warning" class="my-4" :text="t('profile.secretKeyTip')">
|
<VAlert v-if="secret" :title="secret" variant="tonal" type="warning" class="my-4" :text="t('profile.secretKeyTip')">
|
||||||
<template #prepend />
|
<template #prepend />
|
||||||
</VAlert>
|
</VAlert>
|
||||||
<VForm @submit.prevent="judgeOtpPassword">
|
<VForm @submit.prevent="judgeOtpPassword">
|
||||||
@@ -220,7 +259,7 @@ watch(
|
|||||||
<VBtn variant="outlined" color="secondary" @click="show = false">
|
<VBtn variant="outlined" color="secondary" @click="show = false">
|
||||||
{{ t('common.cancel') }}
|
{{ t('common.cancel') }}
|
||||||
</VBtn>
|
</VBtn>
|
||||||
<VBtn type="submit">
|
<VBtn type="submit" :disabled="!otpUri || otpLoading">
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<VIcon icon="mdi-check" />
|
<VIcon icon="mdi-check" />
|
||||||
</template>
|
</template>
|
||||||
@@ -233,3 +272,10 @@ watch(
|
|||||||
</VCard>
|
</VCard>
|
||||||
</VDialog>
|
</VDialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.otp-qrcode-image {
|
||||||
|
inline-size: 200px;
|
||||||
|
block-size: 200px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -10,27 +10,121 @@ const display = useDisplay()
|
|||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const $toast = useToast()
|
const $toast = useToast()
|
||||||
|
|
||||||
|
type EditorMode = 'list' | 'text'
|
||||||
|
|
||||||
|
interface RepoParseResult {
|
||||||
|
repos: string[]
|
||||||
|
invalidRepos: string[]
|
||||||
|
duplicateRepos: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const editorMode = ref<EditorMode>('list')
|
||||||
const repoList = ref<string[]>([])
|
const repoList = ref<string[]>([])
|
||||||
|
const repoText = ref('')
|
||||||
const newRepoUrl = ref('')
|
const newRepoUrl = ref('')
|
||||||
const editingIndex = ref<number | null>(null)
|
const editingIndex = ref<number | null>(null)
|
||||||
const editingUrl = ref('')
|
const editingUrl = ref('')
|
||||||
|
|
||||||
const emit = defineEmits(['save', 'close'])
|
const emit = defineEmits(['save', 'close'])
|
||||||
|
|
||||||
|
const parsedTextRepos = computed(() => parseRepoInput(repoText.value))
|
||||||
|
const activeRepoCount = computed(() => (editorMode.value === 'text' ? parsedTextRepos.value.repos.length : repoList.value.length))
|
||||||
|
const saveDisabled = computed(
|
||||||
|
() => activeRepoCount.value === 0 || (editorMode.value === 'text' && parsedTextRepos.value.invalidRepos.length > 0),
|
||||||
|
)
|
||||||
|
|
||||||
|
/** 判断仓库地址是否为可保存的 HTTP URL。 */
|
||||||
|
function isValidRepoUrl(url: string) {
|
||||||
|
return /^https?:\/\//i.test(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将粘贴的仓库地址文本解析为有效、无效和重复地址列表。 */
|
||||||
|
function parseRepoInput(value: string): RepoParseResult {
|
||||||
|
const repos: string[] = []
|
||||||
|
const invalidRepos: string[] = []
|
||||||
|
const duplicateRepos: string[] = []
|
||||||
|
const seenRepos = new Set<string>()
|
||||||
|
|
||||||
|
value
|
||||||
|
.split(/[\n,,]+/)
|
||||||
|
.map(repo => repo.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.forEach(repo => {
|
||||||
|
if (!isValidRepoUrl(repo)) {
|
||||||
|
invalidRepos.push(repo)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seenRepos.has(repo)) {
|
||||||
|
duplicateRepos.push(repo)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
seenRepos.add(repo)
|
||||||
|
repos.push(repo)
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
repos,
|
||||||
|
invalidRepos,
|
||||||
|
duplicateRepos: [...new Set(duplicateRepos)],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将列表模式中的仓库地址同步到文本模式。 */
|
||||||
|
function syncTextFromList() {
|
||||||
|
repoText.value = repoList.value.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将文本模式中的仓库地址同步到列表模式,并忽略无法加入列表的无效地址。 */
|
||||||
|
function syncListFromText() {
|
||||||
|
const result = parseRepoInput(repoText.value)
|
||||||
|
|
||||||
|
repoList.value = result.repos
|
||||||
|
syncTextFromList()
|
||||||
|
|
||||||
|
if (result.invalidRepos.length > 0) {
|
||||||
|
$toast.warning(t('dialog.pluginMarketSetting.invalidTextIgnored', { count: result.invalidRepos.length }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 切换仓库维护模式,并在切换时同步当前模式的编辑内容。 */
|
||||||
|
function switchEditorMode(mode: EditorMode | undefined) {
|
||||||
|
if (!mode || mode === editorMode.value) return
|
||||||
|
|
||||||
|
if (editorMode.value === 'text') {
|
||||||
|
syncListFromText()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode === 'text') {
|
||||||
|
syncTextFromList()
|
||||||
|
}
|
||||||
|
|
||||||
|
editorMode.value = mode
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 加载插件市场仓库配置。 */
|
||||||
async function queryMarketRepoSetting() {
|
async function queryMarketRepoSetting() {
|
||||||
try {
|
try {
|
||||||
const result: { [key: string]: any } = await api.get('system/setting/PLUGIN_MARKET')
|
const result: { [key: string]: any } = await api.get('system/setting/PLUGIN_MARKET')
|
||||||
if (result && result.data && result.data.value) {
|
if (result && result.data && result.data.value) {
|
||||||
repoList.value = result.data.value.split(',').filter((repo: string) => repo.trim() !== '')
|
repoList.value = parseRepoInput(result.data.value).repos
|
||||||
|
syncTextFromList()
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 保存插件市场仓库配置。 */
|
||||||
async function saveHandle() {
|
async function saveHandle() {
|
||||||
try {
|
try {
|
||||||
const repoStringToSave = repoList.value.join(',')
|
const reposToSave = normalizeCurrentRepos()
|
||||||
|
if (!reposToSave) return
|
||||||
|
|
||||||
|
const repoStringToSave = reposToSave.join(',')
|
||||||
const result: { [key: string]: any } = await api.post('system/setting/PLUGIN_MARKET', repoStringToSave)
|
const result: { [key: string]: any } = await api.post('system/setting/PLUGIN_MARKET', repoStringToSave)
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
@@ -42,54 +136,88 @@ async function saveHandle() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 获取当前维护模式下可保存的仓库地址。 */
|
||||||
|
function normalizeCurrentRepos() {
|
||||||
|
if (editorMode.value === 'text') {
|
||||||
|
const result = parseRepoInput(repoText.value)
|
||||||
|
|
||||||
|
if (result.invalidRepos.length > 0) {
|
||||||
|
$toast.error(t('dialog.pluginMarketSetting.invalidText', { count: result.invalidRepos.length }))
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
repoList.value = result.repos
|
||||||
|
syncTextFromList()
|
||||||
|
|
||||||
|
return result.repos
|
||||||
|
}
|
||||||
|
|
||||||
|
return repoList.value
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 校验单个仓库地址是否可以加入或更新到列表。 */
|
||||||
|
function validateRepoUrl(url: string, editingRepoIndex: number | null = null) {
|
||||||
|
if (!url) return false
|
||||||
|
|
||||||
|
if (!isValidRepoUrl(url)) {
|
||||||
|
$toast.error(t('dialog.pluginMarketSetting.invalidUrl'))
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const duplicated = repoList.value.some((repo, index) => repo === url && index !== editingRepoIndex)
|
||||||
|
if (duplicated) {
|
||||||
|
$toast.error(t('dialog.pluginMarketSetting.duplicateUrl'))
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 添加一个仓库地址到列表。 */
|
||||||
function addRepo() {
|
function addRepo() {
|
||||||
const url = newRepoUrl.value.trim()
|
const url = newRepoUrl.value.trim()
|
||||||
if (!url) return
|
if (!validateRepoUrl(url)) return
|
||||||
|
|
||||||
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
|
||||||
$toast.error(t('dialog.pluginMarketSetting.invalidUrl'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (repoList.value.includes(url)) {
|
|
||||||
$toast.error(t('dialog.pluginMarketSetting.duplicateUrl'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
repoList.value.push(url)
|
repoList.value.push(url)
|
||||||
newRepoUrl.value = ''
|
newRepoUrl.value = ''
|
||||||
|
syncTextFromList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 从列表中删除一个仓库地址。 */
|
||||||
function removeRepo(index: number) {
|
function removeRepo(index: number) {
|
||||||
repoList.value.splice(index, 1)
|
repoList.value.splice(index, 1)
|
||||||
|
syncTextFromList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 进入指定仓库地址的行内编辑状态。 */
|
||||||
function startEdit(index: number) {
|
function startEdit(index: number) {
|
||||||
editingIndex.value = index
|
editingIndex.value = index
|
||||||
editingUrl.value = repoList.value[index]
|
editingUrl.value = repoList.value[index]
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveEdit() {
|
/** 保存当前行内编辑的仓库地址。 */
|
||||||
if (editingIndex.value === null) return
|
function saveEdit(index = editingIndex.value) {
|
||||||
|
if (index === null) return
|
||||||
|
|
||||||
const url = editingUrl.value.trim()
|
const url = editingUrl.value.trim()
|
||||||
if (!url) return
|
if (!validateRepoUrl(url, index)) return
|
||||||
|
|
||||||
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
repoList.value[index] = url
|
||||||
$toast.error(t('dialog.pluginMarketSetting.invalidUrl'))
|
syncTextFromList()
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
repoList.value[editingIndex.value] = url
|
|
||||||
editingIndex.value = null
|
editingIndex.value = null
|
||||||
editingUrl.value = ''
|
editingUrl.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 取消当前行内编辑状态。 */
|
||||||
function cancelEdit() {
|
function cancelEdit() {
|
||||||
editingIndex.value = null
|
editingIndex.value = null
|
||||||
editingUrl.value = ''
|
editingUrl.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 将仓库地址格式化为更易扫描的显示名称。 */
|
||||||
function formatRepoDisplay(url: string) {
|
function formatRepoDisplay(url: string) {
|
||||||
try {
|
try {
|
||||||
const parsedUrl = new URL(url)
|
const parsedUrl = new URL(url)
|
||||||
@@ -108,6 +236,7 @@ function formatRepoDisplay(url: string) {
|
|||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 返回拖拽列表项的稳定键。 */
|
||||||
function repoItemKey(repo: string) {
|
function repoItemKey(repo: string) {
|
||||||
return repo
|
return repo
|
||||||
}
|
}
|
||||||
@@ -118,108 +247,192 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<VDialog width="50rem" scrollable :fullscreen="!display.mdAndUp.value">
|
<VDialog width="56rem" :fullscreen="!display.mdAndUp.value">
|
||||||
<VCard class="plugin-market-dialog-card">
|
<VCard class="plugin-market-dialog-card">
|
||||||
<VCardItem>
|
<VCardItem class="plugin-market-card-item">
|
||||||
<VCardTitle>
|
<div class="plugin-market-header">
|
||||||
<VIcon icon="mdi-store-cog" class="me-2" />
|
<VCardTitle class="plugin-market-title d-flex align-center pa-0">
|
||||||
{{ t('dialog.pluginMarketSetting.title') }}
|
<VIcon icon="mdi-store-cog" class="me-2" />
|
||||||
</VCardTitle>
|
{{ t('dialog.pluginMarketSetting.title') }}
|
||||||
|
</VCardTitle>
|
||||||
|
</div>
|
||||||
<VDialogCloseBtn @click="emit('close')" />
|
<VDialogCloseBtn @click="emit('close')" />
|
||||||
</VCardItem>
|
</VCardItem>
|
||||||
<VDivider />
|
|
||||||
<VCardText class="plugin-market-dialog-body pt-4">
|
<VCardText class="plugin-market-dialog-body pt-4">
|
||||||
<div class="plugin-market-input mb-4">
|
<div class="plugin-market-toolbar">
|
||||||
<VTextField
|
<VBtnToggle
|
||||||
v-model="newRepoUrl"
|
:model-value="editorMode"
|
||||||
density="compact"
|
mandatory
|
||||||
:placeholder="t('dialog.pluginMarketSetting.urlPlaceholder')"
|
color="primary"
|
||||||
prepend-inner-icon="mdi-link-plus"
|
density="comfortable"
|
||||||
clearable
|
variant="tonal"
|
||||||
@keyup.enter="addRepo"
|
class="plugin-market-mode-toggle"
|
||||||
|
@update:model-value="switchEditorMode"
|
||||||
>
|
>
|
||||||
<template #append>
|
<VBtn value="list" prepend-icon="mdi-format-list-bulleted">
|
||||||
<VBtn icon="mdi-plus" variant="text" color="primary" @click="addRepo" />
|
{{ t('dialog.pluginMarketSetting.listMode') }}
|
||||||
</template>
|
</VBtn>
|
||||||
</VTextField>
|
<VBtn value="text" prepend-icon="mdi-text-box-edit-outline">
|
||||||
|
{{ t('dialog.pluginMarketSetting.textMode') }}
|
||||||
|
</VBtn>
|
||||||
|
</VBtnToggle>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="plugin-market-list-wrap">
|
<div v-if="editorMode === 'list'" class="plugin-market-list-panel">
|
||||||
<VList v-if="repoList.length > 0" class="px-0">
|
<div class="plugin-market-input">
|
||||||
<draggable
|
<VTextField
|
||||||
v-model="repoList"
|
v-model="newRepoUrl"
|
||||||
:item-key="repoItemKey"
|
density="compact"
|
||||||
handle=".drag-handle"
|
:placeholder="t('dialog.pluginMarketSetting.urlPlaceholder')"
|
||||||
animation="200"
|
prepend-inner-icon="mdi-link-plus"
|
||||||
:disabled="editingIndex !== null"
|
clearable
|
||||||
|
hide-details
|
||||||
|
@keyup.enter="addRepo"
|
||||||
>
|
>
|
||||||
<template #item="{ element: repo, index }">
|
<template #append>
|
||||||
<div>
|
<VBtn
|
||||||
<VListItem class="py-2">
|
icon="mdi-plus"
|
||||||
<template #prepend>
|
variant="tonal"
|
||||||
<VBtn
|
color="primary"
|
||||||
icon="mdi-drag-vertical"
|
:aria-label="t('dialog.pluginMarketSetting.addRepo')"
|
||||||
size="small"
|
@click="addRepo"
|
||||||
variant="text"
|
/>
|
||||||
color="primary"
|
</template>
|
||||||
class="drag-handle me-2"
|
</VTextField>
|
||||||
:disabled="editingIndex !== null"
|
</div>
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<VListItemTitle v-if="editingIndex !== index">
|
<div class="plugin-market-list-wrap">
|
||||||
<span class="text-truncate" :title="repo">{{ formatRepoDisplay(repo) }}</span>
|
<VList v-if="repoList.length > 0" class="plugin-market-repo-list px-0">
|
||||||
</VListItemTitle>
|
<draggable
|
||||||
|
v-model="repoList"
|
||||||
<VTextField
|
:item-key="repoItemKey"
|
||||||
v-else
|
handle=".drag-handle"
|
||||||
v-model="editingUrl"
|
animation="200"
|
||||||
density="compact"
|
:disabled="editingIndex !== null"
|
||||||
variant="outlined"
|
@end="syncTextFromList"
|
||||||
hide-details
|
>
|
||||||
@keyup.enter="saveEdit"
|
<template #item="{ element: repo, index }">
|
||||||
@keyup.escape="cancelEdit"
|
<div>
|
||||||
/>
|
<VListItem class="plugin-market-repo-item py-3">
|
||||||
|
<template #prepend>
|
||||||
<template #append v-if="editingIndex !== index">
|
<VBtn
|
||||||
<div class="d-flex align-center">
|
icon="mdi-drag-vertical"
|
||||||
<IconBtn icon="mdi-pencil" size="small" variant="text" @click="startEdit(index)" />
|
|
||||||
<IconBtn
|
|
||||||
icon="mdi-delete"
|
|
||||||
size="small"
|
size="small"
|
||||||
variant="text"
|
variant="text"
|
||||||
color="error"
|
color="primary"
|
||||||
@click="removeRepo(index)"
|
class="drag-handle me-2"
|
||||||
|
:disabled="editingIndex !== null"
|
||||||
/>
|
/>
|
||||||
</div>
|
</template>
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #append v-else>
|
<template v-if="editingIndex !== index">
|
||||||
<div class="d-flex align-center">
|
<VListItemTitle>
|
||||||
<IconBtn icon="mdi-check" size="small" variant="text" color="success" @click="saveEdit" />
|
<div class="plugin-market-repo-title">
|
||||||
<IconBtn icon="mdi-close" size="small" variant="text" @click="cancelEdit" />
|
<span class="plugin-market-repo-index">{{ index + 1 }}</span>
|
||||||
</div>
|
<span class="plugin-market-repo-name" :title="repo">{{ formatRepoDisplay(repo) }}</span>
|
||||||
</template>
|
</div>
|
||||||
</VListItem>
|
</VListItemTitle>
|
||||||
<VDivider v-if="index < repoList.length - 1" class="mx-4" />
|
<VListItemSubtitle class="plugin-market-repo-url mt-1" :title="repo">
|
||||||
</div>
|
{{ repo }}
|
||||||
</template>
|
</VListItemSubtitle>
|
||||||
</draggable>
|
</template>
|
||||||
</VList>
|
|
||||||
|
|
||||||
<div v-else class="text-center text-medium-emphasis py-8">
|
<VTextField
|
||||||
<VIcon icon="mdi-folder-open-outline" size="48" class="mb-2" />
|
v-else
|
||||||
<div>{{ t('dialog.pluginMarketSetting.noRepos') }}</div>
|
v-model="editingUrl"
|
||||||
|
density="compact"
|
||||||
|
variant="outlined"
|
||||||
|
hide-details
|
||||||
|
autofocus
|
||||||
|
@keyup.enter="saveEdit(index)"
|
||||||
|
@keyup.escape="cancelEdit"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<template #append v-if="editingIndex !== index">
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<IconBtn icon="mdi-pencil" size="small" variant="text" @click="startEdit(index)" />
|
||||||
|
<IconBtn
|
||||||
|
icon="mdi-delete"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
color="error"
|
||||||
|
@click="removeRepo(index)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #append v-else>
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<VBtn
|
||||||
|
icon="mdi-check"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
color="success"
|
||||||
|
@click.stop="saveEdit(index)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</VListItem>
|
||||||
|
<VDivider v-if="index < repoList.length - 1" class="mx-4" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</draggable>
|
||||||
|
</VList>
|
||||||
|
|
||||||
|
<div v-else class="plugin-market-empty text-center text-medium-emphasis">
|
||||||
|
<VIcon icon="mdi-source-repository-multiple" size="48" class="mb-2" />
|
||||||
|
<div>{{ t('dialog.pluginMarketSetting.noRepos') }}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="plugin-market-text-panel">
|
||||||
|
<div class="plugin-market-textarea-field">
|
||||||
|
<VIcon icon="mdi-text-box-edit-outline" class="plugin-market-textarea-icon" />
|
||||||
|
<textarea
|
||||||
|
v-model="repoText"
|
||||||
|
class="plugin-market-textarea"
|
||||||
|
:placeholder="t('dialog.pluginMarketSetting.textPlaceholder')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="plugin-market-text-hint">
|
||||||
|
{{ t('dialog.pluginMarketSetting.textHint') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<VAlert
|
||||||
|
v-if="parsedTextRepos.invalidRepos.length > 0"
|
||||||
|
type="error"
|
||||||
|
variant="tonal"
|
||||||
|
density="compact"
|
||||||
|
class="plugin-market-invalid-alert"
|
||||||
|
>
|
||||||
|
<div>{{ t('dialog.pluginMarketSetting.invalidText', { count: parsedTextRepos.invalidRepos.length }) }}</div>
|
||||||
|
<div class="text-truncate">
|
||||||
|
{{ parsedTextRepos.invalidRepos.slice(0, 3).join(', ') }}
|
||||||
|
</div>
|
||||||
|
</VAlert>
|
||||||
|
|
||||||
|
<VAlert
|
||||||
|
v-else-if="parsedTextRepos.duplicateRepos.length > 0"
|
||||||
|
type="warning"
|
||||||
|
variant="tonal"
|
||||||
|
density="compact"
|
||||||
|
>
|
||||||
|
{{ t('dialog.pluginMarketSetting.duplicateTextIgnored') }}
|
||||||
|
</VAlert>
|
||||||
|
</div>
|
||||||
</VCardText>
|
</VCardText>
|
||||||
<VCardActions>
|
|
||||||
|
<VCardActions class="plugin-market-actions">
|
||||||
<VSpacer />
|
<VSpacer />
|
||||||
<VBtn
|
<VBtn
|
||||||
|
color="primary"
|
||||||
|
variant="flat"
|
||||||
@click="saveHandle"
|
@click="saveHandle"
|
||||||
prepend-icon="mdi-content-save-check"
|
prepend-icon="mdi-content-save-check"
|
||||||
class="px-5 me-3"
|
class="px-5"
|
||||||
:disabled="repoList.length === 0"
|
:disabled="saveDisabled"
|
||||||
>
|
>
|
||||||
{{ t('dialog.pluginMarketSetting.save') }}
|
{{ t('dialog.pluginMarketSetting.save') }}
|
||||||
</VBtn>
|
</VBtn>
|
||||||
@@ -232,6 +445,24 @@ onMounted(() => {
|
|||||||
.plugin-market-dialog-card {
|
.plugin-market-dialog-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
block-size: min(82vh, 50rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-card-item {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
padding-block: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
padding-inline-end: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-title {
|
||||||
|
min-inline-size: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plugin-market-dialog-body {
|
.plugin-market-dialog-body {
|
||||||
@@ -239,6 +470,31 @@ onMounted(() => {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
gap: 0.875rem;
|
||||||
|
min-block-size: 0;
|
||||||
|
padding-block: 0.875rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-toolbar {
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-mode-toggle {
|
||||||
|
inline-size: 100%;
|
||||||
|
|
||||||
|
:deep(.v-btn) {
|
||||||
|
flex: 1;
|
||||||
|
min-inline-size: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-list-panel,
|
||||||
|
.plugin-market-text-panel {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
min-block-size: 0;
|
min-block-size: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,7 +504,173 @@ onMounted(() => {
|
|||||||
|
|
||||||
.plugin-market-list-wrap {
|
.plugin-market-list-wrap {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(var(--v-theme-surface), 0.72);
|
||||||
min-block-size: 0;
|
min-block-size: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.plugin-market-repo-list {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-repo-item {
|
||||||
|
min-block-size: 4.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-repo-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
min-inline-size: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-repo-name,
|
||||||
|
.plugin-market-repo-url {
|
||||||
|
display: -webkit-box;
|
||||||
|
overflow: hidden;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
line-break: anywhere;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
white-space: normal;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-repo-url {
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-repo-index {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
color: rgba(var(--v-theme-on-surface), 0.48);
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
inline-size: 1.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-empty {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
min-block-size: 14rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-textarea-field {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(var(--v-theme-surface), 0.72);
|
||||||
|
min-block-size: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
border-color: rgb(var(--v-theme-primary));
|
||||||
|
box-shadow: 0 0 0 1px rgb(var(--v-theme-primary));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-textarea-icon {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
color: rgba(var(--v-theme-on-surface), 0.62);
|
||||||
|
inset-block-start: 1.25rem;
|
||||||
|
inset-inline-start: 1rem;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-textarea {
|
||||||
|
flex: 1;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
block-size: 100%;
|
||||||
|
color: rgb(var(--v-theme-on-surface));
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
min-block-size: 0;
|
||||||
|
outline: none;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 1rem 1rem 1rem 3.25rem;
|
||||||
|
resize: none;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-text-hint {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
color: rgba(var(--v-theme-on-surface), 0.62);
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
padding-inline: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-invalid-alert {
|
||||||
|
:deep(.v-alert__content) {
|
||||||
|
min-inline-size: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-actions {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.75rem 1.5rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.plugin-market-dialog-card {
|
||||||
|
block-size: 100dvh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-card-item {
|
||||||
|
padding: 0.75rem 1rem 0.625rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-header {
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding-inline-end: 2.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-header :deep(.v-card-title) {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-dialog-body {
|
||||||
|
gap: 0.625rem;
|
||||||
|
padding: 0.75rem 1rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-mode-toggle {
|
||||||
|
inline-size: 100%;
|
||||||
|
|
||||||
|
:deep(.v-btn) {
|
||||||
|
flex: 1;
|
||||||
|
min-inline-size: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-list-panel,
|
||||||
|
.plugin-market-text-panel {
|
||||||
|
gap: 0.625rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-list-wrap {
|
||||||
|
min-block-size: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-empty {
|
||||||
|
min-block-size: 10rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plugin-market-actions {
|
||||||
|
padding: 0.75rem 1rem calc(0.75rem + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
ManualTransferPayload,
|
ManualTransferPayload,
|
||||||
ManualTransferPreviewData,
|
ManualTransferPreviewData,
|
||||||
ManualTransferPreviewItem,
|
ManualTransferPreviewItem,
|
||||||
|
ManualTransferTargetPathData,
|
||||||
StorageConf,
|
StorageConf,
|
||||||
TransferDirectoryConf,
|
TransferDirectoryConf,
|
||||||
TransferForm,
|
TransferForm,
|
||||||
@@ -113,6 +114,14 @@ const episodeFormatRecommendState = reactive<{
|
|||||||
|
|
||||||
const episodeFormatRuleConfigured = ref<boolean | undefined>(undefined)
|
const episodeFormatRuleConfigured = ref<boolean | undefined>(undefined)
|
||||||
|
|
||||||
|
interface ManualTransferTargetPathRequest {
|
||||||
|
fileitem?: FileItem
|
||||||
|
fileitems?: FileItem[]
|
||||||
|
logid?: number
|
||||||
|
logids?: number[]
|
||||||
|
target_storage?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
// 生成文件项稳定键,用于去重和状态同步。
|
// 生成文件项稳定键,用于去重和状态同步。
|
||||||
function getFileItemKey(item?: FileItem) {
|
function getFileItemKey(item?: FileItem) {
|
||||||
return [item?.storage ?? '', item?.type ?? '', item?.path ?? ''].join('|')
|
return [item?.storage ?? '', item?.type ?? '', item?.path ?? ''].join('|')
|
||||||
@@ -265,7 +274,7 @@ const transferForm = reactive<TransferForm>({
|
|||||||
fileitem: {} as FileItem,
|
fileitem: {} as FileItem,
|
||||||
logid: 0,
|
logid: 0,
|
||||||
target_storage: props.target_storage ?? 'local',
|
target_storage: props.target_storage ?? 'local',
|
||||||
target_path: props.target_path ?? '',
|
target_path: normalizeTargetPath(props.target_path),
|
||||||
transfer_type: '',
|
transfer_type: '',
|
||||||
min_filesize: 0,
|
min_filesize: 0,
|
||||||
scrape: false,
|
scrape: false,
|
||||||
@@ -292,6 +301,79 @@ const targetDirectories = computed(() => {
|
|||||||
return [...new Set(libraryDirectories)]
|
return [...new Set(libraryDirectories)]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 构造目的路径自动匹配请求,只传用户真实上下文,避免用默认存储误导后端匹配。
|
||||||
|
function createTargetPathMatchRequest(): ManualTransferTargetPathRequest | undefined {
|
||||||
|
const payload: ManualTransferTargetPathRequest = {}
|
||||||
|
|
||||||
|
if (props.target_storage) {
|
||||||
|
payload.target_storage = props.target_storage
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedItems.value.length === 1) {
|
||||||
|
payload.fileitem = normalizedItems.value[0]
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedItems.value.length > 1) {
|
||||||
|
payload.fileitems = normalizedItems.value
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.logids?.length) {
|
||||||
|
if (props.logids.length > 1) {
|
||||||
|
payload.logids = props.logids
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
|
payload.logid = props.logids[0]
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用后端匹配到的目的路径配置,未匹配时保持 null 等待用户手工选择。
|
||||||
|
function applyMatchedTargetPath(data?: ManualTransferTargetPathData) {
|
||||||
|
const matchedTargetPath = normalizeTargetPath(data?.target_path)
|
||||||
|
if (!matchedTargetPath) {
|
||||||
|
transferForm.target_path = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
transferForm.target_storage = data?.target_storage || transferForm.target_storage || 'local'
|
||||||
|
transferForm.transfer_type = data?.transfer_type || transferForm.transfer_type
|
||||||
|
transferForm.scrape = data?.scrape ?? false
|
||||||
|
transferForm.library_type_folder = data?.library_type_folder ?? false
|
||||||
|
transferForm.library_category_folder = data?.library_category_folder ?? false
|
||||||
|
transferForm.target_path = matchedTargetPath
|
||||||
|
}
|
||||||
|
|
||||||
|
// 请求后端按源目录匹配最合适的手动整理目的路径。
|
||||||
|
async function autoSelectTargetPath() {
|
||||||
|
if (normalizeTargetPath(props.target_path) || transferForm.target_path) return
|
||||||
|
|
||||||
|
const payload = createTargetPathMatchRequest()
|
||||||
|
if (!payload) {
|
||||||
|
transferForm.target_path = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await api.post<ApiResponse<ManualTransferTargetPathData>, ApiResponse<ManualTransferTargetPathData>>(
|
||||||
|
'transfer/manual/target-path',
|
||||||
|
payload,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
transferForm.target_path = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
applyMatchedTargetPath(result.data)
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
transferForm.target_path = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 监听目的路径变化,配置默认值
|
// 监听目的路径变化,配置默认值
|
||||||
watch(
|
watch(
|
||||||
() => transferForm.target_path,
|
() => transferForm.target_path,
|
||||||
@@ -397,6 +479,12 @@ function getUniqueValues(values: (string | undefined)[]) {
|
|||||||
return [...new Set(values.map(item => item?.trim()).filter(Boolean) as string[])]
|
return [...new Set(values.map(item => item?.trim()).filter(Boolean) as string[])]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 归一化可选目的路径,保证未指定时向接口传递 null 而不是空字符串。
|
||||||
|
function normalizeTargetPath(path?: string | null) {
|
||||||
|
const normalizedPath = path?.trim()
|
||||||
|
return normalizedPath || null
|
||||||
|
}
|
||||||
|
|
||||||
// 统一解析接口返回的数字字段,兼容 string/number
|
// 统一解析接口返回的数字字段,兼容 string/number
|
||||||
function toPreviewNumber(value: unknown) {
|
function toPreviewNumber(value: unknown) {
|
||||||
if (value === undefined || value === null || value === '') return undefined
|
if (value === undefined || value === null || value === '') return undefined
|
||||||
@@ -511,6 +599,22 @@ const previewFileRows = computed(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 标准化预览项中的识别词命中详情
|
||||||
|
function getPreviewApplyWords(item: ManualTransferPreviewItem) {
|
||||||
|
return (item.apply_words ?? []).filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 手动整理识别词应用详情
|
||||||
|
const previewCustomWordDetails = computed(() => {
|
||||||
|
return filteredPreviewItems.value
|
||||||
|
.map(item => ({
|
||||||
|
sourceName: getFileName(item.source),
|
||||||
|
orgString: item.org_string,
|
||||||
|
applyWords: getPreviewApplyWords(item),
|
||||||
|
}))
|
||||||
|
.filter(item => item.applyWords.length > 0)
|
||||||
|
})
|
||||||
|
|
||||||
// 是否需要拓宽窗口
|
// 是否需要拓宽窗口
|
||||||
const previewNeedsWideLayout = computed(() => {
|
const previewNeedsWideLayout = computed(() => {
|
||||||
const candidates = [...previewFileRows.value.map(item => `${item.sourceName}${item.targetName}`)]
|
const candidates = [...previewFileRows.value.map(item => `${item.sourceName}${item.targetName}`)]
|
||||||
@@ -620,6 +724,7 @@ function createTransferPayload(options: { item?: FileItem; items?: FileItem[]; l
|
|||||||
...transferForm,
|
...transferForm,
|
||||||
fileitem: sourceItem,
|
fileitem: sourceItem,
|
||||||
logid: options.logid ?? 0,
|
logid: options.logid ?? 0,
|
||||||
|
target_path: normalizeTargetPath(transferForm.target_path),
|
||||||
episode_group: transferForm.episode_group?.trim() || null,
|
episode_group: transferForm.episode_group?.trim() || null,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1099,8 +1204,9 @@ async function transfer(background: boolean = false) {
|
|||||||
emit('done')
|
emit('done')
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
loadDirectories()
|
await loadDirectories()
|
||||||
|
await autoSelectTargetPath()
|
||||||
loadStorages()
|
loadStorages()
|
||||||
loadEpisodeFormatRuleConfiguration()
|
loadEpisodeFormatRuleConfiguration()
|
||||||
})
|
})
|
||||||
@@ -1439,6 +1545,36 @@ onUnmounted(() => {
|
|||||||
<span class="preview-overview-card__value">{{ previewEpisodeCountText }}</span>
|
<span class="preview-overview-card__value">{{ previewEpisodeCountText }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="previewCustomWordDetails.length" class="preview-custom-words">
|
||||||
|
<div class="preview-custom-words__title">
|
||||||
|
<VIcon icon="mdi-tag-text-outline" size="16" />
|
||||||
|
<span>{{ t('dialog.reorganize.customWordsApplied') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="preview-custom-words__items">
|
||||||
|
<div
|
||||||
|
v-for="(detail, index) in previewCustomWordDetails"
|
||||||
|
:key="`${detail.sourceName}-${index}`"
|
||||||
|
class="preview-custom-words__item"
|
||||||
|
>
|
||||||
|
<div class="preview-custom-words__source">{{ detail.sourceName }}</div>
|
||||||
|
<div v-if="detail.orgString" class="preview-custom-words__original">
|
||||||
|
{{ detail.orgString }}
|
||||||
|
</div>
|
||||||
|
<div class="preview-custom-words__chips">
|
||||||
|
<VChip
|
||||||
|
v-for="(word, wordIndex) in detail.applyWords"
|
||||||
|
:key="`${word}-${wordIndex}`"
|
||||||
|
variant="outlined"
|
||||||
|
color="info"
|
||||||
|
size="small"
|
||||||
|
class="preview-custom-words__chip"
|
||||||
|
>
|
||||||
|
{{ word }}
|
||||||
|
</VChip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="reorganize-preview-list">
|
<div class="reorganize-preview-list">
|
||||||
<div v-if="pagedPreviewRows.length" ref="previewFileBodyRef" class="preview-file-body">
|
<div v-if="pagedPreviewRows.length" ref="previewFileBodyRef" class="preview-file-body">
|
||||||
@@ -1698,6 +1834,66 @@ onUnmounted(() => {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.preview-custom-words {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding-block: 0.875rem;
|
||||||
|
padding-inline: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-custom-words__title {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
color: rgb(var(--v-theme-info));
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
gap: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-custom-words__items {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
min-inline-size: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-custom-words__item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.375rem;
|
||||||
|
min-inline-size: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-custom-words__source {
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
color: rgb(var(--v-theme-on-surface));
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-custom-words__original {
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-custom-words__chips {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.375rem;
|
||||||
|
min-inline-size: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-custom-words__chip {
|
||||||
|
max-inline-size: 100%;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
.reorganize-preview-pane__scroll {
|
.reorganize-preview-pane__scroll {
|
||||||
display: flex;
|
display: flex;
|
||||||
overflow: hidden auto;
|
overflow: hidden auto;
|
||||||
@@ -1797,11 +1993,13 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.preview-file-row__path {
|
.preview-file-row__path {
|
||||||
overflow: hidden;
|
overflow: visible;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
||||||
font-size: 0.8125rem;
|
font-size: 0.8125rem;
|
||||||
text-overflow: ellipsis;
|
line-height: 1.4;
|
||||||
white-space: nowrap;
|
white-space: normal;
|
||||||
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-file-row__card--target .preview-file-row__name {
|
.preview-file-row__card--target .preview-file-row__name {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const props = defineProps({
|
|||||||
type: Array as PropType<Site[]>,
|
type: Array as PropType<Site[]>,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
selected: Array as PropType<Number[]>,
|
selected: Array as PropType<number[]>,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 定义事件
|
// 定义事件
|
||||||
@@ -20,38 +20,66 @@ const emit = defineEmits(['close', 'search', 'reload'])
|
|||||||
const siteFilter = ref('')
|
const siteFilter = ref('')
|
||||||
|
|
||||||
// 已选择站点
|
// 已选择站点
|
||||||
const selectedSites = ref<any[]>(props.selected || [])
|
const selectedSites = ref<number[]>([])
|
||||||
|
|
||||||
|
// 根据当前可用站点清理选中项,避免停用或已删除站点参与计数。
|
||||||
|
function normalizeSelectedSites(selectedSiteIds: number[] = []) {
|
||||||
|
const availableSiteIds = new Set(props.sites.map((site: Site) => site.id))
|
||||||
|
const normalizedSiteIds: number[] = []
|
||||||
|
|
||||||
|
selectedSiteIds.forEach(siteId => {
|
||||||
|
if (availableSiteIds.has(siteId) && !normalizedSiteIds.includes(siteId)) {
|
||||||
|
normalizedSiteIds.push(siteId)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return normalizedSiteIds
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.selected,
|
[() => props.selected, () => props.sites],
|
||||||
value => {
|
([value]) => {
|
||||||
if (selectedSites.value.length == 0 && value) {
|
selectedSites.value = normalizeSelectedSites(value || [])
|
||||||
selectedSites.value = value
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
// 全选/全不选按钮文字
|
// 全选/全不选按钮文字
|
||||||
const checkAllText = computed(() => {
|
const checkAllText = computed(() => {
|
||||||
return selectedSites.value.length < props.sites?.length
|
return selectedSites.value.length < props.sites.length
|
||||||
? t('dialog.searchSite.selectAll')
|
? t('dialog.searchSite.selectAll')
|
||||||
: t('dialog.searchSite.deselectAll')
|
: t('dialog.searchSite.deselectAll')
|
||||||
})
|
})
|
||||||
|
|
||||||
// 全选/全不选
|
// 全选/全不选
|
||||||
const checkAllSitesorNot = () => {
|
const checkAllSitesorNot = () => {
|
||||||
if (selectedSites.value.length < props.sites?.length) {
|
if (selectedSites.value.length < props.sites.length) {
|
||||||
selectedSites.value = props.sites?.map((item: Site) => item.id)
|
selectedSites.value = props.sites.map((item: Site) => item.id)
|
||||||
} else {
|
} else {
|
||||||
selectedSites.value = []
|
selectedSites.value = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 切换单个站点的选择状态。
|
||||||
|
function toggleSiteSelection(siteId: number) {
|
||||||
|
const index = selectedSites.value.indexOf(siteId)
|
||||||
|
if (index === -1) {
|
||||||
|
selectedSites.value.push(siteId)
|
||||||
|
} else {
|
||||||
|
selectedSites.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认搜索时只提交当前可用站点。
|
||||||
|
function confirmSearch() {
|
||||||
|
emit('search', normalizeSelectedSites(selectedSites.value))
|
||||||
|
}
|
||||||
|
|
||||||
// 根据筛选条件过滤站点
|
// 根据筛选条件过滤站点
|
||||||
const filteredSites = computed(() => {
|
const filteredSites = computed(() => {
|
||||||
if (!siteFilter.value) return props.sites
|
if (!siteFilter.value) return props.sites
|
||||||
const filter = siteFilter.value.toLowerCase()
|
const filter = siteFilter.value.toLowerCase()
|
||||||
return props.sites?.filter((site: Site) => site.name.toLowerCase().includes(filter))
|
return props.sites.filter((site: Site) => site.name.toLowerCase().includes(filter))
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
@@ -107,16 +135,7 @@ const filteredSites = computed(() => {
|
|||||||
'site-hover': isHovering && !selectedSites.includes(site.id),
|
'site-hover': isHovering && !selectedSites.includes(site.id),
|
||||||
},
|
},
|
||||||
]"
|
]"
|
||||||
@click="
|
@click="toggleSiteSelection(site.id)"
|
||||||
() => {
|
|
||||||
const index = selectedSites.indexOf(site.id)
|
|
||||||
if (index === -1) {
|
|
||||||
selectedSites.push(site.id)
|
|
||||||
} else {
|
|
||||||
selectedSites.splice(index, 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<VIcon
|
<VIcon
|
||||||
:icon="selectedSites.includes(site.id) ? 'mdi-check-circle' : 'mdi-checkbox-blank-circle-outline'"
|
:icon="selectedSites.includes(site.id) ? 'mdi-check-circle' : 'mdi-checkbox-blank-circle-outline'"
|
||||||
@@ -161,7 +180,7 @@ const filteredSites = computed(() => {
|
|||||||
<VBtn
|
<VBtn
|
||||||
color="primary"
|
color="primary"
|
||||||
:disabled="selectedSites.length === 0"
|
:disabled="selectedSites.length === 0"
|
||||||
@click="emit('search', selectedSites)"
|
@click="confirmSearch"
|
||||||
prepend-icon="mdi-magnify"
|
prepend-icon="mdi-magnify"
|
||||||
class="d-flex align-center justify-center px-5"
|
class="d-flex align-center justify-center px-5"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -83,6 +83,8 @@ interface UseLlmProviderDirectoryOptions {
|
|||||||
apiKey: Ref<string>
|
apiKey: Ref<string>
|
||||||
baseUrl: Ref<string>
|
baseUrl: Ref<string>
|
||||||
baseUrlPreset?: Ref<string>
|
baseUrlPreset?: Ref<string>
|
||||||
|
useProxy?: Ref<boolean>
|
||||||
|
userAgent?: Ref<string>
|
||||||
model: Ref<string>
|
model: Ref<string>
|
||||||
maxContextTokens?: Ref<number>
|
maxContextTokens?: Ref<number>
|
||||||
authConnected?: Ref<boolean>
|
authConnected?: Ref<boolean>
|
||||||
@@ -253,6 +255,8 @@ export function useLlmProviderDirectory(options: UseLlmProviderDirectoryOptions)
|
|||||||
api_key: normalizeValue(options.apiKey.value) || undefined,
|
api_key: normalizeValue(options.apiKey.value) || undefined,
|
||||||
base_url: normalizeValue(options.baseUrl.value) || undefined,
|
base_url: normalizeValue(options.baseUrl.value) || undefined,
|
||||||
base_url_preset: normalizeValue(options.baseUrlPreset?.value) || undefined,
|
base_url_preset: normalizeValue(options.baseUrlPreset?.value) || undefined,
|
||||||
|
use_proxy: options.useProxy?.value,
|
||||||
|
user_agent: normalizeValue(options.userAgent?.value) || undefined,
|
||||||
force_refresh: forceRefresh,
|
force_refresh: forceRefresh,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -61,8 +61,10 @@ export interface WizardData {
|
|||||||
supportAudioOutput: boolean
|
supportAudioOutput: boolean
|
||||||
apiKey: string
|
apiKey: string
|
||||||
baseUrl: string
|
baseUrl: string
|
||||||
|
useProxy: boolean
|
||||||
baseUrlPreset: string
|
baseUrlPreset: string
|
||||||
maxContextTokens: number
|
maxContextTokens: number
|
||||||
|
userAgent: string
|
||||||
audioInputProvider: string
|
audioInputProvider: string
|
||||||
audioInputApiKey: string
|
audioInputApiKey: string
|
||||||
audioInputBaseUrl: string
|
audioInputBaseUrl: string
|
||||||
@@ -247,8 +249,10 @@ const wizardData = ref<WizardData>({
|
|||||||
supportAudioOutput: false,
|
supportAudioOutput: false,
|
||||||
apiKey: '',
|
apiKey: '',
|
||||||
baseUrl: 'https://api.deepseek.com',
|
baseUrl: 'https://api.deepseek.com',
|
||||||
|
useProxy: true,
|
||||||
baseUrlPreset: '',
|
baseUrlPreset: '',
|
||||||
maxContextTokens: 64,
|
maxContextTokens: 64,
|
||||||
|
userAgent: '',
|
||||||
audioInputProvider: 'openai',
|
audioInputProvider: 'openai',
|
||||||
audioInputApiKey: '',
|
audioInputApiKey: '',
|
||||||
audioInputBaseUrl: '',
|
audioInputBaseUrl: '',
|
||||||
@@ -1444,8 +1448,10 @@ export function useSetupWizard() {
|
|||||||
LLM_SUPPORT_AUDIO_OUTPUT: wizardData.value.agent.supportAudioOutput,
|
LLM_SUPPORT_AUDIO_OUTPUT: wizardData.value.agent.supportAudioOutput,
|
||||||
LLM_API_KEY: wizardData.value.agent.apiKey,
|
LLM_API_KEY: wizardData.value.agent.apiKey,
|
||||||
LLM_BASE_URL: wizardData.value.agent.baseUrl || null,
|
LLM_BASE_URL: wizardData.value.agent.baseUrl || null,
|
||||||
|
LLM_USE_PROXY: wizardData.value.agent.useProxy,
|
||||||
LLM_BASE_URL_PRESET: wizardData.value.agent.baseUrlPreset || null,
|
LLM_BASE_URL_PRESET: wizardData.value.agent.baseUrlPreset || null,
|
||||||
LLM_MAX_CONTEXT_TOKENS: wizardData.value.agent.maxContextTokens,
|
LLM_MAX_CONTEXT_TOKENS: wizardData.value.agent.maxContextTokens,
|
||||||
|
LLM_USER_AGENT: wizardData.value.agent.userAgent || null,
|
||||||
AUDIO_INPUT_PROVIDER: wizardData.value.agent.audioInputProvider || 'openai',
|
AUDIO_INPUT_PROVIDER: wizardData.value.agent.audioInputProvider || 'openai',
|
||||||
AUDIO_INPUT_API_KEY: wizardData.value.agent.audioInputApiKey || null,
|
AUDIO_INPUT_API_KEY: wizardData.value.agent.audioInputApiKey || null,
|
||||||
AUDIO_INPUT_BASE_URL: wizardData.value.agent.audioInputBaseUrl || null,
|
AUDIO_INPUT_BASE_URL: wizardData.value.agent.audioInputBaseUrl || null,
|
||||||
@@ -1557,8 +1563,10 @@ export function useSetupWizard() {
|
|||||||
wizardData.value.agent.supportAudioOutput = Boolean(result.data.LLM_SUPPORT_AUDIO_OUTPUT)
|
wizardData.value.agent.supportAudioOutput = Boolean(result.data.LLM_SUPPORT_AUDIO_OUTPUT)
|
||||||
wizardData.value.agent.apiKey = result.data.LLM_API_KEY || ''
|
wizardData.value.agent.apiKey = result.data.LLM_API_KEY || ''
|
||||||
wizardData.value.agent.baseUrl = result.data.LLM_BASE_URL || ''
|
wizardData.value.agent.baseUrl = result.data.LLM_BASE_URL || ''
|
||||||
|
wizardData.value.agent.useProxy = result.data.LLM_USE_PROXY ?? true
|
||||||
wizardData.value.agent.baseUrlPreset = result.data.LLM_BASE_URL_PRESET || ''
|
wizardData.value.agent.baseUrlPreset = result.data.LLM_BASE_URL_PRESET || ''
|
||||||
wizardData.value.agent.maxContextTokens = result.data.LLM_MAX_CONTEXT_TOKENS || 64
|
wizardData.value.agent.maxContextTokens = result.data.LLM_MAX_CONTEXT_TOKENS || 64
|
||||||
|
wizardData.value.agent.userAgent = result.data.LLM_USER_AGENT || ''
|
||||||
wizardData.value.agent.audioInputProvider = result.data.AUDIO_INPUT_PROVIDER || 'openai'
|
wizardData.value.agent.audioInputProvider = result.data.AUDIO_INPUT_PROVIDER || 'openai'
|
||||||
wizardData.value.agent.audioInputApiKey = result.data.AUDIO_INPUT_API_KEY || ''
|
wizardData.value.agent.audioInputApiKey = result.data.AUDIO_INPUT_API_KEY || ''
|
||||||
wizardData.value.agent.audioInputBaseUrl = result.data.AUDIO_INPUT_BASE_URL || ''
|
wizardData.value.agent.audioInputBaseUrl = result.data.AUDIO_INPUT_BASE_URL || ''
|
||||||
|
|||||||
@@ -1378,6 +1378,18 @@ export default {
|
|||||||
expand: 'Expand',
|
expand: 'Expand',
|
||||||
collapse: 'Collapse',
|
collapse: 'Collapse',
|
||||||
clearCache: 'Clear Cache',
|
clearCache: 'Clear Cache',
|
||||||
|
versionStatistic: 'Version Statistics',
|
||||||
|
versionStatisticTitle: 'Installation Version Statistics',
|
||||||
|
totalInstallUsers: 'Install Users',
|
||||||
|
activeToday: 'Active Today',
|
||||||
|
active7Days: 'Active 7 Days',
|
||||||
|
active30Days: 'Active 30 Days',
|
||||||
|
backendVersionStatistic: 'Backend Versions',
|
||||||
|
frontendVersionStatistic: 'Frontend Versions',
|
||||||
|
version: 'Version',
|
||||||
|
users: 'Users',
|
||||||
|
lastUpdated: 'Updated At',
|
||||||
|
noVersionStatisticData: 'No statistics data',
|
||||||
},
|
},
|
||||||
system: {
|
system: {
|
||||||
custom: 'Custom',
|
custom: 'Custom',
|
||||||
@@ -1444,6 +1456,11 @@ export default {
|
|||||||
llmApiKeyPlaceholder: 'Please enter API key',
|
llmApiKeyPlaceholder: 'Please enter API key',
|
||||||
llmBaseUrl: 'LLM Base URL',
|
llmBaseUrl: 'LLM Base URL',
|
||||||
llmBaseUrlHint: 'Base URL for LLM API, used for custom API endpoints',
|
llmBaseUrlHint: 'Base URL for LLM API, used for custom API endpoints',
|
||||||
|
llmUseProxy: 'Use System Proxy',
|
||||||
|
llmUseProxyHint:
|
||||||
|
'When enabled, Agent connections to the current LLM provider use the system proxy from advanced settings.',
|
||||||
|
llmUserAgent: 'User-Agent',
|
||||||
|
llmUserAgentHint: 'User-Agent sent to OpenAI-compatible APIs. Leave empty to use the SDK default.',
|
||||||
llmProviderAuth: 'Provider Authorization',
|
llmProviderAuth: 'Provider Authorization',
|
||||||
llmProviderAuthHint:
|
llmProviderAuthHint:
|
||||||
'Providers that support account authorization can complete sign-in here and reuse the saved auth state.',
|
'Providers that support account authorization can complete sign-in here and reuse the saved auth state.',
|
||||||
@@ -1458,15 +1475,16 @@ export default {
|
|||||||
llmProviderCheckAuthStatus: 'Check Authorization Status',
|
llmProviderCheckAuthStatus: 'Check Authorization Status',
|
||||||
audioInputProvider: 'Audio Input Provider',
|
audioInputProvider: 'Audio Input Provider',
|
||||||
audioInputProviderHint:
|
audioInputProviderHint:
|
||||||
'Service used to transcribe incoming audio messages. Supports OpenAI audio, Chat Audio compatible APIs, and Xiaomi MiMo.',
|
'Service used to transcribe incoming audio messages. Supports OpenAI audio, Chat Audio compatible APIs, Xiaomi MiMo, and MiniMax.',
|
||||||
audioProviderOpenAiAudio: 'OpenAI Audio Compatible',
|
audioProviderOpenAiAudio: 'OpenAI Audio Compatible',
|
||||||
audioProviderChatAudio: 'Chat Audio Compatible',
|
audioProviderChatAudio: 'Chat Audio Compatible',
|
||||||
audioProviderMimo: 'Xiaomi MiMo',
|
audioProviderMimo: 'Xiaomi MiMo',
|
||||||
|
audioProviderMinimax: 'MiniMax',
|
||||||
audioInputApiKey: 'Audio Input API Key',
|
audioInputApiKey: 'Audio Input API Key',
|
||||||
audioInputApiKeyHint: 'API key used for audio transcription.',
|
audioInputApiKeyHint: 'API key used for audio transcription.',
|
||||||
audioInputBaseUrl: 'Audio Input Base URL',
|
audioInputBaseUrl: 'Audio Input Base URL',
|
||||||
audioInputBaseUrlHint:
|
audioInputBaseUrlHint:
|
||||||
'Base URL for audio input. Use the matching compatible endpoint for Chat Audio services; MiMo defaults to https://api.xiaomimimo.com/v1.',
|
'Base URL for audio input. Use the matching compatible endpoint for Chat Audio services; MiMo defaults to https://api.xiaomimimo.com/v1, MiniMax defaults to https://api.minimaxi.com/v1.',
|
||||||
audioInputModel: 'Audio Input Model',
|
audioInputModel: 'Audio Input Model',
|
||||||
audioInputModelHint: 'Model name used to convert audio content into text.',
|
audioInputModelHint: 'Model name used to convert audio content into text.',
|
||||||
audioInputLanguage: 'Recognition Language',
|
audioInputLanguage: 'Recognition Language',
|
||||||
@@ -1474,12 +1492,12 @@ export default {
|
|||||||
'Default language for audio transcription, such as zh or en. Leave blank to use the backend default.',
|
'Default language for audio transcription, such as zh or en. Leave blank to use the backend default.',
|
||||||
audioOutputProvider: 'Audio Output Provider',
|
audioOutputProvider: 'Audio Output Provider',
|
||||||
audioOutputProviderHint:
|
audioOutputProviderHint:
|
||||||
'Service used to generate voice replies. Supports OpenAI audio, Chat Audio compatible APIs, and Xiaomi MiMo.',
|
'Service used to generate voice replies. Supports OpenAI audio, Chat Audio compatible APIs, Xiaomi MiMo, and MiniMax.',
|
||||||
audioOutputApiKey: 'Audio Output API Key',
|
audioOutputApiKey: 'Audio Output API Key',
|
||||||
audioOutputApiKeyHint: 'API key used for speech synthesis.',
|
audioOutputApiKeyHint: 'API key used for speech synthesis.',
|
||||||
audioOutputBaseUrl: 'Audio Output Base URL',
|
audioOutputBaseUrl: 'Audio Output Base URL',
|
||||||
audioOutputBaseUrlHint:
|
audioOutputBaseUrlHint:
|
||||||
'Base URL for audio output. Use the matching compatible endpoint for Chat Audio services; MiMo defaults to https://api.xiaomimimo.com/v1.',
|
'Base URL for audio output. Use the matching compatible endpoint for Chat Audio services; MiMo defaults to https://api.xiaomimimo.com/v1, MiniMax defaults to https://api.minimaxi.com/v1.',
|
||||||
audioOutputModel: 'Audio Output Model',
|
audioOutputModel: 'Audio Output Model',
|
||||||
audioOutputModelHint: 'Model name used to convert text content into speech.',
|
audioOutputModelHint: 'Model name used to convert text content into speech.',
|
||||||
audioOutputVoice: 'Voice Preset',
|
audioOutputVoice: 'Voice Preset',
|
||||||
@@ -1561,6 +1579,9 @@ export default {
|
|||||||
'Share subscription statistics to popular subscriptions for other MP users to reference',
|
'Share subscription statistics to popular subscriptions for other MP users to reference',
|
||||||
pluginStatisticShare: 'Report Plugin Installation Data',
|
pluginStatisticShare: 'Report Plugin Installation Data',
|
||||||
pluginStatisticShareHint: 'Report plugin installation data to the server for statistics and display purposes',
|
pluginStatisticShareHint: 'Report plugin installation data to the server for statistics and display purposes',
|
||||||
|
usageStatisticShare: 'Report Installation Version Statistics',
|
||||||
|
usageStatisticShareHint:
|
||||||
|
'Report anonymous installation ID and current backend/frontend versions to count users by version',
|
||||||
workflowStatisticShare: 'Share Workflow Data',
|
workflowStatisticShare: 'Share Workflow Data',
|
||||||
workflowStatisticShareHint: 'Share workflow statistics to popular workflows for other MP users to reference',
|
workflowStatisticShareHint: 'Share workflow statistics to popular workflows for other MP users to reference',
|
||||||
bigMemoryMode: 'Large Memory Mode',
|
bigMemoryMode: 'Large Memory Mode',
|
||||||
@@ -2469,11 +2490,19 @@ export default {
|
|||||||
title: 'Plugin Market Settings',
|
title: 'Plugin Market Settings',
|
||||||
repoUrl: 'Plugin Repository URL',
|
repoUrl: 'Plugin Repository URL',
|
||||||
repoPlaceholder: 'Format: https://github.com/jxxghp/MoviePilot-Plugins/,https://github.com/xxxx/xxxxxx/',
|
repoPlaceholder: 'Format: https://github.com/jxxghp/MoviePilot-Plugins/,https://github.com/xxxx/xxxxxx/',
|
||||||
repoHint: 'Multiple URLs separated by lines, only Github repositories are supported',
|
repoHint: 'Separate multiple URLs with new lines or commas',
|
||||||
urlPlaceholder: 'Enter plugin repository URL',
|
urlPlaceholder: 'Enter plugin repository URL',
|
||||||
|
textPlaceholder: 'https://github.com/jxxghp/MoviePilot-Plugins/\nhttps://github.com/xxxx/xxxxxx/',
|
||||||
|
listMode: 'List',
|
||||||
|
textMode: 'Text',
|
||||||
|
textHint: 'Paste repository URLs one per line or separated by commas.',
|
||||||
|
addRepo: 'Add repository',
|
||||||
noRepos: 'No plugin repository URLs',
|
noRepos: 'No plugin repository URLs',
|
||||||
invalidUrl: 'Please enter a valid URL',
|
invalidUrl: 'Please enter a valid URL',
|
||||||
duplicateUrl: 'This URL already exists',
|
duplicateUrl: 'This URL already exists',
|
||||||
|
invalidText: 'There are {count} invalid URLs in the text. Fix them before saving.',
|
||||||
|
invalidTextIgnored: '{count} invalid URLs ignored',
|
||||||
|
duplicateTextIgnored: 'Duplicate URLs will be removed automatically when saving.',
|
||||||
close: 'Close',
|
close: 'Close',
|
||||||
save: 'Save',
|
save: 'Save',
|
||||||
saveSuccess: 'Plugin repository saved successfully',
|
saveSuccess: 'Plugin repository saved successfully',
|
||||||
@@ -2614,6 +2643,7 @@ export default {
|
|||||||
previewSeasonInfo: 'Season',
|
previewSeasonInfo: 'Season',
|
||||||
previewSeasonLabel: 'Season',
|
previewSeasonLabel: 'Season',
|
||||||
previewEpisodeCount: 'Episodes',
|
previewEpisodeCount: 'Episodes',
|
||||||
|
customWordsApplied: 'Recognition Word Details',
|
||||||
previewAfterColumn: 'After',
|
previewAfterColumn: 'After',
|
||||||
previewBeforeColumn: 'Before',
|
previewBeforeColumn: 'Before',
|
||||||
previewFileNameColumn: 'Filename',
|
previewFileNameColumn: 'Filename',
|
||||||
|
|||||||
@@ -1373,6 +1373,18 @@ export default {
|
|||||||
expand: '展开',
|
expand: '展开',
|
||||||
collapse: '收起',
|
collapse: '收起',
|
||||||
clearCache: '清除缓存',
|
clearCache: '清除缓存',
|
||||||
|
versionStatistic: '版本统计',
|
||||||
|
versionStatisticTitle: '安装版本统计',
|
||||||
|
totalInstallUsers: '安装用户',
|
||||||
|
activeToday: '今日活跃',
|
||||||
|
active7Days: '7日活跃',
|
||||||
|
active30Days: '30日活跃',
|
||||||
|
backendVersionStatistic: '后端版本',
|
||||||
|
frontendVersionStatistic: '前端版本',
|
||||||
|
version: '版本',
|
||||||
|
users: '用户数',
|
||||||
|
lastUpdated: '更新时间',
|
||||||
|
noVersionStatisticData: '暂无统计数据',
|
||||||
},
|
},
|
||||||
system: {
|
system: {
|
||||||
custom: '自定义',
|
custom: '自定义',
|
||||||
@@ -1436,6 +1448,10 @@ export default {
|
|||||||
llmApiKeyPlaceholder: '请输入API密钥',
|
llmApiKeyPlaceholder: '请输入API密钥',
|
||||||
llmBaseUrl: 'LLM基础URL',
|
llmBaseUrl: 'LLM基础URL',
|
||||||
llmBaseUrlHint: 'LLM API的基础URL地址,用于自定义API端点',
|
llmBaseUrlHint: 'LLM API的基础URL地址,用于自定义API端点',
|
||||||
|
llmUseProxy: '使用系统代理',
|
||||||
|
llmUseProxyHint: '启用后,Agent 连接当前 LLM 提供商时会应用高级设置中的系统代理',
|
||||||
|
llmUserAgent: 'User-Agent',
|
||||||
|
llmUserAgentHint: 'OpenAI 兼容接口请求使用的 User-Agent,留空则使用 SDK 默认值',
|
||||||
llmProviderAuth: '提供商授权',
|
llmProviderAuth: '提供商授权',
|
||||||
llmProviderAuthHint: '支持账号登录授权的提供商,可以直接在这里完成登录并复用授权状态。',
|
llmProviderAuthHint: '支持账号登录授权的提供商,可以直接在这里完成登录并复用授权状态。',
|
||||||
llmProviderConnectedAs: '当前已连接:{label}',
|
llmProviderConnectedAs: '当前已连接:{label}',
|
||||||
@@ -1447,26 +1463,29 @@ export default {
|
|||||||
llmProviderOpenAuthPage: '打开授权页面',
|
llmProviderOpenAuthPage: '打开授权页面',
|
||||||
llmProviderCheckAuthStatus: '检查授权状态',
|
llmProviderCheckAuthStatus: '检查授权状态',
|
||||||
audioInputProvider: '音频输入提供商',
|
audioInputProvider: '音频输入提供商',
|
||||||
audioInputProviderHint: '用于识别用户音频消息的服务,支持 OpenAI 音频接口、Chat Audio 兼容接口和 Xiaomi MiMo',
|
audioInputProviderHint:
|
||||||
|
'用于识别用户音频消息的服务,支持 OpenAI 音频接口、Chat Audio 兼容接口、Xiaomi MiMo 和 MiniMax',
|
||||||
audioProviderOpenAiAudio: 'OpenAI Audio 兼容',
|
audioProviderOpenAiAudio: 'OpenAI Audio 兼容',
|
||||||
audioProviderChatAudio: 'Chat Audio 兼容',
|
audioProviderChatAudio: 'Chat Audio 兼容',
|
||||||
audioProviderMimo: '小米 MiMo',
|
audioProviderMimo: '小米 MiMo',
|
||||||
|
audioProviderMinimax: 'MiniMax',
|
||||||
audioInputApiKey: '音频输入 API密钥',
|
audioInputApiKey: '音频输入 API密钥',
|
||||||
audioInputApiKeyHint: '音频输入转写使用的 API 密钥',
|
audioInputApiKeyHint: '音频输入转写使用的 API 密钥',
|
||||||
audioInputBaseUrl: '音频输入基础URL',
|
audioInputBaseUrl: '音频输入基础URL',
|
||||||
audioInputBaseUrlHint:
|
audioInputBaseUrlHint:
|
||||||
'音频输入接口基础URL,Chat Audio 类服务可填写对应兼容地址,MiMo 默认 https://api.xiaomimimo.com/v1',
|
'音频输入接口基础URL,Chat Audio 类服务可填写对应兼容地址,MiMo 默认 https://api.xiaomimimo.com/v1,MiniMax 默认 https://api.minimaxi.com/v1',
|
||||||
audioInputModel: '音频输入模型',
|
audioInputModel: '音频输入模型',
|
||||||
audioInputModelHint: '用于将音频内容转换为文字的模型名称',
|
audioInputModelHint: '用于将音频内容转换为文字的模型名称',
|
||||||
audioInputLanguage: '识别语言',
|
audioInputLanguage: '识别语言',
|
||||||
audioInputLanguageHint: '音频转写默认语言,例如 zh、en,留空时按后端默认处理',
|
audioInputLanguageHint: '音频转写默认语言,例如 zh、en,留空时按后端默认处理',
|
||||||
audioOutputProvider: '音频输出提供商',
|
audioOutputProvider: '音频输出提供商',
|
||||||
audioOutputProviderHint: '用于生成语音回复的服务,支持 OpenAI 音频接口、Chat Audio 兼容接口和 Xiaomi MiMo',
|
audioOutputProviderHint:
|
||||||
|
'用于生成语音回复的服务,支持 OpenAI 音频接口、Chat Audio 兼容接口、Xiaomi MiMo 和 MiniMax',
|
||||||
audioOutputApiKey: '音频输出 API密钥',
|
audioOutputApiKey: '音频输出 API密钥',
|
||||||
audioOutputApiKeyHint: '文字转语音使用的 API 密钥',
|
audioOutputApiKeyHint: '文字转语音使用的 API 密钥',
|
||||||
audioOutputBaseUrl: '音频输出基础URL',
|
audioOutputBaseUrl: '音频输出基础URL',
|
||||||
audioOutputBaseUrlHint:
|
audioOutputBaseUrlHint:
|
||||||
'音频输出接口基础URL,Chat Audio 类服务可填写对应兼容地址,MiMo 默认 https://api.xiaomimimo.com/v1',
|
'音频输出接口基础URL,Chat Audio 类服务可填写对应兼容地址,MiMo 默认 https://api.xiaomimimo.com/v1,MiniMax 默认 https://api.minimaxi.com/v1',
|
||||||
audioOutputModel: '音频输出模型',
|
audioOutputModel: '音频输出模型',
|
||||||
audioOutputModelHint: '用于将文字内容转换为语音的模型名称',
|
audioOutputModelHint: '用于将文字内容转换为语音的模型名称',
|
||||||
audioOutputVoice: '语音音色',
|
audioOutputVoice: '语音音色',
|
||||||
@@ -1545,6 +1564,8 @@ export default {
|
|||||||
subscribeStatisticShareHint: '分享订阅统计数据到热门订阅,供其他MPer参考',
|
subscribeStatisticShareHint: '分享订阅统计数据到热门订阅,供其他MPer参考',
|
||||||
pluginStatisticShare: '上报插件安装数据',
|
pluginStatisticShare: '上报插件安装数据',
|
||||||
pluginStatisticShareHint: '上报插件安装数据给服务器,用于统计展示插件安装情况',
|
pluginStatisticShareHint: '上报插件安装数据给服务器,用于统计展示插件安装情况',
|
||||||
|
usageStatisticShare: '上报安装版本统计',
|
||||||
|
usageStatisticShareHint: '上报匿名安装ID和当前前后端版本,用于统计各版本安装用户数',
|
||||||
workflowStatisticShare: '分享工作流数据',
|
workflowStatisticShare: '分享工作流数据',
|
||||||
workflowStatisticShareHint: '分享工作流统计数据到热门工作流,供其他MPer参考',
|
workflowStatisticShareHint: '分享工作流统计数据到热门工作流,供其他MPer参考',
|
||||||
bigMemoryMode: '大内存模式',
|
bigMemoryMode: '大内存模式',
|
||||||
@@ -2423,11 +2444,19 @@ export default {
|
|||||||
title: '插件市场设置',
|
title: '插件市场设置',
|
||||||
repoUrl: '插件仓库地址',
|
repoUrl: '插件仓库地址',
|
||||||
repoPlaceholder: '格式:https://github.com/jxxghp/MoviePilot-Plugins/,https://github.com/xxxx/xxxxxx/',
|
repoPlaceholder: '格式:https://github.com/jxxghp/MoviePilot-Plugins/,https://github.com/xxxx/xxxxxx/',
|
||||||
repoHint: '多个地址使用换行分隔,仅支持Github仓库',
|
repoHint: '多个地址可使用换行或英文逗号分隔',
|
||||||
urlPlaceholder: '输入插件仓库地址',
|
urlPlaceholder: '输入插件仓库地址',
|
||||||
|
textPlaceholder: 'https://github.com/jxxghp/MoviePilot-Plugins/\nhttps://github.com/xxxx/xxxxxx/',
|
||||||
|
listMode: '列表维护',
|
||||||
|
textMode: '文本维护',
|
||||||
|
textHint: '直接粘贴仓库地址串,一行一个或使用英文逗号分隔。',
|
||||||
|
addRepo: '添加仓库',
|
||||||
noRepos: '暂无插件仓库地址',
|
noRepos: '暂无插件仓库地址',
|
||||||
invalidUrl: '请输入有效的URL地址',
|
invalidUrl: '请输入有效的URL地址',
|
||||||
duplicateUrl: '该地址已存在',
|
duplicateUrl: '该地址已存在',
|
||||||
|
invalidText: '文本中有 {count} 个无效地址,请修正后保存。',
|
||||||
|
invalidTextIgnored: '已忽略 {count} 个无效地址',
|
||||||
|
duplicateTextIgnored: '重复地址会在保存时自动去重。',
|
||||||
close: '关闭',
|
close: '关闭',
|
||||||
save: '保存',
|
save: '保存',
|
||||||
saveSuccess: '插件仓库保存成功',
|
saveSuccess: '插件仓库保存成功',
|
||||||
@@ -2567,6 +2596,7 @@ export default {
|
|||||||
previewSeasonInfo: '季信息',
|
previewSeasonInfo: '季信息',
|
||||||
previewSeasonLabel: '季',
|
previewSeasonLabel: '季',
|
||||||
previewEpisodeCount: '总集数',
|
previewEpisodeCount: '总集数',
|
||||||
|
customWordsApplied: '识别词应用详情',
|
||||||
previewAfterColumn: '整理后',
|
previewAfterColumn: '整理后',
|
||||||
previewBeforeColumn: '整理前',
|
previewBeforeColumn: '整理前',
|
||||||
previewFileNameColumn: '文件名',
|
previewFileNameColumn: '文件名',
|
||||||
|
|||||||
@@ -1374,6 +1374,18 @@ export default {
|
|||||||
expand: '展開',
|
expand: '展開',
|
||||||
collapse: '收起',
|
collapse: '收起',
|
||||||
clearCache: '清除快取',
|
clearCache: '清除快取',
|
||||||
|
versionStatistic: '版本統計',
|
||||||
|
versionStatisticTitle: '安裝版本統計',
|
||||||
|
totalInstallUsers: '安裝用戶',
|
||||||
|
activeToday: '今日活躍',
|
||||||
|
active7Days: '7日活躍',
|
||||||
|
active30Days: '30日活躍',
|
||||||
|
backendVersionStatistic: '後端版本',
|
||||||
|
frontendVersionStatistic: '前端版本',
|
||||||
|
version: '版本',
|
||||||
|
users: '用戶數',
|
||||||
|
lastUpdated: '更新時間',
|
||||||
|
noVersionStatisticData: '暫無統計數據',
|
||||||
},
|
},
|
||||||
system: {
|
system: {
|
||||||
custom: '自定義',
|
custom: '自定義',
|
||||||
@@ -1437,6 +1449,10 @@ export default {
|
|||||||
llmApiKeyPlaceholder: '請輸入API密鑰',
|
llmApiKeyPlaceholder: '請輸入API密鑰',
|
||||||
llmBaseUrl: 'LLM基礎URL',
|
llmBaseUrl: 'LLM基礎URL',
|
||||||
llmBaseUrlHint: 'LLM API的基礎URL地址,用於自定義API端點',
|
llmBaseUrlHint: 'LLM API的基礎URL地址,用於自定義API端點',
|
||||||
|
llmUseProxy: '使用系統代理',
|
||||||
|
llmUseProxyHint: '啟用後,Agent 連接目前 LLM 提供商時會套用進階設定中的系統代理',
|
||||||
|
llmUserAgent: 'User-Agent',
|
||||||
|
llmUserAgentHint: 'OpenAI 兼容接口請求使用的 User-Agent,留空則使用 SDK 預設值',
|
||||||
llmProviderAuth: '提供商授權',
|
llmProviderAuth: '提供商授權',
|
||||||
llmProviderAuthHint: '支援帳號登入授權的提供商,可以直接在這裡完成登入並重用授權狀態。',
|
llmProviderAuthHint: '支援帳號登入授權的提供商,可以直接在這裡完成登入並重用授權狀態。',
|
||||||
llmProviderConnectedAs: '目前已連接:{label}',
|
llmProviderConnectedAs: '目前已連接:{label}',
|
||||||
@@ -1448,26 +1464,29 @@ export default {
|
|||||||
llmProviderOpenAuthPage: '開啟授權頁面',
|
llmProviderOpenAuthPage: '開啟授權頁面',
|
||||||
llmProviderCheckAuthStatus: '檢查授權狀態',
|
llmProviderCheckAuthStatus: '檢查授權狀態',
|
||||||
audioInputProvider: '音頻輸入提供商',
|
audioInputProvider: '音頻輸入提供商',
|
||||||
audioInputProviderHint: '用於識別用戶音頻消息的服務,支援 OpenAI 音頻接口、Chat Audio 兼容接口和 Xiaomi MiMo',
|
audioInputProviderHint:
|
||||||
|
'用於識別用戶音頻消息的服務,支援 OpenAI 音頻接口、Chat Audio 兼容接口、Xiaomi MiMo 和 MiniMax',
|
||||||
audioProviderOpenAiAudio: 'OpenAI Audio 兼容',
|
audioProviderOpenAiAudio: 'OpenAI Audio 兼容',
|
||||||
audioProviderChatAudio: 'Chat Audio 兼容',
|
audioProviderChatAudio: 'Chat Audio 兼容',
|
||||||
audioProviderMimo: '小米 MiMo',
|
audioProviderMimo: '小米 MiMo',
|
||||||
|
audioProviderMinimax: 'MiniMax',
|
||||||
audioInputApiKey: '音頻輸入 API密鑰',
|
audioInputApiKey: '音頻輸入 API密鑰',
|
||||||
audioInputApiKeyHint: '音頻輸入轉寫使用的 API 密鑰',
|
audioInputApiKeyHint: '音頻輸入轉寫使用的 API 密鑰',
|
||||||
audioInputBaseUrl: '音頻輸入基礎URL',
|
audioInputBaseUrl: '音頻輸入基礎URL',
|
||||||
audioInputBaseUrlHint:
|
audioInputBaseUrlHint:
|
||||||
'音頻輸入接口基礎URL,Chat Audio 類服務可填寫對應兼容地址,MiMo 預設 https://api.xiaomimimo.com/v1',
|
'音頻輸入接口基礎URL,Chat Audio 類服務可填寫對應兼容地址,MiMo 預設 https://api.xiaomimimo.com/v1,MiniMax 預設 https://api.minimaxi.com/v1',
|
||||||
audioInputModel: '音頻輸入模型',
|
audioInputModel: '音頻輸入模型',
|
||||||
audioInputModelHint: '用於將音頻內容轉換為文字的模型名稱',
|
audioInputModelHint: '用於將音頻內容轉換為文字的模型名稱',
|
||||||
audioInputLanguage: '識別語言',
|
audioInputLanguage: '識別語言',
|
||||||
audioInputLanguageHint: '音頻轉寫預設語言,例如 zh、en,留空時按後端預設處理',
|
audioInputLanguageHint: '音頻轉寫預設語言,例如 zh、en,留空時按後端預設處理',
|
||||||
audioOutputProvider: '音頻輸出提供商',
|
audioOutputProvider: '音頻輸出提供商',
|
||||||
audioOutputProviderHint: '用於生成語音回覆的服務,支援 OpenAI 音頻接口、Chat Audio 兼容接口和 Xiaomi MiMo',
|
audioOutputProviderHint:
|
||||||
|
'用於生成語音回覆的服務,支援 OpenAI 音頻接口、Chat Audio 兼容接口、Xiaomi MiMo 和 MiniMax',
|
||||||
audioOutputApiKey: '音頻輸出 API密鑰',
|
audioOutputApiKey: '音頻輸出 API密鑰',
|
||||||
audioOutputApiKeyHint: '文字轉語音使用的 API 密鑰',
|
audioOutputApiKeyHint: '文字轉語音使用的 API 密鑰',
|
||||||
audioOutputBaseUrl: '音頻輸出基礎URL',
|
audioOutputBaseUrl: '音頻輸出基礎URL',
|
||||||
audioOutputBaseUrlHint:
|
audioOutputBaseUrlHint:
|
||||||
'音頻輸出接口基礎URL,Chat Audio 類服務可填寫對應兼容地址,MiMo 預設 https://api.xiaomimimo.com/v1',
|
'音頻輸出接口基礎URL,Chat Audio 類服務可填寫對應兼容地址,MiMo 預設 https://api.xiaomimimo.com/v1,MiniMax 預設 https://api.minimaxi.com/v1',
|
||||||
audioOutputModel: '音頻輸出模型',
|
audioOutputModel: '音頻輸出模型',
|
||||||
audioOutputModelHint: '用於將文字內容轉換為語音的模型名稱',
|
audioOutputModelHint: '用於將文字內容轉換為語音的模型名稱',
|
||||||
audioOutputVoice: '語音音色',
|
audioOutputVoice: '語音音色',
|
||||||
@@ -1546,6 +1565,8 @@ export default {
|
|||||||
subscribeStatisticShareHint: '分享訂閱統計數據到熱門訂閱,供其他MPer參考',
|
subscribeStatisticShareHint: '分享訂閱統計數據到熱門訂閱,供其他MPer參考',
|
||||||
pluginStatisticShare: '上報插件安裝數據',
|
pluginStatisticShare: '上報插件安裝數據',
|
||||||
pluginStatisticShareHint: '上報插件安裝數據給服務器,用於統計展示插件安裝情況',
|
pluginStatisticShareHint: '上報插件安裝數據給服務器,用於統計展示插件安裝情況',
|
||||||
|
usageStatisticShare: '上報安裝版本統計',
|
||||||
|
usageStatisticShareHint: '上報匿名安裝ID和當前前後端版本,用於統計各版本安裝用戶數',
|
||||||
workflowStatisticShare: '分享工作流數據',
|
workflowStatisticShare: '分享工作流數據',
|
||||||
workflowStatisticShareHint: '分享工作流統計數據到熱門工作流,供其他MPer參考',
|
workflowStatisticShareHint: '分享工作流統計數據到熱門工作流,供其他MPer參考',
|
||||||
bigMemoryMode: '大內存模式',
|
bigMemoryMode: '大內存模式',
|
||||||
@@ -2424,11 +2445,19 @@ export default {
|
|||||||
title: '插件市場設置',
|
title: '插件市場設置',
|
||||||
repoUrl: '插件倉庫地址',
|
repoUrl: '插件倉庫地址',
|
||||||
repoPlaceholder: '格式:https://github.com/jxxghp/MoviePilot-Plugins/,https://github.com/xxxx/xxxxxx/',
|
repoPlaceholder: '格式:https://github.com/jxxghp/MoviePilot-Plugins/,https://github.com/xxxx/xxxxxx/',
|
||||||
repoHint: '多個地址使用换行分隔,僅支援Github倉庫',
|
repoHint: '多個地址可使用換行或英文逗號分隔',
|
||||||
urlPlaceholder: '輸入插件倉庫地址',
|
urlPlaceholder: '輸入插件倉庫地址',
|
||||||
|
textPlaceholder: 'https://github.com/jxxghp/MoviePilot-Plugins/\nhttps://github.com/xxxx/xxxxxx/',
|
||||||
|
listMode: '列表維護',
|
||||||
|
textMode: '文字維護',
|
||||||
|
textHint: '直接貼上倉庫地址串,一行一個或使用英文逗號分隔。',
|
||||||
|
addRepo: '新增倉庫',
|
||||||
noRepos: '暫無插件倉庫地址',
|
noRepos: '暫無插件倉庫地址',
|
||||||
invalidUrl: '請輸入有效的URL地址',
|
invalidUrl: '請輸入有效的URL地址',
|
||||||
duplicateUrl: '該地址已存在',
|
duplicateUrl: '該地址已存在',
|
||||||
|
invalidText: '文字中有 {count} 個無效地址,請修正後儲存。',
|
||||||
|
invalidTextIgnored: '已忽略 {count} 個無效地址',
|
||||||
|
duplicateTextIgnored: '重複地址會在儲存時自動去重。',
|
||||||
close: '關閉',
|
close: '關閉',
|
||||||
save: '儲存',
|
save: '儲存',
|
||||||
saveSuccess: '插件倉庫儲存成功',
|
saveSuccess: '插件倉庫儲存成功',
|
||||||
@@ -2568,6 +2597,7 @@ export default {
|
|||||||
previewSeasonInfo: '季資訊',
|
previewSeasonInfo: '季資訊',
|
||||||
previewSeasonLabel: '季',
|
previewSeasonLabel: '季',
|
||||||
previewEpisodeCount: '總集數',
|
previewEpisodeCount: '總集數',
|
||||||
|
customWordsApplied: '識別詞應用詳情',
|
||||||
previewAfterColumn: '整理後',
|
previewAfterColumn: '整理後',
|
||||||
previewBeforeColumn: '整理前',
|
previewBeforeColumn: '整理前',
|
||||||
previewFileNameColumn: '文件名',
|
previewFileNameColumn: '文件名',
|
||||||
|
|||||||
@@ -57,7 +57,10 @@ const SystemSettings = ref<any>({
|
|||||||
LLM_SUPPORT_AUDIO_OUTPUT: false,
|
LLM_SUPPORT_AUDIO_OUTPUT: false,
|
||||||
LLM_API_KEY: null,
|
LLM_API_KEY: null,
|
||||||
LLM_BASE_URL: 'https://api.deepseek.com',
|
LLM_BASE_URL: 'https://api.deepseek.com',
|
||||||
|
LLM_USE_PROXY: true,
|
||||||
LLM_BASE_URL_PRESET: null,
|
LLM_BASE_URL_PRESET: null,
|
||||||
|
LLM_MAX_CONTEXT_TOKENS: 64,
|
||||||
|
LLM_USER_AGENT: null,
|
||||||
AUDIO_INPUT_PROVIDER: 'openai',
|
AUDIO_INPUT_PROVIDER: 'openai',
|
||||||
AUDIO_INPUT_API_KEY: null,
|
AUDIO_INPUT_API_KEY: null,
|
||||||
AUDIO_INPUT_BASE_URL: null,
|
AUDIO_INPUT_BASE_URL: null,
|
||||||
@@ -73,7 +76,6 @@ const SystemSettings = ref<any>({
|
|||||||
AI_RECOMMEND_ENABLED: false,
|
AI_RECOMMEND_ENABLED: false,
|
||||||
AI_RECOMMEND_USER_PREFERENCE: null,
|
AI_RECOMMEND_USER_PREFERENCE: null,
|
||||||
AI_RECOMMEND_MAX_ITEMS: 50,
|
AI_RECOMMEND_MAX_ITEMS: 50,
|
||||||
LLM_MAX_CONTEXT_TOKENS: 64,
|
|
||||||
},
|
},
|
||||||
// 高级系统设置
|
// 高级系统设置
|
||||||
Advanced: {
|
Advanced: {
|
||||||
@@ -82,6 +84,7 @@ const SystemSettings = ref<any>({
|
|||||||
GLOBAL_IMAGE_CACHE: false,
|
GLOBAL_IMAGE_CACHE: false,
|
||||||
SUBSCRIBE_STATISTIC_SHARE: true,
|
SUBSCRIBE_STATISTIC_SHARE: true,
|
||||||
PLUGIN_STATISTIC_SHARE: true,
|
PLUGIN_STATISTIC_SHARE: true,
|
||||||
|
USAGE_STATISTIC_SHARE: true,
|
||||||
WORKFLOW_STATISTIC_SHARE: true,
|
WORKFLOW_STATISTIC_SHARE: true,
|
||||||
BIG_MEMORY_MODE: false,
|
BIG_MEMORY_MODE: false,
|
||||||
DB_WAL_ENABLE: false,
|
DB_WAL_ENABLE: false,
|
||||||
@@ -132,6 +135,7 @@ const audioProviderItems = computed(() => [
|
|||||||
{ title: t('setting.system.audioProviderOpenAiAudio'), value: 'openai' },
|
{ title: t('setting.system.audioProviderOpenAiAudio'), value: 'openai' },
|
||||||
{ title: t('setting.system.audioProviderChatAudio'), value: 'openai_chat_audio' },
|
{ title: t('setting.system.audioProviderChatAudio'), value: 'openai_chat_audio' },
|
||||||
{ title: t('setting.system.audioProviderMimo'), value: 'mimo' },
|
{ title: t('setting.system.audioProviderMimo'), value: 'mimo' },
|
||||||
|
{ title: t('setting.system.audioProviderMinimax'), value: 'minimax' },
|
||||||
])
|
])
|
||||||
|
|
||||||
// 刮削配置
|
// 刮削配置
|
||||||
@@ -211,7 +215,9 @@ type LlmSettingsSnapshot = {
|
|||||||
LLM_THINKING_LEVEL: string
|
LLM_THINKING_LEVEL: string
|
||||||
LLM_API_KEY: string
|
LLM_API_KEY: string
|
||||||
LLM_BASE_URL: string
|
LLM_BASE_URL: string
|
||||||
|
LLM_USE_PROXY: boolean
|
||||||
LLM_BASE_URL_PRESET: string
|
LLM_BASE_URL_PRESET: string
|
||||||
|
LLM_USER_AGENT: string
|
||||||
}
|
}
|
||||||
|
|
||||||
let llmTestRequestId = 0
|
let llmTestRequestId = 0
|
||||||
@@ -245,6 +251,20 @@ const llmBaseUrlPresetRef = computed({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const llmUseProxyRef = computed({
|
||||||
|
get: () => Boolean(SystemSettings.value.Basic.LLM_USE_PROXY),
|
||||||
|
set: value => {
|
||||||
|
SystemSettings.value.Basic.LLM_USE_PROXY = Boolean(value)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const llmUserAgentRef = computed({
|
||||||
|
get: () => String(SystemSettings.value.Basic.LLM_USER_AGENT ?? ''),
|
||||||
|
set: value => {
|
||||||
|
SystemSettings.value.Basic.LLM_USER_AGENT = value || ''
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const llmModelRef = computed({
|
const llmModelRef = computed({
|
||||||
get: () => String(SystemSettings.value.Basic.LLM_MODEL ?? ''),
|
get: () => String(SystemSettings.value.Basic.LLM_MODEL ?? ''),
|
||||||
set: value => {
|
set: value => {
|
||||||
@@ -290,6 +310,8 @@ const {
|
|||||||
apiKey: llmApiKeyRef,
|
apiKey: llmApiKeyRef,
|
||||||
baseUrl: llmBaseUrlRef,
|
baseUrl: llmBaseUrlRef,
|
||||||
baseUrlPreset: llmBaseUrlPresetRef,
|
baseUrlPreset: llmBaseUrlPresetRef,
|
||||||
|
useProxy: llmUseProxyRef,
|
||||||
|
userAgent: llmUserAgentRef,
|
||||||
model: llmModelRef,
|
model: llmModelRef,
|
||||||
maxContextTokens: llmMaxContextRef,
|
maxContextTokens: llmMaxContextRef,
|
||||||
})
|
})
|
||||||
@@ -350,7 +372,9 @@ function buildLlmSnapshot(): LlmSettingsSnapshot {
|
|||||||
LLM_THINKING_LEVEL: String(SystemSettings.value.Basic.LLM_THINKING_LEVEL ?? 'off'),
|
LLM_THINKING_LEVEL: String(SystemSettings.value.Basic.LLM_THINKING_LEVEL ?? 'off'),
|
||||||
LLM_API_KEY: String(SystemSettings.value.Basic.LLM_API_KEY ?? ''),
|
LLM_API_KEY: String(SystemSettings.value.Basic.LLM_API_KEY ?? ''),
|
||||||
LLM_BASE_URL: String(SystemSettings.value.Basic.LLM_BASE_URL ?? ''),
|
LLM_BASE_URL: String(SystemSettings.value.Basic.LLM_BASE_URL ?? ''),
|
||||||
|
LLM_USE_PROXY: Boolean(SystemSettings.value.Basic.LLM_USE_PROXY),
|
||||||
LLM_BASE_URL_PRESET: String(SystemSettings.value.Basic.LLM_BASE_URL_PRESET ?? ''),
|
LLM_BASE_URL_PRESET: String(SystemSettings.value.Basic.LLM_BASE_URL_PRESET ?? ''),
|
||||||
|
LLM_USER_AGENT: String(SystemSettings.value.Basic.LLM_USER_AGENT ?? ''),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,7 +390,9 @@ function buildLlmTestPayload(snapshot: LlmSettingsSnapshot) {
|
|||||||
thinking_level: snapshot.LLM_THINKING_LEVEL.trim(),
|
thinking_level: snapshot.LLM_THINKING_LEVEL.trim(),
|
||||||
api_key: snapshot.LLM_API_KEY.trim(),
|
api_key: snapshot.LLM_API_KEY.trim(),
|
||||||
base_url: snapshot.LLM_BASE_URL.trim(),
|
base_url: snapshot.LLM_BASE_URL.trim(),
|
||||||
|
use_proxy: snapshot.LLM_USE_PROXY,
|
||||||
base_url_preset: snapshot.LLM_BASE_URL_PRESET.trim(),
|
base_url_preset: snapshot.LLM_BASE_URL_PRESET.trim(),
|
||||||
|
user_agent: snapshot.LLM_USER_AGENT.trim(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -640,9 +666,9 @@ async function loadSystemSettings() {
|
|||||||
if (result.data.hasOwnProperty(key)) (SystemSettings.value[sectionKey] as any)[key] = result.data[key]
|
if (result.data.hasOwnProperty(key)) (SystemSettings.value[sectionKey] as any)[key] = result.data[key]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const accelEnabled = Boolean(result.data.RUST_ACCEL_ENABLED)
|
const accelAvailable = Boolean(result.data.RUST_ACCEL_AVAILABLE ?? result.data.RUST_ACCEL_ENABLED)
|
||||||
rustAccelAvailable.value = accelEnabled
|
rustAccelAvailable.value = accelAvailable
|
||||||
if (!accelEnabled) SystemSettings.value.Advanced.RUST_ACCEL = false
|
if (!accelAvailable) SystemSettings.value.Advanced.RUST_ACCEL = false
|
||||||
SystemSettings.value.Basic.LLM_THINKING_LEVEL = resolveThinkingLevelValue(result.data)
|
SystemSettings.value.Basic.LLM_THINKING_LEVEL = resolveThinkingLevelValue(result.data)
|
||||||
await loadLlmProviders()
|
await loadLlmProviders()
|
||||||
}
|
}
|
||||||
@@ -1192,6 +1218,14 @@ watch(currentLlmSnapshotKey, (snapshotKey, previousSnapshotKey) => {
|
|||||||
</template>
|
</template>
|
||||||
</VCombobox>
|
</VCombobox>
|
||||||
</VCol>
|
</VCol>
|
||||||
|
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE && showBaseUrlField" cols="12">
|
||||||
|
<VSwitch
|
||||||
|
v-model="SystemSettings.Basic.LLM_USE_PROXY"
|
||||||
|
:label="t('setting.system.llmUseProxy')"
|
||||||
|
:hint="t('setting.system.llmUseProxyHint')"
|
||||||
|
persistent-hint
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE && showApiKeyField" cols="12" md="6">
|
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE && showApiKeyField" cols="12" md="6">
|
||||||
<VTextField
|
<VTextField
|
||||||
v-model="SystemSettings.Basic.LLM_API_KEY"
|
v-model="SystemSettings.Basic.LLM_API_KEY"
|
||||||
@@ -1306,6 +1340,15 @@ watch(currentLlmSnapshotKey, (snapshotKey, previousSnapshotKey) => {
|
|||||||
prepend-inner-icon="mdi-counter"
|
prepend-inner-icon="mdi-counter"
|
||||||
/>
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
|
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE && showBaseUrlField" cols="12" md="6">
|
||||||
|
<VTextField
|
||||||
|
v-model="SystemSettings.Basic.LLM_USER_AGENT"
|
||||||
|
:label="t('setting.system.llmUserAgent')"
|
||||||
|
:hint="t('setting.system.llmUserAgentHint')"
|
||||||
|
persistent-hint
|
||||||
|
prepend-inner-icon="mdi-card-account-details-outline"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="6">
|
<VCol v-if="SystemSettings.Basic.AI_AGENT_ENABLE" cols="12" md="6">
|
||||||
<VSelect
|
<VSelect
|
||||||
v-model="SystemSettings.Basic.LLM_THINKING_LEVEL"
|
v-model="SystemSettings.Basic.LLM_THINKING_LEVEL"
|
||||||
@@ -1767,6 +1810,14 @@ watch(currentLlmSnapshotKey, (snapshotKey, previousSnapshotKey) => {
|
|||||||
persistent-hint
|
persistent-hint
|
||||||
/>
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
|
<VCol cols="12" md="6">
|
||||||
|
<VSwitch
|
||||||
|
v-model="SystemSettings.Advanced.USAGE_STATISTIC_SHARE"
|
||||||
|
:label="t('setting.system.usageStatisticShare')"
|
||||||
|
:hint="t('setting.system.usageStatisticShareHint')"
|
||||||
|
persistent-hint
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
<VCol cols="12" md="6">
|
<VCol cols="12" md="6">
|
||||||
<VSwitch
|
<VSwitch
|
||||||
v-model="SystemSettings.Advanced.WORKFLOW_STATISTIC_SHARE"
|
v-model="SystemSettings.Advanced.WORKFLOW_STATISTIC_SHARE"
|
||||||
|
|||||||
@@ -40,6 +40,20 @@ const baseUrlPresetRef = computed({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const useProxyRef = computed({
|
||||||
|
get: () => wizardData.value.agent.useProxy,
|
||||||
|
set: value => {
|
||||||
|
wizardData.value.agent.useProxy = Boolean(value)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const userAgentRef = computed({
|
||||||
|
get: () => wizardData.value.agent.userAgent,
|
||||||
|
set: value => {
|
||||||
|
wizardData.value.agent.userAgent = value || ''
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const modelRef = computed({
|
const modelRef = computed({
|
||||||
get: () => wizardData.value.agent.model,
|
get: () => wizardData.value.agent.model,
|
||||||
set: value => {
|
set: value => {
|
||||||
@@ -92,6 +106,8 @@ const {
|
|||||||
apiKey: apiKeyRef,
|
apiKey: apiKeyRef,
|
||||||
baseUrl: baseUrlRef,
|
baseUrl: baseUrlRef,
|
||||||
baseUrlPreset: baseUrlPresetRef,
|
baseUrlPreset: baseUrlPresetRef,
|
||||||
|
useProxy: useProxyRef,
|
||||||
|
userAgent: userAgentRef,
|
||||||
model: modelRef,
|
model: modelRef,
|
||||||
maxContextTokens: maxContextTokensRef,
|
maxContextTokens: maxContextTokensRef,
|
||||||
authConnected: authConnectedRef,
|
authConnected: authConnectedRef,
|
||||||
@@ -171,6 +187,7 @@ const audioProviderItems = computed(() => [
|
|||||||
{ title: t('setting.system.audioProviderOpenAiAudio'), value: 'openai' },
|
{ title: t('setting.system.audioProviderOpenAiAudio'), value: 'openai' },
|
||||||
{ title: t('setting.system.audioProviderChatAudio'), value: 'openai_chat_audio' },
|
{ title: t('setting.system.audioProviderChatAudio'), value: 'openai_chat_audio' },
|
||||||
{ title: t('setting.system.audioProviderMimo'), value: 'mimo' },
|
{ title: t('setting.system.audioProviderMimo'), value: 'mimo' },
|
||||||
|
{ title: t('setting.system.audioProviderMinimax'), value: 'minimax' },
|
||||||
])
|
])
|
||||||
|
|
||||||
const providerAuthMethods = computed(() => selectedProvider.value?.oauth_methods || [])
|
const providerAuthMethods = computed(() => selectedProvider.value?.oauth_methods || [])
|
||||||
@@ -332,6 +349,16 @@ onMounted(async () => {
|
|||||||
</VCombobox>
|
</VCombobox>
|
||||||
</VCol>
|
</VCol>
|
||||||
|
|
||||||
|
<VCol v-if="showBaseUrlField" cols="12">
|
||||||
|
<VSwitch
|
||||||
|
v-model="wizardData.agent.useProxy"
|
||||||
|
:label="t('setting.system.llmUseProxy')"
|
||||||
|
:hint="t('setting.system.llmUseProxyHint')"
|
||||||
|
persistent-hint
|
||||||
|
color="primary"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
|
||||||
<VCol v-if="showApiKeyField" cols="12" md="6">
|
<VCol v-if="showApiKeyField" cols="12" md="6">
|
||||||
<VTextField
|
<VTextField
|
||||||
v-model="wizardData.agent.apiKey"
|
v-model="wizardData.agent.apiKey"
|
||||||
@@ -437,6 +464,16 @@ onMounted(async () => {
|
|||||||
/>
|
/>
|
||||||
</VCol>
|
</VCol>
|
||||||
|
|
||||||
|
<VCol v-if="showBaseUrlField" cols="12" md="6">
|
||||||
|
<VTextField
|
||||||
|
v-model="wizardData.agent.userAgent"
|
||||||
|
:label="t('setting.system.llmUserAgent')"
|
||||||
|
:hint="t('setting.system.llmUserAgentHint')"
|
||||||
|
persistent-hint
|
||||||
|
prepend-inner-icon="mdi-card-account-details-outline"
|
||||||
|
/>
|
||||||
|
</VCol>
|
||||||
|
|
||||||
<VCol cols="12" md="6">
|
<VCol cols="12" md="6">
|
||||||
<VSelect
|
<VSelect
|
||||||
v-model="wizardData.agent.thinkingLevel"
|
v-model="wizardData.agent.thinkingLevel"
|
||||||
|
|||||||
Reference in New Issue
Block a user