index.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  1. <script setup>
  2. import { onMounted, ref } from 'vue'
  3. import { setPageSetting } from '@/api/sys/pageSetting'
  4. import { ElMessage, ElMessageBox } from 'element-plus'
  5. import { usePageSettingStore } from '@/pinia/modules/pageSetting'
  6. import 'vue-cropper/dist/index.css'
  7. import { VueCropper } from 'vue-cropper'
  8. import { resolvePageLogo } from '@/utils/resolvePageLogo'
  9. defineOptions({
  10. name: 'MaskPageSetting'
  11. })
  12. const pageSettingStore = usePageSettingStore()
  13. const formRef = ref(null)
  14. const form = ref({
  15. name: '',
  16. logo: '',
  17. isLoaded: false
  18. })
  19. const rules = ref({
  20. name: [{ min: 2, max: 10, message: '自定义名称长度为2-10个字符', trigger: 'blur' }]
  21. })
  22. const handleGetPageSetting = async() => {
  23. await pageSettingStore.initPageSetting()
  24. form.value = {
  25. name: pageSettingStore.pageSetting.name || '非结构化数据透明加密',
  26. logo: pageSettingStore.pageSetting.logo || '',
  27. }
  28. option.value.img = resolvePageLogo(form.value.logo)
  29. }
  30. const changeSetting = async() => {
  31. await formRef.value.validate()
  32. const params = {
  33. name: form.value.name,
  34. logo: form.value.logo
  35. }
  36. const res = await setPageSetting(params)
  37. if (res.code === 100000) {
  38. ElMessage.success('修改成功')
  39. // 保存成功后直接同步 store,布局/登录页立即生效
  40. pageSettingStore.setPageSetting(params)
  41. form.value = {
  42. ...form.value,
  43. ...params
  44. }
  45. option.value.img = resolvePageLogo(params.logo)
  46. } else {
  47. ElMessage.error(res.message || res.msg || '修改失败')
  48. }
  49. }
  50. onMounted(() => {
  51. handleGetPageSetting()
  52. })
  53. const dialogImageVisible = ref(false)
  54. const option = ref({
  55. img: resolvePageLogo(pageSettingStore.pageSetting.logo), // 裁剪图片的地址
  56. outputSize: 1, // 裁剪生成图片的质量(可选0.1 - 1)
  57. outputType: 'jpeg', // 裁剪生成图片的格式(jpeg || png || webp)
  58. info: true, // 图片大小信息
  59. canScale: true, // 图片是否允许滚轮缩放
  60. autoCrop: true, // 是否默认生成截图框
  61. autoCropWidth: 150, // 默认生成截图框宽度
  62. autoCropHeight: 150, // 默认生成截图框高度
  63. fixed: true, // 是否开启截图框宽高固定比例
  64. fixedNumber: [1, 1], // 截图框的宽高比例
  65. full: false, // false按原比例裁切图片,不失真
  66. fixedBox: false, // 固定截图框大小,不允许改变
  67. canMove: true, // 上传图片是否可以移动
  68. canMoveBox: true, // 截图框能否拖动
  69. original: false, // 上传图片按照原始比例渲染
  70. centerBox: false, // 截图框是否被限制在图片里面
  71. height: true, // 是否按照设备的dpr 输出等比例图片
  72. infoTrue: false, // true为展示真实输出图片宽高,false展示看到的截图框宽高
  73. maxImgSize: 3000, // 限制图片最大宽度和高度
  74. enlarge: 1, // 图片根据截图框输出比例倍数
  75. mode: '300px 300px' // 图片默认渲染方式
  76. })
  77. const imgLoad = msg => {
  78. if (msg === 'success') {
  79. isReady.value = true
  80. }
  81. }
  82. const loading = ref(false)
  83. // 实时的刷新
  84. const realTime = () => {
  85. if (loading.value) return
  86. loading.value = true
  87. // 节流
  88. setTimeout(() => {
  89. cropper.value.getCropBlob(data => {
  90. baseUrl.value = URL.createObjectURL(data) // 创建获取Blob对象的URL
  91. loading.value = false
  92. })
  93. }, 500)
  94. }
  95. const cropper = ref(null)
  96. const fileName = ref('')
  97. const isReady = ref(false)
  98. const baseUrl = ref()
  99. // 选择图片
  100. const selectClick = () => {
  101. inputRef.value?.click()
  102. }
  103. // 选择图片
  104. const selectImg = e => {
  105. const eTarget = e.target
  106. if (!eTarget.files) return
  107. const file = eTarget.files[0]
  108. // 确保 fileName 被正确赋值
  109. fileName.value = file.name
  110. if (!/\.(jpg|jpeg|png|JPG|PNG)$/.test(eTarget.value)) {
  111. ElMessage.warning('图片格式错误')
  112. return false
  113. }
  114. const reader = new FileReader()
  115. reader.readAsDataURL(file) // 将文件转化为base64
  116. reader.onload = e => {
  117. let data
  118. if (!e.target) return
  119. if (typeof e.target.result === 'object') {
  120. data = URL.createObjectURL(new Blob([e.target.result ?? '']))
  121. } else {
  122. data = e.target.result
  123. }
  124. option.value.img = data
  125. }
  126. }
  127. const inputRef = ref()
  128. // 向左旋转
  129. const rotateLeft = () => {
  130. cropper.value.rotateLeft()
  131. }
  132. // 向右旋转
  133. const rotateRight = () => {
  134. cropper.value.rotateRight()
  135. }
  136. // 上传图片
  137. const imgUpload = () => {
  138. // 获取截图的 blob 数据
  139. cropper.value.getCropBlob(async data => {
  140. // 上传到服务器
  141. // 获取原始文件类型
  142. const fileType = fileName.value.split('.').pop().toLowerCase()
  143. const mimeType = `image/${fileType === 'jpg' ? 'jpeg' : fileType}`
  144. const file = new File([data], fileName.value, { type: mimeType })
  145. // 校验图片尺寸
  146. const isSizeValid = await validateImageSize(file)
  147. if (!isSizeValid) {
  148. ElMessage.warning('图片尺寸不符合要求,请重新裁剪!')
  149. return
  150. }
  151. // 把图片转成base64
  152. form.value.logo = await new Promise(resolve => {
  153. const reader = new FileReader()
  154. reader.readAsDataURL(file)
  155. reader.onloadend = () => resolve(reader.result)
  156. })
  157. // 重置裁剪组件状态
  158. resetCropper()
  159. dialogImageVisible.value = false
  160. })
  161. }
  162. // 校验图片尺寸的函数
  163. const validateImageSize = file => {
  164. return new Promise(resolve => {
  165. const reader = new FileReader()
  166. reader.onload = e => {
  167. const img = new Image()
  168. img.src = e.target.result
  169. img.onload = () => {
  170. const width = img.width
  171. const height = img.height
  172. // 这里可以根据实际需求设置图片尺寸的限制
  173. const minWidth = 150
  174. const minHeight = 150
  175. const isValid =
  176. width <= minWidth &&
  177. height <= minHeight &&
  178. width / height === minWidth / minHeight
  179. resolve(isValid)
  180. }
  181. }
  182. reader.readAsDataURL(file)
  183. })
  184. }
  185. // 重置裁剪组件状态的函数
  186. const resetCropper = () => {
  187. option.value.img = `${baseUrl.value}`
  188. fileName.value = ''
  189. isReady.value = false
  190. baseUrl.value = ''
  191. // 你还可以根据需要重置其他相关状态
  192. }
  193. const openFileDialog = () => {
  194. dialogImageVisible.value = true
  195. }
  196. const resetSetting = async() => {
  197. const confirm = await ElMessageBox.confirm(
  198. '确认恢复默认设置吗?',
  199. '提示',
  200. {
  201. confirmButtonText: '确定',
  202. cancelButtonText: '取消',
  203. type: 'warning'
  204. }
  205. ).catch(() => {
  206. ElMessage.info('已取消操作')
  207. // 取消操作
  208. })
  209. if (!confirm) return
  210. await pageSettingStore.resetPageSetting()
  211. form.value = {
  212. name: pageSettingStore.pageSetting.name || '非结构化数据透明加密',
  213. logo: pageSettingStore.pageSetting.logo || '',
  214. }
  215. option.value.img = resolvePageLogo(form.value.logo)
  216. }
  217. </script>
  218. <template>
  219. <div >
  220. <div class="p-6 bg-[#fff] space-y-4 h-[calc(100vh-220px)]">
  221. <div class="text-3xl">页面配置</div>
  222. <el-divider class="my-4" />
  223. <el-form
  224. ref="formRef"
  225. :model="form"
  226. :rules="rules"
  227. label-position="left"
  228. >
  229. <el-form-item
  230. label="自定义名称"
  231. prop="name"
  232. class="my-10"
  233. >
  234. <el-input
  235. v-model="form.name"
  236. placeholder="请输入自定义名称"
  237. class="w-1/6 "
  238. />
  239. </el-form-item>
  240. <el-form-item
  241. label="自定义Logo"
  242. prop="logo"
  243. class="form-item-center "
  244. >
  245. <div class="image-container">
  246. <el-image
  247. :src="resolvePageLogo(form.logo)"
  248. class="image-position"
  249. fit="cover"
  250. />
  251. <el-button
  252. v-preReClick
  253. class="button-position"
  254. @click="openFileDialog()"
  255. >修改头像</el-button
  256. >
  257. </div>
  258. </el-form-item>
  259. </el-form>
  260. <div class="mt-10">
  261. <el-button
  262. v-preReClick
  263. type="primary"
  264. @click="changeSetting()"
  265. >修改配置</el-button>
  266. <el-button
  267. v-preReClick
  268. type="primary"
  269. @click="resetSetting()"
  270. >
  271. 恢复默认
  272. </el-button>
  273. </div>
  274. </div>
  275. <el-dialog
  276. v-model="dialogImageVisible"
  277. class="p-[50px]"
  278. width="900"
  279. >
  280. <div class="flex items-center justify-center">
  281. <div class="w-[710px]">
  282. <div class="w-[710px] h-[300px] flex items-center">
  283. <!-- 给组件固定宽高 -->
  284. <div
  285. class="h-[300px] w-[510px]"
  286. style="width: 500px"
  287. >
  288. <VueCropper
  289. ref="cropper"
  290. :auto-crop="option.autoCrop"
  291. :auto-crop-height="option.autoCropHeight"
  292. :auto-crop-width="option.autoCropWidth"
  293. :can-move="option.canMove"
  294. :can-move-box="option.canMoveBox"
  295. :can-scale="option.canScale"
  296. :center-box="option.centerBox"
  297. :enlarge="option.enlarge"
  298. :fixed="option.fixed"
  299. :fixed-box="option.fixedBox"
  300. :fixed-number="option.fixedNumber"
  301. :full="option.full"
  302. :height="option.height"
  303. :img="option.img"
  304. :info="option.info"
  305. :info-true="option.infoTrue"
  306. :max-img-size="option.maxImgSize"
  307. :mode="option.mode"
  308. :original="option.original"
  309. :output-size="option.outputSize"
  310. :output-type="option.outputType"
  311. @imgLoad="imgLoad"
  312. @realTime="realTime"
  313. />
  314. </div>
  315. <div class="ml-[60px]">
  316. <div
  317. class="text-center text-[#409EFF] mb-[20px] text-[20px] font-medium"
  318. >
  319. 预览
  320. </div>
  321. <div class="flex items-center">
  322. <div v-if="isReady">
  323. <el-image
  324. :src="baseUrl"
  325. class="rounded-[50%] w-[150px] h-[150px]"
  326. />
  327. </div>
  328. </div>
  329. </div>
  330. </div>
  331. <div class="flex items-center justify-center mt-[30px]">
  332. <div class="relative">
  333. <el-button
  334. class="mr-[15px]"
  335. plain
  336. type="primary"
  337. @click="selectClick"
  338. >
  339. 选择图片
  340. </el-button>
  341. <input
  342. ref="inputRef"
  343. accept="image/png, image/jpeg, image/gif, image/jpg"
  344. style="position: absolute; clip: rect(0 0 0 0)"
  345. type="file"
  346. @change="selectImg"
  347. >
  348. </div>
  349. <el-button
  350. style="width: 96px"
  351. type="primary"
  352. @click="rotateLeft"
  353. >↺ 左旋转</el-button>
  354. <el-button
  355. style="width: 96px"
  356. type="primary"
  357. @click="rotateRight"
  358. >↻ 右旋转</el-button>
  359. <el-button
  360. plain
  361. type="primary"
  362. @click="imgUpload"
  363. >上传图片</el-button>
  364. </div>
  365. </div>
  366. </div>
  367. </el-dialog>
  368. </div>
  369. </template>
  370. <style scoped lang="scss">
  371. .image-position {
  372. border-radius: 50%;
  373. width: 75px;
  374. height: 75px;
  375. }
  376. /* 让该行整体垂直居中 */
  377. .form-item-center {
  378. display: flex;
  379. align-items: center; /* 垂直居中 label 和内容 */
  380. /* 注意:Element Plus 的 label 宽度可能会影响,这里不改 label-width,仅做垂直对齐 */
  381. gap: 12px;
  382. }
  383. /* 保证 label 本身也是纵向居中(增强兼容性) */
  384. .form-item-center :deep(.el-form-item__label) {
  385. display: flex;
  386. align-items: center;
  387. height: 100%;
  388. /* 可选:居中对齐文本 */
  389. justify-content: center;
  390. padding-top: 0; /* 覆盖默认上内边距 */
  391. }
  392. /* 内容区也用 flex 对齐,方便图片和按钮横向对齐 */
  393. .form-item-center :deep(.el-form-item__content) {
  394. display: flex;
  395. align-items: center;
  396. gap: 10px;
  397. }
  398. /* 限制图片尺寸并确保内部 img cover 填充 */
  399. .image-position {
  400. width: 75px;
  401. height: 75px;
  402. border-radius: 50%;
  403. overflow: hidden;
  404. display: inline-block;
  405. }
  406. /* 因为 el-image 是个包装元素,强制其内部 img 填充容器 */
  407. .form-item-center :deep(.image-position) img {
  408. width: 100%;
  409. height: 100%;
  410. object-fit: cover;
  411. display: block;
  412. }
  413. .button-position {
  414. border-radius: 50%;
  415. width: 75px;
  416. height: 75px;
  417. background-color: rgba(0, 0, 0, 0);
  418. color: rgba(0, 0, 0, 0);
  419. }
  420. .button-position:hover {
  421. border-radius: 50%;
  422. width: 75px;
  423. height: 75px;
  424. background-color: rgba(0, 0, 0, 0.5);
  425. color: #ffffff;
  426. font-weight: bolder;
  427. }
  428. .image-container {
  429. position: relative;
  430. }
  431. .image-container .button-position {
  432. position: absolute;
  433. top: 0;
  434. left: 0;
  435. }
  436. </style>