FileManage.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  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">
  82. <template slot-scope="scope">{{ scope.row.createdAt }}</template>
  83. <template slot="header">
  84. <span class="sort-header" @click.stop="onSortClick('createdAt', $event)">上传时间<span v-if="sortIndex('createdAt') !== null" class="sort-index" :class="sortClass('createdAt') === 'sort-asc' ? 'sort-asc-badge' : 'sort-desc-badge'">{{ sortIndex('createdAt') }}</span>{{ sortIcon('createdAt') }}</span>
  85. </template>
  86. </el-table-column>
  87. <el-table-column label="操作" width="120" fixed="right">
  88. <template slot-scope="scope">
  89. <el-button size="mini" type="text" @click="openEdit(scope.row)">编辑</el-button>
  90. <el-button size="mini" type="text" style="color:#F56C6C" @click="handleDelete(scope.row)">删除</el-button>
  91. </template>
  92. </el-table-column>
  93. </el-table>
  94. <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>
  95. </el-card>
  96. <el-dialog title="编辑文件" :visible.sync="editVisible" width="500px">
  97. <el-form label-width="80px">
  98. <el-form-item label="文件名"><el-input v-model="editForm.originalFilename" disabled></el-input></el-form-item>
  99. <el-form-item label="URL">
  100. <el-input v-model="editForm.url" disabled>
  101. <el-button slot="append" size="mini" @click="copyUrl(editForm.url)">复制</el-button>
  102. </el-input>
  103. </el-form-item>
  104. <el-form-item label="描述"><el-input v-model="editForm.description" type="textarea" :rows="3" maxlength="500"></el-input></el-form-item>
  105. <el-form-item label="标签">
  106. <el-select v-model="editForm.tagIds" multiple placeholder="选择标签" style="width:100%">
  107. <el-option v-for="t in tagOptions" :key="t.id" :label="t.name" :value="t.id">
  108. <span :style="{ color: t.color }">{{ t.name }}</span>
  109. </el-option>
  110. </el-select>
  111. </el-form-item>
  112. </el-form>
  113. <span slot="footer">
  114. <el-button @click="editVisible = false">取消</el-button>
  115. <el-button type="primary" @click="saveEdit" :loading="saving">保存</el-button>
  116. </span>
  117. </el-dialog>
  118. </div>
  119. </template>
  120. <script>
  121. import SortMixin from '@/mixins/SortMixin'
  122. import { listFiles, updateFile, deleteFile, getFileTags, getFileStats } from '@/api/file'
  123. export default {
  124. name: 'FileManage',
  125. mixins: [SortMixin],
  126. data() {
  127. return {
  128. tableData: [], total: 0, loading: false, stats: {}, tagOptions: [], dateRange: null,
  129. query: { page: 1, pageSize: 20, category: '', fileType: '', tagIds: [], search: '', dateFrom: '', dateTo: '' },
  130. editVisible: false, saving: false,
  131. editForm: { id: null, url: '', originalFilename: '', description: '', tagIds: [] },
  132. sortableColumns: { createdAt: '上传时间' }
  133. }
  134. },
  135. mounted() { this.loadStats(); this.loadTags(); this.loadData() },
  136. methods: {
  137. loadList() { this.loadData() },
  138. async loadData() {
  139. this.loading = true
  140. try {
  141. if (this.dateRange && this.dateRange.length === 2) {
  142. this.query.dateFrom = this.dateRange[0].toISOString().slice(0, 10)
  143. this.query.dateTo = this.dateRange[1].toISOString().slice(0, 10)
  144. } else { this.query.dateFrom = ''; this.query.dateTo = '' }
  145. if (this.sortState.length > 0) this.query.sort = this.sortState
  146. const res = await listFiles(this.query)
  147. if (res.data.code === 200) { this.tableData = res.data.data.records; this.total = res.data.data.total }
  148. } catch (e) { this.$message.error('加载失败') }
  149. this.loading = false
  150. },
  151. async loadStats() { try { const res = await getFileStats(); if (res.data.code === 200) this.stats = res.data.data } catch (e) {} },
  152. async loadTags() { try { const res = await getFileTags(); if (res.data.code === 200) this.tagOptions = res.data.data } catch (e) {} },
  153. openEdit(row) {
  154. this.editForm = { id: row.id, url: row.url, originalFilename: row.originalFilename, description: row.description || '', tagIds: (row.tags || []).map(t => t.id) }
  155. this.editVisible = true
  156. },
  157. async saveEdit() {
  158. this.saving = true
  159. try {
  160. const res = await updateFile({ id: this.editForm.id, description: this.editForm.description, tagIds: this.editForm.tagIds })
  161. if (res.data.code === 200) { this.$message.success('更新成功'); this.editVisible = false; this.loadData() }
  162. else this.$message.error(res.data.message || '更新失败')
  163. } catch (e) { this.$message.error('更新失败') }
  164. this.saving = false
  165. },
  166. handleDelete(row) {
  167. this.$confirm('确定删除该文件?删除后不可恢复。', '警告', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' })
  168. .then(async () => {
  169. 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 || '删除失败') }
  170. catch (e) { this.$message.error('删除失败') }
  171. }).catch(() => {})
  172. },
  173. 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('已复制') },
  174. handleSizeChange(size) { this.query.pageSize = size; this.query.page = 1; this.loadData() },
  175. handleCurrentChange(page) { this.query.page = page; this.loadData() },
  176. formatSize(bytes) {
  177. if (!bytes) return '0B'; const units = ['B', 'KB', 'MB', 'GB']; let i = 0; let size = bytes
  178. while (size >= 1024 && i < units.length - 1) { size /= 1024; i++ }
  179. return size.toFixed(i > 0 ? 1 : 0) + units[i]
  180. },
  181. typeTag(type) { return { image: 'success', pdf: 'danger', video: 'primary', audio: 'warning' }[type] || 'info' }
  182. }
  183. }
  184. </script>
  185. <style scoped>
  186. .filter-bar { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
  187. .stat-card { text-align: center; }
  188. .stat-value { font-size: 24px; font-weight: bold; color: #303133; }
  189. .stat-label { font-size: 12px; color: #909399; margin-top: 4px; }
  190. .no-tag { color: #C0C4CC; }
  191. </style>