FileManage.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. <template>
  2. <div class="file-manage">
  3. <el-card>
  4. <el-row :gutter="16" style="margin-bottom:16px">
  5. <el-col :span="6">
  6. <el-card shadow="never" class="stat-card">
  7. <div class="stat-value">{{ stats.total || 0 }}</div>
  8. <div class="stat-label">总文件数</div>
  9. </el-card>
  10. </el-col>
  11. <el-col :span="6">
  12. <el-card shadow="never" class="stat-card">
  13. <div class="stat-value">{{ formatSize(stats.totalBytes || 0) }}</div>
  14. <div class="stat-label">总大小</div>
  15. </el-card>
  16. </el-col>
  17. <el-col :span="12">
  18. <el-card shadow="never" class="stat-card">
  19. <div style="display:flex;gap:12px;flex-wrap:wrap">
  20. <span v-for="c in stats.byCategory || []" :key="c.category">
  21. <el-tag size="small">{{ c.category }}: {{ c.cnt }}</el-tag>
  22. </span>
  23. </div>
  24. <div class="stat-label">分类分布</div>
  25. </el-card>
  26. </el-col>
  27. </el-row>
  28. <div class="filter-bar">
  29. <el-select v-model="query.category" placeholder="分类" clearable style="width:130px">
  30. <el-option label="全部" value=""></el-option>
  31. <el-option label="articles" value="articles"></el-option>
  32. <el-option label="kb-images" value="kb-images"></el-option>
  33. <el-option label="dan-reports" value="dan-reports"></el-option>
  34. <el-option label="assessment-reports" value="assessment-reports"></el-option>
  35. <el-option label="health-reports" value="health-reports"></el-option>
  36. <el-option label="general" value="general"></el-option>
  37. </el-select>
  38. <el-select v-model="query.fileType" placeholder="文件类型" clearable style="width:120px">
  39. <el-option label="全部" value=""></el-option>
  40. <el-option label="image" value="image"></el-option>
  41. <el-option label="pdf" value="pdf"></el-option>
  42. <el-option label="video" value="video"></el-option>
  43. <el-option label="audio" value="audio"></el-option>
  44. <el-option label="other" value="other"></el-option>
  45. </el-select>
  46. <el-select v-model="query.tagIds" placeholder="标签" multiple clearable style="width:200px">
  47. <el-option v-for="t in tagOptions" :key="t.id" :label="t.name" :value="t.id"></el-option>
  48. </el-select>
  49. <el-input v-model="query.search" placeholder="搜索文件名/描述" style="width:200px" @keyup.enter.native="loadData"></el-input>
  50. <el-date-picker v-model="dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" style="width:240px"></el-date-picker>
  51. <el-button type="primary" @click="loadData">查询</el-button>
  52. </div>
  53. <el-table :data="tableData" border stripe v-loading="loading" style="margin-top:16px">
  54. <el-table-column prop="id" label="ID" width="70"></el-table-column>
  55. <el-table-column label="预览" width="70">
  56. <template slot-scope="scope">
  57. <el-image v-if="scope.row.fileType === 'image'" :src="scope.row.url" style="width:40px;height:40px;border-radius:4px" fit="cover" :preview-src-list="[scope.row.url]"></el-image>
  58. <i v-else-if="scope.row.fileType === 'pdf'" class="el-icon-document" style="font-size:28px;color:#F56C6C;display:block;text-align:center"></i>
  59. <i v-else-if="scope.row.fileType === 'video'" class="el-icon-video-camera" style="font-size:28px;color:#409EFF;display:block;text-align:center"></i>
  60. <i v-else-if="scope.row.fileType === 'audio'" class="el-icon-headset" style="font-size:28px;color:#67C23A;display:block;text-align:center"></i>
  61. <i v-else class="el-icon-document" style="font-size:28px;color:#909399;display:block;text-align:center"></i>
  62. </template>
  63. </el-table-column>
  64. <el-table-column prop="originalFilename" label="文件名" min-width="180" show-overflow-tooltip></el-table-column>
  65. <el-table-column prop="fileType" label="类型" width="70">
  66. <template slot-scope="scope">
  67. <el-tag size="mini" :type="typeTag(scope.row.fileType)">{{ scope.row.fileType || '?' }}</el-tag>
  68. </template>
  69. </el-table-column>
  70. <el-table-column prop="fileSize" label="大小" width="90">
  71. <template slot-scope="scope">{{ formatSize(scope.row.fileSize) }}</template>
  72. </el-table-column>
  73. <el-table-column prop="category" label="分类" width="110"></el-table-column>
  74. <el-table-column label="标签" min-width="140">
  75. <template slot-scope="scope">
  76. <el-tag v-for="t in (scope.row.tags || [])" :key="t.id" size="mini" style="margin-right:4px;margin-bottom:2px;border:0" :style="{ backgroundColor: t.color + '22', color: t.color }">{{ t.name }}</el-tag>
  77. <span v-if="!scope.row.tags || scope.row.tags.length === 0" class="no-tag">-</span>
  78. </template>
  79. </el-table-column>
  80. <el-table-column prop="description" label="描述" min-width="120" show-overflow-tooltip></el-table-column>
  81. <el-table-column prop="createdAt" label="上传时间" width="160"></el-table-column>
  82. <el-table-column label="操作" width="120" fixed="right">
  83. <template slot-scope="scope">
  84. <el-button size="mini" type="text" @click="openEdit(scope.row)">编辑</el-button>
  85. <el-button size="mini" type="text" style="color:#F56C6C" @click="handleDelete(scope.row)">删除</el-button>
  86. </template>
  87. </el-table-column>
  88. </el-table>
  89. <el-pagination @size-change="handleSizeChange" @current-change="handleCurrentChange" :current-page="query.page" :page-size="query.pageSize" :total="total" layout="total, sizes, prev, pager, next, jumper" :page-sizes="[10, 20, 50, 100]" style="margin-top:16px"></el-pagination>
  90. </el-card>
  91. <el-dialog title="编辑文件" :visible.sync="editVisible" width="500px">
  92. <el-form label-width="80px">
  93. <el-form-item label="文件名"><el-input v-model="editForm.originalFilename" disabled></el-input></el-form-item>
  94. <el-form-item label="URL">
  95. <el-input v-model="editForm.url" disabled>
  96. <el-button slot="append" size="mini" @click="copyUrl(editForm.url)">复制</el-button>
  97. </el-input>
  98. </el-form-item>
  99. <el-form-item label="描述"><el-input v-model="editForm.description" type="textarea" :rows="3" maxlength="500"></el-input></el-form-item>
  100. <el-form-item label="标签">
  101. <el-select v-model="editForm.tagIds" multiple placeholder="选择标签" style="width:100%">
  102. <el-option v-for="t in tagOptions" :key="t.id" :label="t.name" :value="t.id">
  103. <span :style="{ color: t.color }">{{ t.name }}</span>
  104. </el-option>
  105. </el-select>
  106. </el-form-item>
  107. </el-form>
  108. <span slot="footer">
  109. <el-button @click="editVisible = false">取消</el-button>
  110. <el-button type="primary" @click="saveEdit" :loading="saving">保存</el-button>
  111. </span>
  112. </el-dialog>
  113. </div>
  114. </template>
  115. <script>
  116. import { listFiles, updateFile, deleteFile, getFileTags, getFileStats } from '@/api/file'
  117. export default {
  118. name: 'FileManage',
  119. data() {
  120. return {
  121. tableData: [], total: 0, loading: false, stats: {}, tagOptions: [], dateRange: null,
  122. query: { page: 1, pageSize: 20, category: '', fileType: '', tagIds: [], search: '', dateFrom: '', dateTo: '' },
  123. editVisible: false, saving: false,
  124. editForm: { id: null, url: '', originalFilename: '', description: '', tagIds: [] }
  125. }
  126. },
  127. mounted() { this.loadStats(); this.loadTags(); this.loadData() },
  128. methods: {
  129. async loadData() {
  130. this.loading = true
  131. try {
  132. if (this.dateRange && this.dateRange.length === 2) {
  133. this.query.dateFrom = this.dateRange[0].toISOString().slice(0, 10)
  134. this.query.dateTo = this.dateRange[1].toISOString().slice(0, 10)
  135. } else { this.query.dateFrom = ''; this.query.dateTo = '' }
  136. const res = await listFiles(this.query)
  137. if (res.data.code === 200) { this.tableData = res.data.data.records; this.total = res.data.data.total }
  138. } catch (e) { this.$message.error('加载失败') }
  139. this.loading = false
  140. },
  141. async loadStats() { try { const res = await getFileStats(); if (res.data.code === 200) this.stats = res.data.data } catch (e) {} },
  142. async loadTags() { try { const res = await getFileTags(); if (res.data.code === 200) this.tagOptions = res.data.data } catch (e) {} },
  143. openEdit(row) {
  144. this.editForm = { id: row.id, url: row.url, originalFilename: row.originalFilename, description: row.description || '', tagIds: (row.tags || []).map(t => t.id) }
  145. this.editVisible = true
  146. },
  147. async saveEdit() {
  148. this.saving = true
  149. try {
  150. const res = await updateFile({ id: this.editForm.id, description: this.editForm.description, tagIds: this.editForm.tagIds })
  151. if (res.data.code === 200) { this.$message.success('更新成功'); this.editVisible = false; this.loadData() }
  152. else this.$message.error(res.data.message || '更新失败')
  153. } catch (e) { this.$message.error('更新失败') }
  154. this.saving = false
  155. },
  156. handleDelete(row) {
  157. this.$confirm('确定删除该文件?删除后不可恢复。', '警告', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' })
  158. .then(async () => {
  159. try { const res = await deleteFile({ id: row.id }); if (res.data.code === 200) { this.$message.success('删除成功'); this.loadData(); this.loadStats() } else this.$message.error(res.data.message || '删除失败') }
  160. catch (e) { this.$message.error('删除失败') }
  161. }).catch(() => {})
  162. },
  163. copyUrl(url) { const input = document.createElement('input'); input.value = url; document.body.appendChild(input); input.select(); document.execCommand('copy'); document.body.removeChild(input); this.$message.success('已复制') },
  164. handleSizeChange(size) { this.query.pageSize = size; this.query.page = 1; this.loadData() },
  165. handleCurrentChange(page) { this.query.page = page; this.loadData() },
  166. formatSize(bytes) {
  167. if (!bytes) return '0B'; const units = ['B', 'KB', 'MB', 'GB']; let i = 0; let size = bytes
  168. while (size >= 1024 && i < units.length - 1) { size /= 1024; i++ }
  169. return size.toFixed(i > 0 ? 1 : 0) + units[i]
  170. },
  171. typeTag(type) { return { image: 'success', pdf: 'danger', video: 'primary', audio: 'warning' }[type] || 'info' }
  172. }
  173. }
  174. </script>
  175. <style scoped>
  176. .filter-bar { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
  177. .stat-card { text-align: center; }
  178. .stat-value { font-size: 24px; font-weight: bold; color: #303133; }
  179. .stat-label { font-size: 12px; color: #909399; margin-top: 4px; }
  180. .no-tag { color: #C0C4CC; }
  181. </style>