feat(electron): 添加系统级对话框功能
- 在 ElectronAPI 中新增对话框相关方法:showQuestionDialog、dialogResponse、resizeDialogWindow - 实现独立的对话框窗口 dialog.html,支持自定义标题、消息和选项- 添加对话框窗口的键盘导航和焦点管理功能 - 在主进程中实现对话框窗口的创建、显示和响应处理 - 更新 WebSocket 消息监听器组件,使用系统级对话框替代应用内弹窗- 优化进程清理逻辑,增加多种清理方法并行执行 - 重构日志工具类,改进 Electron API 调用方式 - 调整 Git 更新检查逻辑,避免直接访问 GitHub - 移除冗余的类型定义文件,统一 Electron API 接口定义
This commit is contained in:
@@ -1,51 +1,26 @@
|
||||
<template>
|
||||
<div style="display: none">
|
||||
<!-- 这是一个隐藏的监听组件,不需要UI -->
|
||||
</div>
|
||||
|
||||
<!-- 简单的自定义对话框 -->
|
||||
<div v-if="showDialog" class="dialog-overlay" @click.self="showDialog = false">
|
||||
<div class="dialog-container">
|
||||
<div class="dialog-header">
|
||||
<h3>{{ dialogData.title }}</h3>
|
||||
</div>
|
||||
<div class="dialog-content">
|
||||
<p>{{ dialogData.message }}</p>
|
||||
</div>
|
||||
<div class="dialog-actions">
|
||||
<button
|
||||
v-for="(option, index) in dialogData.options"
|
||||
:key="index"
|
||||
class="dialog-button"
|
||||
@click="handleChoice(index)"
|
||||
>
|
||||
{{ option }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 现在使用系统级对话框窗口而不是应用内弹窗 -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, ref, nextTick } from 'vue'
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
import { useWebSocket, type WebSocketBaseMessage } from '@/composables/useWebSocket'
|
||||
import { logger } from '@/utils/logger'
|
||||
|
||||
// WebSocket hook
|
||||
const { subscribe, unsubscribe, sendRaw } = useWebSocket()
|
||||
|
||||
// 对话框状态
|
||||
const showDialog = ref(false)
|
||||
const dialogData = ref({
|
||||
title: '',
|
||||
message: '',
|
||||
options: ['确定', '取消'],
|
||||
messageId: ''
|
||||
})
|
||||
|
||||
// 存储订阅ID用于取消订阅
|
||||
let subscriptionId: string
|
||||
|
||||
// 检查是否在 Electron 环境中
|
||||
const isElectron = () => {
|
||||
return typeof window !== 'undefined' && (window as any).electronAPI
|
||||
}
|
||||
|
||||
// 发送用户选择结果到后端
|
||||
const sendResponse = (messageId: string, choice: boolean) => {
|
||||
const response = {
|
||||
@@ -59,39 +34,40 @@ const sendResponse = (messageId: string, choice: boolean) => {
|
||||
sendRaw('Response', response)
|
||||
}
|
||||
|
||||
// 处理用户选择
|
||||
const handleChoice = (choiceIndex: number) => {
|
||||
const choice = choiceIndex === 0 // 第一个选项为true,其他为false
|
||||
sendResponse(dialogData.value.messageId, choice)
|
||||
showDialog.value = false
|
||||
}
|
||||
|
||||
// 显示问题对话框
|
||||
const showQuestion = (questionData: any) => {
|
||||
// 显示系统级问题对话框
|
||||
const showQuestion = async (questionData: any) => {
|
||||
const title = questionData.title || '操作提示'
|
||||
const message = questionData.message || ''
|
||||
const options = questionData.options || ['确定', '取消']
|
||||
const messageId = questionData.message_id || 'fallback_' + Date.now()
|
||||
|
||||
logger.info('[WebSocket消息监听器] 显示自定义对话框:', questionData)
|
||||
logger.info('[WebSocket消息监听器] 显示系统级对话框:', questionData)
|
||||
|
||||
// 设置对话框数据
|
||||
dialogData.value = {
|
||||
title,
|
||||
message,
|
||||
options,
|
||||
messageId
|
||||
if (!isElectron()) {
|
||||
logger.error('[WebSocket消息监听器] 不在 Electron 环境中,无法显示系统级对话框')
|
||||
// 在非 Electron 环境中,使用默认响应
|
||||
sendResponse(messageId, false)
|
||||
return
|
||||
}
|
||||
|
||||
showDialog.value = true
|
||||
|
||||
// 在下一个tick自动聚焦第一个按钮
|
||||
nextTick(() => {
|
||||
const firstButton = document.querySelector('.dialog-button:first-child') as HTMLButtonElement
|
||||
if (firstButton) {
|
||||
firstButton.focus()
|
||||
}
|
||||
})
|
||||
try {
|
||||
// 调用 Electron API 显示系统级对话框
|
||||
const result = await (window as any).electronAPI.showQuestionDialog({
|
||||
title,
|
||||
message,
|
||||
options,
|
||||
messageId
|
||||
})
|
||||
|
||||
logger.info('[WebSocket消息监听器] 系统级对话框返回结果:', result)
|
||||
|
||||
// 发送结果到后端
|
||||
sendResponse(messageId, result)
|
||||
} catch (error) {
|
||||
logger.error('[WebSocket消息监听器] 显示系统级对话框失败:', error)
|
||||
// 出错时发送默认响应
|
||||
sendResponse(messageId, false)
|
||||
}
|
||||
}
|
||||
|
||||
// 消息处理函数
|
||||
@@ -137,14 +113,14 @@ const handleObjectMessage = (data: any) => {
|
||||
logger.info('[WebSocket消息监听器] 发现Question类型消息')
|
||||
|
||||
if (data.message_id) {
|
||||
logger.info('[WebSocket消息监听器] message_id存在,显示选择弹窗')
|
||||
logger.info('[WebSocket消息监听器] message_id存在,显示系统级对话框')
|
||||
showQuestion(data)
|
||||
return
|
||||
} else {
|
||||
logger.warn('[WebSocket消息监听器] Question消息缺少message_id字段:', data)
|
||||
// 即使缺少message_id,也尝试显示弹窗,使用当前时间戳作为ID
|
||||
// 即使缺少message_id,也尝试显示对话框,使用当前时间戳作为ID
|
||||
const fallbackId = 'fallback_' + Date.now()
|
||||
logger.info('[WebSocket消息监听器] 使用备用ID显示弹窗:', fallbackId)
|
||||
logger.info('[WebSocket消息监听器] 使用备用ID显示对话框:', fallbackId)
|
||||
showQuestion({
|
||||
...data,
|
||||
message_id: fallbackId
|
||||
@@ -210,150 +186,4 @@ onUnmounted(() => {
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 对话框遮罩层 */
|
||||
.dialog-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
/* 对话框容器 */
|
||||
.dialog-container {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
min-width: 300px;
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
animation: dialogAppear 0.2s ease-out;
|
||||
}
|
||||
|
||||
/* 对话框头部 */
|
||||
.dialog-header {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.dialog-header h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 对话框内容 */
|
||||
.dialog-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.dialog-content p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: #666;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* 按钮区域 */
|
||||
.dialog-actions {
|
||||
padding: 12px 20px 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* 按钮样式 */
|
||||
.dialog-button {
|
||||
padding: 8px 16px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s ease;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.dialog-button:hover {
|
||||
background: #f5f5f5;
|
||||
border-color: #999;
|
||||
}
|
||||
|
||||
.dialog-button:focus {
|
||||
outline: 2px solid #007bff;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.dialog-button:first-child {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.dialog-button:first-child:hover {
|
||||
background: #0056b3;
|
||||
border-color: #0056b3;
|
||||
}
|
||||
|
||||
/* 出现动画 */
|
||||
@keyframes dialogAppear {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* 暗色主题适配 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.dialog-container {
|
||||
background: #2d2d2d;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
border-bottom-color: #444;
|
||||
}
|
||||
|
||||
.dialog-header h3 {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.dialog-content p {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.dialog-button {
|
||||
background: #444;
|
||||
color: #fff;
|
||||
border-color: #555;
|
||||
}
|
||||
|
||||
.dialog-button:hover {
|
||||
background: #555;
|
||||
border-color: #666;
|
||||
}
|
||||
|
||||
.dialog-button:first-child {
|
||||
background: #0d6efd;
|
||||
border-color: #0d6efd;
|
||||
}
|
||||
|
||||
.dialog-button:first-child:hover {
|
||||
background: #0b5ed7;
|
||||
border-color: #0b5ed7;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
130
frontend/src/types/electron.d.ts
vendored
130
frontend/src/types/electron.d.ts
vendored
@@ -1,64 +1,74 @@
|
||||
export interface ElectronAPI {
|
||||
openDevTools: () => Promise<void>
|
||||
selectFolder: () => Promise<string | null>
|
||||
selectFile: (filters?: any[]) => Promise<string[]>
|
||||
openUrl: (url: string) => Promise<{ success: boolean; error?: string }>
|
||||
|
||||
// 窗口控制
|
||||
windowMinimize: () => Promise<void>
|
||||
windowMaximize: () => Promise<void>
|
||||
windowClose: () => Promise<void>
|
||||
windowIsMaximized: () => Promise<boolean>
|
||||
appQuit: () => Promise<void>
|
||||
|
||||
// 进程管理
|
||||
getRelatedProcesses: () => Promise<any[]>
|
||||
killAllProcesses: () => Promise<{ success: boolean; error?: string }>
|
||||
forceExit: () => Promise<{ success: boolean }>
|
||||
|
||||
// 初始化相关API
|
||||
checkEnvironment: () => Promise<any>
|
||||
checkCriticalFiles: () => Promise<{ pythonExists: boolean; gitExists: boolean; mainPyExists: boolean }>
|
||||
checkGitUpdate: () => Promise<{ hasUpdate: boolean; error?: string }>
|
||||
downloadPython: (mirror?: string) => Promise<any>
|
||||
installPip: () => Promise<any>
|
||||
downloadGit: () => Promise<any>
|
||||
installDependencies: (mirror?: string) => Promise<any>
|
||||
cloneBackend: (repoUrl?: string) => Promise<any>
|
||||
updateBackend: (repoUrl?: string) => Promise<any>
|
||||
startBackend: () => Promise<{ success: boolean; error?: string }>
|
||||
stopBackend?: () => Promise<{ success: boolean; error?: string }>
|
||||
|
||||
// 管理员权限相关
|
||||
checkAdmin: () => Promise<boolean>
|
||||
restartAsAdmin: () => Promise<void>
|
||||
|
||||
// 配置文件操作
|
||||
saveConfig: (config: any) => Promise<void>
|
||||
loadConfig: () => Promise<any>
|
||||
resetConfig: () => Promise<void>
|
||||
|
||||
// 日志文件操作
|
||||
getLogPath: () => Promise<string>
|
||||
getLogFiles: () => Promise<string[]>
|
||||
getLogs: (lines?: number, fileName?: string) => Promise<string>
|
||||
clearLogs: (fileName?: string) => Promise<void>
|
||||
cleanOldLogs: (daysToKeep?: number) => Promise<void>
|
||||
|
||||
// 保留原有方法以兼容现有代码
|
||||
saveLogsToFile: (logs: string) => Promise<void>
|
||||
loadLogsFromFile: () => Promise<string | null>
|
||||
|
||||
// 文件系统操作
|
||||
openFile: (filePath: string) => Promise<void>
|
||||
showItemInFolder: (filePath: string) => Promise<void>
|
||||
|
||||
// 监听下载进度
|
||||
onDownloadProgress: (callback: (progress: any) => void) => void
|
||||
removeDownloadProgressListener: () => void
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface ElectronAPI {
|
||||
openDevTools: () => Promise<void>
|
||||
selectFolder: () => Promise<string | null>
|
||||
selectFile: (filters?: any[]) => Promise<string[]>
|
||||
openUrl: (url: string) => Promise<{ success: boolean; error?: string }>
|
||||
|
||||
// 窗口控制
|
||||
windowMinimize: () => Promise<void>
|
||||
windowMaximize: () => Promise<void>
|
||||
windowClose: () => Promise<void>
|
||||
windowIsMaximized: () => Promise<boolean>
|
||||
appQuit: () => Promise<void>
|
||||
|
||||
// 进程管理
|
||||
getRelatedProcesses: () => Promise<any[]>
|
||||
killAllProcesses: () => Promise<{ success: boolean; error?: string }>
|
||||
forceExit: () => Promise<{ success: boolean }>
|
||||
|
||||
// 初始化相关API
|
||||
checkEnvironment: () => Promise<any>
|
||||
checkCriticalFiles: () => Promise<{ pythonExists: boolean; gitExists: boolean; mainPyExists: boolean }>
|
||||
checkGitUpdate: () => Promise<{ hasUpdate: boolean; error?: string }>
|
||||
downloadPython: (mirror?: string) => Promise<any>
|
||||
installPip: () => Promise<any>
|
||||
downloadGit: () => Promise<any>
|
||||
installDependencies: (mirror?: string) => Promise<any>
|
||||
cloneBackend: (repoUrl?: string) => Promise<any>
|
||||
updateBackend: (repoUrl?: string) => Promise<any>
|
||||
startBackend: () => Promise<{ success: boolean; error?: string }>
|
||||
stopBackend?: () => Promise<{ success: boolean; error?: string }>
|
||||
|
||||
// 管理员权限相关
|
||||
checkAdmin: () => Promise<boolean>
|
||||
restartAsAdmin: () => Promise<void>
|
||||
|
||||
// 配置文件操作
|
||||
saveConfig: (config: any) => Promise<void>
|
||||
loadConfig: () => Promise<any>
|
||||
resetConfig: () => Promise<void>
|
||||
|
||||
// 日志文件操作
|
||||
getLogPath: () => Promise<string>
|
||||
getLogFiles: () => Promise<string[]>
|
||||
getLogs: (lines?: number, fileName?: string) => Promise<string>
|
||||
clearLogs: (fileName?: string) => Promise<void>
|
||||
cleanOldLogs: (daysToKeep?: number) => Promise<void>
|
||||
|
||||
// 保留原有方法以兼容现有代码
|
||||
saveLogsToFile: (logs: string) => Promise<void>
|
||||
loadLogsFromFile: () => Promise<string | null>
|
||||
|
||||
// 文件系统操作
|
||||
openFile: (filePath: string) => Promise<void>
|
||||
showItemInFolder: (filePath: string) => Promise<void>
|
||||
|
||||
// 对话框相关
|
||||
showQuestionDialog: (questionData: {
|
||||
title?: string
|
||||
message?: string
|
||||
options?: string[]
|
||||
messageId?: string
|
||||
}) => Promise<boolean>
|
||||
dialogResponse: (messageId: string, choice: boolean) => Promise<boolean>
|
||||
resizeDialogWindow: (height: number) => Promise<void>
|
||||
|
||||
// 监听下载进度
|
||||
onDownloadProgress: (callback: (progress: any) => void) => void
|
||||
removeDownloadProgressListener: () => void
|
||||
}
|
||||
|
||||
interface Window {
|
||||
electronAPI: ElectronAPI
|
||||
}
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
// Electron API 类型定义
|
||||
export interface ElectronAPI {
|
||||
// 开发工具
|
||||
openDevTools: () => Promise<void>
|
||||
selectFolder: () => Promise<string | null>
|
||||
selectFile: (filters?: Array<{ name: string; extensions: string[] }>) => Promise<string | null>
|
||||
|
||||
// 窗口控制
|
||||
windowMinimize: () => Promise<void>
|
||||
windowMaximize: () => Promise<void>
|
||||
windowClose: () => Promise<void>
|
||||
windowIsMaximized: () => Promise<boolean>
|
||||
|
||||
// 管理员权限检查
|
||||
checkAdmin: () => Promise<boolean>
|
||||
|
||||
// 重启为管理员
|
||||
restartAsAdmin: () => Promise<void>
|
||||
appQuit: () => Promise<void>
|
||||
|
||||
// 进程管理
|
||||
getRelatedProcesses: () => Promise<any[]>
|
||||
killAllProcesses: () => Promise<{ success: boolean; error?: string }>
|
||||
forceExit: () => Promise<{ success: boolean }>
|
||||
// 环境检查
|
||||
checkEnvironment: () => Promise<{
|
||||
pythonExists: boolean
|
||||
gitExists: boolean
|
||||
backendExists: boolean
|
||||
dependenciesInstalled: boolean
|
||||
isInitialized: boolean
|
||||
}>
|
||||
|
||||
// 关键文件检查
|
||||
checkCriticalFiles: () => Promise<{
|
||||
pythonExists: boolean
|
||||
pipExists: boolean
|
||||
gitExists: boolean
|
||||
mainPyExists: boolean
|
||||
}>
|
||||
|
||||
// Python相关
|
||||
downloadPython: (mirror: string) => Promise<{ success: boolean; error?: string }>
|
||||
deletePython: () => Promise<{ success: boolean; error?: string }>
|
||||
|
||||
// pip相关
|
||||
installPip: () => Promise<{ success: boolean; error?: string }>
|
||||
deletePip: () => Promise<{ success: boolean; error?: string }>
|
||||
|
||||
// Git相关
|
||||
downloadGit: () => Promise<{ success: boolean; error?: string }>
|
||||
deleteGit: () => Promise<{ success: boolean; error?: string }>
|
||||
checkGitUpdate: () => Promise<{ hasUpdate: boolean; error?: string }>
|
||||
|
||||
// 后端代码相关
|
||||
cloneBackend: (gitUrl: string) => Promise<{ success: boolean; error?: string }>
|
||||
updateBackend: (gitUrl: string) => Promise<{ success: boolean; error?: string }>
|
||||
|
||||
// 依赖安装
|
||||
installDependencies: (mirror: string) => Promise<{ success: boolean; error?: string }>
|
||||
|
||||
// 后端服务
|
||||
startBackend: () => Promise<{ success: boolean; error?: string }>
|
||||
|
||||
// 下载进度监听
|
||||
onDownloadProgress: (
|
||||
callback: (progress: { progress: number; status: string; message: string }) => void
|
||||
) => void
|
||||
removeDownloadProgressListener: () => void
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
electronAPI: ElectronAPI
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
||||
@@ -1,24 +1,13 @@
|
||||
// 渲染进程日志工具
|
||||
interface ElectronAPI {
|
||||
getLogPath: () => Promise<string>
|
||||
getLogFiles: () => Promise<string[]>
|
||||
getLogs: (lines?: number, fileName?: string) => Promise<string>
|
||||
clearLogs: (fileName?: string) => Promise<void>
|
||||
cleanOldLogs: (daysToKeep?: number) => Promise<void>
|
||||
}
|
||||
const LogLevel = {
|
||||
DEBUG: 'DEBUG',
|
||||
INFO: 'INFO',
|
||||
WARN: 'WARN',
|
||||
ERROR: 'ERROR'
|
||||
} as const
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
electronAPI: ElectronAPI
|
||||
}
|
||||
}
|
||||
|
||||
export enum LogLevel {
|
||||
DEBUG = 'DEBUG',
|
||||
INFO = 'INFO',
|
||||
WARN = 'WARN',
|
||||
ERROR = 'ERROR'
|
||||
}
|
||||
export type LogLevel = typeof LogLevel[keyof typeof LogLevel]
|
||||
export { LogLevel }
|
||||
|
||||
class Logger {
|
||||
// 直接使用原生console,主进程会自动处理日志记录
|
||||
@@ -40,32 +29,32 @@ class Logger {
|
||||
|
||||
// 获取日志文件路径
|
||||
async getLogPath(): Promise<string> {
|
||||
if (window.electronAPI) {
|
||||
return await window.electronAPI.getLogPath()
|
||||
if ((window as any).electronAPI) {
|
||||
return await (window as any).electronAPI.getLogPath()
|
||||
}
|
||||
throw new Error('Electron API not available')
|
||||
}
|
||||
|
||||
// 获取日志文件列表
|
||||
async getLogFiles(): Promise<string[]> {
|
||||
if (window.electronAPI) {
|
||||
return await window.electronAPI.getLogFiles()
|
||||
if ((window as any).electronAPI) {
|
||||
return await (window as any).electronAPI.getLogFiles()
|
||||
}
|
||||
throw new Error('Electron API not available')
|
||||
}
|
||||
|
||||
// 获取日志内容
|
||||
async getLogs(lines?: number, fileName?: string): Promise<string> {
|
||||
if (window.electronAPI) {
|
||||
return await window.electronAPI.getLogs(lines, fileName)
|
||||
if ((window as any).electronAPI) {
|
||||
return await (window as any).electronAPI.getLogs(lines, fileName)
|
||||
}
|
||||
throw new Error('Electron API not available')
|
||||
}
|
||||
|
||||
// 清空日志
|
||||
async clearLogs(fileName?: string): Promise<void> {
|
||||
if (window.electronAPI) {
|
||||
await window.electronAPI.clearLogs(fileName)
|
||||
if ((window as any).electronAPI) {
|
||||
await (window as any).electronAPI.clearLogs(fileName)
|
||||
console.info(`日志已清空: ${fileName || '当前文件'}`)
|
||||
} else {
|
||||
throw new Error('Electron API not available')
|
||||
@@ -74,8 +63,8 @@ class Logger {
|
||||
|
||||
// 清理旧日志
|
||||
async cleanOldLogs(daysToKeep: number = 7): Promise<void> {
|
||||
if (window.electronAPI) {
|
||||
await window.electronAPI.cleanOldLogs(daysToKeep)
|
||||
if ((window as any).electronAPI) {
|
||||
await (window as any).electronAPI.cleanOldLogs(daysToKeep)
|
||||
console.info(`已清理${daysToKeep}天前的旧日志`)
|
||||
} else {
|
||||
throw new Error('Electron API not available')
|
||||
|
||||
Reference in New Issue
Block a user