Feature: 支持批量删除对话历史 (#2859)
* feat: 支持批量删除对话 closes: #2784 * feat: 添加加载状态禁用功能,优化用户交互体验
This commit is contained in:
@@ -169,15 +169,65 @@ class ConversationRoute(Route):
|
||||
"""删除对话"""
|
||||
try:
|
||||
data = await request.get_json()
|
||||
user_id = data.get("user_id")
|
||||
cid = data.get("cid")
|
||||
|
||||
if not user_id or not cid:
|
||||
return Response().error("缺少必要参数: user_id 和 cid").__dict__
|
||||
await self.core_lifecycle.conversation_manager.delete_conversation(
|
||||
unified_msg_origin=user_id, conversation_id=cid
|
||||
)
|
||||
return Response().ok({"message": "对话删除成功"}).__dict__
|
||||
# 检查是否是批量删除
|
||||
if "conversations" in data:
|
||||
# 批量删除
|
||||
conversations = data.get("conversations", [])
|
||||
if not conversations:
|
||||
return (
|
||||
Response().error("批量删除时conversations参数不能为空").__dict__
|
||||
)
|
||||
|
||||
deleted_count = 0
|
||||
failed_items = []
|
||||
|
||||
for conv in conversations:
|
||||
user_id = conv.get("user_id")
|
||||
cid = conv.get("cid")
|
||||
|
||||
if not user_id or not cid:
|
||||
failed_items.append(
|
||||
f"user_id:{user_id}, cid:{cid} - 缺少必要参数"
|
||||
)
|
||||
continue
|
||||
|
||||
try:
|
||||
await self.core_lifecycle.conversation_manager.delete_conversation(
|
||||
unified_msg_origin=user_id, conversation_id=cid
|
||||
)
|
||||
deleted_count += 1
|
||||
except Exception as e:
|
||||
failed_items.append(f"user_id:{user_id}, cid:{cid} - {str(e)}")
|
||||
|
||||
message = f"成功删除 {deleted_count} 个对话"
|
||||
if failed_items:
|
||||
message += f",失败 {len(failed_items)} 个"
|
||||
|
||||
return (
|
||||
Response()
|
||||
.ok(
|
||||
{
|
||||
"message": message,
|
||||
"deleted_count": deleted_count,
|
||||
"failed_count": len(failed_items),
|
||||
"failed_items": failed_items,
|
||||
}
|
||||
)
|
||||
.__dict__
|
||||
)
|
||||
else:
|
||||
# 单个删除
|
||||
user_id = data.get("user_id")
|
||||
cid = data.get("cid")
|
||||
|
||||
if not user_id or not cid:
|
||||
return Response().error("缺少必要参数: user_id 和 cid").__dict__
|
||||
|
||||
await self.core_lifecycle.conversation_manager.delete_conversation(
|
||||
unified_msg_origin=user_id, conversation_id=cid
|
||||
)
|
||||
return Response().ok({"message": "对话删除成功"}).__dict__
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"删除对话失败: {str(e)}\n{traceback.format_exc()}")
|
||||
|
||||
@@ -12,6 +12,13 @@
|
||||
"title": "Conversation History",
|
||||
"refresh": "Refresh"
|
||||
},
|
||||
"batch": {
|
||||
"deleteSelected": "Delete Selected ({count})"
|
||||
},
|
||||
"pagination": {
|
||||
"itemsPerPage": "Items per page",
|
||||
"showingItems": "Showing {start}-{end} of {total} items"
|
||||
},
|
||||
"table": {
|
||||
"headers": {
|
||||
"title": "Conversation Title",
|
||||
@@ -61,6 +68,13 @@
|
||||
"message": "Are you sure you want to delete conversation {title}? This action cannot be undone.",
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Delete"
|
||||
},
|
||||
"batchDelete": {
|
||||
"title": "Batch Delete Confirmation",
|
||||
"message": "Are you sure you want to delete the selected {count} conversations? This action cannot be undone, please proceed with caution!",
|
||||
"andMore": "and {count} more",
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Batch Delete"
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
@@ -72,6 +86,10 @@
|
||||
"historyError": "Failed to fetch conversation history",
|
||||
"historySaveSuccess": "Conversation history saved successfully",
|
||||
"historySaveError": "Failed to save conversation history",
|
||||
"invalidJson": "Invalid JSON format"
|
||||
"invalidJson": "Invalid JSON format",
|
||||
"noItemSelected": "Please select conversations to delete first",
|
||||
"batchDeleteSuccess": "Successfully deleted {count} conversations",
|
||||
"batchDeleteError": "Batch delete failed",
|
||||
"batchDeletePartial": "Delete completed: {deleted} successful, {failed} failed"
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,13 @@
|
||||
"title": "对话历史",
|
||||
"refresh": "刷新"
|
||||
},
|
||||
"batch": {
|
||||
"deleteSelected": "删除选中 ({count})"
|
||||
},
|
||||
"pagination": {
|
||||
"itemsPerPage": "每页",
|
||||
"showingItems": "显示 {start}-{end} 项,共 {total} 项"
|
||||
},
|
||||
"table": {
|
||||
"headers": {
|
||||
"title": "对话标题",
|
||||
@@ -61,6 +68,13 @@
|
||||
"message": "确定要删除对话 {title} 吗?此操作不可恢复。",
|
||||
"cancel": "取消",
|
||||
"confirm": "删除"
|
||||
},
|
||||
"batchDelete": {
|
||||
"title": "批量删除确认",
|
||||
"message": "确定要删除选中的 {count} 个对话吗?此操作不可恢复,请谨慎操作!",
|
||||
"andMore": "等 {count} 个",
|
||||
"cancel": "取消",
|
||||
"confirm": "批量删除"
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
@@ -72,6 +86,10 @@
|
||||
"historyError": "获取对话历史失败",
|
||||
"historySaveSuccess": "对话历史保存成功",
|
||||
"historySaveError": "对话历史保存失败",
|
||||
"invalidJson": "JSON格式无效"
|
||||
"invalidJson": "JSON格式无效",
|
||||
"noItemSelected": "请先选择要删除的对话",
|
||||
"batchDeleteSuccess": "成功删除 {count} 个对话",
|
||||
"batchDeleteError": "批量删除失败",
|
||||
"batchDeletePartial": "删除完成:成功 {deleted} 个,失败 {failed} 个"
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-combobox v-model="platformFilter" :label="tm('filters.platform')"
|
||||
:items="availablePlatforms" chips multiple clearable variant="solo-filled" flat
|
||||
density="compact" hide-details>
|
||||
density="compact" hide-details :disabled="loading">
|
||||
<template v-slot:selection="{ item }">
|
||||
<v-chip size="small" label>
|
||||
{{ item.title }}
|
||||
@@ -21,7 +21,8 @@
|
||||
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select v-model="messageTypeFilter" :label="tm('filters.type')" :items="messageTypeItems"
|
||||
chips multiple clearable variant="solo-filled" density="compact" hide-details flat>
|
||||
chips multiple clearable variant="solo-filled" density="compact" hide-details flat
|
||||
:disabled="loading">
|
||||
<template v-slot:selection="{ item }">
|
||||
<v-chip size="small" variant="solo-filled" label>
|
||||
{{ item.title }}
|
||||
@@ -33,22 +34,33 @@
|
||||
<v-col cols="12" sm="12" md="4">
|
||||
<v-text-field v-model="search" prepend-inner-icon="mdi-magnify"
|
||||
:label="tm('filters.search')" hide-details density="compact" variant="solo-filled" flat
|
||||
clearable></v-text-field>
|
||||
clearable :disabled="loading"></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-btn color="primary" prepend-icon="mdi-refresh" variant="tonal" @click="fetchConversations"
|
||||
:loading="loading" size="small">
|
||||
:loading="loading" size="small" class="mr-2">
|
||||
{{ tm('history.refresh') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="selectedItems.length > 0"
|
||||
color="error"
|
||||
prepend-icon="mdi-delete"
|
||||
variant="tonal"
|
||||
@click="confirmBatchDelete"
|
||||
:disabled="loading"
|
||||
size="small">
|
||||
{{ tm('batch.deleteSelected', { count: selectedItems.length }) }}
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-text class="pa-0">
|
||||
<v-data-table :headers="tableHeaders" :items="conversations" :loading="loading"
|
||||
style="font-size: 12px;" density="comfortable" hide-default-footer items-per-page="10"
|
||||
<v-data-table v-model="selectedItems" :headers="tableHeaders" :items="conversations"
|
||||
:loading="loading" style="font-size: 12px;" density="comfortable" hide-default-footer
|
||||
class="elevation-0" :items-per-page="pagination.page_size"
|
||||
:items-per-page-options="[10, 20, 50, 100]" @update:options="handleTableOptions">
|
||||
:items-per-page-options="pageSizeOptions" show-select return-object
|
||||
:disabled="loading" @update:options="handleTableOptions">
|
||||
<template v-slot:item.title="{ item }">
|
||||
<div class="d-flex align-center">
|
||||
<span>{{ item.title || tm('status.noTitle') }}</span>
|
||||
@@ -82,15 +94,15 @@
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<div class="actions-wrapper">
|
||||
<v-btn icon variant="plain" size="x-small" class="action-button"
|
||||
@click="viewConversation(item)">
|
||||
@click="viewConversation(item)" :disabled="loading">
|
||||
<v-icon>mdi-eye</v-icon>
|
||||
</v-btn>
|
||||
<v-btn icon variant="plain" size="x-small" class="action-button"
|
||||
@click="editConversation(item)">
|
||||
@click="editConversation(item)" :disabled="loading">
|
||||
<v-icon>mdi-pencil</v-icon>
|
||||
</v-btn>
|
||||
<v-btn icon color="error" variant="plain" size="x-small" class="action-button"
|
||||
@click="confirmDeleteConversation(item)">
|
||||
@click="confirmDeleteConversation(item)" :disabled="loading">
|
||||
<v-icon>mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
@@ -105,9 +117,25 @@
|
||||
</v-data-table>
|
||||
|
||||
<!-- 分页控制 -->
|
||||
<div class="d-flex justify-end">
|
||||
<div class="d-flex justify-center py-3">
|
||||
<!-- 每页大小选择器 -->
|
||||
<div class="d-flex justify-between align-center px-4 py-2 bg-grey-lighten-5">
|
||||
<div class="d-flex align-center">
|
||||
<span class="text-caption mr-2">{{ tm('pagination.itemsPerPage') }}:</span>
|
||||
<v-select v-model="pagination.page_size" :items="pageSizeOptions" variant="outlined"
|
||||
density="compact" hide-details style="max-width: 100px;"
|
||||
:disabled="loading" @update:model-value="onPageSizeChange"></v-select>
|
||||
</div>
|
||||
<div class="text-caption ml-4">
|
||||
{{ tm('pagination.showingItems', {
|
||||
start: Math.min((pagination.page - 1) * pagination.page_size + 1, pagination.total),
|
||||
end: Math.min(pagination.page * pagination.page_size, pagination.total),
|
||||
total: pagination.total
|
||||
}) }}
|
||||
</div>
|
||||
</div>
|
||||
<v-pagination v-model="pagination.page" :length="pagination.total_pages" :disabled="loading"
|
||||
@update:model-value="fetchConversations" rounded="circle"></v-pagination>
|
||||
@update:model-value="fetchConversations" rounded="circle" :total-visible="7"></v-pagination>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
@@ -116,24 +144,20 @@
|
||||
<!-- 对话详情对话框 -->
|
||||
<v-dialog v-model="dialogView" max-width="900px" scrollable>
|
||||
<v-card class="conversation-detail-card">
|
||||
<v-card-title class="bg-primary text-white py-3 d-flex align-center">
|
||||
<v-icon color="white" class="me-2">mdi-eye</v-icon>
|
||||
<v-card-title class="ml-2 mt-2 d-flex align-center">
|
||||
<span class="text-truncate">{{ selectedConversation?.title || tm('status.noTitle') }}</span>
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<div class="d-flex align-center" v-if="selectedConversation?.sessionInfo">
|
||||
<v-chip color="white" text-color="primary" size="small" class="mr-2">
|
||||
<v-chip text-color="primary" size="small" class="mr-2" rounded="md">
|
||||
{{ selectedConversation.sessionInfo.platform }}
|
||||
</v-chip>
|
||||
<v-chip color="white" text-color="secondary" size="small">
|
||||
<v-chip text-color="secondary" size="small" rounded="md">
|
||||
{{ getMessageTypeDisplay(selectedConversation.sessionInfo.messageType) }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</v-card-title>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-text class="py-4">
|
||||
<v-card-text>
|
||||
<div class="mb-4 d-flex align-center">
|
||||
<v-btn color="secondary" variant="tonal" size="small" class="mr-2"
|
||||
@click="isEditingHistory = !isEditingHistory">
|
||||
@@ -168,16 +192,10 @@
|
||||
</div>
|
||||
|
||||
<!-- 消息列表组件 -->
|
||||
<MessageList
|
||||
v-else
|
||||
:messages="formattedMessages"
|
||||
:isDark="false"
|
||||
/>
|
||||
<MessageList v-else :messages="formattedMessages" :isDark="false" />
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="closeHistoryDialog">
|
||||
@@ -227,7 +245,7 @@
|
||||
|
||||
<v-card-text class="py-4">
|
||||
<p>{{ tm('dialogs.delete.message', { title: selectedConversation?.title || tm('status.noTitle') })
|
||||
}}</p>
|
||||
}}</p>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider></v-divider>
|
||||
@@ -244,6 +262,48 @@
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 批量删除确认对话框 -->
|
||||
<v-dialog v-model="dialogBatchDelete" max-width="600px">
|
||||
<v-card>
|
||||
<v-card-title class="bg-error text-white py-3">
|
||||
<v-icon color="white" class="me-2">mdi-delete</v-icon>
|
||||
<span>{{ tm('dialogs.batchDelete.title') }}</span>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="py-4">
|
||||
<p class="mb-3">{{ tm('dialogs.batchDelete.message', { count: selectedItems.length }) }}</p>
|
||||
|
||||
<!-- 显示前几个要删除的对话 -->
|
||||
<div v-if="selectedItems.length > 0" class="mb-3">
|
||||
<v-chip v-for="(item, index) in selectedItems.slice(0, 5)" :key="`${item.user_id}-${item.cid}`"
|
||||
size="small" class="mr-1 mb-1" closable @click:close="removeFromSelection(item)"
|
||||
:disabled="loading">
|
||||
{{ item.title || tm('status.noTitle') }}
|
||||
</v-chip>
|
||||
<v-chip v-if="selectedItems.length > 5" size="small" class="mr-1 mb-1">
|
||||
{{ tm('dialogs.batchDelete.andMore', { count: selectedItems.length - 5 }) }}
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<v-alert type="warning" variant="tonal" class="mb-3">
|
||||
{{ tm('dialogs.batchDelete.warning') }}
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider></v-divider>
|
||||
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="dialogBatchDelete = false" :disabled="loading">
|
||||
{{ tm('dialogs.batchDelete.cancel') }}
|
||||
</v-btn>
|
||||
<v-btn color="error" @click="batchDeleteConversations" :loading="loading">
|
||||
{{ tm('dialogs.batchDelete.confirm') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 消息提示 -->
|
||||
<v-snackbar :timeout="3000" elevation="24" :color="messageType" v-model="showMessage" location="top">
|
||||
{{ message }}
|
||||
@@ -291,32 +351,13 @@ export default {
|
||||
conversations: [],
|
||||
search: '',
|
||||
headers: [],
|
||||
selectedItems: [], // 批量选择的项目
|
||||
|
||||
// 筛选条件
|
||||
platformFilter: [],
|
||||
messageTypeFilter: [],
|
||||
lastAppliedFilters: null, // 记录上次应用的筛选条件
|
||||
|
||||
// 平台颜色映射
|
||||
platformColors: {
|
||||
'telegram': 'blue-lighten-1',
|
||||
'qq_official': 'purple-lighten-1',
|
||||
'qq_official_webhook': 'purple-lighten-2',
|
||||
'aiocqhttp': 'deep-purple-lighten-1',
|
||||
'lark': 'cyan-darken-1',
|
||||
'wecom': 'green-darken-1',
|
||||
'dingtalk': 'blue-darken-2',
|
||||
'default': 'grey-lighten-1'
|
||||
},
|
||||
|
||||
// 消息类型颜色映射
|
||||
messageTypeColors: {
|
||||
'GroupMessage': 'green',
|
||||
'FriendMessage': 'blue',
|
||||
'GuildMessage': 'purple',
|
||||
'default': 'grey'
|
||||
},
|
||||
|
||||
// 分页数据
|
||||
pagination: {
|
||||
page: 1,
|
||||
@@ -324,11 +365,13 @@ export default {
|
||||
total: 0,
|
||||
total_pages: 0
|
||||
},
|
||||
pageSizeOptions: [10, 20, 50, 100], // 每页大小选项
|
||||
|
||||
// 对话框控制
|
||||
dialogView: false,
|
||||
dialogEdit: false,
|
||||
dialogDelete: false,
|
||||
dialogBatchDelete: false, // 批量删除对话框
|
||||
|
||||
// 选中的对话
|
||||
selectedConversation: null,
|
||||
@@ -340,11 +383,6 @@ export default {
|
||||
cid: '',
|
||||
title: ''
|
||||
},
|
||||
defaultItem: {
|
||||
user_id: '',
|
||||
cid: '',
|
||||
title: ''
|
||||
},
|
||||
|
||||
// 表单验证
|
||||
valid: true,
|
||||
@@ -431,17 +469,6 @@ export default {
|
||||
];
|
||||
},
|
||||
|
||||
// 筛选后的对话 - 现在只用于额外的客户端筛选(排除astrbot和webchat)
|
||||
filteredConversations() {
|
||||
return this.conversations.filter(conv => {
|
||||
// 排除 user_id 为 astrbot 或 platform 为 webchat 的对话
|
||||
if (conv.user_id === 'astrbot' || conv.sessionInfo?.platform === 'webchat') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
},
|
||||
|
||||
// 当前的筛选条件对象
|
||||
currentFilters() {
|
||||
const platforms = this.platformFilter.map(item =>
|
||||
@@ -790,6 +817,88 @@ export default {
|
||||
}
|
||||
} catch (error) {
|
||||
this.showErrorMessage(error.response?.data?.message || error.message || this.tm('messages.deleteError'));
|
||||
} finally {
|
||||
this.loading = false;
|
||||
this.selectedItems = this.selectedItems.filter(item =>
|
||||
!(item.user_id === this.selectedConversation.user_id && item.cid === this.selectedConversation.cid)
|
||||
);
|
||||
this.selectedConversation = null;
|
||||
}
|
||||
},
|
||||
|
||||
// 处理页面大小变更
|
||||
onPageSizeChange() {
|
||||
this.pagination.page = 1; // 重置到第一页
|
||||
this.fetchConversations();
|
||||
},
|
||||
|
||||
// 确认批量删除
|
||||
confirmBatchDelete() {
|
||||
if (this.selectedItems.length === 0) {
|
||||
this.showErrorMessage(this.tm('messages.noItemSelected'));
|
||||
return;
|
||||
}
|
||||
this.dialogBatchDelete = true;
|
||||
},
|
||||
|
||||
// 从选择中移除项目
|
||||
removeFromSelection(item) {
|
||||
const index = this.selectedItems.findIndex(selected =>
|
||||
selected.user_id === item.user_id && selected.cid === item.cid
|
||||
);
|
||||
if (index !== -1) {
|
||||
this.selectedItems.splice(index, 1);
|
||||
}
|
||||
},
|
||||
|
||||
// 批量删除对话
|
||||
async batchDeleteConversations() {
|
||||
if (this.selectedItems.length === 0) {
|
||||
this.showErrorMessage(this.tm('messages.noItemSelected'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
try {
|
||||
// 准备批量删除的数据
|
||||
const conversations = this.selectedItems.map(item => ({
|
||||
user_id: item.user_id,
|
||||
cid: item.cid
|
||||
}));
|
||||
|
||||
const response = await axios.post('/api/conversation/delete', {
|
||||
conversations: conversations
|
||||
});
|
||||
|
||||
if (response.data.status === "ok") {
|
||||
const result = response.data.data;
|
||||
this.dialogBatchDelete = false;
|
||||
this.selectedItems = []; // 清空选择
|
||||
|
||||
// 显示结果消息
|
||||
if (result.failed_count > 0) {
|
||||
this.showErrorMessage(
|
||||
this.tm('messages.batchDeletePartial', {
|
||||
deleted: result.deleted_count,
|
||||
failed: result.failed_count
|
||||
})
|
||||
);
|
||||
} else {
|
||||
this.showSuccessMessage(
|
||||
this.tm('messages.batchDeleteSuccess', {
|
||||
count: result.deleted_count
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// 刷新列表
|
||||
this.fetchConversations();
|
||||
} else {
|
||||
this.showErrorMessage(response.data.message || this.tm('messages.batchDeleteError'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('批量删除对话出错:', error);
|
||||
this.showErrorMessage(error.response?.data?.message || error.message || this.tm('messages.batchDeleteError'));
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
@@ -812,35 +921,6 @@ export default {
|
||||
}).format(date);
|
||||
},
|
||||
|
||||
// 格式化消息内容
|
||||
formatMessage(content) {
|
||||
|
||||
// content 可能是数组
|
||||
// [{"type": "image_url", "image_url": {"url": url_or_base64}}, {"type": "text", "text": "text"}]
|
||||
|
||||
let final_content = content;
|
||||
if (Array.isArray(content)) {
|
||||
// 处理数组内容
|
||||
final_content = content.map(item => {
|
||||
if (item.type === 'image_url') {
|
||||
return `<img src="${item.image_url.url}" alt="Image" />`;
|
||||
} else if (item.type === 'text') {
|
||||
return item.text;
|
||||
}
|
||||
return '';
|
||||
}).join('\n');
|
||||
} else if (typeof content === 'object') {
|
||||
// 处理对象内容
|
||||
final_content = Object.values(content).join('');
|
||||
} else if (typeof content === 'string') {
|
||||
// 处理字符串内容
|
||||
final_content = content;
|
||||
} else if (!final_content) return this.tm('status.emptyContent');
|
||||
|
||||
// 使用markdown-it处理,默认安全(html: false会禁用HTML标签)
|
||||
return md.render(final_content);
|
||||
},
|
||||
|
||||
// 显示成功消息
|
||||
showSuccessMessage(message) {
|
||||
this.message = message;
|
||||
|
||||
Reference in New Issue
Block a user