684 lines
26 KiB
HTML
684 lines
26 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}群权限管理 - 机器人管理后台{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="page-shell robot-page">
|
|
<div class="page-hero">
|
|
<div class="page-hero-copy">
|
|
<div class="page-eyebrow">Groups Workspace</div>
|
|
<h1>群权限与机器人管理</h1>
|
|
<p>集中管理群组启停、权限开关与消息趋势,减少传统后台式分散操作。</p>
|
|
</div>
|
|
<div class="page-hero-actions">
|
|
<el-input
|
|
placeholder="搜索群组名称 / ID"
|
|
v-model="searchQuery"
|
|
class="hero-search"
|
|
clearable>
|
|
<i slot="prefix" class="el-input__icon el-icon-search"></i>
|
|
</el-input>
|
|
<el-button type="primary" @click="showAddGroupDialog">
|
|
<i class="el-icon-plus"></i> 添加群组
|
|
</el-button>
|
|
</div>
|
|
</div>
|
|
|
|
<el-row :gutter="16" class="overview-grid">
|
|
<el-col :span="6">
|
|
<el-card class="overview-card overview-card--primary" shadow="hover">
|
|
<div class="overview-label">群组总数</div>
|
|
<div class="overview-value">{% raw %}{{ groups.length }}{% endraw %}</div>
|
|
<div class="overview-note">已纳入机器人管理的群组</div>
|
|
</el-card>
|
|
</el-col>
|
|
<el-col :span="6">
|
|
<el-card class="overview-card" shadow="hover">
|
|
<div class="overview-label">已启用</div>
|
|
<div class="overview-value">{% raw %}{{ enabledGroupsCount }}{% endraw %}</div>
|
|
<div class="overview-note">机器人功能已开启</div>
|
|
</el-card>
|
|
</el-col>
|
|
<el-col :span="6">
|
|
<el-card class="overview-card" shadow="hover">
|
|
<div class="overview-label">已关闭</div>
|
|
<div class="overview-value">{% raw %}{{ disabledGroupsCount }}{% endraw %}</div>
|
|
<div class="overview-note">等待重新启用或清理</div>
|
|
</el-card>
|
|
</el-col>
|
|
<el-col :span="6">
|
|
<el-card class="overview-card overview-card--soft" shadow="hover">
|
|
<div class="overview-label">当前筛选结果</div>
|
|
<div class="overview-value">{% raw %}{{ filteredGroups.length }}{% endraw %}</div>
|
|
<div class="overview-note">按搜索条件即时过滤</div>
|
|
</el-card>
|
|
</el-col>
|
|
</el-row>
|
|
|
|
<el-card class="workspace-card" shadow="hover">
|
|
<div slot="header" class="workspace-header">
|
|
<div>
|
|
<h3>群组列表</h3>
|
|
<p>先处理状态,再深入查看权限与趋势。</p>
|
|
</div>
|
|
<div class="workspace-actions" v-if="selectedGroups.length > 0">
|
|
<span class="selection-summary">已选择 {% raw %}{{ selectedGroups.length }}{% endraw %} 个群组</span>
|
|
<el-button type="success" size="small" @click="batchEnableRobot">批量启用</el-button>
|
|
<el-button type="danger" size="small" @click="batchDisableRobot">批量关闭</el-button>
|
|
<el-button type="warning" size="small" @click="batchRemoveGroups">批量移除</el-button>
|
|
</div>
|
|
</div>
|
|
|
|
<el-table
|
|
:data="filteredGroups"
|
|
style="width: 100%"
|
|
@selection-change="handleSelectionChange">
|
|
<el-table-column type="selection" width="52"></el-table-column>
|
|
<el-table-column label="群组信息" min-width="300">
|
|
<template slot-scope="scope">
|
|
<div class="entity-cell">
|
|
<div class="entity-badge">{% raw %}{{ scope.$index + 1 }}{% endraw %}</div>
|
|
<div class="entity-copy">
|
|
<div class="entity-title">{% raw %}{{ scope.row.group_name || scope.row.group_id }}{% endraw %}</div>
|
|
<div class="entity-subtitle">Group ID: {% raw %}{{ scope.row.group_id }}{% endraw %}</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="机器人状态" width="140" align="center">
|
|
<template slot-scope="scope">
|
|
<el-tag :type="scope.row.robot_status === 'enabled' ? 'success' : 'info'">
|
|
{% raw %}{{ scope.row.robot_status === 'enabled' ? '已启用' : '已关闭' }}{% endraw %}
|
|
</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="操作" min-width="360">
|
|
<template slot-scope="scope">
|
|
<div class="action-row">
|
|
<el-button size="mini" type="primary" plain @click="viewGroupPermissions(scope.row)">
|
|
查看权限
|
|
</el-button>
|
|
<el-button
|
|
size="mini"
|
|
:type="scope.row.robot_status === 'enabled' ? 'danger' : 'success'"
|
|
@click="toggleRobotStatus(scope.row)">
|
|
{% raw %}{{ scope.row.robot_status === 'enabled' ? '关闭机器人' : '启用机器人' }}{% endraw %}
|
|
</el-button>
|
|
<el-button size="mini" type="info" plain @click="viewMessageTrend(scope.row)">
|
|
消息趋势
|
|
</el-button>
|
|
</div>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
</el-card>
|
|
|
|
<el-dialog
|
|
:title="currentGroupName + ' 功能权限管理'"
|
|
:visible.sync="permissionDialogVisible"
|
|
width="72%">
|
|
<div class="dialog-intro">按功能维度快速启停,适合对群组做差异化权限控制。</div>
|
|
<el-table :data="permissions" style="width: 100%">
|
|
<el-table-column prop="feature_id" label="功能 ID" width="90"></el-table-column>
|
|
<el-table-column prop="feature_description" label="功能描述"></el-table-column>
|
|
<el-table-column label="当前状态" width="120" align="center">
|
|
<template slot-scope="scope">
|
|
<el-tag :type="scope.row.status === 'enabled' ? 'success' : 'info'">
|
|
{% raw %}{{ scope.row.status === 'enabled' ? '已启用' : '已关闭' }}{% endraw %}
|
|
</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="切换" width="140" align="center">
|
|
<template slot-scope="scope">
|
|
<el-switch
|
|
v-model="scope.row.statusBool"
|
|
active-color="#10b981"
|
|
inactive-color="#cbd5e1"
|
|
@change="togglePermission(scope.row)">
|
|
</el-switch>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
<div slot="footer" class="dialog-footer dialog-footer-split">
|
|
<div>
|
|
<el-button @click="enableAllPermissions" type="success">一键启用全部</el-button>
|
|
<el-button @click="disableAllPermissions" type="danger">一键关闭全部</el-button>
|
|
</div>
|
|
<el-button @click="permissionDialogVisible = false">关闭</el-button>
|
|
</div>
|
|
</el-dialog>
|
|
|
|
<el-dialog title="添加群组" :visible.sync="addGroupDialogVisible" width="34%">
|
|
<div class="dialog-intro">输入需要纳入机器人管理的群组 ID。</div>
|
|
<el-form :model="addGroupForm" :rules="addGroupRules" ref="addGroupForm">
|
|
<el-form-item label="群组ID" prop="groupId">
|
|
<el-input v-model="addGroupForm.groupId" placeholder="请输入群组ID"></el-input>
|
|
</el-form-item>
|
|
</el-form>
|
|
<span slot="footer" class="dialog-footer">
|
|
<el-button @click="addGroupDialogVisible = false">取消</el-button>
|
|
<el-button type="primary" @click="submitAddGroup">确定</el-button>
|
|
</span>
|
|
</el-dialog>
|
|
|
|
<el-dialog title="群组消息趋势" :visible.sync="messageTrendDialogVisible" width="72%">
|
|
<div class="dialog-intro">观察该群组在当前统计区间内的消息节奏与活跃度变化。</div>
|
|
<div class="chart-shell">
|
|
<div class="chart-heading">{% raw %}{{ currentGroupName }}{% endraw %} · 消息趋势</div>
|
|
<canvas id="messageTrendChart" width="800" height="360"></canvas>
|
|
</div>
|
|
</el-dialog>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
new Vue({
|
|
el: '#app',
|
|
mixins: [baseApp],
|
|
data() {
|
|
return {
|
|
groups: [],
|
|
permissions: [],
|
|
currentGroupId: null,
|
|
currentGroupName: '',
|
|
searchQuery: '',
|
|
selectedGroups: [],
|
|
permissionDialogVisible: false,
|
|
addGroupDialogVisible: false,
|
|
addGroupForm: {
|
|
groupId: ''
|
|
},
|
|
addGroupRules: {
|
|
groupId: [
|
|
{ required: true, message: '请输入群组ID', trigger: 'blur' },
|
|
{ pattern: /^\S+$/, message: '群组ID不能包含空格', trigger: 'blur' }
|
|
]
|
|
},
|
|
messageTrendDialogVisible: false,
|
|
messageTrendData: {
|
|
dates: [],
|
|
counts: []
|
|
}
|
|
}
|
|
},
|
|
computed: {
|
|
filteredGroups() {
|
|
if (!this.searchQuery) return this.groups;
|
|
const query = this.searchQuery.toLowerCase();
|
|
return this.groups.filter(group =>
|
|
(group.group_id && group.group_id.toLowerCase().includes(query)) ||
|
|
(group.group_name && group.group_name.toLowerCase().includes(query))
|
|
);
|
|
},
|
|
enabledGroupsCount() {
|
|
return this.groups.filter(group => group.robot_status === 'enabled').length;
|
|
},
|
|
disabledGroupsCount() {
|
|
return this.groups.filter(group => group.robot_status !== 'enabled').length;
|
|
}
|
|
},
|
|
mounted() {
|
|
this.currentView = '6';
|
|
this.loadGroups();
|
|
},
|
|
methods: {
|
|
loadGroups() {
|
|
axios.get('/robot/api/groups')
|
|
.then(response => {
|
|
if (response.data.success) {
|
|
this.groups = response.data.data || [];
|
|
} else {
|
|
this.$message.error('加载群组失败');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('加载群组失败:', error);
|
|
this.$message.error('加载群组失败: ' + error.message);
|
|
});
|
|
},
|
|
handleSelectionChange(selection) {
|
|
this.selectedGroups = selection.map(item => item.group_id);
|
|
},
|
|
viewGroupPermissions(group) {
|
|
this.currentGroupId = group.group_id;
|
|
this.currentGroupName = group.group_name || group.group_id;
|
|
|
|
axios.get(`/robot/api/group/${group.group_id}/permissions`)
|
|
.then(response => {
|
|
if (response.data.success) {
|
|
const permissionsData = response.data.data || [];
|
|
this.permissions = permissionsData.map(p => ({
|
|
...p,
|
|
statusBool: p.status === 'enabled'
|
|
}));
|
|
this.permissionDialogVisible = true;
|
|
} else {
|
|
this.$message.error('加载权限失败');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('加载权限失败:', error);
|
|
this.$message.error('加载权限失败: ' + error.message);
|
|
});
|
|
},
|
|
togglePermission(permission) {
|
|
const newStatus = permission.statusBool ? 'enabled' : 'disabled';
|
|
|
|
axios.post(`/robot/api/group/${this.currentGroupId}/permissions`, {
|
|
feature_id: permission.feature_id,
|
|
status: newStatus
|
|
})
|
|
.then(response => {
|
|
if (response.data.success) {
|
|
permission.status = newStatus;
|
|
this.$message.success('更新权限成功');
|
|
} else {
|
|
permission.statusBool = !permission.statusBool;
|
|
this.$message.error('更新权限失败');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
permission.statusBool = !permission.statusBool;
|
|
console.error('更新权限失败:', error);
|
|
this.$message.error('更新权限失败: ' + error.message);
|
|
});
|
|
},
|
|
enableAllPermissions() {
|
|
this.updateAllPermissions('enabled');
|
|
},
|
|
disableAllPermissions() {
|
|
this.updateAllPermissions('disabled');
|
|
},
|
|
updateAllPermissions(status) {
|
|
axios.post(`/robot/api/group/${this.currentGroupId}/permissions`, {
|
|
status: status
|
|
})
|
|
.then(response => {
|
|
if (response.data.success) {
|
|
this.permissions.forEach(p => {
|
|
p.status = status;
|
|
p.statusBool = status === 'enabled';
|
|
});
|
|
this.$message.success('批量更新权限成功');
|
|
} else {
|
|
this.$message.error('批量更新权限失败');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('批量更新权限失败:', error);
|
|
this.$message.error('批量更新权限失败: ' + error.message);
|
|
});
|
|
},
|
|
toggleRobotStatus(group) {
|
|
const newStatus = group.robot_status === 'enabled' ? 'disabled' : 'enabled';
|
|
|
|
axios.post(`/robot/api/group/${group.group_id}/status`, {
|
|
status: newStatus
|
|
})
|
|
.then(response => {
|
|
if (response.data.success) {
|
|
group.robot_status = newStatus;
|
|
this.$message.success('更新机器人状态成功');
|
|
} else {
|
|
this.$message.error('更新机器人状态失败');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('更新机器人状态失败:', error);
|
|
this.$message.error('更新机器人状态失败: ' + error.message);
|
|
});
|
|
},
|
|
showAddGroupDialog() {
|
|
this.addGroupForm.groupId = '';
|
|
this.addGroupDialogVisible = true;
|
|
},
|
|
submitAddGroup() {
|
|
this.$refs.addGroupForm.validate(valid => {
|
|
if (valid) {
|
|
axios.post('/robot/api/add_group', {
|
|
group_id: this.addGroupForm.groupId
|
|
})
|
|
.then(response => {
|
|
if (response.data.success) {
|
|
this.addGroupDialogVisible = false;
|
|
this.$message.success('添加群组成功');
|
|
this.loadGroups();
|
|
} else {
|
|
this.$message.error('添加群组失败');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('添加群组失败:', error);
|
|
this.$message.error('添加群组失败: ' + error.message);
|
|
});
|
|
}
|
|
});
|
|
},
|
|
batchEnableRobot() {
|
|
this.batchUpdateRobotStatus('enabled');
|
|
},
|
|
batchDisableRobot() {
|
|
this.batchUpdateRobotStatus('disabled');
|
|
},
|
|
batchUpdateRobotStatus(status) {
|
|
if (this.selectedGroups.length === 0) {
|
|
this.$message.warning('请先选择群组');
|
|
return;
|
|
}
|
|
|
|
axios.post('/robot/api/batch_operation', {
|
|
operation: 'update_status',
|
|
group_ids: this.selectedGroups,
|
|
status: status
|
|
})
|
|
.then(response => {
|
|
if (response.data.success) {
|
|
this.groups.forEach(g => {
|
|
if (this.selectedGroups.includes(g.group_id)) {
|
|
g.robot_status = status;
|
|
}
|
|
});
|
|
this.$message.success('批量更新状态成功');
|
|
} else {
|
|
this.$message.error('批量更新状态失败');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('批量更新状态失败:', error);
|
|
this.$message.error('批量更新状态失败: ' + error.message);
|
|
});
|
|
},
|
|
batchRemoveGroups() {
|
|
if (this.selectedGroups.length === 0) {
|
|
this.$message.warning('请先选择群组');
|
|
return;
|
|
}
|
|
|
|
this.$confirm(`确定要批量移除 ${this.selectedGroups.length} 个群组吗? 此操作将清除这些群组的所有设置。`, '警告', {
|
|
confirmButtonText: '确定',
|
|
cancelButtonText: '取消',
|
|
type: 'danger'
|
|
}).then(() => {
|
|
axios.post('/robot/api/batch_operation', {
|
|
operation: 'remove_groups',
|
|
group_ids: this.selectedGroups
|
|
})
|
|
.then(response => {
|
|
if (response.data.success) {
|
|
this.groups = this.groups.filter(g => !this.selectedGroups.includes(g.group_id));
|
|
this.selectedGroups = [];
|
|
this.$message.success('批量移除成功');
|
|
} else {
|
|
this.$message.error('批量移除失败');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('批量移除失败:', error);
|
|
this.$message.error('批量移除失败: ' + error.message);
|
|
});
|
|
}).catch(() => {});
|
|
},
|
|
viewMessageTrend(group) {
|
|
this.currentGroupId = group.group_id;
|
|
this.currentGroupName = group.group_name || group.group_id;
|
|
|
|
const days = parseInt(this.timeRange || 7);
|
|
axios.get(`/robot/api/group/${group.group_id}/message_trend?days=${days}`)
|
|
.then(response => {
|
|
if (response.data.success) {
|
|
this.messageTrendData = response.data.data || { dates: [], counts: [] };
|
|
this.messageTrendDialogVisible = true;
|
|
|
|
this.$nextTick(() => {
|
|
this.renderMessageTrendChart();
|
|
});
|
|
} else {
|
|
this.$message.error('加载消息趋势失败');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('加载消息趋势失败:', error);
|
|
this.$message.error('加载消息趋势失败: ' + error.message);
|
|
});
|
|
},
|
|
renderMessageTrendChart() {
|
|
const ctx = document.getElementById('messageTrendChart').getContext('2d');
|
|
|
|
if (this.charts && this.charts.messageTrendChart) {
|
|
this.charts.messageTrendChart.destroy();
|
|
}
|
|
|
|
if (!this.charts) {
|
|
this.charts = {};
|
|
}
|
|
|
|
this.charts.messageTrendChart = new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: this.messageTrendData.dates,
|
|
datasets: [
|
|
{
|
|
label: '消息数量',
|
|
data: this.messageTrendData.counts,
|
|
fill: true,
|
|
backgroundColor: 'rgba(79, 70, 229, 0.10)',
|
|
borderColor: 'rgba(79, 70, 229, 1)',
|
|
tension: 0.28,
|
|
borderWidth: 3,
|
|
pointRadius: 2
|
|
}
|
|
]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: true,
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
grid: {
|
|
color: 'rgba(148,163,184,0.12)'
|
|
}
|
|
},
|
|
x: {
|
|
grid: {
|
|
display: false
|
|
}
|
|
}
|
|
},
|
|
plugins: {
|
|
legend: {
|
|
display: false
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
</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,0.10), rgba(59,130,246,0.08), rgba(255,255,255,0.9));
|
|
border: 1px solid rgba(148, 163, 184, 0.16);
|
|
box-shadow: 0 18px 40px rgba(15, 23, 42, 0.06);
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.page-hero-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
|
|
.hero-search {
|
|
width: 260px;
|
|
}
|
|
|
|
.overview-grid .el-col {
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.overview-card {
|
|
min-height: 112px;
|
|
}
|
|
|
|
.overview-card--primary {
|
|
background: linear-gradient(180deg, rgba(79,70,229,0.10), rgba(255,255,255,0.94)) !important;
|
|
}
|
|
|
|
.overview-card--soft {
|
|
background: linear-gradient(180deg, rgba(59,130,246,0.08), rgba(255,255,255,0.94)) !important;
|
|
}
|
|
|
|
.overview-label {
|
|
font-size: 13px;
|
|
color: #64748b;
|
|
margin-bottom: 14px;
|
|
}
|
|
|
|
.overview-value {
|
|
font-size: 30px;
|
|
font-weight: 700;
|
|
color: #0f172a;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.overview-note {
|
|
font-size: 12px;
|
|
color: #94a3b8;
|
|
}
|
|
|
|
.workspace-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 16px;
|
|
}
|
|
|
|
.workspace-header h3 {
|
|
font-size: 18px;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.workspace-header p {
|
|
font-size: 13px;
|
|
color: #64748b;
|
|
}
|
|
|
|
.workspace-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.selection-summary {
|
|
font-size: 13px;
|
|
color: #475569;
|
|
margin-right: 4px;
|
|
}
|
|
|
|
.entity-cell {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
|
|
.entity-badge {
|
|
width: 30px;
|
|
height: 30px;
|
|
border-radius: 50%;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: rgba(79,70,229,0.10);
|
|
color: #4f46e5;
|
|
font-size: 12px;
|
|
font-weight: 700;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.entity-title {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: #0f172a;
|
|
}
|
|
|
|
.entity-subtitle {
|
|
margin-top: 4px;
|
|
font-size: 12px;
|
|
color: #94a3b8;
|
|
}
|
|
|
|
.action-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.dialog-intro {
|
|
margin-bottom: 14px;
|
|
color: #64748b;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.dialog-footer-split {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
width: 100%;
|
|
}
|
|
|
|
.chart-shell {
|
|
padding: 16px;
|
|
border-radius: 18px;
|
|
background: linear-gradient(180deg, rgba(248,250,252,0.78), rgba(255,255,255,0.96));
|
|
border: 1px solid rgba(148,163,184,0.12);
|
|
}
|
|
|
|
.chart-heading {
|
|
margin-bottom: 12px;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: #334155;
|
|
}
|
|
</style>
|
|
{% endblock %}
|