新增群级插件配置表与服务层,采用MySQL持久化+Redis长期缓存(TTL=-1);后台新增群级插件配置管理页面与API,支持按群按插件维护JSON配置并在修改后同步回填MySQL和刷新Redis;已将群成员变更监控插件接入该配置,支持欢迎文案与卡片URL等按群差异化。
224 lines
9.7 KiB
HTML
224 lines
9.7 KiB
HTML
{% extends "base.html" %}
|
||
|
||
{% block title %}群级插件配置 - 机器人管理后台{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="page-shell">
|
||
<div class="page-hero">
|
||
<div class="page-hero-copy">
|
||
<div class="page-eyebrow">Group Plugin Config</div>
|
||
<h1>群级插件配置</h1>
|
||
<p>后台维护按群差异化配置(文案、URL、发送内容)。保存时写入 MySQL 并刷新 Redis 缓存(TTL 永久)。</p>
|
||
</div>
|
||
<div class="page-hero-actions">
|
||
<el-button type="success" @click="loadRows">刷新</el-button>
|
||
<el-button type="primary" @click="openCreate">新增配置</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<el-card shadow="hover" style="margin-bottom: 14px;">
|
||
<el-form inline>
|
||
<el-form-item label="群">
|
||
<el-select v-model="filters.group_id" filterable clearable placeholder="全部群" style="width:280px" @change="loadRows">
|
||
<el-option v-for="item in groupOptions" :key="item.wxid" :label="item.name" :value="item.wxid"></el-option>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="插件">
|
||
<el-select v-model="filters.plugin_name" filterable clearable placeholder="全部插件" style="width:220px" @change="loadRows">
|
||
<el-option v-for="name in pluginOptions" :key="name" :label="name" :value="name"></el-option>
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-form>
|
||
</el-card>
|
||
|
||
<el-card shadow="hover">
|
||
<el-table :data="rows" style="width:100%" v-loading="loading">
|
||
<el-table-column prop="id" label="ID" width="70"></el-table-column>
|
||
<el-table-column prop="group_id" label="群ID" min-width="190"></el-table-column>
|
||
<el-table-column prop="plugin_name" label="插件" min-width="140"></el-table-column>
|
||
<el-table-column prop="config_key" label="配置键" width="120"></el-table-column>
|
||
<el-table-column label="启用" width="90">
|
||
<template slot-scope="scope">
|
||
<el-tag :type="scope.row.enabled ? 'success' : 'info'">{% raw %}{{ scope.row.enabled ? '是' : '否' }}{% endraw %}</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column prop="version" label="版本" width="90"></el-table-column>
|
||
<el-table-column prop="updated_by" label="更新人" width="120"></el-table-column>
|
||
<el-table-column prop="updated_at" label="更新时间" width="180"></el-table-column>
|
||
<el-table-column label="配置预览" min-width="260">
|
||
<template slot-scope="scope">
|
||
<pre class="detail-pre">{% raw %}{{ JSON.stringify(scope.row.config_json || {}, null, 2) }}{% endraw %}</pre>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="180">
|
||
<template slot-scope="scope">
|
||
<el-button size="mini" type="primary" plain @click="openEdit(scope.row)">编辑</el-button>
|
||
<el-button size="mini" type="danger" plain @click="removeRow(scope.row)">删除</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</el-card>
|
||
|
||
<el-dialog :title="editing ? '编辑配置' : '新增配置'" :visible.sync="dialogVisible" width="720px">
|
||
<el-form label-width="95px">
|
||
<el-form-item label="群ID">
|
||
<el-select v-model="form.group_id" filterable placeholder="请选择群" style="width:100%">
|
||
<el-option v-for="item in groupOptions" :key="item.wxid" :label="item.name" :value="item.wxid"></el-option>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="插件">
|
||
<el-select v-model="form.plugin_name" filterable placeholder="请选择插件" style="width:100%">
|
||
<el-option v-for="name in pluginOptions" :key="name" :label="name" :value="name"></el-option>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="配置键">
|
||
<el-input v-model="form.config_key" placeholder="default"></el-input>
|
||
</el-form-item>
|
||
<el-form-item label="启用">
|
||
<el-switch v-model="form.enabled"></el-switch>
|
||
</el-form-item>
|
||
<el-form-item label="JSON配置">
|
||
<el-input type="textarea" :rows="12" v-model="form.config_json_text"></el-input>
|
||
</el-form-item>
|
||
</el-form>
|
||
<div slot="footer">
|
||
<el-button @click="dialogVisible=false">取消</el-button>
|
||
<el-button type="primary" @click="saveForm">保存</el-button>
|
||
</div>
|
||
</el-dialog>
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block scripts %}
|
||
<script>
|
||
new Vue({
|
||
el: '#app',
|
||
mixins: [baseApp],
|
||
data() {
|
||
return {
|
||
loading: false,
|
||
rows: [],
|
||
groupOptions: [],
|
||
pluginOptions: [],
|
||
filters: { group_id: '', plugin_name: '' },
|
||
dialogVisible: false,
|
||
editing: false,
|
||
form: {
|
||
group_id: '',
|
||
plugin_name: '',
|
||
config_key: 'default',
|
||
enabled: true,
|
||
config_json_text: '{}'
|
||
}
|
||
}
|
||
},
|
||
mounted() {
|
||
this.loadGroups()
|
||
this.loadPlugins()
|
||
this.loadRows()
|
||
},
|
||
methods: {
|
||
async loadGroups() {
|
||
const resp = await axios.get('/contacts/api/groups')
|
||
const groups = (resp.data && resp.data.data && resp.data.data.groups) || {}
|
||
this.groupOptions = Object.entries(groups).map(([wxid, name]) => ({ wxid, name: String(name || wxid) }))
|
||
},
|
||
async loadPlugins() {
|
||
const resp = await axios.get('/group_plugin_config/api/plugins')
|
||
if (resp.data && resp.data.success) {
|
||
this.pluginOptions = resp.data.data || []
|
||
}
|
||
},
|
||
async loadRows() {
|
||
this.loading = true
|
||
try {
|
||
const resp = await axios.get('/group_plugin_config/api/list', { params: this.filters })
|
||
if (resp.data && resp.data.success) {
|
||
this.rows = resp.data.data || []
|
||
}
|
||
} finally {
|
||
this.loading = false
|
||
}
|
||
},
|
||
openCreate() {
|
||
this.editing = false
|
||
this.form = {
|
||
group_id: '',
|
||
plugin_name: '',
|
||
config_key: 'default',
|
||
enabled: true,
|
||
config_json_text: '{}'
|
||
}
|
||
this.dialogVisible = true
|
||
},
|
||
openEdit(row) {
|
||
this.editing = true
|
||
this.form = {
|
||
group_id: row.group_id || '',
|
||
plugin_name: row.plugin_name || '',
|
||
config_key: row.config_key || 'default',
|
||
enabled: !!row.enabled,
|
||
config_json_text: JSON.stringify(row.config_json || {}, null, 2)
|
||
}
|
||
this.dialogVisible = true
|
||
},
|
||
async saveForm() {
|
||
let parsed = {}
|
||
try {
|
||
parsed = JSON.parse(this.form.config_json_text || '{}')
|
||
} catch (e) {
|
||
this.$message.error('JSON 配置格式错误')
|
||
return
|
||
}
|
||
const payload = {
|
||
group_id: this.form.group_id,
|
||
plugin_name: this.form.plugin_name,
|
||
config_key: this.form.config_key || 'default',
|
||
enabled: !!this.form.enabled,
|
||
config_json: parsed,
|
||
updated_by: 'dashboard'
|
||
}
|
||
const resp = await axios.post('/group_plugin_config/api/upsert', payload)
|
||
if (resp.data && resp.data.success) {
|
||
this.$message.success(resp.data.message || '保存成功')
|
||
this.dialogVisible = false
|
||
await this.loadRows()
|
||
return
|
||
}
|
||
this.$message.error((resp.data && resp.data.message) || '保存失败')
|
||
},
|
||
async removeRow(row) {
|
||
try {
|
||
await this.$confirm('确认删除该配置吗?', '提示', { type: 'warning' })
|
||
} catch (e) {
|
||
return
|
||
}
|
||
const resp = await axios.post('/group_plugin_config/api/delete', {
|
||
group_id: row.group_id,
|
||
plugin_name: row.plugin_name,
|
||
config_key: row.config_key
|
||
})
|
||
if (resp.data && resp.data.success) {
|
||
this.$message.success(resp.data.message || '删除成功')
|
||
await this.loadRows()
|
||
return
|
||
}
|
||
this.$message.error((resp.data && resp.data.message) || '删除失败')
|
||
}
|
||
}
|
||
})
|
||
</script>
|
||
{% endblock %}
|
||
|
||
{% block styles %}
|
||
<style>
|
||
.page-shell{display:flex;flex-direction:column;gap:16px}
|
||
.page-hero{display:flex;align-items:flex-end;justify-content:space-between;gap:18px;padding:24px 26px;border-radius:24px;background:linear-gradient(135deg, rgba(79,70,229,.10), rgba(59,130,246,.08), rgba(255,255,255,.9));border:1px solid rgba(148,163,184,.16);box-shadow:0 18px 40px rgba(15,23,42,.06)}
|
||
.page-hero-actions{display:flex;align-items:center;gap:12px}
|
||
.page-eyebrow{font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:#6366f1;font-weight:700;margin-bottom:8px}
|
||
.page-hero-copy h1{font-size:30px;line-height:1.1;margin-bottom:10px;color:#0f172a}
|
||
.page-hero-copy p{color:#64748b;font-size:14px}
|
||
.detail-pre{white-space:pre-wrap;word-break:break-word;background:rgba(248,250,252,.85);border:1px solid rgba(148,163,184,.12);border-radius:14px;padding:10px;color:#334155;max-height:180px;overflow:auto}
|
||
</style>
|
||
{% endblock %}
|