| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491 |
- import Teleport from '@/components/Teleport'
- import * as TextboxConfig from './module/TextboxConfig'
- import fabric from "../../js/fabric-adapter";
- import {markRaw,reactive} from "vue";
- // 将默认的 clipSettings 提取为常量
- const DEFAULT_CLIP_SETTINGS = {
- shape: '',
- width: 100,
- height: 100,
- radius: 50,
- offsetX: 50,
- offsetY: 50,
- showClipStroke: true,
- strokeColor: '#000000',
- strokeWidth: 1,
- rectRadius: 0,
- svgUrl: '',
- svgWidth: 100,
- svgHeight: 100,
- };
- export default {
- components: {
- Teleport,
- },
- data() {
- return {
- TextboxConfig:TextboxConfig,
- options:TextboxConfig.fontFamily,
- fontFamilyStyle:"",
- opacityValue: 1,
- layerState: {
- fontSize: 16,
- lineHeight: 1.2,
- charSpacing: 0,
- fill: '#000000',
- textAlign: 'left',
- },
- shadowText:{
- x:0,
- y:0,
- vague:0,
- color:'#000',
- },
- clipSettings: {
- ...DEFAULT_CLIP_SETTINGS,
- },
- strokeObj: null // 用于引用当前描边图形
- }
- },
- props: {
- },
- computed: {
- editLayer(){
- let object = {}
- if(this.selected && this.selected.length === 1){
- object = this.selected[0]
- }
- // return reactive(object)
- return object
- },
- fontFamily(){
- let obj = {}
- this.TextboxConfig.fontFamily.map(item=>{
- obj[item.name] = item
- })
- return obj
- },
- shadow(){
- let shadowText = this.shadowText
- return `${shadowText.x}px ${shadowText.y}px ${shadowText.vague}px ${shadowText.color} `
- }
- },
- watch: {
- editLayer(){
- this.fontFamilyStyle = this.editLayer.fontFamily
- this.opacityValue = (this.editLayer && typeof this.editLayer.opacity === 'number')
- ? this.editLayer.opacity
- : 1
- this.layerState = {
- ...this.layerState,
- fontSize: this.editLayer?.fontSize ?? 16,
- lineHeight: this.editLayer?.lineHeight ?? 1,
- charSpacing: this.editLayer?.charSpacing ?? 0,
- fill: this.editLayer?.fill ?? '#000000',
- textAlign: this.editLayer?.textAlign ?? 'left',
- }
- switch (this.editLayer.type){
- case "textbox":
- if(this.editLayer.shadow){
- this.shadowText = {
- x:this.editLayer.shadow.offsetX || 0,
- y:this.editLayer.shadow.offsetY || 0,
- vague:this.editLayer.shadow.blur || 0,
- color:this.editLayer.shadow.color || '#000',
- }
- }else{
- this.shadowText = {
- x:0,
- y:0,
- vague:0,
- color:'#000'
- }
- }
- break;
- case "image":
- // 优先使用默认值
- let imageSettings = { ...DEFAULT_CLIP_SETTINGS };
- // 若已有 clipPath,合并其中的属性
- if (this.editLayer.clipPath) {
- imageSettings = {
- ...imageSettings,
- width: this.editLayer.clipPath.width || imageSettings.width,
- height: this.editLayer.clipPath.height || imageSettings.height,
- offsetX: this.editLayer.clipPath.left || imageSettings.offsetX,
- offsetY: this.editLayer.clipPath.top || imageSettings.offsetY,
- shape: this.editLayer.clipPath.type || imageSettings.shape,
- };
- }
- // 如果没有剪裁路径,则 shape 为 ""
- if (!this.editLayer.clipPath) {
- imageSettings.shape = '';
- }
- // 更新 clipSettings,用于 UI 显示
- this.clipSettings = imageSettings;
- // 同步描边图形(如果存在)
- if (this.editLayer.strokeObj) {
- this.fcanvas.add(markRaw(this.editLayer.strokeObj));
- }
- break;
- }
- },
- selected(){
- this.opacityValue = (this.editLayer && typeof this.editLayer.opacity === 'number')
- ? this.editLayer.opacity
- : 1
- this.layerState = {
- ...this.layerState,
- fontSize: this.editLayer?.fontSize ?? 16,
- lineHeight: this.editLayer?.lineHeight ?? 1,
- charSpacing: this.editLayer?.charSpacing ?? 0,
- fill: this.editLayer?.fill ?? '#000000',
- textAlign: this.editLayer?.textAlign ?? 'left',
- }
- }
- },
- created() {
- },
- mounted() {
- },
- methods:{
- judge(){
- return this.editLayer?.id
- },
- //翻转
- Flip(type){
- if(!this.judge()) return;
- switch (type){
- case 'X':
- this.editLayer.set({
- flipX:!this.editLayer.flipX,
- })
- break;
- case 'Y':
- this.editLayer.set({
- flipY:!this.editLayer.flipY,
- })
- break;
- }
- this.updateCanvasState()
- this.fcanvas.requestRenderAll()
- },
- //合并组
- setGroup(){
- const group = this.fcanvas.getActiveObject().toGroup();
- this.fcanvas.requestRenderAll();
- this.selected = [group]
- if(this.getLayers) this.getLayers()
- this.updateCanvasState()
- },
- //取消组
- unGroup(){
- if (!this.fcanvas.getActiveObject()) {
- return;
- }
- if (this.fcanvas.getActiveObject().type !== 'group') {
- return;
- }
- this.fcanvas.getActiveObject().toActiveSelection();
- this.selected = this.fcanvas.getActiveObject()._objects
- this.fcanvas.requestRenderAll();
- if(this.getLayers) this.getLayers()
- this.updateCanvasState()
- },
- //编辑文字
- editObj(data = {
- label:'',
- value:'',
- action:'set',
- }){
- if(!this.judge()) return;
- let {label,value,action } = data
- action = action || 'set'
- // 构造待设置的参数;仅当 label 为 fill 且当前无填充时兜底 #000
- let params = value
- if (label) {
- params = { [label]: value }
- if (label === 'fill' && !this.editLayer.fill) {
- params.fill = value || '#000'
- }
- }
- this.editLayer[action](params)
- if (Array.isArray(this.editLayer._objects)){
- this.editLayer._objects?.forEach(item=>{
- item[action](params)
- })
- }
- // 同步常用文本状态到 layerState,避免 UI 视图不同步
- if (['fontSize','lineHeight','charSpacing','fill','textAlign'].includes(label)) {
- this.layerState = {
- ...this.layerState,
- [label]: value,
- }
- }
- this.updateCanvasState()
- this.fcanvas.requestRenderAll()
- },
- // 通用设置图层属性,解决 markRaw 不响应问题
- setLayerAttr(name, val){
- if(!this.editLayer) return
- this.layerState = {
- ...this.layerState,
- [name]: val,
- }
- this.editLayer.set(name, val)
- if(Array.isArray(this.editLayer._objects)){
- this.editLayer._objects.forEach(o => o.set(name, val))
- }
- this.updateCanvasState()
- this.fcanvas && this.fcanvas.requestRenderAll()
- },
- // 设置透明度(解决 Vue3 markRaw 不响应)
- setOpacity(val){
- if(!this.editLayer) return
- const num = Number(val)
- this.opacityValue = num
- this.editLayer.set('opacity', num)
- if(Array.isArray(this.editLayer._objects)){
- this.editLayer._objects.forEach(o => o.set('opacity', num))
- }
- this.updateCanvasState()
- this.fcanvas && this.fcanvas.requestRenderAll()
- },
- async setFontFamily(val,item=null){
- console.log(val)
- console.log(item)
- console.log(this.fontFamily)
- if(!item && !this.judge()) return;
- let font = this.fontFamily[val]
- console.log(font)
- if(!font) return;
- await loadFont(font.name,font.src)
- let obj = item || this.editLayer
- obj.set({'fontFamily': val});
- obj.setSelectionStyles({'fontFamily': val});
- this.fontFamilyStyle = val
- this.updateCanvasState()
- await this.$nextTick()
- this.fcanvas.requestRenderAll()
- async function loadFont(_fontName, _fontUrl) {
- return new Promise((resolve, reject) => {
- if (checkFont(_fontName)) {
- console.log('已有字体:', _fontName)
- return resolve(true)
- }
- let prefont = new FontFace(
- _fontName,
- 'url(' + _fontUrl + ')'
- );
- prefont.load().then(function (loaded_face) {
- document.fonts.add(loaded_face);
- // document.body.style.fontFamily = (_fontFamily ? _fontFamily + ',' : _fontFamily) + _fontName;
- console.log('字体加载成功', loaded_face, document.fonts)
- return resolve(true)
- }).catch(function (error) {
- console.log('字体加载失败', error)
- return reject(error)
- })
- })
- }
- function checkFont(name){
- let values = document.fonts.values();
- let isHave=false;
- let item = values.next();
- while(!item.done&&!isHave)
- {
- let fontFace=item.value;
- if(fontFace.family==name)
- {
- isHave=true;
- }
- item=values.next();
- }
- return isHave;
- }
- },
- remoteMethod(query){
- if (query !== '') {
- this.options = this.TextboxConfig.fontFamily.filter(item => {
- return item.localFile.toLowerCase()
- .indexOf(query.toLowerCase()) > -1;
- });
- } else {
- this.options = this.TextboxConfig.fontFamily
- }
- },
- async applyClipPath() {
- if (!this.judge()) return;
- const { shape, width, height, radius, offsetX, offsetY, rectRadius } = this.clipSettings;
- if (shape === '') {
- // 选择“无”时,直接清除剪裁
- this.clearClipPath();
- return;
- }
- try {
- // 创建纯剪裁区域(不带描边)
- let clipObj;
- if (shape === 'svg') {
- // 使用 fabric.js 加载远程 SVG
- clipObj = await new Promise((resolve, reject) => {
- fabric.loadSVGFromURL(
- this.clipSettings.svgUrl,
- (loadedObjects, options) => {
- console.log('=============');
- // 创建组合对象
- const svgGroup = fabric.util.groupSVGElements(loadedObjects, options);
- console.log(svgGroup);
- // 设置尺寸
- svgGroup.scaleToWidth(this.clipSettings.svgWidth);
- console.log(this.clipSettings.svgWidth);
- // svgGroup.scaleToHeight(this.clipSettings.svgHeight);
- svgGroup.set({
- absolutePositioned: true,
- left: offsetX,
- top: offsetY,
- })
- resolve(svgGroup);
- }
- )
- });
- }else if (shape === 'rect') {
- if (width <= 0 || height <= 0) throw new Error('尺寸必须大于0');
- clipObj = new fabric.Rect({
- width: width,
- height: height,
- left: offsetX,
- top: offsetY,
- rx: rectRadius,
- ry: rectRadius,
- absolutePositioned: true
- });
- } else if(shape === 'circle') {
- if (radius <= 0) throw new Error('半径必须大于0');
- clipObj = new fabric.Circle({
- radius: radius,
- left: offsetX,
- top: offsetY,
- absolutePositioned: true
- });
- }
- // 应用剪裁区域
- this.editLayer.set({ clipPath: clipObj });
- // 更新画布
- this.updateClipStroke()
- this.updateCanvasState();
- this.fcanvas.requestRenderAll();
- } catch (error) {
- console.error('剪裁区域创建失败:', error);
- this.$message.error('剪裁设置错误: ' + error.message);
- }
- },
- updateClipStroke() {
- if (!this.judge() || !this.clipSettings.showClipStroke) return;
- return;;
- const { shape, width, height, radius, offsetX, offsetY, strokeColor, strokeWidth, rectRadius } = this.clipSettings;
- // 移除当前对象已有的描边图形(如果存在)
- if (this.editLayer.strokeObj) {
- this.fcanvas.remove(this.editLayer.strokeObj);
- this.editLayer.strokeObj = null;
- }
- // 创建新的描边图形
- let strokeObj;
- if (shape === 'rect') {
- strokeObj = new fabric.Rect({
- width: width,
- height: height,
- left: offsetX,
- top: offsetY,
- rx: rectRadius,
- ry: rectRadius,
- stroke: strokeColor,
- sort:this.editLayer.sort,
- strokeWidth: strokeWidth,
- fill: null,
- selectable: false,
- evented: false
- });
- } else {
- strokeObj = new fabric.Circle({
- radius: radius,
- left: offsetX,
- top: offsetY,
- sort:this.editLayer.sort,
- stroke: strokeColor,
- strokeWidth: strokeWidth,
- fill: null,
- selectable: false,
- evented: false
- });
- }
- // 将描边图形绑定到当前对象
- this.editLayer.strokeObj = strokeObj;
- this.fcanvas.add(markRaw(strokeObj));
- this.fcanvas.requestRenderAll();
- },
- removeClipStroke() {
- if (this.clipStrokeObject) {
- this.fcanvas.remove(this.clipStrokeObject);
- this.clipStrokeObject = null;
- }
- },
- clearClipPath() {
- if (!this.judge()) return;
- // 清除剪裁路径
- this.editLayer.set({ clipPath: null });
- // 移除描边图形(如果存在)
- if (this.editLayer.strokeObj) {
- this.fcanvas.remove(this.editLayer.strokeObj);
- this.editLayer.strokeObj = null;
- }
- // 更新画布状态
- this.updateCanvasState();
- this.fcanvas.requestRenderAll();
- }
- }
- }
|