feat(admin): 工单管理 UI 优化与回复模板、文件管理增强
Build and Deploy Vue3 / build (push) Failing after 48s
Build and Deploy Vue3 / deploy (push) Has been skipped

工单列表与详情 UI/交互优化及新工单提醒;新增回复模板与工单类型管理;文件管理增加管理员筛选并优化详情展示。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
shiran
2026-06-02 17:28:11 +08:00
parent 928d14aada
commit c18622226e
12 changed files with 2480 additions and 477 deletions
+1 -6
View File
@@ -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
View File
@@ -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;
}
/* 表格样式优化 */