Implement instance deletion feature with confirmation notification and navigation back to instances list. Also adjust header layout to accommodate the new button.
846 lines
27 KiB
Vue
846 lines
27 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 } 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 loadInstanceConfig = async () => {
|
|
try {
|
|
const result = await instanceApi.listInstances();
|
|
const instance = result.data.find(i => String(i.instanceID) === instanceID.value);
|
|
if (instance) {
|
|
instanceName.value = instance.name;
|
|
instanceConfig.value = {
|
|
'Server Address': instance.serverAddr,
|
|
'Server Port': instance.serverPort,
|
|
'Auth Method': instance.auth_method,
|
|
'Boot At Start': instance.bootAtStart ? 'Yes' : 'No',
|
|
'Run User': instance.runUser,
|
|
'Config Path': instance.configPath,
|
|
'Created At': formatDate(instance.createdAt)
|
|
};
|
|
}
|
|
} catch (error) {
|
|
showNotification('Load instance configuration failed', 'error');
|
|
}
|
|
};
|
|
|
|
const loadInstanceStatus = async () => {
|
|
try {
|
|
const result = await instanceApi.getInstanceStatus(instanceID.value);
|
|
instanceStatus.value = result.data.status;
|
|
statusInfo.value = result.data;
|
|
} catch (error) {
|
|
showNotification('Load instance status failed', 'error');
|
|
}
|
|
};
|
|
|
|
const startInstance = async () => {
|
|
try {
|
|
await instanceApi.startInstance(instanceID.value);
|
|
showNotification('Instance started successfully', 'success');
|
|
await loadInstanceStatus();
|
|
} catch (error) {
|
|
showNotification(error.message || 'Start instance failed', 'error');
|
|
}
|
|
};
|
|
|
|
const stopInstance = async () => {
|
|
try {
|
|
await instanceApi.stopInstance(instanceID.value);
|
|
showNotification('Instance stopped successfully', 'success');
|
|
await loadInstanceStatus();
|
|
} 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 loadInstanceStatus();
|
|
} 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');
|
|
}
|
|
};
|
|
|
|
const loadLogs = async () => {
|
|
try {
|
|
const result = await instanceApi.getInstanceLogs(instanceID.value);
|
|
logs.value = result.data.map(log => ({
|
|
time: formatDate(log.timestamp),
|
|
level: log.level,
|
|
message: log.message
|
|
}));
|
|
} catch (error) {
|
|
showNotification('Load logs failed', 'error');
|
|
}
|
|
};
|
|
|
|
const goBack = () => {
|
|
router.push('/instances');
|
|
};
|
|
|
|
const editConfig = async () => {
|
|
try {
|
|
const result = await instanceApi.listInstances();
|
|
const instance = result.data.find(i => String(i.instanceID) === instanceID.value);
|
|
if (instance) {
|
|
instanceName.value = instance.name;
|
|
formData.value = {
|
|
name: instance.name,
|
|
auth_method: instance.auth_method || 'token',
|
|
serverAddr: instance.serverAddr || '',
|
|
serverPort: instance.serverPort || '',
|
|
token: '',
|
|
clientId: '',
|
|
clientSecret: '',
|
|
audience: '',
|
|
tokenEndpoint: '',
|
|
bootAtStart: instance.bootAtStart || false,
|
|
runUser: instance.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
|
|
server_addr: formData.value.serverAddr,
|
|
server_port: 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 loadInstanceConfig();
|
|
} 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 loadInstanceConfig();
|
|
await loadInstanceStatus();
|
|
await loadProxies();
|
|
await loadLogs();
|
|
});
|
|
|
|
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;
|
|
}
|
|
|
|
.log-level.INFO {
|
|
color: #1890ff;
|
|
}
|
|
|
|
.log-level.WARN {
|
|
color: #faad14;
|
|
}
|
|
|
|
.log-level.ERROR {
|
|
color: #ff4d4f;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
</style>
|