refactor(instance): migrate from name-based to ID-based instance management
feat(instanceDetail): add instance status display and restart && stop/start control - Change instance identification from name to ID in API endpoints and routes - Add instance status management functionality with start/stop/restart - Implement user type checks for instance operations - Update UI to reflect instance status changes - Remove completed TODO notes for implemented APIs
This commit is contained in:
@@ -67,32 +67,33 @@ export const instanceApi = {
|
||||
runUser,
|
||||
additionalProperties
|
||||
}),
|
||||
deleteInstance: (instanceName) =>
|
||||
api.post('/frpcAct/instanceMgr/delete', { instanceName }),
|
||||
modifyInstance: (instanceName, instanceID, type, modifiedData) =>
|
||||
deleteInstance: (instanceID) =>
|
||||
api.post('/frpcAct/instanceMgr/delete', { instanceID }),
|
||||
modifyInstance: (instanceID, type, modifiedData) =>
|
||||
api.post('/frpcAct/instanceMgr/modify', {
|
||||
instanceName,
|
||||
instanceID,
|
||||
type,
|
||||
modifiedData
|
||||
}),
|
||||
listInstances: () => api.get('/frpcAct/instanceMgr/list'),
|
||||
startInstance: (instanceName) =>
|
||||
api.post('/frpcAct/instanceMgr/start', { instanceName }),
|
||||
stopInstance: (instanceName) =>
|
||||
api.post('/frpcAct/instanceMgr/stop', { instanceName }),
|
||||
restartInstance: (instanceName) =>
|
||||
api.post('/frpcAct/instanceMgr/restart', { instanceName }),
|
||||
getInstanceLogs: (instanceName) =>
|
||||
api.get('/frpcAct/instanceMgr/logs', { params: { instanceName } }),
|
||||
startInstance: (instanceID) =>
|
||||
api.post('/frpcAct/instanceMgr/start', { instanceID }),
|
||||
stopInstance: (instanceID) =>
|
||||
api.post('/frpcAct/instanceMgr/stop', { instanceID }),
|
||||
restartInstance: (instanceID) =>
|
||||
api.post('/frpcAct/instanceMgr/restart', { instanceID }),
|
||||
getInstanceStatus: (instanceID) =>
|
||||
api.post('/frpcAct/instanceMgr/status', { instanceID }),
|
||||
getInstanceLogs: (instanceID) =>
|
||||
api.post('/frpcAct/instanceMgr/logs', { instanceID }),
|
||||
getInstanceProxies: (instanceID) =>
|
||||
api.get('/frpcAct/proxyMgr/list', { params: { instanceID } }),
|
||||
api.get(`/frpcAct/proxyMgr/list?instanceID=${instanceID}`),
|
||||
createProxy: (instanceID, proxyInfo) =>
|
||||
api.post('/frpcAct/proxyMgr/create', { instanceID, proxyInfo })
|
||||
};
|
||||
|
||||
export const logApi = {
|
||||
getLogs: (level) => api.get('/logMgr/list', { params: { level } })
|
||||
getLogs: (level) => api.post('/logMgr/list', { level })
|
||||
};
|
||||
|
||||
export const monitorApi = {
|
||||
@@ -106,9 +107,6 @@ export const systemInfoApi = {
|
||||
export default api;
|
||||
|
||||
export const TODO_API_NOTES = {
|
||||
instanceStart: 'TODO: /frpcAct/instanceMgr/start - 启动实例 API 未在 api-backend.md 中定义',
|
||||
instanceStop: 'TODO: /frpcAct/instanceMgr/stop - 停止实例 API 未在 api-backend.md 中定义',
|
||||
instanceRestart: 'TODO: /frpcAct/instanceMgr/restart - 重启实例 API 未在 api-backend.md 中定义',
|
||||
instanceLogs: 'TODO: /frpcAct/instanceMgr/logs - 获取实例日志 API 未在 api-backend.md 中定义',
|
||||
instanceProxies: 'TODO: /frpcAct/instanceMgr/proxies - 获取实例代理列表 API 未在 api-backend.md 中定义',
|
||||
logList: 'TODO: /logMgr/list - 获取系统日志 API 未在 api-backend.md 中定义',
|
||||
|
||||
@@ -114,7 +114,7 @@ export default {
|
||||
onMounted(() => {
|
||||
checkStatus();
|
||||
getSoftwareInfo();
|
||||
setInterval(checkStatus, 5000);
|
||||
setInterval(checkStatus, 60000);
|
||||
document.addEventListener('click', closeMenu);
|
||||
});
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ const routes = [
|
||||
component: () => import('../views/Instances.vue')
|
||||
},
|
||||
{
|
||||
path: 'instances/:name',
|
||||
path: 'instances/:id',
|
||||
name: 'InstanceDetail',
|
||||
component: () => import('../views/InstanceDetail.vue')
|
||||
},
|
||||
|
||||
@@ -4,10 +4,53 @@
|
||||
<button class="common-btn" @click="goBack">
|
||||
<i class="fas fa-arrow-left"></i> Back
|
||||
</button>
|
||||
<h2>{{ instanceName }} - Details</h2>
|
||||
<h2>{{ instanceID }} - Details</h2>
|
||||
</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.initSystem">
|
||||
<span class="status-key">Init System:</span>
|
||||
<span class="status-value">{{ statusInfo.initSystem }}</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">
|
||||
@@ -233,21 +276,23 @@
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { instanceApi } from '../api/index.js';
|
||||
import { showNotification, formatDate } from '../utils/functions.js';
|
||||
import { showNotification, formatDate, getCookie } from '../utils/functions.js';
|
||||
|
||||
export default {
|
||||
name: 'InstanceDetail',
|
||||
setup() {
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const instanceName = ref(route.params.name);
|
||||
const instanceID = ref('');
|
||||
const instanceID = ref(route.params.id);
|
||||
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',
|
||||
@@ -265,9 +310,8 @@ export default {
|
||||
const loadInstanceConfig = async () => {
|
||||
try {
|
||||
const result = await instanceApi.listInstances();
|
||||
const instance = result.data.find(i => i.name === instanceName.value);
|
||||
const instance = result.data.find(i => String(i.instanceID) === instanceID.value);
|
||||
if (instance) {
|
||||
instanceID.value = String(instance.instanceID);
|
||||
instanceConfig.value = {
|
||||
'Server Address': instance.serverAddr,
|
||||
'Server Port': instance.serverPort,
|
||||
@@ -283,6 +327,54 @@ export default {
|
||||
}
|
||||
};
|
||||
|
||||
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 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);
|
||||
@@ -301,7 +393,7 @@ export default {
|
||||
|
||||
const loadLogs = async () => {
|
||||
try {
|
||||
const result = await instanceApi.getInstanceLogs(instanceName.value);
|
||||
const result = await instanceApi.getInstanceLogs(instanceID.value);
|
||||
logs.value = result.data.map(log => ({
|
||||
time: formatDate(log.timestamp),
|
||||
level: log.level,
|
||||
@@ -318,10 +410,8 @@ export default {
|
||||
|
||||
const editConfig = async () => {
|
||||
try {
|
||||
const result = await instanceApi.listInstances();
|
||||
const instance = result.data.find(i => i.name === instanceName.value);
|
||||
const instance = instanceID.value;
|
||||
if (instance) {
|
||||
instanceID.value = String(instance.instanceID);
|
||||
formData.value = {
|
||||
name: instance.name,
|
||||
auth_method: instance.auth_method || 'token',
|
||||
@@ -365,7 +455,6 @@ export default {
|
||||
};
|
||||
|
||||
await instanceApi.modifyInstance(
|
||||
instanceName.value,
|
||||
instanceID.value,
|
||||
'systemConfig',
|
||||
systemConfigData
|
||||
@@ -387,7 +476,6 @@ export default {
|
||||
}
|
||||
|
||||
await instanceApi.modifyInstance(
|
||||
instanceName.value,
|
||||
instanceID.value,
|
||||
'configFile',
|
||||
configFileData
|
||||
@@ -397,7 +485,6 @@ export default {
|
||||
closeEditConfigModal();
|
||||
closeAddProxyModal();
|
||||
// 重新加载配置
|
||||
instanceName.value = formData.value.name;
|
||||
await loadInstanceConfig();
|
||||
} catch (error) {
|
||||
showNotification(error.message || 'Save configuration failed', 'error');
|
||||
@@ -431,15 +518,19 @@ export default {
|
||||
|
||||
onMounted(async () => {
|
||||
await loadInstanceConfig();
|
||||
await loadInstanceStatus();
|
||||
await loadProxies();
|
||||
await loadLogs();
|
||||
});
|
||||
|
||||
return {
|
||||
instanceName,
|
||||
instanceID,
|
||||
instanceConfig,
|
||||
proxies,
|
||||
logs,
|
||||
instanceStatus,
|
||||
statusInfo,
|
||||
userType,
|
||||
showEditConfigModal,
|
||||
showAddProxyModal,
|
||||
loading,
|
||||
@@ -450,7 +541,9 @@ export default {
|
||||
addProxy,
|
||||
closeAddProxyModal,
|
||||
handleEditConfiguration,
|
||||
handleAddProxySubmit
|
||||
handleAddProxySubmit,
|
||||
handleStatusClick,
|
||||
restartInstance
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -643,4 +736,94 @@ export default {
|
||||
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>
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
<div class="instances-grid">
|
||||
<div
|
||||
v-for="instance in filteredInstances"
|
||||
:key="instance.name"
|
||||
:key="instance.instanceID"
|
||||
class="instance-card"
|
||||
@click="goToDetail(instance.name)"
|
||||
@click="goToDetail(instance.instanceID)"
|
||||
>
|
||||
<div class="card-header">
|
||||
<h3 class="instance-name">{{ highlightText(instance.name, searchQuery) }}</h3>
|
||||
@@ -35,18 +35,18 @@
|
||||
</div>
|
||||
<div class="card-footer" @click.stop>
|
||||
<button
|
||||
v-if="instance.status !== 'running'"
|
||||
v-if="userType !== 'visitor' && instance.status !== 'running'"
|
||||
class="action-btn start-btn"
|
||||
@click="startInstance(instance.name)"
|
||||
@click="startInstance(instance.instanceID)"
|
||||
>
|
||||
Start
|
||||
<i class="fas fa-play"></i> Start
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
v-if="userType !== 'visitor' && instance.status === 'running'"
|
||||
class="action-btn stop-btn"
|
||||
@click="stopInstance(instance.name)"
|
||||
@click="stopInstance(instance.instanceID)"
|
||||
>
|
||||
Stop
|
||||
<i class="fas fa-stop"></i> Stop
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -164,7 +164,7 @@
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { instanceApi } from '../api/index.js';
|
||||
import { showNotification, formatDate, highlightText } from '../utils/functions.js';
|
||||
import { showNotification, formatDate, highlightText, getCookie } from '../utils/functions.js';
|
||||
|
||||
export default {
|
||||
name: 'Instances',
|
||||
@@ -179,6 +179,7 @@ export default {
|
||||
const instances = ref([]);
|
||||
const showAddModal = ref(false);
|
||||
const loading = ref(false);
|
||||
const userType = ref(getCookie('user-type') || 'visitor');
|
||||
const formData = ref({
|
||||
name: '',
|
||||
auth_method: 'token',
|
||||
@@ -207,14 +208,23 @@ export default {
|
||||
...instance,
|
||||
status: 'stopped'
|
||||
}));
|
||||
|
||||
for (const instance of instances.value) {
|
||||
try {
|
||||
const statusResult = await instanceApi.getInstanceStatus(String(instance.instanceID));
|
||||
instance.status = statusResult.data.status;
|
||||
} catch (error) {
|
||||
console.error(`Failed to load status for instance ${instance.instanceID}:`, error);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
showNotification('Load instance list failed', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
const startInstance = async (instanceName) => {
|
||||
const startInstance = async (instanceID) => {
|
||||
try {
|
||||
await instanceApi.startInstance(instanceName);
|
||||
await instanceApi.startInstance(instanceID);
|
||||
showNotification('Instance started successfully', 'success');
|
||||
await loadInstances();
|
||||
} catch (error) {
|
||||
@@ -222,9 +232,9 @@ export default {
|
||||
}
|
||||
};
|
||||
|
||||
const stopInstance = async (instanceName) => {
|
||||
const stopInstance = async (instanceID) => {
|
||||
try {
|
||||
await instanceApi.stopInstance(instanceName);
|
||||
await instanceApi.stopInstance(instanceID);
|
||||
showNotification('Instance stopped successfully', 'success');
|
||||
await loadInstances();
|
||||
} catch (error) {
|
||||
@@ -232,8 +242,8 @@ export default {
|
||||
}
|
||||
};
|
||||
|
||||
const goToDetail = (instanceName) => {
|
||||
router.push(`/instances/${instanceName}`);
|
||||
const goToDetail = (instanceID) => {
|
||||
router.push(`/instances/${instanceID}`);
|
||||
};
|
||||
|
||||
const handleAddInstance = async () => {
|
||||
@@ -297,6 +307,7 @@ export default {
|
||||
return {
|
||||
instances,
|
||||
filteredInstances,
|
||||
userType,
|
||||
startInstance,
|
||||
stopInstance,
|
||||
goToDetail,
|
||||
|
||||
Reference in New Issue
Block a user