Move logger.js from root to utils directory for better project structure Update InstanceDetail view to show full remote address instead of just port
1055 lines
35 KiB
Vue
1055 lines
35 KiB
Vue
<template>
|
|
<div class="instance-detail-page">
|
|
<div class="page-header">
|
|
<button class="common-btn" @click="goBack">
|
|
<i class="fas fa-arrow-left"></i> Back
|
|
</button>
|
|
<h2>{{ instanceName }} - Details</h2>
|
|
<button class="common-btn" @click="deleteInstance">
|
|
<i class="fas fa-trash"></i> Delete
|
|
</button>
|
|
</div>
|
|
|
|
<div class="detail-content">
|
|
<div class="section">
|
|
<h3>Instance Status
|
|
<div class="action-buttons">
|
|
<button
|
|
v-if="userType !== 'visitor' && instanceStatus === 'running'"
|
|
class="common-btn restart-btn"
|
|
@click="restartInstance"
|
|
>
|
|
<i class="fas fa-redo"></i> Restart
|
|
</button>
|
|
<button
|
|
v-if="userType !== 'visitor'"
|
|
class="common-btn status-btn"
|
|
:class="instanceStatus === 'running' ? 'stop-btn' : 'start-btn'"
|
|
@click="handleStatusClick"
|
|
>
|
|
<i :class="['fas', instanceStatus === 'running' ? 'fa-stop' : 'fa-play']"></i>
|
|
{{ instanceStatus === 'running' ? 'Stop' : 'Start' }}
|
|
</button>
|
|
</div>
|
|
</h3>
|
|
<div class="status-list">
|
|
<div class="status-item">
|
|
<span class="status-key">Status:</span>
|
|
<span :class="['status-value', instanceStatus]">
|
|
{{ instanceStatus === 'running' ? 'Running' : 'Stopped' }}
|
|
</span>
|
|
</div>
|
|
<div class="status-item" v-if="instanceStatus === 'running' && statusInfo.pid">
|
|
<span class="status-key">PID:</span>
|
|
<span class="status-value">{{ statusInfo.pid }}</span>
|
|
</div>
|
|
<div class="status-item" v-if="statusInfo.serviceName">
|
|
<span class="status-key">Service Name:</span>
|
|
<span class="status-value">{{ statusInfo.serviceName }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h3>Configuration
|
|
<button class="common-btn" @click="editConfig">
|
|
<i class="fas fa-edit"></i> Edit
|
|
</button>
|
|
</h3>
|
|
<div class="config-list">
|
|
<div class="config-item" v-for="(value, key) in instanceConfig" :key="key">
|
|
<span class="config-key">{{ key }}:</span>
|
|
<span class="config-value">{{ value }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="showEditConfigModal" class="modal-overlay" @click="closeEditConfigModal">
|
|
<div class="modal" @click.stop>
|
|
<h3>Edit Configuration</h3>
|
|
<form @submit.prevent="handleEditConfiguration" class="form">
|
|
<div class="form-group">
|
|
<label>Instance Name</label>
|
|
<input
|
|
type="text"
|
|
v-model="formData.name"
|
|
required
|
|
>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Authentication Method</label>
|
|
<select v-model="formData.auth_method" required>
|
|
<option value="token">Token</option>
|
|
<option value="oidc">OIDC</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Server Address</label>
|
|
<input
|
|
type="text"
|
|
v-model="formData.serverAddr"
|
|
required
|
|
>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Server Port</label>
|
|
<input
|
|
type="text"
|
|
v-model="formData.serverPort"
|
|
required
|
|
>
|
|
</div>
|
|
<div class="form-group" v-if="formData.auth_method === 'token'">
|
|
<label>Token</label>
|
|
<input
|
|
type="text"
|
|
v-model="formData.token"
|
|
required
|
|
>
|
|
</div>
|
|
<div v-if="formData.auth_method === 'oidc'">
|
|
<div class="form-group">
|
|
<label>Client ID</label>
|
|
<input
|
|
type="text"
|
|
v-model="formData.clientId"
|
|
required
|
|
>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Client Secret</label>
|
|
<input
|
|
type="text"
|
|
v-model="formData.clientSecret"
|
|
required
|
|
>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Audience</label>
|
|
<input
|
|
type="text"
|
|
v-model="formData.audience"
|
|
required
|
|
>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Token Endpoint URL</label>
|
|
<input
|
|
type="url"
|
|
v-model="formData.tokenEndpoint"
|
|
required
|
|
>
|
|
</div>
|
|
</div>
|
|
<div class="form-group checkbox-group">
|
|
<input
|
|
type="checkbox"
|
|
id="bootAtStart"
|
|
v-model="formData.bootAtStart"
|
|
>
|
|
<label for="bootAtStart">Boot at Start</label>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Run User</label>
|
|
<input
|
|
type="text"
|
|
v-model="formData.runUser"
|
|
placeholder="Default: root"
|
|
>
|
|
</div>
|
|
<div class="form-actions">
|
|
<button type="button" class="cancel-btn" @click="closeEditConfigModal">Cancel</button>
|
|
<button type="submit" class="submit-btn" :disabled="loading">{{ loading ? 'Processing...' : 'Save' }}</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h3>Proxy List
|
|
<button class="common-btn" @click="addProxy">
|
|
<i class="fas fa-plus"></i> Add Proxy
|
|
</button>
|
|
</h3>
|
|
<div v-if="proxies.length > 0" class="proxy-list">
|
|
<div v-for="proxy in proxies" :key="proxy.name" class="proxy-item">
|
|
<div class="proxy-header">
|
|
<span class="proxy-name">{{ proxy.name }}</span>
|
|
<div class="proxy-actions-right">
|
|
<span class="proxy-type">{{ proxy.type }}</span>
|
|
<div class="proxy-actions">
|
|
<button
|
|
class="common-btn edit-btn"
|
|
@click="editProxy(proxy)"
|
|
>
|
|
<i class="fas fa-edit"></i> Edit
|
|
</button>
|
|
<button
|
|
class="common-btn delete-btn"
|
|
@click="deleteProxy(proxy)"
|
|
>
|
|
<i class="fas fa-trash"></i> Delete
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="proxy-details">
|
|
<div class="config-item proxy-detail">
|
|
<span class="config-key">Local Address</span>
|
|
<span class="config-value">{{ proxy.localIP }}:{{ proxy.localPort }}</span>
|
|
</div>
|
|
<div class="config-item proxy-detail">
|
|
<span class="config-key">Remote Address</span>
|
|
<span class="config-value">{{ instanceConfig['Server Address'] || '' }}:{{ proxy.remotePort }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-else class="empty-state">
|
|
<p>No proxies available</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="showAddProxyModal" class="modal-overlay" @click="closeAddProxyModal">
|
|
<div class="modal" @click.stop>
|
|
<h3>{{ isEditProxy ? 'Edit Proxy' : 'Add Proxy' }}</h3>
|
|
<form @submit.prevent="handleProxySubmit" class="form">
|
|
<div class="form-group">
|
|
<label>Proxy Name</label>
|
|
<input
|
|
type="text"
|
|
v-model="formData.name"
|
|
required
|
|
>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Type</label>
|
|
<input
|
|
type="text"
|
|
v-model="formData.type"
|
|
required
|
|
>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Local IP</label>
|
|
<input
|
|
type="text"
|
|
v-model="formData.localIP"
|
|
required
|
|
>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Local Port</label>
|
|
<input
|
|
type="number"
|
|
v-model="formData.localPort"
|
|
required
|
|
min="0"
|
|
max="65535"
|
|
step="1"
|
|
>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Remote Port</label>
|
|
<input
|
|
type="number"
|
|
v-model="formData.remotePort"
|
|
required
|
|
min="0"
|
|
max="65535"
|
|
step="1"
|
|
>
|
|
</div>
|
|
<div class="form-actions">
|
|
<button type="button" class="cancel-btn" @click="closeAddProxyModal">Cancel</button>
|
|
<button type="submit" class="submit-btn" :disabled="loading">{{ loading ? 'Processing...' : (isEditProxy ? 'Save' : 'Submit') }}</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="showDeleteProxyModal" class="modal-overlay" @click="closeDeleteProxyModal">
|
|
<div class="modal" @click.stop>
|
|
<h3>Delete Proxy</h3>
|
|
<p>Are you sure you want to delete proxy <strong>{{ selectedProxyName }}</strong>?</p>
|
|
<div class="form-actions">
|
|
<button type="button" class="cancel-btn" @click="closeDeleteProxyModal">Cancel</button>
|
|
<button type="button" class="submit-btn delete-btn" @click="confirmDeleteProxy" :disabled="loading">
|
|
{{ loading ? 'Processing...' : 'Delete' }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h3>Logs</h3>
|
|
<div class="logs-container">
|
|
<div v-if="logs.length > 0" class="log-list">
|
|
<div v-for="(log, index) in logs" :key="index" class="log-item">
|
|
<span class="log-time">{{ log.time }}</span>
|
|
<span :class="['log-level', log.level]">{{ log.level }}</span>
|
|
<span class="log-message">{{ log.message }}</span>
|
|
</div>
|
|
</div>
|
|
<div v-else class="empty-state">
|
|
<p>No logs available</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import { ref, onMounted, onUnmounted } from 'vue';
|
|
import { useRoute, useRouter } from 'vue-router';
|
|
import { instanceApi } from '../api/index.js';
|
|
import { showNotification, formatDate, getCookie } from '../utils/functions.js';
|
|
|
|
export default {
|
|
name: 'InstanceDetail',
|
|
setup() {
|
|
const route = useRoute();
|
|
const router = useRouter();
|
|
const instanceID = ref(route.params.id);
|
|
const instanceName = ref('');
|
|
const instanceConfig = ref({});
|
|
const proxies = ref([]);
|
|
const logs = ref([]);
|
|
const instanceStatus = ref('stopped');
|
|
const statusInfo = ref({});
|
|
const showEditConfigModal = ref(false);
|
|
const showAddProxyModal = ref(false);
|
|
const showDeleteProxyModal = ref(false);
|
|
const isEditProxy = ref(false);
|
|
const selectedProxyName = ref('');
|
|
const loading = ref(false);
|
|
const userType = ref(getCookie('user-type') || 'visitor');
|
|
const formData = ref({
|
|
name: '',
|
|
auth_method: 'token',
|
|
serverAddr: '',
|
|
serverPort: '',
|
|
token: '',
|
|
clientId: '',
|
|
clientSecret: '',
|
|
audience: '',
|
|
tokenEndpoint: '',
|
|
bootAtStart: false,
|
|
runUser: ''
|
|
});
|
|
|
|
const loadInstanceInfo = async () => {
|
|
try {
|
|
const result = await instanceApi.getInstanceInfo(instanceID.value);
|
|
instanceName.value = result.data.name;
|
|
instanceStatus.value = result.data.isRunning ? 'running' : 'stopped';
|
|
statusInfo.value = {
|
|
pid: result.data.pid,
|
|
serviceName: result.data.serviceName,
|
|
isRunning: result.data.isRunning
|
|
};
|
|
instanceConfig.value = {
|
|
'Server Address': result.data.serverAddr || '',
|
|
'Server Port': result.data.serverPort || '',
|
|
'Auth Method': result.data.auth_method,
|
|
'Boot At Start': result.data.bootAtStart ? 'Yes' : 'No',
|
|
'Run User': result.data.runUser,
|
|
'Config Path': result.data.configPath,
|
|
'Created At': formatDate(result.data.createdAt)
|
|
};
|
|
} catch (error) {
|
|
showNotification('Load instance information failed', 'error');
|
|
}
|
|
};
|
|
|
|
const startInstance = async () => {
|
|
try {
|
|
await instanceApi.startInstance(instanceID.value);
|
|
showNotification('Instance started successfully', 'success');
|
|
await loadInstanceInfo();
|
|
} catch (error) {
|
|
showNotification(error.message || 'Start instance failed', 'error');
|
|
}
|
|
};
|
|
|
|
const stopInstance = async () => {
|
|
try {
|
|
await instanceApi.stopInstance(instanceID.value);
|
|
showNotification('Instance stopped successfully', 'success');
|
|
await loadInstanceInfo();
|
|
} catch (error) {
|
|
showNotification(error.message || 'Stop instance failed', 'error');
|
|
}
|
|
};
|
|
|
|
const deleteInstance = async () => {
|
|
try {
|
|
await instanceApi.deleteInstance(instanceID.value);
|
|
showNotification('Instance deleted successfully', 'success');
|
|
router.push('/instances');
|
|
} catch (error) {
|
|
showNotification(error.message || 'Delete instance failed', 'error');
|
|
}
|
|
};
|
|
|
|
const restartInstance = async () => {
|
|
try {
|
|
await instanceApi.restartInstance(instanceID.value);
|
|
showNotification('Instance restarted successfully', 'success');
|
|
await loadInstanceInfo();
|
|
} catch (error) {
|
|
showNotification(error.message || 'Restart instance failed', 'error');
|
|
}
|
|
};
|
|
|
|
const handleStatusClick = async () => {
|
|
if (instanceStatus.value === 'running') {
|
|
await stopInstance();
|
|
} else {
|
|
await startInstance();
|
|
}
|
|
};
|
|
|
|
const loadProxies = async () => {
|
|
try {
|
|
const result = await instanceApi.getInstanceProxies(instanceID.value);
|
|
const proxyList = result.data.proxies || [];
|
|
proxies.value = proxyList.map(proxy => ({
|
|
name: proxy.name,
|
|
type: proxy.type,
|
|
localIP: proxy.localIP,
|
|
localPort: proxy.localPort,
|
|
remotePort: proxy.remotePort
|
|
}));
|
|
} catch (error) {
|
|
showNotification('Load proxy list failed', 'error');
|
|
}
|
|
};
|
|
|
|
let logSocket = null;
|
|
|
|
const connectLogWebSocket = () => {
|
|
const token = getCookie('token');
|
|
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
const target = __APP_TARGET__;
|
|
const wsUrl = `${wsProtocol}//${target}/frpcAct/instanceMgr/logs?instanceID=${instanceID.value}&token=${token}`;
|
|
|
|
logSocket = new WebSocket(wsUrl);
|
|
|
|
logSocket.onopen = () => {
|
|
console.log(`Connected to instance ${instanceID.value} log server`);
|
|
};
|
|
|
|
logSocket.onmessage = (event) => {
|
|
try {
|
|
const log = JSON.parse(event.data);
|
|
logs.value.unshift({
|
|
time: log.timestamp,
|
|
level: log.level,
|
|
message: log.content
|
|
});
|
|
if (logs.value.length > 100) {
|
|
logs.value.pop();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error parsing log message:', error);
|
|
}
|
|
};
|
|
|
|
logSocket.onclose = () => {
|
|
console.log(`Disconnected from instance ${instanceID.value} log server`);
|
|
};
|
|
|
|
logSocket.onerror = (error) => {
|
|
console.error('WebSocket error:', error);
|
|
};
|
|
};
|
|
|
|
const disconnectLogWebSocket = () => {
|
|
if (logSocket) {
|
|
logSocket.close();
|
|
logSocket = null;
|
|
}
|
|
};
|
|
|
|
const loadLogs = async () => {
|
|
try {
|
|
const result = await instanceApi.getInstanceInfo(instanceID.value);
|
|
logs.value = result.data.logs ? result.data.logs.map(log => ({
|
|
time: formatDate(log.timestamp),
|
|
level: log.level,
|
|
message: log.content
|
|
})) : [];
|
|
} catch (error) {
|
|
showNotification('Load logs failed', 'error');
|
|
}
|
|
connectLogWebSocket();
|
|
};
|
|
|
|
const goBack = () => {
|
|
router.push('/instances');
|
|
};
|
|
|
|
const editConfig = async () => {
|
|
try {
|
|
const result = await instanceApi.getInstanceInfo(instanceID.value);
|
|
instanceName.value = result.data.name;
|
|
formData.value = {
|
|
name: result.data.name,
|
|
auth_method: result.data.auth_method || 'token',
|
|
serverAddr: result.data.serverAddr || '',
|
|
serverPort: result.data.serverPort || '',
|
|
token: '',
|
|
clientId: '',
|
|
clientSecret: '',
|
|
audience: '',
|
|
tokenEndpoint: '',
|
|
bootAtStart: result.data.bootAtStart || false,
|
|
runUser: result.data.runUser || 'root'
|
|
};
|
|
} catch (error) {
|
|
showNotification('Load instance data failed', 'error');
|
|
return;
|
|
}
|
|
showEditConfigModal.value = true;
|
|
};
|
|
|
|
const addProxy = () => {
|
|
isEditProxy.value = false;
|
|
formData.value = {
|
|
name: '',
|
|
type: '',
|
|
localIP: '',
|
|
localPort: '',
|
|
remotePort: '',
|
|
auth_method: formData.value.auth_method,
|
|
serverAddr: formData.value.serverAddr,
|
|
serverPort: formData.value.serverPort,
|
|
token: formData.value.token,
|
|
clientId: formData.value.clientId,
|
|
clientSecret: formData.value.clientSecret,
|
|
audience: formData.value.audience,
|
|
tokenEndpoint: formData.value.tokenEndpoint,
|
|
bootAtStart: formData.value.bootAtStart,
|
|
runUser: formData.value.runUser
|
|
};
|
|
showAddProxyModal.value = true;
|
|
}
|
|
|
|
const editProxy = (proxy) => {
|
|
isEditProxy.value = true;
|
|
formData.value = {
|
|
name: proxy.name,
|
|
type: proxy.type,
|
|
localIP: proxy.localIP,
|
|
localPort: parseInt(proxy.localPort),
|
|
remotePort: parseInt(proxy.remotePort),
|
|
auth_method: formData.value.auth_method,
|
|
serverAddr: formData.value.serverAddr,
|
|
serverPort: formData.value.serverPort,
|
|
token: formData.value.token,
|
|
clientId: formData.value.clientId,
|
|
clientSecret: formData.value.clientSecret,
|
|
audience: formData.value.audience,
|
|
tokenEndpoint: formData.value.tokenEndpoint,
|
|
bootAtStart: formData.value.bootAtStart,
|
|
runUser: formData.value.runUser
|
|
};
|
|
showAddProxyModal.value = true;
|
|
}
|
|
|
|
const deleteProxy = (proxy) => {
|
|
selectedProxyName.value = proxy.name;
|
|
showDeleteProxyModal.value = true;
|
|
}
|
|
|
|
const closeDeleteProxyModal = () => {
|
|
showDeleteProxyModal.value = false;
|
|
selectedProxyName.value = '';
|
|
}
|
|
|
|
const closeAddProxyModal = () => {
|
|
showAddProxyModal.value = false;
|
|
selectedProxyName.value = '';
|
|
};
|
|
|
|
const closeEditConfigModal = () => {
|
|
showEditConfigModal.value = false;
|
|
};
|
|
|
|
const confirmDeleteProxy = async () => {
|
|
loading.value = true;
|
|
try {
|
|
await instanceApi.deleteProxy(instanceID.value, selectedProxyName.value);
|
|
showNotification('Proxy deleted successfully', 'success');
|
|
closeDeleteProxyModal();
|
|
await loadProxies();
|
|
} catch (error) {
|
|
showNotification(error.message || 'Delete proxy failed', 'error');
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
const handleProxySubmit = async () => {
|
|
loading.value = true;
|
|
try {
|
|
const proxyInfo = {
|
|
name: formData.value.name,
|
|
type: formData.value.type,
|
|
localIP: formData.value.localIP,
|
|
localPort: formData.value.localPort.toString(),
|
|
remotePort: formData.value.remotePort.toString()
|
|
};
|
|
|
|
if (isEditProxy.value) {
|
|
await instanceApi.modifyProxy(instanceID.value, proxyInfo);
|
|
showNotification('Proxy updated successfully', 'success');
|
|
} else {
|
|
await instanceApi.createProxy(instanceID.value, proxyInfo);
|
|
showNotification('Proxy created successfully', 'success');
|
|
}
|
|
|
|
closeAddProxyModal();
|
|
await loadProxies();
|
|
} catch (error) {
|
|
showNotification(error.message || (isEditProxy.value ? 'Update proxy failed' : 'Create proxy failed'), 'error');
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
const handleEditConfiguration = async () => {
|
|
loading.value = true;
|
|
try {
|
|
const systemConfigData = { // Update system config
|
|
name: formData.value.name,
|
|
bootAtStart: formData.value.bootAtStart,
|
|
runUser: formData.value.runUser || 'root'
|
|
};
|
|
|
|
await instanceApi.modifyInstance(
|
|
instanceID.value,
|
|
'systemConfig',
|
|
systemConfigData
|
|
);
|
|
|
|
const configFileData = { // Update config file
|
|
serverAddr: formData.value.serverAddr,
|
|
serverPort: formData.value.serverPort,
|
|
auth_method: formData.value.auth_method
|
|
};
|
|
|
|
if (formData.value.auth_method === 'token') {
|
|
configFileData.auth_token = formData.value.token;
|
|
} else if (formData.value.auth_method === 'oidc') {
|
|
configFileData.oidc_client_id = formData.value.clientId;
|
|
configFileData.oidc_client_secret = formData.value.clientSecret;
|
|
configFileData.oidc_audience = formData.value.audience;
|
|
configFileData.oidc_token_endpoint = formData.value.tokenEndpoint;
|
|
}
|
|
|
|
await instanceApi.modifyInstance(
|
|
instanceID.value,
|
|
'configFile',
|
|
configFileData
|
|
);
|
|
|
|
showNotification('Configuration saved successfully', 'success');
|
|
closeEditConfigModal();
|
|
closeAddProxyModal();
|
|
await loadInstanceInfo();
|
|
} catch (error) {
|
|
showNotification(error.message || 'Save configuration failed', 'error');
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
const handleAddProxySubmit = async () => {
|
|
loading.value = true;
|
|
try {
|
|
const proxyInfo = {
|
|
name: formData.value.name,
|
|
type: formData.value.type,
|
|
localIP: formData.value.localIP,
|
|
localPort: formData.value.localPort.toString(),
|
|
remotePort: formData.value.remotePort.toString()
|
|
};
|
|
|
|
await instanceApi.createProxy(instanceID.value, proxyInfo);
|
|
|
|
showNotification('Proxy created successfully', 'success');
|
|
closeAddProxyModal();
|
|
await loadProxies();
|
|
} catch (error) {
|
|
showNotification(error.message || 'Create proxy failed', 'error');
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
onMounted(async () => {
|
|
await loadInstanceInfo();
|
|
await loadProxies();
|
|
await loadLogs();
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
disconnectLogWebSocket();
|
|
});
|
|
|
|
return {
|
|
instanceID,
|
|
instanceName,
|
|
instanceConfig,
|
|
proxies,
|
|
logs,
|
|
instanceStatus,
|
|
statusInfo,
|
|
userType,
|
|
showEditConfigModal,
|
|
showAddProxyModal,
|
|
showDeleteProxyModal,
|
|
isEditProxy,
|
|
selectedProxyName,
|
|
loading,
|
|
formData,
|
|
goBack,
|
|
editConfig,
|
|
closeEditConfigModal,
|
|
addProxy,
|
|
editProxy,
|
|
deleteProxy,
|
|
closeAddProxyModal,
|
|
closeDeleteProxyModal,
|
|
confirmDeleteProxy,
|
|
handleEditConfiguration,
|
|
handleProxySubmit,
|
|
handleStatusClick,
|
|
restartInstance,
|
|
deleteInstance
|
|
};
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
@import '../styles/common.css';
|
|
|
|
.instance-detail-page {
|
|
padding: 24px;
|
|
}
|
|
|
|
.page-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.detail-content {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 24px;
|
|
}
|
|
|
|
.config-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
}
|
|
|
|
.proxy-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
}
|
|
|
|
.proxy-item {
|
|
padding: 16px;
|
|
border-radius: 8px;
|
|
background: var(--card-bg);
|
|
border: 1px solid var(--border-color);
|
|
}
|
|
|
|
.proxy-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 12px;
|
|
gap: 12px;
|
|
}
|
|
|
|
.proxy-name {
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
color: var(--text-color);
|
|
}
|
|
|
|
.proxy-type {
|
|
display: inline-block;
|
|
padding: 4px 12px;
|
|
border-radius: 12px;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
background: var(--primary-bg);
|
|
color: var(--primary-color);
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.proxy-detail {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-size: 14px;
|
|
border: 5px solid var(--border-color);
|
|
}
|
|
|
|
.proxy-key {
|
|
color: var(--text-secondary);
|
|
font-weight: 500;
|
|
min-width: 90px;
|
|
}
|
|
|
|
.proxy-value {
|
|
color: var(--text-color);
|
|
font-family: monospace;
|
|
}
|
|
|
|
.log-item {
|
|
padding: 8px;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.log-time {
|
|
min-width: 140px;
|
|
}
|
|
|
|
.log-level {
|
|
min-width: 60px;
|
|
font-weight: 500;
|
|
padding: 2px 8px;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.log-level.DEBUG {
|
|
color: #52c41a;
|
|
background: #f6ffed;
|
|
}
|
|
|
|
.log-level.INFO {
|
|
color: #1890ff;
|
|
background: #e6f7ff;
|
|
}
|
|
|
|
.log-level.WARN {
|
|
color: #faad14;
|
|
background: #fffbe6;
|
|
}
|
|
|
|
.log-level.ERROR {
|
|
color: #ff4d4f;
|
|
background: #fff1f0;
|
|
}
|
|
|
|
.log-level.FATAL {
|
|
color: #cf1322;
|
|
background: #fff1f0;
|
|
}
|
|
|
|
.empty-state {
|
|
padding: 40px 20px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.modal-overlay {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 1000;
|
|
}
|
|
|
|
.modal {
|
|
background: var(--card-bg);
|
|
border-radius: 8px;
|
|
padding: 24px;
|
|
width: 90%;
|
|
max-width: 500px;
|
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
|
}
|
|
|
|
.modal h3 {
|
|
margin-top: 0;
|
|
margin-bottom: 20px;
|
|
color: var(--text-color);
|
|
}
|
|
|
|
.form-actions {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 12px;
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.cancel-btn {
|
|
padding: 10px 20px;
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 4px;
|
|
background: transparent;
|
|
color: var(--text-color);
|
|
cursor: pointer;
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.cancel-btn:hover {
|
|
background: var(--hover-bg);
|
|
}
|
|
|
|
.submit-btn {
|
|
padding: 10px 20px;
|
|
border: none;
|
|
border-radius: 4px;
|
|
background: var(--primary-color);
|
|
color: white;
|
|
cursor: pointer;
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.submit-btn:hover {
|
|
background: var(--primary-hover);
|
|
}
|
|
|
|
.submit-btn:disabled {
|
|
background: var(--disabled-bg);
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.section h3 {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.section h3 .action-buttons {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.status-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
}
|
|
|
|
.status-item {
|
|
padding: 16px;
|
|
border-radius: 8px;
|
|
background: var(--card-bg);
|
|
border: 1px solid var(--border-color);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
|
|
.status-key {
|
|
color: var(--text-secondary);
|
|
font-weight: 500;
|
|
min-width: 90px;
|
|
}
|
|
|
|
.status-value {
|
|
color: var(--text-color);
|
|
font-family: monospace;
|
|
}
|
|
|
|
.status-value.running {
|
|
color: #52c41a;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.status-value.stopped {
|
|
color: #ff4d4f;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.common-btn.status-btn {
|
|
padding: 8px 16px;
|
|
border: none;
|
|
border-radius: 4px;
|
|
background: #52c41a;
|
|
color: white;
|
|
cursor: pointer;
|
|
transition: all 0.3s;
|
|
display: flex;
|
|
align-items: flex-end;
|
|
gap: 8px;
|
|
}
|
|
|
|
.common-btn.status-btn:hover {
|
|
background: #40a412;
|
|
}
|
|
|
|
.common-btn.stop-btn {
|
|
background: #ff4d4f;
|
|
}
|
|
|
|
.common-btn.stop-btn:hover {
|
|
background: #cf1616;
|
|
}
|
|
|
|
.common-btn.restart-btn {
|
|
padding: 8px 16px;
|
|
border: none;
|
|
border-radius: 4px;
|
|
background: #1890ff;
|
|
color: white;
|
|
cursor: pointer;
|
|
transition: all 0.3s;
|
|
display: flex;
|
|
align-items: flex-end;
|
|
gap: 8px;
|
|
}
|
|
|
|
.common-btn.restart-btn:hover {
|
|
background: #096dd9;
|
|
}
|
|
|
|
.status-btn i,
|
|
.restart-btn i {
|
|
font-size: 14px;
|
|
}
|
|
|
|
.section:last-child .logs-container {
|
|
max-height: calc(100vh - 200px);
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.section:last-child .logs-container::-webkit-scrollbar {
|
|
width: 8px;
|
|
}
|
|
|
|
.section:last-child .logs-container::-webkit-scrollbar-track {
|
|
background: var(--bg-color);
|
|
border-radius: 0 8px 8px 0;
|
|
}
|
|
|
|
.section:last-child .logs-container::-webkit-scrollbar-thumb {
|
|
background: var(--primary-color);
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.section:last-child .logs-container::-webkit-scrollbar-thumb:hover {
|
|
background: var(--sidebar-active-bg);
|
|
}
|
|
</style>
|