|
|
@@ -38,60 +38,19 @@
|
|
|
<h1>服务器配置</h1>
|
|
|
<p>配置后端服务器地址以连接系统</p>
|
|
|
</div>
|
|
|
-
|
|
|
- <!-- 服务器列表 -->
|
|
|
- <div class="server-list" v-if="serverStore.servers.length > 0">
|
|
|
- <div
|
|
|
- v-for="server in serverStore.servers"
|
|
|
- :key="server.id"
|
|
|
- class="server-item"
|
|
|
- :class="{ active: server.id === serverStore.currentServerId }"
|
|
|
- @click="selectServer(server.id)"
|
|
|
- >
|
|
|
- <div class="server-info">
|
|
|
- <div class="server-name">{{ server.name }}</div>
|
|
|
- <div class="server-url">{{ server.url }}</div>
|
|
|
- </div>
|
|
|
- <div class="server-actions">
|
|
|
- <el-tag v-if="server.id === serverStore.currentServerId" type="success" size="small">
|
|
|
- 当前使用
|
|
|
- </el-tag>
|
|
|
- <el-button
|
|
|
- type="danger"
|
|
|
- link
|
|
|
- size="small"
|
|
|
- @click.stop="deleteServer(server.id)"
|
|
|
- >
|
|
|
- 删除
|
|
|
- </el-button>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
-
|
|
|
- <el-divider v-if="serverStore.servers.length > 0" />
|
|
|
-
|
|
|
- <!-- 添加服务器表单 -->
|
|
|
+
|
|
|
<el-form
|
|
|
- ref="formRef"
|
|
|
- :model="form"
|
|
|
- :rules="rules"
|
|
|
+ ref="serverFormRef"
|
|
|
+ :model="serverForm"
|
|
|
+ :rules="serverRules"
|
|
|
label-position="top"
|
|
|
class="config-form"
|
|
|
>
|
|
|
- <el-form-item label="服务器名称" prop="name">
|
|
|
- <el-input
|
|
|
- v-model="form.name"
|
|
|
- placeholder="例如:本地服务器"
|
|
|
- size="large"
|
|
|
- :prefix-icon="Connection"
|
|
|
- />
|
|
|
- </el-form-item>
|
|
|
-
|
|
|
<el-form-item label="服务器地址" prop="url">
|
|
|
<div class="url-input-row">
|
|
|
- <el-input
|
|
|
- v-model="form.url"
|
|
|
- placeholder="例如:http://localhost:3000"
|
|
|
+ <el-input
|
|
|
+ v-model="serverForm.url"
|
|
|
+ placeholder="例如:https://media-manage.example.com"
|
|
|
size="large"
|
|
|
:prefix-icon="Link"
|
|
|
/>
|
|
|
@@ -100,19 +59,6 @@
|
|
|
</el-button>
|
|
|
</div>
|
|
|
</el-form-item>
|
|
|
-
|
|
|
- <el-form-item class="checkbox-row">
|
|
|
- <el-checkbox v-model="form.isDefault">设为默认服务器</el-checkbox>
|
|
|
- </el-form-item>
|
|
|
-
|
|
|
- <el-form-item class="action-row">
|
|
|
- <el-button type="primary" @click="addServer" :loading="loading" class="primary-btn">
|
|
|
- 添加服务器
|
|
|
- </el-button>
|
|
|
- <el-button v-if="serverStore.isConfigured" @click="goBack" class="back-btn">
|
|
|
- 返回
|
|
|
- </el-button>
|
|
|
- </el-form-item>
|
|
|
</el-form>
|
|
|
|
|
|
<div class="connection-status" v-if="connectionResult !== null">
|
|
|
@@ -123,24 +69,67 @@
|
|
|
show-icon
|
|
|
/>
|
|
|
</div>
|
|
|
+
|
|
|
+ <el-divider />
|
|
|
+
|
|
|
+ <div class="python-config">
|
|
|
+ <div class="section-title-row">
|
|
|
+ <div class="section-title">Python 服务配置</div>
|
|
|
+ <el-tag v-if="!canManagePythonService" type="info" size="small">需管理员登录</el-tag>
|
|
|
+ </div>
|
|
|
+ <el-form :model="pythonService" label-position="top" class="config-form">
|
|
|
+ <el-form-item label="服务地址">
|
|
|
+ <div class="url-input-row">
|
|
|
+ <el-input
|
|
|
+ v-model="pythonService.url"
|
|
|
+ placeholder="例如:http://localhost:5005"
|
|
|
+ size="large"
|
|
|
+ :prefix-icon="Link"
|
|
|
+ :disabled="!pythonFormEnabled"
|
|
|
+ />
|
|
|
+ <el-button @click="checkPythonService" :loading="checkingPython" class="test-btn" :disabled="!pythonFormEnabled">
|
|
|
+ 测试连接
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+
|
|
|
+ <div class="connection-status" v-if="pythonCheckResult">
|
|
|
+ <el-alert
|
|
|
+ :type="pythonCheckResult.ok ? 'success' : 'error'"
|
|
|
+ :title="pythonCheckResult.ok ? '连接成功' : '连接失败'"
|
|
|
+ :description="pythonCheckResult.ok ? 'Python 服务响应正常' : (pythonCheckResult.error || '无法连接到 Python 服务')"
|
|
|
+ show-icon
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="bottom-actions">
|
|
|
+ <el-button type="primary" @click="saveAll" :loading="savingAll" class="primary-btn">
|
|
|
+ 保存配置
|
|
|
+ </el-button>
|
|
|
+ <el-button @click="goBack" class="back-btn">
|
|
|
+ 返回
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
-import { ref, reactive, onMounted } from 'vue';
|
|
|
+import { ref, reactive, onMounted, computed, watch } from 'vue';
|
|
|
import { useRouter } from 'vue-router';
|
|
|
-import { Setting, Connection, Link } from '@element-plus/icons-vue';
|
|
|
-import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus';
|
|
|
+import { Setting, Link } from '@element-plus/icons-vue';
|
|
|
+import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
|
|
|
import { useServerStore } from '@/stores/server';
|
|
|
import { useAuthStore } from '@/stores/auth';
|
|
|
+import request from '@/api/request';
|
|
|
|
|
|
const router = useRouter();
|
|
|
const serverStore = useServerStore();
|
|
|
const authStore = useAuthStore();
|
|
|
|
|
|
-const formRef = ref<FormInstance>();
|
|
|
-const loading = ref(false);
|
|
|
+const serverFormRef = ref<FormInstance>();
|
|
|
const testing = ref(false);
|
|
|
const connectionResult = ref<boolean | null>(null);
|
|
|
const isMaximized = ref(false);
|
|
|
@@ -168,22 +157,141 @@ onMounted(async () => {
|
|
|
});
|
|
|
});
|
|
|
|
|
|
-const form = reactive({
|
|
|
- name: '',
|
|
|
+const serverForm = reactive({
|
|
|
url: '',
|
|
|
- isDefault: true,
|
|
|
});
|
|
|
|
|
|
-const rules: FormRules = {
|
|
|
- name: [{ required: true, message: '请输入服务器名称', trigger: 'blur' }],
|
|
|
+const serverRules: FormRules = {
|
|
|
url: [
|
|
|
{ required: true, message: '请输入服务器地址', trigger: 'blur' },
|
|
|
{ pattern: /^https?:\/\/.+/, message: '请输入有效的 URL', trigger: 'blur' },
|
|
|
],
|
|
|
};
|
|
|
|
|
|
+const pythonService = reactive({
|
|
|
+ url: '',
|
|
|
+});
|
|
|
+const savingAll = ref(false);
|
|
|
+const checkingPython = ref(false);
|
|
|
+const pythonCheckResult = ref<any | null>(null);
|
|
|
+
|
|
|
+function normalizeBaseUrl(url?: string): string {
|
|
|
+ const raw = String(url || '').trim();
|
|
|
+ if (!raw) return '';
|
|
|
+ try {
|
|
|
+ const u = new URL(raw);
|
|
|
+ return `${u.protocol}//${u.host}`.replace(/\/$/, '');
|
|
|
+ } catch {
|
|
|
+ return raw.replace(/\/$/, '');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function isLocalServerUrl(url?: string): boolean {
|
|
|
+ if (!url) return false;
|
|
|
+ try {
|
|
|
+ const u = new URL(url);
|
|
|
+ const host = u.hostname;
|
|
|
+ return host === 'localhost' || host === '127.0.0.1' || host === '::1';
|
|
|
+ } catch {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const apiBaseUrl = computed(() => {
|
|
|
+ return normalizeBaseUrl(serverStore.currentServer?.url) || normalizeBaseUrl(serverForm.url);
|
|
|
+});
|
|
|
+
|
|
|
+const canManagePythonService = computed(() => authStore.isAdmin || isLocalServerUrl(apiBaseUrl.value));
|
|
|
+const pythonFormEnabled = computed(() => !!apiBaseUrl.value && canManagePythonService.value);
|
|
|
+
|
|
|
+async function loadPythonService() {
|
|
|
+ try {
|
|
|
+ if (!pythonFormEnabled.value) return;
|
|
|
+ const config = await request.get('/api/system/python-service', { baseURL: apiBaseUrl.value });
|
|
|
+ pythonService.url = String(config.url || '');
|
|
|
+ } catch {
|
|
|
+ // 错误已处理
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function saveAll() {
|
|
|
+ if (!serverFormRef.value) return;
|
|
|
+
|
|
|
+ const valid = await serverFormRef.value.validate().catch(() => false);
|
|
|
+ if (!valid) return;
|
|
|
+
|
|
|
+ const normalizedServerUrl = normalizeBaseUrl(serverForm.url);
|
|
|
+ if (!normalizedServerUrl) {
|
|
|
+ ElMessage.warning('请输入服务器地址');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ savingAll.value = true;
|
|
|
+ try {
|
|
|
+ serverStore.setSingleServer({
|
|
|
+ url: normalizedServerUrl,
|
|
|
+ });
|
|
|
+
|
|
|
+ if (pythonService.url && canManagePythonService.value) {
|
|
|
+ await request.put('/api/system/python-service', { url: normalizeBaseUrl(pythonService.url) }, { baseURL: normalizedServerUrl });
|
|
|
+ }
|
|
|
+
|
|
|
+ ElMessage.success('配置已保存');
|
|
|
+ } catch {
|
|
|
+ // 错误已处理
|
|
|
+ } finally {
|
|
|
+ savingAll.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function checkPythonService() {
|
|
|
+ if (!canManagePythonService.value) {
|
|
|
+ ElMessage.warning('需要管理员登录后才能配置');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (!apiBaseUrl.value) {
|
|
|
+ ElMessage.warning('请先填写服务器地址');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ checkingPython.value = true;
|
|
|
+ pythonCheckResult.value = null;
|
|
|
+ try {
|
|
|
+ const result = await request.post(
|
|
|
+ '/api/system/python-service/check',
|
|
|
+ { url: pythonService.url ? normalizeBaseUrl(pythonService.url) : undefined },
|
|
|
+ { baseURL: apiBaseUrl.value }
|
|
|
+ );
|
|
|
+ pythonCheckResult.value = result;
|
|
|
+ if (result.ok) {
|
|
|
+ ElMessage.success('连接成功');
|
|
|
+ } else {
|
|
|
+ ElMessage.error('连接失败');
|
|
|
+ }
|
|
|
+ } catch {
|
|
|
+ pythonCheckResult.value = { ok: false, error: '请求失败' };
|
|
|
+ ElMessage.error('连接失败');
|
|
|
+ } finally {
|
|
|
+ checkingPython.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ serverForm.url = serverStore.currentServer?.url || '';
|
|
|
+ if (pythonFormEnabled.value) loadPythonService();
|
|
|
+});
|
|
|
+
|
|
|
+watch(
|
|
|
+ () => serverStore.currentServerId,
|
|
|
+ () => {
|
|
|
+ pythonCheckResult.value = null;
|
|
|
+ pythonService.url = '';
|
|
|
+ serverForm.url = serverStore.currentServer?.url || serverForm.url;
|
|
|
+ if (pythonFormEnabled.value) loadPythonService();
|
|
|
+ }
|
|
|
+);
|
|
|
+
|
|
|
async function testConnection() {
|
|
|
- if (!form.url) {
|
|
|
+ if (!serverForm.url) {
|
|
|
ElMessage.warning('请先输入服务器地址');
|
|
|
return;
|
|
|
}
|
|
|
@@ -192,7 +300,7 @@ async function testConnection() {
|
|
|
connectionResult.value = null;
|
|
|
|
|
|
try {
|
|
|
- const result = await serverStore.checkConnection(form.url);
|
|
|
+ const result = await serverStore.checkConnection(normalizeBaseUrl(serverForm.url));
|
|
|
connectionResult.value = result;
|
|
|
if (result) {
|
|
|
ElMessage.success('连接成功');
|
|
|
@@ -207,66 +315,6 @@ async function testConnection() {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-async function addServer() {
|
|
|
- if (!formRef.value) return;
|
|
|
-
|
|
|
- const valid = await formRef.value.validate().catch(() => false);
|
|
|
- if (!valid) return;
|
|
|
-
|
|
|
- loading.value = true;
|
|
|
- try {
|
|
|
- // 先测试连接
|
|
|
- const connected = await serverStore.checkConnection(form.url);
|
|
|
- if (!connected) {
|
|
|
- ElMessage.warning('服务器连接失败,仍要添加吗?');
|
|
|
- }
|
|
|
-
|
|
|
- serverStore.addServer({
|
|
|
- name: form.name,
|
|
|
- url: form.url.replace(/\/$/, ''), // 移除末尾斜杠
|
|
|
- isDefault: form.isDefault,
|
|
|
- });
|
|
|
-
|
|
|
- ElMessage.success('服务器添加成功');
|
|
|
-
|
|
|
- // 清空表单
|
|
|
- form.name = '';
|
|
|
- form.url = '';
|
|
|
- form.isDefault = false;
|
|
|
- connectionResult.value = null;
|
|
|
-
|
|
|
- // 如果已登录且切换了服务器,清除登录状态
|
|
|
- if (authStore.isAuthenticated) {
|
|
|
- authStore.clearTokens();
|
|
|
- }
|
|
|
- } finally {
|
|
|
- loading.value = false;
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-function selectServer(id: string) {
|
|
|
- if (id === serverStore.currentServerId) return;
|
|
|
-
|
|
|
- serverStore.setCurrentServer(id);
|
|
|
- // 切换服务器后清除登录状态
|
|
|
- authStore.clearTokens();
|
|
|
- ElMessage.success('已切换服务器');
|
|
|
-}
|
|
|
-
|
|
|
-async function deleteServer(id: string) {
|
|
|
- try {
|
|
|
- await ElMessageBox.confirm('确定要删除这个服务器配置吗?', '提示', {
|
|
|
- confirmButtonText: '确定',
|
|
|
- cancelButtonText: '取消',
|
|
|
- type: 'warning',
|
|
|
- });
|
|
|
- serverStore.removeServer(id);
|
|
|
- ElMessage.success('已删除');
|
|
|
- } catch {
|
|
|
- // 取消
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
function goBack() {
|
|
|
if (authStore.isAuthenticated) {
|
|
|
router.push('/');
|
|
|
@@ -590,4 +638,73 @@ function goBack() {
|
|
|
border-radius: $radius-base;
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+.python-config {
|
|
|
+ margin-top: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.section-title {
|
|
|
+ font-size: 15px;
|
|
|
+ font-weight: 700;
|
|
|
+ color: $text-primary;
|
|
|
+ margin-bottom: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.section-title-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ gap: 12px;
|
|
|
+ margin-bottom: 12px;
|
|
|
+
|
|
|
+ .section-title {
|
|
|
+ margin-bottom: 0;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.bottom-actions {
|
|
|
+ display: flex;
|
|
|
+ gap: 12px;
|
|
|
+ margin-top: 28px;
|
|
|
+
|
|
|
+ .primary-btn {
|
|
|
+ flex: 1;
|
|
|
+ height: 44px;
|
|
|
+ padding: 0 28px;
|
|
|
+ font-size: 15px;
|
|
|
+ font-weight: 600;
|
|
|
+ border-radius: $radius-base;
|
|
|
+ background: linear-gradient(135deg, $primary-color 0%, #3a7bd5 100%);
|
|
|
+ border: none;
|
|
|
+ box-shadow: 0 4px 16px rgba($primary-color, 0.3);
|
|
|
+ transition: all 0.3s;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ transform: translateY(-1px);
|
|
|
+ box-shadow: 0 6px 20px rgba($primary-color, 0.4);
|
|
|
+ }
|
|
|
+
|
|
|
+ &:active {
|
|
|
+ transform: translateY(0);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .back-btn {
|
|
|
+ height: 44px;
|
|
|
+ padding: 0 28px;
|
|
|
+ font-size: 15px;
|
|
|
+ font-weight: 500;
|
|
|
+ border-radius: $radius-base;
|
|
|
+ border: 1px solid $border-base;
|
|
|
+ background: #fff;
|
|
|
+ color: $text-regular;
|
|
|
+ transition: all 0.2s;
|
|
|
+ white-space: nowrap;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ border-color: $primary-color;
|
|
|
+ color: $primary-color;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
</style>
|