index.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652
  1. <template>
  2. <div class="canvas-design-panel">
  3. <div class="mb-10">
  4. <b>客户可编辑项</b>
  5. </div>
  6. <div class="mb-10">
  7. <el-checkbox-group v-model="backgroundPerms" size="small">
  8. <el-checkbox v-for="perm in selectablePerms" :key="perm.code" :value="perm.code"
  9. @change="backgroundPermChange">
  10. {{ perm.name }}
  11. </el-checkbox>
  12. </el-checkbox-group>
  13. </div>
  14. <el-divider style="margin: 12px 0"/>
  15. <div class="mb-10">
  16. <b>{{ t("style.canvasSize") }}</b>
  17. </div>
  18. <div class="mb-10">
  19. <el-row class="size-row">
  20. <el-col :span="11">
  21. <el-input
  22. v-model="canvasWidth"
  23. :value="pageSizeWidth"
  24. @change="changeTemplateWidth"
  25. oninput="value=value.replace(/[^\d.]/g,'')"
  26. >
  27. <template #prepend>{{ t("style.w") }}</template>
  28. </el-input>
  29. </el-col>
  30. <el-col :span="2" class="fixed-ratio">
  31. <el-tooltip
  32. effect="dark"
  33. placement="top"
  34. :content="t('style.unlockAspectRatio')"
  35. v-if="isFixed"
  36. >
  37. <IconLock class="icon-btn" @click="changeFixedRatio(false)"/>
  38. </el-tooltip>
  39. <el-tooltip
  40. effect="dark"
  41. placement="top"
  42. :content="t('style.lockAspectRatio')"
  43. v-else
  44. >
  45. <IconUnlock class="icon-btn" @click="changeFixedRatio(true)"/>
  46. </el-tooltip>
  47. </el-col>
  48. <el-col :span="11">
  49. <el-input
  50. v-model="canvasHeight"
  51. :value="pageSizeHeight"
  52. @change="changeTemplateHeight"
  53. oninput="value=value.replace(/[^\d.]/g,'')"
  54. >
  55. <template #prepend>{{ t("style.h") }}</template>
  56. </el-input>
  57. </el-col>
  58. </el-row>
  59. </div>
  60. <!-- <div class="mb-10">-->
  61. <!-- <el-row>-->
  62. <!-- <el-col :span="11">-->
  63. <!-- <el-input-->
  64. <!-- v-model="clip"-->
  65. <!-- @change="changeTemplateClip"-->
  66. <!-- oninput="value=value.replace(/[^\d]/g,'')"-->
  67. <!-- :disabled="unitMode === 1"-->
  68. <!-- >-->
  69. <!-- <template #prepend>-->
  70. <!-- <el-tooltip-->
  71. <!-- placement="top"-->
  72. <!-- :hide-after="0"-->
  73. <!-- :content="t('style.bleedingLine')"-->
  74. <!-- >-->
  75. <!-- <IconCuttingOne/>-->
  76. <!-- </el-tooltip>-->
  77. <!-- </template>-->
  78. <!-- </el-input>-->
  79. <!-- </el-col>-->
  80. <!-- <el-col :span="2" class="fixed-ratio">-->
  81. <!-- <el-tooltip-->
  82. <!-- effect="dark"-->
  83. <!-- placement="top"-->
  84. <!-- :content="t('style.fillet')"-->
  85. <!-- v-if="isRound"-->
  86. <!-- >-->
  87. <!-- <IconRound class="icon-btn" @click="changeWorkRound(false)"/>-->
  88. <!-- </el-tooltip>-->
  89. <!-- <el-tooltip-->
  90. <!-- effect="dark"-->
  91. <!-- placement="top"-->
  92. <!-- :content="t('style.rightAngle')"-->
  93. <!-- v-else-->
  94. <!-- >-->
  95. <!-- <IconRightAngle class="icon-btn" @click="changeWorkRound(true)"/>-->
  96. <!-- </el-tooltip>-->
  97. <!-- </el-col>-->
  98. <!-- <el-col :span="11">-->
  99. <!-- <el-input-->
  100. <!-- v-model="safe"-->
  101. <!-- @change="changeTemplateSafe"-->
  102. <!-- oninput="value=value.replace(/[^\d]/g,'')"-->
  103. <!-- :disabled="unitMode === 1"-->
  104. <!-- >-->
  105. <!-- <template #prepend>-->
  106. <!-- <el-tooltip-->
  107. <!-- placement="top"-->
  108. <!-- :hide-after="0"-->
  109. <!-- :content="t('style.safetyLine')"-->
  110. <!-- >-->
  111. <!-- <IconShield/>-->
  112. <!-- </el-tooltip>-->
  113. <!-- </template>-->
  114. <!-- </el-input>-->
  115. <!-- </el-col>-->
  116. <!-- </el-row>-->
  117. <!-- </div>-->
  118. <!-- <div class="mt-10">-->
  119. <!-- <el-row>-->
  120. <!-- <el-col :span="11">-->
  121. <!-- <el-select v-model="unitMode" @change="changeUnitMode">-->
  122. <!-- <template #prefix>-->
  123. <!-- <el-tooltip-->
  124. <!-- placement="top"-->
  125. <!-- :hide-after="0"-->
  126. <!-- :content="t('style.unit')"-->
  127. <!-- >-->
  128. <!-- <IconRuler/>-->
  129. <!-- </el-tooltip>-->
  130. <!-- </template>-->
  131. <!-- <el-option-->
  132. <!-- v-for="item in DesignUnitMode"-->
  133. <!-- :key="item.id"-->
  134. <!-- :label="item.name"-->
  135. <!-- :value="item.id"-->
  136. <!-- ></el-option>-->
  137. <!-- </el-select>-->
  138. <!-- </el-col>-->
  139. <!-- <el-col :span="2"></el-col>-->
  140. <!-- &lt;!&ndash; <el-col :span="11">&ndash;&gt;-->
  141. <!-- &lt;!&ndash; <el-select v-model="sizeMode">&ndash;&gt;-->
  142. <!-- &lt;!&ndash; <template #prefix>&ndash;&gt;-->
  143. <!-- &lt;!&ndash; <el-tooltip&ndash;&gt;-->
  144. <!-- &lt;!&ndash; placement="top"&ndash;&gt;-->
  145. <!-- &lt;!&ndash; :hide-after="0"&ndash;&gt;-->
  146. <!-- &lt;!&ndash; :content="t('style.template')"&ndash;&gt;-->
  147. <!-- &lt;!&ndash; >&ndash;&gt;-->
  148. <!-- &lt;!&ndash; <IconIdCard />&ndash;&gt;-->
  149. <!-- &lt;!&ndash; </el-tooltip>&ndash;&gt;-->
  150. <!-- &lt;!&ndash; </template>&ndash;&gt;-->
  151. <!-- &lt;!&ndash; <el-option&ndash;&gt;-->
  152. <!-- &lt;!&ndash; v-for="item in DesignSizeMode"&ndash;&gt;-->
  153. <!-- &lt;!&ndash; :key="item.id"&ndash;&gt;-->
  154. <!-- &lt;!&ndash; :label="item.name"&ndash;&gt;-->
  155. <!-- &lt;!&ndash; :value="item.id"&ndash;&gt;-->
  156. <!-- &lt;!&ndash; :disabled="item.disabled"&ndash;&gt;-->
  157. <!-- &lt;!&ndash; ></el-option>&ndash;&gt;-->
  158. <!-- &lt;!&ndash; </el-select>&ndash;&gt;-->
  159. <!-- &lt;!&ndash; </el-col>&ndash;&gt;-->
  160. <!-- </el-row>-->
  161. <!-- </div>-->
  162. <el-divider style="margin: 12px 0"/>
  163. <div class="title">
  164. <b>{{ t("style.canvasFill") }}</b>
  165. </div>
  166. <!-- <div class="row">-->
  167. <!-- <el-button class="full-row" @click="changeAllBackgroud">{{ t("style.applyCanvasToAll") }}</el-button>-->
  168. <!-- </div>-->
  169. <Backgrounds/>
  170. <el-divider style="margin: 12px 0"/>
  171. <!-- <Watermark/>-->
  172. <!-- <el-divider style="margin: 12px 0"/>-->
  173. <!-- <div class="title">-->
  174. <!-- <b>{{ t("style.canvasMask") }}</b>-->
  175. <!-- </div>-->
  176. <!-- <el-row>-->
  177. <!-- <el-col :span="7" class="slider-name">{{ t("style.opacity") }}:</el-col>-->
  178. <!-- <el-col :span="13">-->
  179. <!-- <el-slider :min="0.1" :max="1" :step="0.01" v-model="opacity" @change="changeMaskOpacity"></el-slider>-->
  180. <!-- </el-col>-->
  181. <!-- <el-col :span="4" class="slider-num">{{ opacity }}</el-col>-->
  182. <!-- </el-row>-->
  183. </div>
  184. </template>
  185. <script lang="ts" setup>
  186. import {Rect} from "fabric";
  187. import {storeToRefs} from "pinia";
  188. import {ElMessage} from "element-plus";
  189. import {ref, watch, onMounted, computed, nextTick} from "vue";
  190. import {mm2px, px2mm} from "@/utils/image";
  191. import useI18n from "@/hooks/useI18n";
  192. import {useFabricStore, useMainStore, useTemplatesStore} from "@/store";
  193. import {
  194. WorkSpaceClipType,
  195. WorkSpaceDrawType,
  196. WorkSpaceMaskType,
  197. } from "@/configs/canvas";
  198. import {
  199. DesignUnitMode,
  200. DesignSizeMode,
  201. MinSize,
  202. MaxSize,
  203. } from "@/configs/background";
  204. import useCanvas from "@/views/Canvas/useCanvas";
  205. import Backgrounds from "../Backgrounds/index.vue";
  206. import Watermark from "./Watermark/index.vue";
  207. import useHistorySnapshot from "@/hooks/useHistorySnapshot";
  208. import useCanvasScale from '@/hooks/useCanvasScale'
  209. import {WorkSpaceElement} from "@/types/canvas";
  210. import {GradientColorLibs} from "@/configs/colorGradient";
  211. const {t} = useI18n();
  212. const mainStore = useMainStore();
  213. const templatesStore = useTemplatesStore();
  214. const fabricStore = useFabricStore();
  215. const {addHistorySnapshot} = useHistorySnapshot();
  216. const {sizeMode, unitMode} = storeToRefs(mainStore);
  217. const {currentTemplate} = storeToRefs(templatesStore);
  218. const {clip, safe, zoom, opacity} = storeToRefs(fabricStore);
  219. const {setCanvasSize, resetCanvas} = useCanvasScale()
  220. const pageSizeWidth = computed(() => {
  221. return Math.round(templateWidth.value * 100) / 100
  222. })
  223. const pageSizeHeight = computed(() => {
  224. return Math.round(templateHeight.value * 100) / 100
  225. })
  226. const templateWidth = computed(() => {
  227. // const [ canvas ] = useCanvas()
  228. // if (!canvas) return 0
  229. const workWidth = currentTemplate.value.width / currentTemplate.value.zoom;
  230. return unitMode.value === 0 ? px2mm(workWidth) : workWidth;
  231. });
  232. const templateHeight = computed(() => {
  233. // const [ canvas ] = useCanvas()
  234. // if (!canvas) return 0
  235. const workHeight = currentTemplate.value.height / currentTemplate.value.zoom;
  236. return unitMode.value === 0 ? px2mm(workHeight) : workHeight;
  237. });
  238. // const canvasWidth = ref<number>(px2mm(currentTemplate.value.width / currentTemplate.value.zoom))
  239. const canvasWidth = ref<number>(templateWidth.value);
  240. const canvasHeight = ref<number>(templateHeight.value);
  241. const backgroundPerms = ref([]);
  242. const selectablePerms = [
  243. {
  244. "name": "背景颜色",
  245. "code": "0"
  246. },
  247. {
  248. "name": "背景尺寸",
  249. "code": "1"
  250. },
  251. // {
  252. // "name": "上传背景",
  253. // "code": "2"
  254. // }
  255. ]
  256. // 固定宽高
  257. const isFixed = ref(false);
  258. // 直角圆角
  259. const isRound = ref(false);
  260. // 网格 预定义 参数
  261. const RECENT_GRIDS = "RECENT_GRIDS";
  262. const gridColorRecent = ref<[string[]]>([[]]);
  263. //修改背景的可编辑参数
  264. const backgroundPermChange = () => {
  265. const [canvas] = useCanvas();
  266. canvas.getObjects()
  267. .filter((item) => item.id === WorkSpaceDrawType).forEach(workSpaceDraw => {
  268. workSpaceDraw.set({permissionsConfig: backgroundPerms.value})
  269. })
  270. }
  271. // 获取画布尺寸
  272. const getCanvasSize = () => {
  273. let width =
  274. unitMode.value === 0 ? mm2px(canvasWidth.value) : canvasWidth.value;
  275. let height =
  276. unitMode.value === 0 ? mm2px(canvasHeight.value) : canvasHeight.value;
  277. width = width * zoom.value;
  278. height = height * zoom.value;
  279. return {width, height};
  280. };
  281. // 修改画布宽度
  282. const changeTemplateWidth = () => {
  283. const [canvas] = useCanvas();
  284. const workSpaceDraw = canvas
  285. .getObjects()
  286. .filter((item) => item.id === WorkSpaceDrawType)[0];
  287. if (!workSpaceDraw) return;
  288. const ratio = currentTemplate.value.height / currentTemplate.value.width;
  289. let {width, height} = getCanvasSize();
  290. if (width / zoom.value < mm2px(MinSize)) {
  291. ElMessage({
  292. message: t("style.minimumSizeLimit") + MinSize,
  293. type: "warning",
  294. });
  295. width = mm2px(MinSize) * zoom.value;
  296. }
  297. if (width / zoom.value > mm2px(MaxSize)) {
  298. ElMessage({
  299. message: t("style.maximumSizeLimit") + MaxSize,
  300. type: "warning",
  301. });
  302. width = mm2px(MaxSize) * zoom.value;
  303. }
  304. height = isFixed.value ? width * ratio : height;
  305. workSpaceDraw.set({width: width / zoom.value, height: height / zoom.value});
  306. templatesStore.setSize(width, height, zoom.value);
  307. sizeMode.value = 2;
  308. canvas.renderAll();
  309. // resetCanvas()
  310. addHistorySnapshot();
  311. };
  312. // 修改画布高度
  313. const changeTemplateHeight = () => {
  314. const [canvas] = useCanvas();
  315. const workSpaceDraw = canvas
  316. .getObjects()
  317. .filter((item) => item.id === WorkSpaceDrawType)[0];
  318. if (!workSpaceDraw) return;
  319. const ratio = currentTemplate.value.height / currentTemplate.value.width;
  320. let {width, height} = getCanvasSize();
  321. if (height / zoom.value < mm2px(MinSize)) {
  322. ElMessage({
  323. message: t("style.minimumSizeLimit") + MinSize,
  324. type: "warning",
  325. });
  326. height = mm2px(MinSize) * zoom.value;
  327. }
  328. if (height / zoom.value > mm2px(MaxSize)) {
  329. ElMessage({
  330. message: t("style.maximumSizeLimit") + MaxSize,
  331. type: "warning",
  332. });
  333. height = mm2px(MaxSize) * zoom.value;
  334. }
  335. width = isFixed.value ? height / ratio : width;
  336. workSpaceDraw.set({width: width / zoom.value, height: height / zoom.value});
  337. templatesStore.setSize(width, height, zoom.value);
  338. sizeMode.value = 2;
  339. canvas.renderAll();
  340. // resetCanvas()
  341. addHistorySnapshot();
  342. };
  343. // 修改出血尺寸
  344. const changeTemplateClip = async () => {
  345. templatesStore.setClip(clip.value);
  346. await templatesStore.renderTemplate();
  347. };
  348. // 修改安全尺寸
  349. const changeTemplateSafe = async () => {
  350. safe.value = Number(safe.value);
  351. await templatesStore.renderTemplate();
  352. };
  353. // 修改固定宽高比
  354. const changeFixedRatio = (fixedStatus: boolean) => {
  355. isFixed.value = fixedStatus;
  356. };
  357. // 修改直角圆角
  358. const changeWorkRound = (roundStatus: boolean) => {
  359. const [canvas] = useCanvas();
  360. const workSpaceclip = canvas
  361. .getObjects()
  362. .filter(
  363. (item) => WorkSpaceClipType === item.id && item.isType("Rect")
  364. )[0] as Rect;
  365. let rx = 0,
  366. ry = 0;
  367. isRound.value = roundStatus;
  368. if (isRound.value) rx = ry = 10;
  369. workSpaceclip.set({rx, ry});
  370. canvas.renderAll();
  371. };
  372. // 修改尺寸单位
  373. const changeUnitMode = async () => {
  374. const width = currentTemplate.value.width / currentTemplate.value.zoom;
  375. const heigth = currentTemplate.value.height / currentTemplate.value.zoom;
  376. if (unitMode.value === 0) {
  377. canvasWidth.value = px2mm(width);
  378. canvasHeight.value = px2mm(heigth);
  379. clip.value = 2;
  380. safe.value = 3;
  381. } else {
  382. canvasWidth.value = width;
  383. canvasHeight.value = heigth;
  384. clip.value = safe.value = 0;
  385. }
  386. await changeTemplateClip();
  387. await changeTemplateSafe();
  388. };
  389. // 应用背景到所有页面
  390. const changeAllBackgroud = () => {
  391. templatesStore.templates.forEach((item) => {
  392. item.workSpace = currentTemplate.value.workSpace;
  393. const currentWorkSpace = currentTemplate.value.objects.filter(
  394. (ele) => ele.id === WorkSpaceDrawType
  395. )[0];
  396. item.objects = item.objects.map((ele) =>
  397. ele.id === WorkSpaceDrawType ? currentWorkSpace : ele
  398. ) as any;
  399. });
  400. };
  401. // 加载缓存最近添加的网格
  402. onMounted(() => {
  403. const recentGridCache = localStorage.getItem(RECENT_GRIDS);
  404. if (recentGridCache) gridColorRecent.value = JSON.parse(recentGridCache);
  405. const [canvas] = useCanvas();
  406. const workSpaceDraw = canvas
  407. .getObjects()
  408. .filter((item) => item.id === WorkSpaceDrawType)[0];
  409. if (!workSpaceDraw) return;
  410. if (workSpaceDraw.get('permissionsConfig'))
  411. backgroundPerms.value = workSpaceDraw.get('permissionsConfig')
  412. });
  413. // 保存缓存最近添加的网格
  414. watch(
  415. gridColorRecent,
  416. () => {
  417. const recentGridCache = JSON.stringify(gridColorRecent.value);
  418. localStorage.setItem(RECENT_GRIDS, recentGridCache);
  419. },
  420. {deep: true}
  421. );
  422. const changeMaskOpacity = () => {
  423. const [canvas] = useCanvas();
  424. const workMask = canvas
  425. .getObjects()
  426. .filter((ele) => ele.id === WorkSpaceMaskType)[0];
  427. if (!workMask) return;
  428. workMask.set("opacity", opacity.value);
  429. canvas.renderAll();
  430. };
  431. </script>
  432. <style lang="scss" scoped>
  433. .icon-btn {
  434. cursor: pointer;
  435. }
  436. .canvas-design-panel {
  437. user-select: none;
  438. }
  439. .row {
  440. width: 100%;
  441. display: flex;
  442. align-items: center;
  443. margin-bottom: 10px;
  444. }
  445. .title {
  446. margin-bottom: 10px;
  447. }
  448. .fixed-ratio {
  449. display: flex;
  450. flex-direction: column;
  451. align-items: center;
  452. justify-content: center;
  453. }
  454. .slider-name {
  455. display: flex;
  456. align-items: center;
  457. }
  458. .mb-10 {
  459. margin-bottom: 10px;
  460. }
  461. .full-row {
  462. flex: 1;
  463. width: 100%;
  464. }
  465. .full-group {
  466. display: flex;
  467. flex: 1;
  468. .el-button {
  469. width: 50%;
  470. }
  471. }
  472. .full-ratio {
  473. display: flex;
  474. flex: 1;
  475. .el-radio-button {
  476. width: 50%;
  477. }
  478. .el-radio-button__inner {
  479. width: 100%;
  480. }
  481. }
  482. .background-image {
  483. height: 0;
  484. padding-bottom: 56.25%;
  485. border: 1px dashed var(--el-border-color);
  486. border-radius: $borderRadius;
  487. position: relative;
  488. transition: all $transitionDelay;
  489. &:hover {
  490. border-color: var(--el-color-primary);
  491. color: var(--el-color-primary);
  492. }
  493. .content {
  494. @include absolute-0();
  495. display: flex;
  496. justify-content: center;
  497. align-items: center;
  498. background-position: center;
  499. background-size: contain;
  500. background-repeat: no-repeat;
  501. cursor: pointer;
  502. }
  503. }
  504. .theme-list {
  505. @include flex-grid-layout();
  506. }
  507. .theme-item {
  508. @include flex-grid-layout-children(2, 48%);
  509. padding-bottom: 30%;
  510. border-radius: $borderRadius;
  511. position: relative;
  512. cursor: pointer;
  513. .theme-item-content {
  514. @include absolute-0();
  515. display: flex;
  516. flex-direction: column;
  517. justify-content: center;
  518. padding: 8px;
  519. border: 1px solid $borderColor;
  520. }
  521. .text {
  522. font-size: 16px;
  523. }
  524. .colors {
  525. display: flex;
  526. }
  527. .color-block {
  528. margin-top: 8px;
  529. width: 12px;
  530. height: 12px;
  531. margin-right: 2px;
  532. }
  533. &:hover .btns {
  534. display: flex;
  535. }
  536. .btns {
  537. @include absolute-0();
  538. flex-direction: column;
  539. justify-content: center;
  540. align-items: center;
  541. display: none;
  542. background-color: rgba($color: #000, $alpha: 0.25);
  543. }
  544. .btn {
  545. width: 72px;
  546. padding: 5px 0;
  547. text-align: center;
  548. background-color: $themeColor;
  549. color: #fff;
  550. font-size: 12px;
  551. border-radius: $borderRadius;
  552. &:hover {
  553. background-color: #c42f19;
  554. }
  555. & + .btn {
  556. margin-top: 5px;
  557. }
  558. }
  559. }
  560. .mt-10 {
  561. margin-top: 10px;
  562. }
  563. .slider-num {
  564. display: flex;
  565. align-items: center;
  566. justify-content: center;
  567. }
  568. </style>
  569. <style scoped>
  570. :deep(.el-input .el-input-group__prepend) {
  571. padding: 0 5px;
  572. }
  573. :deep(.el-input .el-input-group__append) {
  574. padding: 0 5px;
  575. }
  576. :deep(.full-ratio .el-radio-button__inner) {
  577. width: 100%;
  578. }
  579. :deep(.size-row .el-input-group__prepend) {
  580. min-width: 24px;
  581. }
  582. </style>