- 列表中群信息改为群名主展示,ID弱化展示 - 模板入口改为按插件动态显示:仅选择群成员变更监控时显示欢迎模板按钮 - 移除顶部通用快捷模板,减少误导 - JSON编辑区升级为多行自适应编辑器,增加等宽字体与格式化按钮
476 lines
22 KiB
HTML
476 lines
22 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 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 %}
|