Files
abot/admin/dashboard/templates/group_plugin_config.html
liuwei d4b7cb32f6 feat(群级配置): 新增MySQL+Redis持久缓存并接入进群欢迎差异化配置
新增群级插件配置表与服务层,采用MySQL持久化+Redis长期缓存(TTL=-1);后台新增群级插件配置管理页面与API,支持按群按插件维护JSON配置并在修改后同步回填MySQL和刷新Redis;已将群成员变更监控插件接入该配置,支持欢迎文案与卡片URL等按群差异化。
2026-04-20 10:42:46 +08:00

224 lines
9.7 KiB
HTML
Raw 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.
{% 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 %}