feat(群级配置): 新增结构化表单模式降低JSON维护门槛

- 为群成员变更监控/welcome 配置新增标准表单编辑模式,覆盖欢迎文本与卡片关键字段
- 保留高级JSON模式,支持标准表单与JSON双模式切换
- 新增变量提示与实时预览,便于运营同学所见即所得配置文案
- 增加URL与必填项校验,保存前拦截常见配置错误
- 标准表单字段变更实时同步JSON文本,确保两种模式数据一致
This commit is contained in:
liuwei
2026-04-20 10:48:31 +08:00
parent d4b7cb32f6
commit d4732d79ee

View File

@@ -77,7 +77,49 @@
<el-form-item label="启用">
<el-switch v-model="form.enabled"></el-switch>
</el-form-item>
<el-form-item label="JSON配置">
<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>
<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'">
<el-input type="textarea" :rows="12" v-model="form.config_json_text"></el-input>
</el-form-item>
</el-form>
@@ -103,6 +145,17 @@ new Vue({
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: '',
@@ -117,7 +170,115 @@ new Vue({
this.loadPlugins()
this.loadRows()
},
computed: {
// 仅当命中当前已接入模板时启用标准表单其它插件继续使用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: {
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) || {}
@@ -149,6 +310,7 @@ new Vue({
enabled: true,
config_json_text: '{}'
}
this.applyWelcomeDefaults()
this.dialogVisible = true
},
openEdit(row) {
@@ -160,12 +322,22 @@ new Vue({
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 = JSON.parse(this.form.config_json_text || '{}')
parsed = this.isWelcomeTemplateForm && this.editorMode === 'simple'
? parsed
: JSON.parse(this.form.config_json_text || '{}')
} catch (e) {
this.$message.error('JSON 配置格式错误')
return
@@ -219,5 +391,9 @@ new Vue({
.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}
</style>
{% endblock %}