feat(admin): 工单管理 UI 优化与回复模板、文件管理增强
工单列表与详情 UI/交互优化及新工单提醒;新增回复模板与工单类型管理;文件管理增加管理员筛选并优化详情展示。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -118,12 +118,7 @@ const domainForm = reactive({
|
||||
// 表单规则
|
||||
const domainRules = {
|
||||
domain: [
|
||||
{ required: true, message: '请输入域名', trigger: 'blur' },
|
||||
{
|
||||
pattern: /^((?!-)[A-Za-z0-9-]{1,63}(?<!-)\.)+[A-Za-z]{2,6}$/,
|
||||
message: '请输入有效的域名格式',
|
||||
trigger: 'blur'
|
||||
}
|
||||
{ required: true, message: '请输入域名', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
+228
-72
@@ -12,6 +12,12 @@
|
||||
<el-form-item label="筛选用户">
|
||||
<el-input-number v-model="queryParams.user_id" placeholder="请输入用户ID" :controls="false" clearable style="width: 150px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="上传来源">
|
||||
<el-select v-model="queryParams.is_admin" placeholder="全部" clearable style="width: 130px">
|
||||
<el-option label="管理员" :value="true" />
|
||||
<el-option label="用户" :value="false" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleQuery">
|
||||
<el-icon><Search /></el-icon>查询
|
||||
@@ -69,6 +75,13 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="上传来源" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.isAdmin ? 'warning' : ''">
|
||||
{{ row.isAdmin ? '管理员' : '用户' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.CreatedAt) }}
|
||||
@@ -102,65 +115,87 @@
|
||||
<!-- 文件详情对话框 -->
|
||||
<el-dialog
|
||||
v-model="detailDialogVisible"
|
||||
title="文件详情"
|
||||
width="700px"
|
||||
title=""
|
||||
width="720px"
|
||||
destroy-on-close
|
||||
class="file-detail-dialog"
|
||||
>
|
||||
<div v-if="fileDetail" class="file-detail-container">
|
||||
<!-- 文件预览区域 -->
|
||||
<div class="file-preview-section">
|
||||
<div class="preview-label">文件预览</div>
|
||||
<div class="preview-content">
|
||||
<!-- 顶部卡片:预览 + 核心信息 -->
|
||||
<div class="detail-top">
|
||||
<div class="detail-preview">
|
||||
<el-image
|
||||
v-if="isImageFile(fileDetail.type, fileDetail.url, fileDetail.realName) && fileDetail.url"
|
||||
:src="fileDetail.url"
|
||||
fit="contain"
|
||||
style="max-width: 100%; max-height: 400px; border-radius: 8px;"
|
||||
class="preview-image"
|
||||
:preview-src-list="[fileDetail.url]"
|
||||
:initial-index="0"
|
||||
>
|
||||
<template #error>
|
||||
<div class="image-error">
|
||||
<el-icon size="40"><Picture /></el-icon>
|
||||
<div>图片加载失败</div>
|
||||
<el-icon size="36"><Picture /></el-icon>
|
||||
<span>加载失败</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<div v-else class="file-icon-large">
|
||||
<el-icon size="80"><Document /></el-icon>
|
||||
<div class="file-type-text">{{ fileDetail.type || '未知类型' }}</div>
|
||||
<el-icon size="56" color="#909399"><Document /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-summary">
|
||||
<div class="detail-filename" :title="fileDetail.realName">{{ fileDetail.realName }}</div>
|
||||
<div class="detail-meta-row">
|
||||
<el-tag :type="getFileTypeColor(fileDetail.type, fileDetail.url, fileDetail.realName)" size="small">{{ fileDetail.type || '未知' }}</el-tag>
|
||||
<span class="detail-size">{{ formatFileSize(fileDetail.size) }}</span>
|
||||
<el-tag :type="fileDetail.openDow ? 'success' : 'info'" size="small">{{ fileDetail.openDow ? '公开' : '私有' }}</el-tag>
|
||||
<el-tag :type="fileDetail.isAdmin ? 'warning' : ''" size="small">{{ fileDetail.isAdmin ? '管理员' : '用户' }}</el-tag>
|
||||
</div>
|
||||
<div class="detail-meta-row secondary">
|
||||
<span>ID: {{ fileDetail.id }}</span>
|
||||
<span>用户: {{ fileDetail.userId }}</span>
|
||||
<span>{{ formatDate(fileDetail.CreatedAt) }}</span>
|
||||
</div>
|
||||
<div class="detail-actions">
|
||||
<el-button v-if="fileDetail.url" type="primary" size="small" @click="openFileUrl(fileDetail.url)">
|
||||
<el-icon><View /></el-icon>查看原文件
|
||||
</el-button>
|
||||
<el-button v-if="fileDetail.url" type="success" size="small" @click="handleDownload(fileDetail)">
|
||||
<el-icon><Download /></el-icon>下载
|
||||
</el-button>
|
||||
<el-button type="default" size="small" @click="copyPath(fileDetail.savePath)">
|
||||
<el-icon><DocumentCopy /></el-icon>复制路径
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文件信息 -->
|
||||
<el-descriptions :column="2" border class="file-info-descriptions">
|
||||
<el-descriptions-item label="文件ID" label-align="right">{{ fileDetail.id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="用户ID" label-align="right">{{ fileDetail.userId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="真实文件名" label-align="right" :span="2">{{ fileDetail.realName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="保存名称" label-align="right">{{ fileDetail.saveName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="文件类型" label-align="right">
|
||||
<el-tag :type="getFileTypeColor(fileDetail.type, fileDetail.url, fileDetail.realName)">{{ fileDetail.type || '未知' }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="文件大小" label-align="right">{{ formatFileSize(fileDetail.size) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="是否公开" label-align="right">
|
||||
<el-tag :type="fileDetail.openDow ? 'success' : 'info'">
|
||||
{{ fileDetail.openDow ? '公开访问' : '私有' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="保存路径" label-align="right" :span="2">
|
||||
<span class="file-path">{{ fileDetail.savePath }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="文件URL" label-align="right" :span="2">
|
||||
<el-link :href="fileDetail.url" target="_blank" type="primary" v-if="fileDetail.url">
|
||||
点击查看文件
|
||||
</el-link>
|
||||
<span v-else style="color: #909399;">无URL</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间" label-align="right">{{ formatDate(fileDetail.CreatedAt) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间" label-align="right">{{ formatDate(fileDetail.UpdatedAt) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" label-align="right" :span="2">{{ fileDetail.content || '无' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<!-- 详细信息网格 -->
|
||||
<div class="detail-grid">
|
||||
<div class="detail-grid-item">
|
||||
<span class="grid-label">保存名称</span>
|
||||
<span class="grid-value mono">{{ fileDetail.saveName }}</span>
|
||||
</div>
|
||||
<div class="detail-grid-item">
|
||||
<span class="grid-label">保存路径</span>
|
||||
<span class="grid-value mono">{{ fileDetail.savePath }}</span>
|
||||
</div>
|
||||
<div class="detail-grid-item" v-if="fileDetail.url">
|
||||
<span class="grid-label">访问地址</span>
|
||||
<div class="grid-value url-row">
|
||||
<el-link :href="fileDetail.url" target="_blank" type="primary" class="url-text">{{ fileDetail.url }}</el-link>
|
||||
<el-icon class="url-copy-icon" @click="copyPath(fileDetail.url)" title="复制地址"><DocumentCopy /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="detail-grid-item" v-if="fileDetail.UpdatedAt && fileDetail.UpdatedAt !== fileDetail.CreatedAt">
|
||||
<span class="grid-label">更新时间</span>
|
||||
<span class="grid-value">{{ formatDate(fileDetail.UpdatedAt) }}</span>
|
||||
</div>
|
||||
<div class="detail-grid-item" v-if="fileDetail.content">
|
||||
<span class="grid-label">备注</span>
|
||||
<span class="grid-value">{{ fileDetail.content }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
@@ -255,13 +290,14 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Upload, Delete, Search, Document, VideoPlay, Folder, UploadFilled, Picture, Refresh } from '@element-plus/icons-vue'
|
||||
import { Upload, Delete, Search, Document, VideoPlay, Folder, UploadFilled, Picture, Refresh, View, Download, DocumentCopy } from '@element-plus/icons-vue'
|
||||
import { getFileList, getFileDetail, updateFile, deleteFile, uploadFile } from '@/api/admin/file'
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
key: '',
|
||||
user_id: undefined,
|
||||
is_admin: undefined,
|
||||
page: 1,
|
||||
count: 10
|
||||
})
|
||||
@@ -391,6 +427,7 @@ const handleQuery = () => {
|
||||
const resetQuery = () => {
|
||||
queryParams.key = ''
|
||||
queryParams.user_id = undefined
|
||||
queryParams.is_admin = undefined
|
||||
queryParams.page = 1
|
||||
fetchFileList()
|
||||
}
|
||||
@@ -676,6 +713,20 @@ const submitEditForm = () => {
|
||||
})
|
||||
}
|
||||
|
||||
// 在新标签页打开文件
|
||||
const openFileUrl = (url) => {
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
// 复制路径
|
||||
const copyPath = (text) => {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
ElMessage.success('已复制到剪贴板')
|
||||
}).catch(() => {
|
||||
ElMessage.info(text)
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchFileList()
|
||||
@@ -750,28 +801,43 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.file-detail-container {
|
||||
padding: 10px 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.file-preview-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.preview-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
/* 顶部区域:预览 + 摘要 */
|
||||
.detail-top {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid #eef0f4;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.detail-preview {
|
||||
flex-shrink: 0;
|
||||
width: 200px;
|
||||
min-height: 160px;
|
||||
max-height: 300px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
min-height: 200px;
|
||||
justify-content: center;
|
||||
background: #f8f9fb;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 280px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.preview-image :deep(img) {
|
||||
object-fit: contain;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.file-icon-large {
|
||||
@@ -780,12 +846,6 @@ onMounted(() => {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #909399;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.file-type-text {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.image-error {
|
||||
@@ -793,23 +853,119 @@ onMounted(() => {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #909399;
|
||||
color: #c0c4cc;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.detail-summary {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.detail-filename {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.detail-meta-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.file-info-descriptions {
|
||||
margin-top: 16px;
|
||||
.detail-meta-row.secondary {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.file-path {
|
||||
font-family: 'Courier New', monospace;
|
||||
.detail-size {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.detail-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* 详细信息网格 */
|
||||
.detail-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.detail-grid-item {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.grid-label {
|
||||
flex-shrink: 0;
|
||||
width: 70px;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.grid-value {
|
||||
font-size: 13px;
|
||||
color: #303133;
|
||||
word-break: break-all;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.grid-value.mono {
|
||||
font-family: 'SF Mono', 'Menlo', 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
:deep(.el-descriptions__label) {
|
||||
width: 120px;
|
||||
.url-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.url-text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 420px;
|
||||
}
|
||||
|
||||
.url-copy-icon {
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
color: #909399;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.url-copy-icon:hover {
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
:deep(.file-detail-dialog .el-dialog__header) {
|
||||
padding: 16px 20px 0;
|
||||
}
|
||||
|
||||
:deep(.file-detail-dialog .el-dialog__body) {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* 表格样式优化 */
|
||||
|
||||
Reference in New Issue
Block a user