hc-form-upload.vue 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. <template>
  2. <div class="upload-container">
  3. <el-upload
  4. v-loading="isLoading"
  5. drag
  6. :accept="accept"
  7. :action="action"
  8. :class="isFocus ? 'is-focus' : ''"
  9. :disabled="isLoading"
  10. :headers="getHeader()"
  11. :keyname="isKeyName"
  12. :on-error="formUploadError"
  13. :on-progress="uploadprogress"
  14. :placeholder="placeholder"
  15. :show-file-list="false"
  16. class="hc-upload-table-form"
  17. element-loading-text="上传中..."
  18. @exceed="formUploadExceed"
  19. @success="formUploadSuccess"
  20. >
  21. <img v-if="isSrc" :src="isSrc" alt="" class="hc-table-form-img">
  22. <div v-else class="hc-table-form-icon">
  23. 点此选择或者拖拽文件并上传
  24. </div>
  25. <div v-if="isSrc" class="hc-table-form-actions">
  26. <el-button plain type="primary" size="small" @click.stop="handlePreview">
  27. 预览
  28. </el-button>
  29. <el-button plain type="danger" size="small" @click.stop="delTableFormFile">
  30. 删除
  31. </el-button>
  32. </div>
  33. <input
  34. :id="isKeyName"
  35. v-model="isSrc"
  36. class="hc-upload-input-src"
  37. @blur="handleBlur"
  38. @focus="handleFocus"
  39. >
  40. </el-upload>
  41. <!-- 预览弹窗 -->
  42. <el-dialog
  43. v-model="previewVisible"
  44. title="图片预览"
  45. :width="dialogWidth"
  46. :before-close="handleClose"
  47. >
  48. <div class="preview-container">
  49. <div
  50. class="preview-image-wrapper"
  51. :style="{ transform: `rotate(${rotation}deg)` }"
  52. >
  53. <img
  54. :src="previewSrc"
  55. alt="预览图片"
  56. class="preview-image"
  57. :style="{ maxHeight: `calc(100vh - 200px)` }"
  58. >
  59. </div>
  60. <div class="rotation-controls">
  61. <el-button type="primary" plain @click="rotate(-90)">向左旋转</el-button>
  62. <el-button type="primary" plain @click="rotate(90)">向右旋转</el-button>
  63. </div>
  64. </div>
  65. <template #footer>
  66. <el-button @click="previewVisible = false">取消</el-button>
  67. <el-button
  68. type="primary"
  69. :loading="confirmLoading"
  70. @click="confirmRotation"
  71. >
  72. 确定
  73. </el-button>
  74. </template>
  75. </el-dialog>
  76. </div>
  77. </template>
  78. <script setup>
  79. import { onMounted, ref, watch } from 'vue'
  80. import { getHeader } from 'hc-vue3-ui'
  81. import { ElMessage } from 'element-plus'
  82. const props = defineProps({
  83. src: {
  84. type: [Number, String],
  85. default: '',
  86. },
  87. keyname: {
  88. type: [Number, String],
  89. default: '',
  90. },
  91. placeholder: {
  92. type: [Number, String],
  93. default: '相片',
  94. },
  95. // 新增pkeyId属性,用于接口参数
  96. pkeyId: {
  97. type: [Number, String],
  98. default: '',
  99. },
  100. })
  101. // 事件
  102. const emit = defineEmits(['success', 'del', 'rotate-success'])
  103. // 变量
  104. const isLoading = ref(false)
  105. const isSrc = ref(props.src)
  106. const isKeyName = ref(props.keyname)
  107. const confirmLoading = ref(false)
  108. // 预览相关变量
  109. const previewVisible = ref(false)
  110. const previewSrc = ref('')
  111. const rotation = ref(0)
  112. const dialogWidth = ref('80%')
  113. const rotatedBlob = ref(null)
  114. const action = '/api/blade-manager/exceltab/add-buss-imginfo'
  115. const accept = 'image/png,image/jpg,image/jpeg'
  116. // 监听props变化
  117. watch(() => [props.src, props.keyname, props.pkeyId],
  118. ([src, keyname]) => {
  119. isSrc.value = src
  120. isKeyName.value = keyname
  121. })
  122. // 上传进度
  123. const uploadprogress = () => {
  124. isLoading.value = true
  125. }
  126. // 上传完成
  127. const formUploadSuccess = (res) => {
  128. isLoading.value = false
  129. if (res.code === 200) {
  130. const link = res.data?.link || ''
  131. emit('success', {
  132. res,
  133. src: link,
  134. key: isKeyName.value,
  135. })
  136. }
  137. }
  138. // 上传失败
  139. const formUploadError = () => {
  140. isLoading.value = false
  141. ElMessage.error('上传失败,请重试')
  142. }
  143. // 格式错误
  144. const formUploadExceed = () => {
  145. isLoading.value = false
  146. ElMessage.error('只能上传一个文件')
  147. }
  148. // 删除上传的文件
  149. const delTableFormFile = () => {
  150. emit('del', isKeyName.value)
  151. }
  152. // 焦点状态管理
  153. const isFocus = ref(false)
  154. const handleFocus = () => {
  155. isFocus.value = true
  156. }
  157. const handleBlur = () => {
  158. isFocus.value = false
  159. }
  160. // 预览图片
  161. const handlePreview = () => {
  162. if (!isSrc.value) return
  163. previewSrc.value = isSrc.value
  164. rotation.value = 0
  165. previewVisible.value = true
  166. }
  167. // 关闭预览弹窗
  168. const handleClose = () => {
  169. previewVisible.value = false
  170. rotation.value = 0
  171. rotatedBlob.value = null
  172. }
  173. // 旋转图片
  174. const rotate = (degrees) => {
  175. rotation.value = (rotation.value + degrees) % 360
  176. }
  177. // 确认旋转并上传
  178. const confirmRotation = async () => {
  179. if (rotation.value % 360 === 0) {
  180. previewVisible.value = false
  181. return
  182. }
  183. confirmLoading.value = true
  184. try {
  185. // 将旋转后的图片转换为blob
  186. const blob = await rotateImageAndGetBlob(previewSrc.value, rotation.value)
  187. if (!blob) {
  188. throw new Error('图片处理失败')
  189. }
  190. // 构建FormData
  191. const formData = new FormData()
  192. formData.append('file', blob, `rotated-${Date.now()}.png`)
  193. // 添加新参数
  194. formData.append('pkeyId', props.pkeyId)
  195. formData.append('keyname', isKeyName.value)
  196. // 调用上传接口
  197. const response = await fetch(action, {
  198. method: 'POST',
  199. headers: getHeader(),
  200. body: formData,
  201. })
  202. const res = await response.json()
  203. if (res.code === 200) {
  204. const link = res.data?.link || ''
  205. isSrc.value = link
  206. emit('success', {
  207. res,
  208. src: link,
  209. key: isKeyName.value,
  210. })
  211. emit('rotate-success', {
  212. src: link,
  213. rotation: rotation.value,
  214. key: isKeyName.value,
  215. })
  216. ElMessage.success(res.msg)
  217. previewVisible.value = false
  218. } else {
  219. throw new Error(res.msg || '上传失败')
  220. }
  221. } catch (error) {
  222. ElMessage.error(error.message || '处理失败,请重试')
  223. } finally {
  224. confirmLoading.value = false
  225. }
  226. }
  227. // 旋转图片并转为Blob
  228. const rotateImageAndGetBlob = (imageUrl, degrees) => {
  229. return new Promise((resolve, reject) => {
  230. const img = new Image()
  231. img.crossOrigin = 'anonymous'
  232. img.onload = function () {
  233. const canvas = document.createElement('canvas')
  234. const ctx = canvas.getContext('2d')
  235. // 根据旋转角度设置画布尺寸
  236. const radians = (degrees * Math.PI) / 180
  237. let width = img.width
  238. let height = img.height
  239. if (degrees % 180 !== 0) {
  240. [width, height] = [height, width]
  241. }
  242. canvas.width = width
  243. canvas.height = height
  244. // 旋转画布
  245. ctx.translate(width / 2, height / 2)
  246. ctx.rotate(radians)
  247. ctx.drawImage(img, -img.width / 2, -img.height / 2)
  248. // 转换为Blob
  249. canvas.toBlob(blob => {
  250. if (blob) {
  251. resolve(blob)
  252. } else {
  253. reject(new Error('无法转换图片为Blob'))
  254. }
  255. }, 'image/png')
  256. }
  257. img.onerror = () => reject(new Error('图片加载失败'))
  258. img.src = imageUrl
  259. })
  260. }
  261. </script>
  262. <style lang="scss" scoped>
  263. .upload-container {
  264. position: relative;
  265. width: 100%;
  266. height: 100%;
  267. }
  268. .hc-upload-table-form {
  269. display: flex;
  270. flex-direction: column;
  271. justify-content: center;
  272. height: 100%;
  273. border-radius: 3px;
  274. &.is-focus, &:hover {
  275. background-color: #eddac4;
  276. box-shadow: 0 0 0 1.5px var(--el-color-primary) inset;
  277. }
  278. .hc-upload-input-src {
  279. position: absolute;
  280. z-index: -1;
  281. right: 10px;
  282. width: 10px;
  283. }
  284. .hc-table-form-img {
  285. max-width: 100%;
  286. max-height: 200px;
  287. object-fit: contain;
  288. margin-bottom: 10px;
  289. }
  290. }
  291. .hc-table-form-icon {
  292. display: flex;
  293. align-items: center;
  294. justify-content: center;
  295. width: 100%;
  296. height: 100%;
  297. padding: 20px;
  298. }
  299. .hc-table-form-actions {
  300. position: absolute;
  301. top: 10px;
  302. right: 10px;
  303. display: flex;
  304. }
  305. .preview-container {
  306. display: flex;
  307. flex-direction: column;
  308. align-items: center;
  309. }
  310. .preview-image-wrapper {
  311. transition: transform 0.3s ease;
  312. margin: 20px 0;
  313. }
  314. .preview-image {
  315. max-width: 100%;
  316. object-fit: contain;
  317. }
  318. .rotation-controls {
  319. display: flex;
  320. gap: 10px;
  321. margin-top: 15px;
  322. }
  323. </style>
  324. <style lang="scss">
  325. .hc-upload-table-form{
  326. border-radius: 3px;
  327. transition: box-shadow 0.3s, background-color 0.3s;
  328. &.is-focus, &:hover {
  329. background-color: #eddac4;
  330. box-shadow: 0 0 0 1.5px var(--el-color-primary) inset;
  331. }
  332. .el-upload-dragger{
  333. height:100%;
  334. width: 100%;
  335. background-color: transparent;
  336. padding: 10px;
  337. text-align: left;
  338. border:none;
  339. }
  340. }
  341. </style>