MediaFileSelector.vue 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. <template>
  2. <div>
  3. <el-button type="primary" @click="dialogVisible = true">选择文件</el-button>
  4. <div v-if="selectedFiles.length > 0" class="selected-files-list">
  5. <el-tag v-for="(file, idx) in selectedFiles" :key="file.id" closable @close="removeFile(idx)" style="margin: 2px">
  6. {{ file.name }}
  7. </el-tag>
  8. </div>
  9. <el-dialog title="选择媒资文件" v-model="dialogVisible" width="900px" append-to-body @close="restoreSelection">
  10. <el-form :inline="true" :model="queryParams" class="mb-2">
  11. <el-form-item label="名称">
  12. <el-input v-model="queryParams.originalName" placeholder="文件名" clearable style="width: 180px"
  13. @keyup.enter="getFileList" />
  14. </el-form-item>
  15. <el-form-item label="类型">
  16. <el-select v-model="queryParams.type" clearable placeholder="全部" style="width: 120px">
  17. <el-option label="全部" :value="''" />
  18. <el-option label="图片" :value="1" />
  19. <el-option label="视频" :value="2" />
  20. <el-option label="音频" :value="3" />
  21. </el-select>
  22. </el-form-item>
  23. <el-form-item>
  24. <el-button type="primary" @click="getFileList">搜索</el-button>
  25. </el-form-item>
  26. </el-form>
  27. <el-table v-loading="dialogLoading" ref="fileTable" :data="fileList" reserve-selection row-key="id"
  28. @selection-change="handleSelectionFile" @select="handleSelect" @select-all="handleSelectAll">
  29. <el-table-column type="selection" width="55" header-align="center" :selectable="isSelectableRow" />
  30. <el-table-column label="类型" header-align="left" prop="type" width="80">
  31. <template #default="scope">
  32. <dict-tag :options="smsb_source_type" :value="scope.row.type" />
  33. </template>
  34. </el-table-column>
  35. <el-table-column label="原名" header-align="left" prop="originalName" width="150" :show-overflow-tooltip="true" />
  36. <el-table-column label="大小" header-align="left" prop="size" />
  37. <el-table-column label="时长" header-align="left" prop="duration" />
  38. <el-table-column label="截图" header-align="left" prop="screenshot">
  39. <template #default="scope">
  40. <image-preview :src="scope.row.screenshot" style="width: 40px; height: 40px; cursor: pointer" />
  41. </template>
  42. </el-table-column>
  43. </el-table>
  44. <pagination v-show="fileTotal > 0" :total="fileTotal" v-model:page="queryParams.pageNum"
  45. v-model:limit="queryParams.pageSize" @pagination="getFileList" />
  46. <template #footer>
  47. <el-button @click="dialogVisible = false">取消</el-button>
  48. <el-button type="primary" @click="confirmSelect">确定</el-button>
  49. </template>
  50. </el-dialog>
  51. </div>
  52. </template>
  53. <script setup lang="ts">
  54. const { proxy } = getCurrentInstance() as ComponentInternalInstance;
  55. const { smsb_source_type } = toRefs<any>(proxy?.useDict('smsb_source_type'));
  56. import { ref, watch, defineProps, defineEmits, nextTick } from 'vue';
  57. import { listMinioData } from '@/api/smsb/source/minioData';
  58. import type { MinioDataVO, MinioDataQuery } from '@/api/smsb/source/minioData_type';
  59. const props = defineProps<{ modelValue: string; single?: boolean; onlyImage?: boolean }>();
  60. const emit = defineEmits(['update:modelValue']);
  61. const dialogVisible = ref(false);
  62. const dialogLoading = ref(false);
  63. const fileList = ref<MinioDataVO[]>([]);
  64. const fileTotal = ref(0);
  65. const selectedFiles = ref<any[]>([]);
  66. // 控制哪些行可选
  67. function isSelectableRow(row: any) {
  68. if (props.onlyImage) return row.type === 1;
  69. return true;
  70. }
  71. const fileTable = ref();
  72. const queryParams = ref<MinioDataQuery>({
  73. pageNum: 1,
  74. pageSize: 10,
  75. originalName: '',
  76. type: ''
  77. });
  78. // 初始化时,如果有值,反序列化
  79. watch(
  80. () => props.modelValue,
  81. (val) => {
  82. if (val) {
  83. try {
  84. selectedFiles.value = JSON.parse(val);
  85. } catch {
  86. selectedFiles.value = [];
  87. }
  88. } else {
  89. selectedFiles.value = [];
  90. }
  91. },
  92. { immediate: true }
  93. );
  94. // 查询文件资源列表
  95. const getFileList = async () => {
  96. dialogLoading.value = true;
  97. try {
  98. const res = await listMinioData(queryParams.value);
  99. console.log('[getFileList] raw:', res);
  100. console.log('[getFileList] rows:', res.rows);
  101. const mapped = (res.rows || []).map((item: any, idx: number) => {
  102. const sizeNum = Number(item.size);
  103. const sizeStr = !isNaN(sizeNum) && sizeNum > 0 ? (sizeNum / 1024).toFixed(3) + 'MB' : '0MB';
  104. if (isNaN(sizeNum)) {
  105. console.warn(`[getFileList] row[${idx}] 非法size:`, item.size, item);
  106. }
  107. return {
  108. ...item,
  109. size: sizeStr
  110. };
  111. });
  112. fileList.value = mapped;
  113. console.log('[getFileList] mapped fileList:', mapped);
  114. console.log('[getFileList] fileList.value:', fileList.value, 'isArray:', Array.isArray(fileList.value), 'length:', fileList.value.length);
  115. fileTotal.value = res.total || 0;
  116. await nextTick();
  117. restoreSelection();
  118. } finally {
  119. dialogLoading.value = false;
  120. }
  121. };
  122. // 多选框选中文件数据
  123. function handleSelectionFile(selection: MinioDataVO[]) {
  124. console.log('[handleSelectionFile] selection:', selection);
  125. if (props.single && props.onlyImage) {
  126. // 单选且仅图片
  127. if (selection.length > 0) {
  128. const img = selection.find((item) => item.type === 1);
  129. if (img) {
  130. selectedFiles.value = [
  131. {
  132. id: img.id,
  133. name: img.originalName,
  134. type: img.type,
  135. duration: 10,
  136. order: 1,
  137. url: img.fileUrl,
  138. md5: img.md5
  139. }
  140. ];
  141. } else {
  142. selectedFiles.value = [];
  143. }
  144. } else {
  145. selectedFiles.value = [];
  146. }
  147. } else if (props.single) {
  148. // 单选任意类型
  149. if (selection.length > 0) {
  150. selectedFiles.value = [
  151. {
  152. id: selection[0].id,
  153. name: selection[0].originalName,
  154. type: selection[0].type,
  155. duration: selection[0].duration,
  156. order: 1,
  157. url: selection[0].fileUrl,
  158. md5: selection[0].md5
  159. }
  160. ];
  161. } else {
  162. selectedFiles.value = [];
  163. }
  164. } else {
  165. // 多选多类型
  166. selectedFiles.value = selection.map((item, idx) => ({
  167. id: item.id,
  168. name: item.originalName,
  169. type: item.type,
  170. duration: item.duration,
  171. order: idx + 1,
  172. url: item.fileUrl,
  173. md5: item.md5
  174. }));
  175. }
  176. }
  177. // 取消单个选中
  178. function handleSelect(selection: MinioDataVO[], row: MinioDataVO) {
  179. if (!selection.some((item) => String(item.id) === String(row.id))) {
  180. selectedFiles.value = selectedFiles.value.filter((f) => String(f.id) !== String(row.id));
  181. selectedFiles.value = selectedFiles.value.map((f, idx) => ({ ...f, order: idx + 1 }));
  182. }
  183. }
  184. // 取消全选
  185. function handleSelectAll(selection: MinioDataVO[]) {
  186. const currentPageIds = new Set(fileList.value.map((item) => String(item.id)));
  187. const selectedIds = new Set(selection.map((item) => String(item.id)));
  188. selectedFiles.value = selectedFiles.value.filter((f) => !currentPageIds.has(String(f.id)) || selectedIds.has(String(f.id)));
  189. selectedFiles.value = selectedFiles.value.map((f, idx) => ({ ...f, order: idx + 1 }));
  190. }
  191. function removeFile(idx: number) {
  192. selectedFiles.value.splice(idx, 1);
  193. emitChange();
  194. }
  195. function confirmSelect() {
  196. if (props.single && props.onlyImage) {
  197. if (selectedFiles.value.length === 0 || selectedFiles.value[0].type !== 1) {
  198. ElMessage.error('只能选择图片文件作为背景');
  199. return;
  200. }
  201. }
  202. dialogVisible.value = false;
  203. emitChange();
  204. }
  205. function emitChange() {
  206. emit('update:modelValue', JSON.stringify(selectedFiles.value));
  207. }
  208. // 弹窗关闭时还原选中状态
  209. function restoreSelection() {
  210. nextTick(() => {
  211. if (!fileTable.value) return;
  212. const selectedIds = new Set(selectedFiles.value.map((f) => String(f.id)));
  213. fileList.value.forEach((row) => {
  214. fileTable.value.toggleRowSelection(row, selectedIds.has(String(row.id)));
  215. });
  216. });
  217. }
  218. // 弹窗首次打开时自动加载
  219. watch(dialogVisible, (val) => {
  220. if (val) getFileList();
  221. });
  222. </script>
  223. <style scoped>
  224. .selected-files-list {
  225. margin: 8px 0;
  226. }
  227. </style>