Files
abot/admin/dashboard/templates/group_plugin_config.html
liuwei f5dfe0b296 feat(群级配置): 优化群显示、模板入口和JSON编辑体验
- 列表中群信息改为群名主展示,ID弱化展示
- 模板入口改为按插件动态显示:仅选择群成员变更监控时显示欢迎模板按钮
- 移除顶部通用快捷模板,减少误导
- JSON编辑区升级为多行自适应编辑器,增加等宽字体与格式化按钮
2026-04-20 11:23:55 +08:00

476 lines
22 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 label="群" min-width="220">
<template slot-scope="scope">
<div class="group-cell">
<div class="group-name">{% raw %}{{ getGroupName(scope.row.group_id) }}{% endraw %}</div>
<div class="group-id">{% raw %}{{ scope.row.group_id }}{% endraw %}</div>
</div>
</template>
</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">
<div class="group-option">
<div class="group-option-name">{% raw %}{{ item.name }}{% endraw %}</div>
<div class="group-option-id">{% raw %}{{ item.wxid }}{% endraw %}</div>
</div>
</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="编辑模式" v-if="isWelcomeTemplateForm">
<el-radio-group v-model="editorMode">
<el-radio-button label="simple">标准表单</el-radio-button>
<el-radio-button label="advanced">高级JSON</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="插件模板" v-if="isMemberChangePlugin">
<div class="quick-actions">
<el-button size="mini" type="primary" plain @click="useWelcomeTemplate">
欢迎配置模板welcome
</el-button>
<span class="quick-tip">仅当插件为“群成员变更监控”时显示</span>
</div>
</el-form-item>
<el-alert v-if="isMemberChangePlugin && !isWelcomeTemplateForm" type="info" :closable="false"
title="点击“欢迎配置模板welcome”可切换到结构化表单。">
</el-alert>
<template v-if="isWelcomeTemplateForm && editorMode === 'simple'">
<div class="form-tip">
变量支持:<code>{nickname}</code> <code>{wxid}</code> <code>{group_id}</code> <code>{now}</code> <code>{head_url}</code>
</div>
<el-form-item label="文本欢迎">
<el-switch v-model="simpleWelcome.welcome_text_enabled"></el-switch>
</el-form-item>
<el-form-item label="文本模板">
<el-input v-model="simpleWelcome.welcome_text_template" placeholder="👏欢迎 {nickname} 加入群聊!🎉"></el-input>
</el-form-item>
<el-form-item label="欢迎卡片">
<el-switch v-model="simpleWelcome.welcome_card_enabled"></el-switch>
</el-form-item>
<el-form-item label="卡片标题">
<el-input v-model="simpleWelcome.card_title_template" placeholder="👏欢迎 {nickname} 加入群聊!🎉"></el-input>
</el-form-item>
<el-form-item label="卡片描述">
<el-input v-model="simpleWelcome.card_desc_template" placeholder="⌚时间:{now}"></el-input>
</el-form-item>
<el-form-item label="卡片URL">
<el-input v-model="simpleWelcome.card_url" placeholder="https://example.com/welcome?gid={group_id}"></el-input>
</el-form-item>
<el-form-item label="缩略图URL">
<el-input v-model="simpleWelcome.card_thumb_url" placeholder="{head_url}"></el-input>
</el-form-item>
<el-form-item label="效果预览">
<div class="preview-box">
<p><strong>文本:</strong>{% raw %}{{ previewWelcomeText }}{% endraw %}</p>
<p><strong>标题:</strong>{% raw %}{{ previewCardTitle }}{% endraw %}</p>
<p><strong>描述:</strong>{% raw %}{{ previewCardDesc }}{% endraw %}</p>
<p><strong>URL</strong>{% raw %}{{ previewCardUrl }}{% endraw %}</p>
</div>
</el-form-item>
</template>
<el-form-item label="JSON配置" v-if="!isWelcomeTemplateForm || editorMode === 'advanced'">
<div class="json-toolbar">
<el-button size="mini" @click="formatJsonText">格式化 JSON</el-button>
</div>
<el-input
class="json-editor"
type="textarea"
:autosize="{ minRows: 14, maxRows: 24 }"
v-model="form.config_json_text"
placeholder='请输入 JSON例如{"key":"value"}'>
</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,
editorMode: 'simple',
// 标准表单模型:用于“群成员变更监控 + welcome”的结构化配置编辑。
simpleWelcome: {
welcome_text_enabled: true,
welcome_text_template: '👏欢迎 {nickname} 加入群聊!🎉',
welcome_card_enabled: true,
card_title_template: '👏欢迎 {nickname} 加入群聊!🎉',
card_desc_template: '⌚时间:{now}',
card_url: 'https://newsnow.busiyi.world/',
card_thumb_url: '{head_url}'
},
form: {
group_id: '',
plugin_name: '',
config_key: 'default',
enabled: true,
config_json_text: '{}'
}
}
},
mounted() {
this.loadGroups()
this.loadPlugins()
this.loadRows()
},
computed: {
groupNameMap() {
const map = {}
;(this.groupOptions || []).forEach(item => {
map[String(item.wxid)] = String(item.name || item.wxid)
})
return map
},
isMemberChangePlugin() {
return this.form.plugin_name === '群成员变更监控'
},
// 仅当命中当前已接入模板时启用标准表单其它插件继续使用JSON高级模式。
isWelcomeTemplateForm() {
return this.form.plugin_name === '群成员变更监控' && this.form.config_key === 'welcome'
},
previewVariables() {
return {
nickname: '张三',
wxid: 'wxid_demo_123',
group_id: this.form.group_id || '123456@chatroom',
now: '2026-04-20 12:00:00',
head_url: 'https://example.com/avatar.png'
}
},
previewWelcomeText() {
return this.renderTemplate(this.simpleWelcome.welcome_text_template)
},
previewCardTitle() {
return this.renderTemplate(this.simpleWelcome.card_title_template)
},
previewCardDesc() {
return this.renderTemplate(this.simpleWelcome.card_desc_template)
},
previewCardUrl() {
return this.renderTemplate(this.simpleWelcome.card_url)
}
},
watch: {
// 当插件/配置键切换到结构化模板时自动从JSON回填表单减少手工搬运。
'form.plugin_name'() {
this.onTemplateTypeChanged()
},
'form.config_key'() {
this.onTemplateTypeChanged()
},
// 在标准表单模式下任何字段变化都实时同步到JSON文本保证两种模式数据一致。
simpleWelcome: {
handler() {
if (this.isWelcomeTemplateForm && this.editorMode === 'simple') {
this.form.config_json_text = JSON.stringify(this.simpleWelcome, null, 2)
}
},
deep: true
}
},
methods: {
getGroupName(groupId) {
return this.groupNameMap[String(groupId)] || String(groupId || '-')
},
formatJsonText() {
try {
const parsed = JSON.parse(this.form.config_json_text || '{}')
this.form.config_json_text = JSON.stringify(parsed, null, 2)
this.$message.success('JSON 已格式化')
} catch (e) {
this.$message.error('JSON 格式错误,无法格式化')
}
},
renderTemplate(template) {
let result = String(template || '')
Object.entries(this.previewVariables).forEach(([k, v]) => {
result = result.replace(new RegExp(`\\{${k}\\}`, 'g'), String(v || ''))
})
return result
},
applyWelcomeDefaults() {
this.simpleWelcome = {
welcome_text_enabled: true,
welcome_text_template: '👏欢迎 {nickname} 加入群聊!🎉',
welcome_card_enabled: true,
card_title_template: '👏欢迎 {nickname} 加入群聊!🎉',
card_desc_template: '⌚时间:{now}',
card_url: 'https://newsnow.busiyi.world/',
card_thumb_url: '{head_url}'
}
this.form.config_json_text = JSON.stringify(this.simpleWelcome, null, 2)
},
onTemplateTypeChanged() {
if (!this.isWelcomeTemplateForm) {
this.editorMode = 'advanced'
return
}
this.editorMode = 'simple'
let parsed = {}
try {
parsed = JSON.parse(this.form.config_json_text || '{}')
} catch (e) {
parsed = {}
}
this.simpleWelcome = {
welcome_text_enabled: parsed.welcome_text_enabled !== undefined ? !!parsed.welcome_text_enabled : true,
welcome_text_template: String(parsed.welcome_text_template || '👏欢迎 {nickname} 加入群聊!🎉'),
welcome_card_enabled: parsed.welcome_card_enabled !== undefined ? !!parsed.welcome_card_enabled : true,
card_title_template: String(parsed.card_title_template || '👏欢迎 {nickname} 加入群聊!🎉'),
card_desc_template: String(parsed.card_desc_template || '⌚时间:{now}'),
card_url: String(parsed.card_url || 'https://newsnow.busiyi.world/'),
card_thumb_url: String(parsed.card_thumb_url || '{head_url}')
}
this.form.config_json_text = JSON.stringify(this.simpleWelcome, null, 2)
},
validateSimpleWelcome() {
// 结构化模式下做强校验避免无效URL或空模板入库。
if (!this.simpleWelcome.welcome_text_template.trim()) {
this.$message.error('文本模板不能为空')
return false
}
if (!this.simpleWelcome.card_title_template.trim()) {
this.$message.error('卡片标题不能为空')
return false
}
if (!this.simpleWelcome.card_url.trim()) {
this.$message.error('卡片URL不能为空')
return false
}
const renderedUrl = this.renderTemplate(this.simpleWelcome.card_url)
if (!/^https?:\/\//i.test(renderedUrl)) {
this.$message.error('卡片URL必须是 http 或 https 开头')
return false
}
return true
},
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.applyWelcomeDefaults()
this.dialogVisible = true
},
useWelcomeTemplate() {
this.form.plugin_name = '群成员变更监控'
this.form.config_key = 'welcome'
this.editorMode = 'simple'
this.applyWelcomeDefaults()
},
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.onTemplateTypeChanged()
this.dialogVisible = true
},
async saveForm() {
let parsed = {}
if (this.isWelcomeTemplateForm && this.editorMode === 'simple') {
if (!this.validateSimpleWelcome()) {
return
}
parsed = { ...this.simpleWelcome }
this.form.config_json_text = JSON.stringify(parsed, null, 2)
}
try {
parsed = this.isWelcomeTemplateForm && this.editorMode === 'simple'
? 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}
.form-tip{padding:10px 12px;border-radius:10px;background:#f8fbff;border:1px solid #d9e8f8;color:#4a6179;margin:0 0 12px 0}
.form-tip code{display:inline-block;margin-right:6px;background:#eef6ff;border:1px solid #d2e6ff;color:#12539a;padding:1px 6px;border-radius:6px;font-size:12px}
.preview-box{padding:12px;border:1px dashed #c7d8ea;background:#f8fbff;border-radius:10px;color:#3f5c77;line-height:1.7}
.preview-box p{margin:0 0 4px 0}
.quick-actions{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.quick-tip{font-size:12px;color:#64748b}
.group-cell{display:flex;flex-direction:column;gap:4px}
.group-name{font-size:14px;font-weight:600;color:#0f172a}
.group-id{font-size:12px;color:#94a3b8}
.group-option{display:flex;flex-direction:column;line-height:1.35;padding:2px 0}
.group-option-name{font-size:13px;color:#0f172a}
.group-option-id{font-size:12px;color:#94a3b8}
.json-toolbar{display:flex;justify-content:flex-end;margin-bottom:8px}
.json-editor .el-textarea__inner{
font-family:Consolas,"SFMono-Regular","Courier New",monospace;
font-size:13px;
line-height:1.6;
background:#f8fbff;
border:1px solid #d7e6f5;
}
</style>
{% endblock %}