Files
squad-rain-ops-mini/wwwroot/plugins.html

465 lines
26 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>插件管理</title>
<link rel="stylesheet" href="/lib/css/bootstrap.min.css">
<link rel="stylesheet" href="/lib/css/font-awesome.min.css">
<style>
body { padding: 20px; background-color: #f8f9fa; font-family: 'Inter', system-ui, -apple-system, sans-serif; }
.card { margin-bottom: 20px; border: none; box-shadow: 0 4px 6px rgba(0,0,0,0.05); border-radius: 12px; transition: transform 0.2s; }
.card:hover { transform: translateY(-2px); }
.card-header { background-color: #fff; border-bottom: 1px solid #f0f0f0; display: flex; justify-content: space-between; align-items: center; padding: 15px 20px; border-radius: 12px 12px 0 0 !important; }
.status-badge { padding: 6px 12px; border-radius: 20px; font-size: 0.8rem; font-weight: 500; }
.status-enabled { background-color: #e6f4ea; color: #1e7e34; }
.status-disabled { background-color: #fce8e6; color: #c5221f; }
.card-body { padding: 20px; }
.card-title { font-weight: 600; margin-bottom: 0; }
.btn-primary { background-color: #4361ee; border-color: #4361ee; }
.btn-primary:hover { background-color: #3f37c9; border-color: #3f37c9; }
.category-header { margin-top: 30px; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 2px solid #e9ecef; color: #495057; font-weight: 700; }
</style>
</head>
<body>
<div id="app" class="container-fluid">
<h3 class="mb-4"><i class="fas fa-plug me-2"></i>插件管理</h3>
<div class="d-flex gap-2 mb-3">
<button class="btn btn-success" :disabled="loading" @click="enableAllPlugins">
<i class="fas fa-toggle-on me-2"></i>一键启用所有插件
</button>
<button class="btn btn-outline-danger" :disabled="loading" @click="disableAllPlugins">
<i class="fas fa-toggle-off me-2"></i>一键关闭所有插件
</button>
</div>
<div v-if="loading" class="text-center py-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
<div v-else>
<div v-for="(group, category) in categorizedPlugins" :key="category">
<h4 class="category-header">{{ category }}</h4>
<div class="row">
<div class="col-md-6 col-lg-2" v-for="plugin in group" :key="plugin.name">
<div class="card h-100">
<div class="card-header">
<h5 class="card-title">{{ plugin.name }}</h5>
<span class="status-badge" :class="plugin.isEnabled ? 'status-enabled' : 'status-disabled'">
{{ plugin.isEnabled ? '已启用' : '已禁用' }}
</span>
</div>
<div class="card-body d-flex flex-column">
<p class="card-text text-muted mb-4">{{ plugin.description }}</p>
<div class="mt-auto">
<button class="btn btn-primary w-100" @click="editConfig(plugin)">
<i class="fas fa-cog me-2"></i>配置
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Config Modal -->
<div class="modal fade" id="configModal" tabindex="-1">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">配置插件: {{ currentPlugin?.name }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="alert alert-info py-2">
<i class="fas fa-info-circle me-2"></i>修改配置后将自动保存并立即生效。
</div>
<div class="d-flex justify-content-between align-items-center mb-3">
<ul class="nav nav-tabs border-0">
<li class="nav-item">
<a class="nav-link" :class="{ active: editMode === 'visual' }" href="#" @click.prevent="switchMode('visual')">
<i class="fas fa-eye me-2"></i>可视化视图
</a>
</li>
<li class="nav-item">
<a class="nav-link" :class="{ active: editMode === 'json' }" href="#" @click.prevent="switchMode('json')">
<i class="fas fa-code me-2"></i>JSON 编辑
</a>
</li>
</ul>
</div>
<!-- Visual Editor -->
<div v-if="editMode === 'visual'" class="border rounded p-3 bg-light" style="max-height: 60vh; overflow-y: auto;">
<div v-for="(value, key) in configObject" :key="key" class="mb-3">
<label class="form-label fw-bold">
{{ currentPlugin.descriptions && currentPlugin.descriptions[key] ? currentPlugin.descriptions[key] : formatLabel(key) }}
<small class="text-muted fw-normal">({{ key }})</small>
</label>
<!-- Boolean -->
<div v-if="typeof value === 'boolean'" class="form-check form-switch">
<input class="form-check-input" type="checkbox" v-model="configObject[key]">
<label class="form-check-label">{{ configObject[key] ? '已启用' : '已禁用' }}</label>
</div>
<!-- Number -->
<input v-else-if="typeof value === 'number'" type="number" class="form-control" v-model.number="configObject[key]">
<!-- Array -->
<div v-else-if="Array.isArray(value)">
<!-- Object Array Mode -->
<div v-if="isObjectList(key, value, currentPlugin.name)">
<div v-if="value.length > 0">
<div class="card mb-2" v-for="(item, index) in value" :key="index">
<div v-if="typeof item !== 'object' || item === null" class="card-body p-2 bg-light text-danger">
<div class="d-flex justify-content-between align-items-center">
<span><i class="fas fa-exclamation-triangle me-2"></i>数据格式错误 ({{ item }})</span>
<button class="btn btn-sm btn-outline-danger" @click="removeItem(key, index)">删除</button>
</div>
</div>
<div v-else class="card-body p-2 bg-white">
<div class="d-flex justify-content-between mb-2 border-bottom pb-1">
<span class="fw-bold small text-secondary">
{{ currentPlugin.descriptions && currentPlugin.descriptions[key + '$Item'] ? currentPlugin.descriptions[key + '$Item'] : '项目' }} #{{ index + 1 }}
</span>
<button class="btn btn-sm btn-outline-danger py-0" @click="removeItem(key, index)">
<i class="fas fa-times"></i>
</button>
</div>
<div class="row g-2">
<div v-for="(subVal, subKey) in item" :key="subKey" class="col-12">
<label class="form-label small text-muted mb-0">
{{ currentPlugin.descriptions && currentPlugin.descriptions[subKey] ? currentPlugin.descriptions[subKey] : formatLabel(subKey) }}
<span class="fw-normal" style="font-size: 0.8em">({{ subKey }})</span>
</label>
<input v-if="typeof subVal === 'number'" type="number" class="form-control form-control-sm" v-model.number="item[subKey]" :min="subKey === 'IntervalSeconds' ? 3 : null">
<div v-else-if="typeof subVal === 'boolean'" class="form-check form-switch ms-1">
<input class="form-check-input" type="checkbox" v-model="item[subKey]">
</div>
<input v-else-if="isDateField(subKey)" type="date" class="form-control form-control-sm" :value="formatDate(item[subKey])" @input="item[subKey] = $event.target.value">
<input v-else type="text" class="form-control form-control-sm" v-model="item[subKey]">
</div>
</div>
</div>
</div>
</div>
<div v-else class="alert alert-light text-center small text-muted">
暂无配置项
</div>
<button class="btn btn-sm btn-outline-primary" @click="addItemObject(key, getTemplate(key, currentPlugin.name))">
<i class="fas fa-plus me-1"></i>添加项
</button>
</div>
<!-- Primitive Array Mode -->
<div v-else>
<div class="input-group mb-2" v-for="(item, index) in value" :key="index">
<input type="text" class="form-control" v-model="configObject[key][index]">
<button class="btn btn-outline-danger" @click="removeItem(key, index)">
<i class="fas fa-trash"></i>
</button>
</div>
<button class="btn btn-sm btn-outline-primary" @click="addItem(key)">
<i class="fas fa-plus me-1"></i>添加项
</button>
</div>
</div>
<!-- String/Other -->
<input v-else type="text" class="form-control" v-model="configObject[key]">
</div>
</div>
<!-- JSON Editor -->
<div v-else class="mb-3">
<textarea class="form-control" v-model="configJson" rows="15" style="font-family: 'Consolas', monospace; font-size: 0.9rem; background-color: #f8f9fa;"></textarea>
</div>
</div>
<div class="modal-footer justify-content-between">
<button type="button" class="btn btn-outline-danger" @click="resetConfig">
<i class="fas fa-undo me-2"></i>恢复默认
</button>
<div>
<button type="button" class="btn btn-secondary me-2" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" @click="saveConfig">
<i class="fas fa-save me-2"></i>保存配置
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="/lib/js/bootstrap.bundle.min.js"></script>
<script src="/lib/js/vue.global.prod.js"></script>
<script>
const { createApp, ref, onMounted, computed } = Vue;
createApp({
setup() {
const plugins = ref([]);
const loading = ref(true);
const currentPlugin = ref(null);
const configJson = ref('');
const configObject = ref({});
const editMode = ref('visual'); // 'visual' or 'json'
let modal = null;
const loadPlugins = async () => {
try {
const res = await fetch('/api/plugins');
if (res.ok) {
plugins.value = await res.json();
}
} catch (e) {
console.error(e);
} finally {
loading.value = false;
}
};
const categorizedPlugins = computed(() => {
const groups = {};
// Sort categories if needed, or just grouping
plugins.value.forEach(p => {
const cat = p.categoryName || '其他';
if (!groups[cat]) groups[cat] = [];
groups[cat].push(p);
});
return groups;
});
const editConfig = (plugin) => {
currentPlugin.value = plugin;
// Deep copy to avoid modifying original until save
configObject.value = JSON.parse(JSON.stringify(plugin.config));
// Fix: Ensure specific object lists are clean from primitive values (e.g. if user added empty strings by mistake)
if (plugin.name === '个人BUFF系统') {
if (Array.isArray(configObject.value.VipList)) {
configObject.value.VipList = configObject.value.VipList.filter(item => typeof item === 'object' && item !== null);
} else if (Array.isArray(configObject.value.vipList)) {
configObject.value.vipList = configObject.value.vipList.filter(item => typeof item === 'object' && item !== null);
}
}
configJson.value = JSON.stringify(plugin.config, null, 4);
editMode.value = 'visual'; // Default to visual
if (!modal) modal = new bootstrap.Modal(document.getElementById('configModal'));
modal.show();
};
const switchMode = (mode) => {
if (mode === 'visual') {
try {
configObject.value = JSON.parse(configJson.value);
editMode.value = 'visual';
} catch (e) {
alert('JSON 格式错误,无法切换到可视化模式: ' + e.message);
}
} else {
configJson.value = JSON.stringify(configObject.value, null, 4);
editMode.value = 'json';
}
};
const formatLabel = (key) => {
// Convert CamelCase to Spaced Words
return key.replace(/([A-Z])/g, ' $1').replace(/^./, str => str.toUpperCase());
};
const addItem = (key) => {
if (Array.isArray(configObject.value[key])) {
configObject.value[key].push('');
}
};
const addItemObject = (key, templateItem) => {
if (Array.isArray(configObject.value[key])) {
// Clone structure of the template item with default empty values
const newItem = JSON.parse(JSON.stringify(templateItem));
// Optional: clear values
for (let k in newItem) {
if (typeof newItem[k] === 'string') newItem[k] = '';
if (typeof newItem[k] === 'number') newItem[k] = 0;
if (typeof newItem[k] === 'boolean') newItem[k] = false;
}
// Special default for IntervalSeconds if detected
if ('IntervalSeconds' in newItem) newItem.IntervalSeconds = 60;
configObject.value[key].push(newItem);
}
};
const removeItem = (key, index) => {
if (Array.isArray(configObject.value[key])) {
configObject.value[key].splice(index, 1);
}
};
const isObjectList = (key, value, pluginName) => {
if (pluginName === '个人BUFF系统' && (key === 'VipList' || key === 'vipList')) return true;
if (pluginName === '自动广播' && (key === 'Messages' || key === 'messages')) return true;
if (value.length > 0 && typeof value[0] === 'object' && value[0] !== null) return true;
return false;
};
const isDateField = (key) => {
const k = key.toLowerCase();
return k === 'expiretime' || k.endsWith('date') || k.endsWith('time');
};
const formatDate = (value) => {
if (!value) return '';
if (typeof value === 'string' && value.includes('T')) {
return value.split('T')[0];
}
return value;
};
const getTemplate = (key, pluginName) => {
if (pluginName === '个人BUFF系统' && (key === 'VipList' || key === 'vipList')) {
return { SteamId: '', Name: '', Reason: '', ExpireTime: '2099-12-31T23:59:59' };
}
if (pluginName === '自动广播' && (key === 'Messages' || key === 'messages')) {
return { Message: '', IntervalSeconds: 300 };
}
const val = configObject.value[key];
if (val && val.length > 0 && typeof val[0] === 'object') {
return val[0];
}
return {};
};
const saveConfig = async () => {
try {
// Ensure config is synced from current mode
let configToSave;
if (editMode.value === 'visual') {
configToSave = configObject.value;
// Update JSON string for consistency if we reopen
configJson.value = JSON.stringify(configObject.value, null, 4);
} else {
configToSave = JSON.parse(configJson.value);
// Update Object for consistency
configObject.value = configToSave;
}
const res = await fetch(`/api/plugins/${currentPlugin.value.name}/config`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(configToSave)
});
if (res.ok) {
alert('配置已保存');
modal.hide();
loadPlugins();
} else {
alert('保存失败');
}
} catch (e) {
alert('保存失败: ' + e.message);
}
};
const resetConfig = async () => {
if (!confirm('确定要恢复默认配置吗?当前所有修改将丢失。')) return;
try {
const res = await fetch(`/api/plugins/${currentPlugin.value.name}/reset`, {
method: 'POST'
});
if (res.ok) {
alert('配置已重置');
modal.hide();
loadPlugins();
} else {
alert('重置失败');
}
} catch (e) {
alert('重置失败: ' + e.message);
}
};
/**
* 一键设置所有插件启用状态
* 参数enable(boolean) - 是否启用
* 返回值Promise<void>
* 异常:网络或解析异常将通过 alert 显示,同时单个插件的更新异常仅记录到控制台,避免影响整体操作
*/
const setAllPluginsEnabled = async (enable) => {
try {
const res = await fetch('/api/plugins');
if (!res.ok) {
alert('获取插件列表失败');
return;
}
const list = await res.json();
for (const p of list) {
// 深拷贝配置,避免直接修改原对象
const cfg = JSON.parse(JSON.stringify(p.config || {}));
// 兼容大小写及缺失字段的情况
if (Object.prototype.hasOwnProperty.call(cfg, 'Enabled')) {
cfg.Enabled = enable;
} else if (Object.prototype.hasOwnProperty.call(cfg, 'enabled')) {
cfg.enabled = enable;
} else {
cfg.Enabled = enable;
}
// 独立 try-catch避免单个失败影响其他插件
try {
const r = await fetch(`/api/plugins/${p.name}/config`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(cfg)
});
if (!r.ok) {
console.warn('更新失败:', p.name);
}
} catch (innerErr) {
console.error('更新异常:', p.name, innerErr);
}
}
alert(enable ? '已启用所有插件' : '已关闭所有插件');
loadPlugins();
} catch (e) {
alert('操作失败: ' + e.message);
}
};
/**
* 一键启用所有插件
* 返回值Promise<void>
* 异常:参见 setAllPluginsEnabled
*/
const enableAllPlugins = async () => setAllPluginsEnabled(true);
/**
* 一键关闭所有插件
* 返回值Promise<void>
* 异常:参见 setAllPluginsEnabled
*/
const disableAllPlugins = async () => setAllPluginsEnabled(false);
onMounted(() => {
loadPlugins();
});
return {
plugins, loading, currentPlugin, configJson, configObject, editMode, categorizedPlugins,
editConfig, saveConfig, resetConfig, switchMode, formatLabel, addItem, addItemObject, removeItem,
isObjectList, getTemplate, isDateField, formatDate,
enableAllPlugins, disableAllPlugins
};
}
}).mount('#app');
</script>
</body>
</html>