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:
2026-03-24 12:36:21 +08:00
parent 5b7c866eb3
commit 7fdf62da7a
5 changed files with 241 additions and 49 deletions

View File

@@ -67,32 +67,33 @@ export const instanceApi = {
runUser, runUser,
additionalProperties additionalProperties
}), }),
deleteInstance: (instanceName) => deleteInstance: (instanceID) =>
api.post('/frpcAct/instanceMgr/delete', { instanceName }), api.post('/frpcAct/instanceMgr/delete', { instanceID }),
modifyInstance: (instanceName, instanceID, type, modifiedData) => modifyInstance: (instanceID, type, modifiedData) =>
api.post('/frpcAct/instanceMgr/modify', { api.post('/frpcAct/instanceMgr/modify', {
instanceName,
instanceID, instanceID,
type, type,
modifiedData modifiedData
}), }),
listInstances: () => api.get('/frpcAct/instanceMgr/list'), listInstances: () => api.get('/frpcAct/instanceMgr/list'),
startInstance: (instanceName) => startInstance: (instanceID) =>
api.post('/frpcAct/instanceMgr/start', { instanceName }), api.post('/frpcAct/instanceMgr/start', { instanceID }),
stopInstance: (instanceName) => stopInstance: (instanceID) =>
api.post('/frpcAct/instanceMgr/stop', { instanceName }), api.post('/frpcAct/instanceMgr/stop', { instanceID }),
restartInstance: (instanceName) => restartInstance: (instanceID) =>
api.post('/frpcAct/instanceMgr/restart', { instanceName }), api.post('/frpcAct/instanceMgr/restart', { instanceID }),
getInstanceLogs: (instanceName) => getInstanceStatus: (instanceID) =>
api.get('/frpcAct/instanceMgr/logs', { params: { instanceName } }), api.post('/frpcAct/instanceMgr/status', { instanceID }),
getInstanceLogs: (instanceID) =>
api.post('/frpcAct/instanceMgr/logs', { instanceID }),
getInstanceProxies: (instanceID) => getInstanceProxies: (instanceID) =>
api.get('/frpcAct/proxyMgr/list', { params: { instanceID } }), api.get(`/frpcAct/proxyMgr/list?instanceID=${instanceID}`),
createProxy: (instanceID, proxyInfo) => createProxy: (instanceID, proxyInfo) =>
api.post('/frpcAct/proxyMgr/create', { instanceID, proxyInfo }) api.post('/frpcAct/proxyMgr/create', { instanceID, proxyInfo })
}; };
export const logApi = { export const logApi = {
getLogs: (level) => api.get('/logMgr/list', { params: { level } }) getLogs: (level) => api.post('/logMgr/list', { level })
}; };
export const monitorApi = { export const monitorApi = {
@@ -106,9 +107,6 @@ export const systemInfoApi = {
export default api; export default api;
export const TODO_API_NOTES = { 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 中定义', instanceLogs: 'TODO: /frpcAct/instanceMgr/logs - 获取实例日志 API 未在 api-backend.md 中定义',
instanceProxies: 'TODO: /frpcAct/instanceMgr/proxies - 获取实例代理列表 API 未在 api-backend.md 中定义', instanceProxies: 'TODO: /frpcAct/instanceMgr/proxies - 获取实例代理列表 API 未在 api-backend.md 中定义',
logList: 'TODO: /logMgr/list - 获取系统日志 API 未在 api-backend.md 中定义', logList: 'TODO: /logMgr/list - 获取系统日志 API 未在 api-backend.md 中定义',

View File

@@ -114,7 +114,7 @@ export default {
onMounted(() => { onMounted(() => {
checkStatus(); checkStatus();
getSoftwareInfo(); getSoftwareInfo();
setInterval(checkStatus, 5000); setInterval(checkStatus, 60000);
document.addEventListener('click', closeMenu); document.addEventListener('click', closeMenu);
}); });

View File

@@ -21,7 +21,7 @@ const routes = [
component: () => import('../views/Instances.vue') component: () => import('../views/Instances.vue')
}, },
{ {
path: 'instances/:name', path: 'instances/:id',
name: 'InstanceDetail', name: 'InstanceDetail',
component: () => import('../views/InstanceDetail.vue') component: () => import('../views/InstanceDetail.vue')
}, },

View File

@@ -4,10 +4,53 @@
<button class="common-btn" @click="goBack"> <button class="common-btn" @click="goBack">
<i class="fas fa-arrow-left"></i> Back <i class="fas fa-arrow-left"></i> Back
</button> </button>
<h2>{{ instanceName }} - Details</h2> <h2>{{ instanceID }} - Details</h2>
</div> </div>
<div class="detail-content"> <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"> <div class="section">
<h3>Configuration <h3>Configuration
<button class="common-btn" @click="editConfig"> <button class="common-btn" @click="editConfig">
@@ -233,21 +276,23 @@
import { ref, onMounted } from 'vue'; import { ref, onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { instanceApi } from '../api/index.js'; import { instanceApi } from '../api/index.js';
import { showNotification, formatDate } from '../utils/functions.js'; import { showNotification, formatDate, getCookie } from '../utils/functions.js';
export default { export default {
name: 'InstanceDetail', name: 'InstanceDetail',
setup() { setup() {
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const instanceName = ref(route.params.name); const instanceID = ref(route.params.id);
const instanceID = ref('');
const instanceConfig = ref({}); const instanceConfig = ref({});
const proxies = ref([]); const proxies = ref([]);
const logs = ref([]); const logs = ref([]);
const instanceStatus = ref('stopped');
const statusInfo = ref({});
const showEditConfigModal = ref(false); const showEditConfigModal = ref(false);
const showAddProxyModal = ref(false); const showAddProxyModal = ref(false);
const loading = ref(false); const loading = ref(false);
const userType = ref(getCookie('user-type') || 'visitor');
const formData = ref({ const formData = ref({
name: '', name: '',
auth_method: 'token', auth_method: 'token',
@@ -265,9 +310,8 @@ export default {
const loadInstanceConfig = async () => { const loadInstanceConfig = async () => {
try { try {
const result = await instanceApi.listInstances(); 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) { if (instance) {
instanceID.value = String(instance.instanceID);
instanceConfig.value = { instanceConfig.value = {
'Server Address': instance.serverAddr, 'Server Address': instance.serverAddr,
'Server Port': instance.serverPort, '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 () => { const loadProxies = async () => {
try { try {
const result = await instanceApi.getInstanceProxies(instanceID.value); const result = await instanceApi.getInstanceProxies(instanceID.value);
@@ -301,7 +393,7 @@ export default {
const loadLogs = async () => { const loadLogs = async () => {
try { try {
const result = await instanceApi.getInstanceLogs(instanceName.value); const result = await instanceApi.getInstanceLogs(instanceID.value);
logs.value = result.data.map(log => ({ logs.value = result.data.map(log => ({
time: formatDate(log.timestamp), time: formatDate(log.timestamp),
level: log.level, level: log.level,
@@ -318,10 +410,8 @@ export default {
const editConfig = async () => { const editConfig = async () => {
try { try {
const result = await instanceApi.listInstances(); const instance = instanceID.value;
const instance = result.data.find(i => i.name === instanceName.value);
if (instance) { if (instance) {
instanceID.value = String(instance.instanceID);
formData.value = { formData.value = {
name: instance.name, name: instance.name,
auth_method: instance.auth_method || 'token', auth_method: instance.auth_method || 'token',
@@ -365,7 +455,6 @@ export default {
}; };
await instanceApi.modifyInstance( await instanceApi.modifyInstance(
instanceName.value,
instanceID.value, instanceID.value,
'systemConfig', 'systemConfig',
systemConfigData systemConfigData
@@ -387,7 +476,6 @@ export default {
} }
await instanceApi.modifyInstance( await instanceApi.modifyInstance(
instanceName.value,
instanceID.value, instanceID.value,
'configFile', 'configFile',
configFileData configFileData
@@ -397,7 +485,6 @@ export default {
closeEditConfigModal(); closeEditConfigModal();
closeAddProxyModal(); closeAddProxyModal();
// 重新加载配置 // 重新加载配置
instanceName.value = formData.value.name;
await loadInstanceConfig(); await loadInstanceConfig();
} catch (error) { } catch (error) {
showNotification(error.message || 'Save configuration failed', 'error'); showNotification(error.message || 'Save configuration failed', 'error');
@@ -431,15 +518,19 @@ export default {
onMounted(async () => { onMounted(async () => {
await loadInstanceConfig(); await loadInstanceConfig();
await loadInstanceStatus();
await loadProxies(); await loadProxies();
await loadLogs(); await loadLogs();
}); });
return { return {
instanceName, instanceID,
instanceConfig, instanceConfig,
proxies, proxies,
logs, logs,
instanceStatus,
statusInfo,
userType,
showEditConfigModal, showEditConfigModal,
showAddProxyModal, showAddProxyModal,
loading, loading,
@@ -450,7 +541,9 @@ export default {
addProxy, addProxy,
closeAddProxyModal, closeAddProxyModal,
handleEditConfiguration, handleEditConfiguration,
handleAddProxySubmit handleAddProxySubmit,
handleStatusClick,
restartInstance
}; };
} }
}; };
@@ -643,4 +736,94 @@ export default {
justify-content: space-between; justify-content: space-between;
margin-bottom: 16px; 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> </style>

View File

@@ -9,9 +9,9 @@
<div class="instances-grid"> <div class="instances-grid">
<div <div
v-for="instance in filteredInstances" v-for="instance in filteredInstances"
:key="instance.name" :key="instance.instanceID"
class="instance-card" class="instance-card"
@click="goToDetail(instance.name)" @click="goToDetail(instance.instanceID)"
> >
<div class="card-header"> <div class="card-header">
<h3 class="instance-name">{{ highlightText(instance.name, searchQuery) }}</h3> <h3 class="instance-name">{{ highlightText(instance.name, searchQuery) }}</h3>
@@ -35,18 +35,18 @@
</div> </div>
<div class="card-footer" @click.stop> <div class="card-footer" @click.stop>
<button <button
v-if="instance.status !== 'running'" v-if="userType !== 'visitor' && instance.status !== 'running'"
class="action-btn start-btn" class="action-btn start-btn"
@click="startInstance(instance.name)" @click="startInstance(instance.instanceID)"
> >
Start <i class="fas fa-play"></i> Start
</button> </button>
<button <button
v-else v-if="userType !== 'visitor' && instance.status === 'running'"
class="action-btn stop-btn" class="action-btn stop-btn"
@click="stopInstance(instance.name)" @click="stopInstance(instance.instanceID)"
> >
Stop <i class="fas fa-stop"></i> Stop
</button> </button>
</div> </div>
</div> </div>
@@ -164,7 +164,7 @@
import { ref, computed, onMounted } from 'vue'; import { ref, computed, onMounted } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { instanceApi } from '../api/index.js'; 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 { export default {
name: 'Instances', name: 'Instances',
@@ -179,6 +179,7 @@ export default {
const instances = ref([]); const instances = ref([]);
const showAddModal = ref(false); const showAddModal = ref(false);
const loading = ref(false); const loading = ref(false);
const userType = ref(getCookie('user-type') || 'visitor');
const formData = ref({ const formData = ref({
name: '', name: '',
auth_method: 'token', auth_method: 'token',
@@ -207,14 +208,23 @@ export default {
...instance, ...instance,
status: 'stopped' 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) { } catch (error) {
showNotification('Load instance list failed', 'error'); showNotification('Load instance list failed', 'error');
} }
}; };
const startInstance = async (instanceName) => { const startInstance = async (instanceID) => {
try { try {
await instanceApi.startInstance(instanceName); await instanceApi.startInstance(instanceID);
showNotification('Instance started successfully', 'success'); showNotification('Instance started successfully', 'success');
await loadInstances(); await loadInstances();
} catch (error) { } catch (error) {
@@ -222,9 +232,9 @@ export default {
} }
}; };
const stopInstance = async (instanceName) => { const stopInstance = async (instanceID) => {
try { try {
await instanceApi.stopInstance(instanceName); await instanceApi.stopInstance(instanceID);
showNotification('Instance stopped successfully', 'success'); showNotification('Instance stopped successfully', 'success');
await loadInstances(); await loadInstances();
} catch (error) { } catch (error) {
@@ -232,8 +242,8 @@ export default {
} }
}; };
const goToDetail = (instanceName) => { const goToDetail = (instanceID) => {
router.push(`/instances/${instanceName}`); router.push(`/instances/${instanceID}`);
}; };
const handleAddInstance = async () => { const handleAddInstance = async () => {
@@ -297,6 +307,7 @@ export default {
return { return {
instances, instances,
filteredInstances, filteredInstances,
userType,
startInstance, startInstance,
stopInstance, stopInstance,
goToDetail, goToDetail,