Implement consistent scrollbar styling across Home, SideBar, and InstanceDetail components using CSS variables for theming. The styling includes track and thumb colors with hover effects for better visual feedback.
923 lines
29 KiB
Vue
923 lines
29 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>
|
|
<span class="proxy-type">{{ proxy.type }}</span>
|
|
</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 Port</span>
|
|
<span class="config-value">{{ 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>Add Proxy</h3>
|
|
<form @submit.prevent="handleAddProxySubmit" 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...' : 'Submit' }}</button>
|
|
</div>
|
|
</form>
|
|
</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 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 = () => {
|
|
showAddProxyModal.value = true;
|
|
}
|
|
|
|
const closeEditConfigModal = () => {
|
|
showEditConfigModal.value = false;
|
|
};
|
|
|
|
const closeAddProxyModal = () => {
|
|
showAddProxyModal.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,
|
|
loading,
|
|
formData,
|
|
goBack,
|
|
editConfig,
|
|
closeEditConfigModal,
|
|
addProxy,
|
|
closeAddProxyModal,
|
|
handleEditConfiguration,
|
|
handleAddProxySubmit,
|
|
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>
|