first commit
This commit is contained in:
8
.env
Normal file
8
.env
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
REACT_SUPABASE_URL=https://base.uppmkt.com
|
||||||
|
REACT_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE
|
||||||
|
|
||||||
|
REACT_SITE_URL=https://limq.upj.to
|
||||||
|
# REACT_API_URL=http://localhost:3005
|
||||||
|
REACT_API_URL=https://limqapi.upj.to
|
||||||
|
|
||||||
|
REACT_ONLY_STANDALONE=false
|
||||||
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
18
manifest.json
Normal file
18
manifest.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "PDF Generator",
|
||||||
|
"id": "your-plugin-id",
|
||||||
|
"api": "1.0.0",
|
||||||
|
"main": "dist/code.js",
|
||||||
|
"ui": "dist/ui.html",
|
||||||
|
"editorType": ["figma"],
|
||||||
|
"networkAccess": {
|
||||||
|
"allowedDomains": [
|
||||||
|
"http://localhost:3000",
|
||||||
|
"http://localhost:3005",
|
||||||
|
"https://base.uppmkt.com",
|
||||||
|
"wss://base.uppmkt.com",
|
||||||
|
"https://limqapi.upj.to"
|
||||||
|
],
|
||||||
|
"reasoning": "This plugin needs to communicate with a local development server to generate PDFs"
|
||||||
|
}
|
||||||
|
}
|
||||||
55
package.json
Normal file
55
package.json
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"name": "figma-pdf-generator",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Figma PDF Generator Plugin",
|
||||||
|
"main": "code.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "webpack --mode=development --watch",
|
||||||
|
"build": "webpack --mode=production"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@ant-design/icons": "^5.4.0",
|
||||||
|
"@dnd-kit/core": "^6.2.0",
|
||||||
|
"@dnd-kit/sortable": "^9.0.0",
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
|
"@fastify/cors": "^10.0.1",
|
||||||
|
"@ffmpeg/ffmpeg": "^0.12.10",
|
||||||
|
"@ffmpeg/util": "^0.12.1",
|
||||||
|
"@supabase/supabase-js": "^2.43.0",
|
||||||
|
"antd": "^5.22.2",
|
||||||
|
"copy-webpack-plugin": "^12.0.2",
|
||||||
|
"css-minimizer-webpack-plugin": "^7.0.0",
|
||||||
|
"dnd-kit": "^0.0.2",
|
||||||
|
"dotenv-webpack": "^8.1.0",
|
||||||
|
"fastify": "^5.1.0",
|
||||||
|
"gifshot": "^0.4.5",
|
||||||
|
"i18next": "^23.15.2",
|
||||||
|
"i18next-browser-languagedetector": "^8.0.2",
|
||||||
|
"jspdf": "^2.5.2",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"nodemon": "^3.1.7",
|
||||||
|
"pdf-lib": "^1.17.1",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-beautiful-dnd": "^13.1.1",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-i18next": "^15.0.2",
|
||||||
|
"uuid": "^11.0.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@figma/plugin-typings": "^1.82.0",
|
||||||
|
"@types/lodash": "^4.17.13",
|
||||||
|
"@types/react": "^18.2.0",
|
||||||
|
"@types/react-beautiful-dnd": "^13.1.8",
|
||||||
|
"@types/react-dom": "^18.2.0",
|
||||||
|
"css-loader": "^6.8.1",
|
||||||
|
"html-loader": "^5.1.0",
|
||||||
|
"html-webpack-plugin": "^5.6.3",
|
||||||
|
"react-dev-utils": "^12.0.1",
|
||||||
|
"style-loader": "^3.3.3",
|
||||||
|
"ts-loader": "^9.5.0",
|
||||||
|
"typescript": "^4.9.5",
|
||||||
|
"url-loader": "^4.1.1",
|
||||||
|
"webpack": "^5.89.0",
|
||||||
|
"webpack-cli": "^5.1.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
5165
pnpm-lock.yaml
generated
Normal file
5165
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
144
server.js
Normal file
144
server.js
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
const fastify = require('fastify')({
|
||||||
|
logger: true,
|
||||||
|
bodyLimit: 100 * 1024 * 1024 // 設置為 100MB
|
||||||
|
});
|
||||||
|
const { PDFDocument, PDFName, StandardFonts, PDFString, PDFPermissionFlag } = require('pdf-lib');
|
||||||
|
const cors = require('@fastify/cors');
|
||||||
|
|
||||||
|
// 更詳細的 CORS 設置
|
||||||
|
fastify.register(cors, {
|
||||||
|
origin: ['*', 'null'],
|
||||||
|
methods: ['GET', 'POST', 'OPTIONS'],
|
||||||
|
allowedHeaders: ['Content-Type']
|
||||||
|
});
|
||||||
|
|
||||||
|
// 處理 PDF 生成請求
|
||||||
|
fastify.post('/generate-pdf', {
|
||||||
|
bodyLimit: 100 * 1024 * 1024, // 100MB
|
||||||
|
}, async (request, reply) => {
|
||||||
|
try {
|
||||||
|
const { frames, settings } = request.body;
|
||||||
|
|
||||||
|
// 檢查 frames 是否為有效數組
|
||||||
|
if (!Array.isArray(frames) || frames.length === 0) {
|
||||||
|
throw new Error('No frames provided or frames is not an array');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 創建 PDF 文件
|
||||||
|
const pdfDoc = await PDFDocument.create();
|
||||||
|
|
||||||
|
// 設置 PDF 元數據
|
||||||
|
if (settings.title) pdfDoc.setTitle(settings.title);
|
||||||
|
if (settings.author) pdfDoc.setAuthor(settings.author);
|
||||||
|
if (settings.subject) pdfDoc.setSubject(settings.subject);
|
||||||
|
if (settings.keywords) pdfDoc.setKeywords(Array.isArray(settings.keywords) ? settings.keywords : []);
|
||||||
|
if (settings.creator) pdfDoc.setCreator(settings.creator);
|
||||||
|
if (settings.producer) pdfDoc.setProducer(settings.producer);
|
||||||
|
if (settings.language) pdfDoc.setLanguage(settings.language);
|
||||||
|
|
||||||
|
// 設置日期
|
||||||
|
pdfDoc.setCreationDate(settings.creationDate ? new Date(settings.creationDate) : new Date());
|
||||||
|
pdfDoc.setModificationDate(settings.modificationDate ? new Date(settings.modificationDate) : new Date());
|
||||||
|
|
||||||
|
// 根據品質設定調整圖片品質
|
||||||
|
const qualitySettings = {
|
||||||
|
low: { scale: 1 },
|
||||||
|
medium: { scale: 2 },
|
||||||
|
high: { scale: 3 }
|
||||||
|
};
|
||||||
|
const quality = qualitySettings[settings.quality || 'medium'];
|
||||||
|
|
||||||
|
// 處理每個 Frame
|
||||||
|
for (const frame of frames) {
|
||||||
|
if (!frame.imageBase64) {
|
||||||
|
fastify.log.warn(`No image data for frame: ${frame.name}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 解碼 base64 圖片數據
|
||||||
|
const imageBuffer = Buffer.from(frame.imageBase64, 'base64');
|
||||||
|
|
||||||
|
// 嵌入圖片到 PDF
|
||||||
|
const image = await pdfDoc.embedPng(imageBuffer);
|
||||||
|
|
||||||
|
// 獲取圖片尺寸
|
||||||
|
const { width, height } = image.scale(1);
|
||||||
|
|
||||||
|
// 添加新頁面
|
||||||
|
const page = pdfDoc.addPage([width, height]);
|
||||||
|
|
||||||
|
// 繪製圖片
|
||||||
|
page.drawImage(image, {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
fastify.log.error(`Error processing frame ${frame.name}: ${error.message}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 檢查是否有頁面被添加
|
||||||
|
if (pdfDoc.getPageCount() === 0) {
|
||||||
|
throw new Error('No valid frames were processed');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 處理加密設置
|
||||||
|
if (settings.encryption?.enabled) {
|
||||||
|
// 定義權限映射
|
||||||
|
const permissionsMap = {
|
||||||
|
printing: PDFPermissionFlag.Print,
|
||||||
|
modifying: PDFPermissionFlag.ModifyDocument,
|
||||||
|
copying: PDFPermissionFlag.Extract,
|
||||||
|
annotating: PDFPermissionFlag.Modify,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 收集啟用的權限
|
||||||
|
const permissions = [];
|
||||||
|
if (settings.encryption.permissions?.printing) permissions.push(permissionsMap.printing);
|
||||||
|
if (settings.encryption.permissions?.modifying) permissions.push(permissionsMap.modifying);
|
||||||
|
if (settings.encryption.permissions?.copying) permissions.push(permissionsMap.copying);
|
||||||
|
if (settings.encryption.permissions?.annotating) permissions.push(permissionsMap.annotating);
|
||||||
|
|
||||||
|
// 應用加密設置
|
||||||
|
await pdfDoc.encrypt({
|
||||||
|
userPassword: settings.encryption.userPassword || undefined,
|
||||||
|
ownerPassword: settings.encryption.ownerPassword || undefined,
|
||||||
|
permissions: permissions
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成 PDF
|
||||||
|
const pdfBytes = await pdfDoc.save();
|
||||||
|
|
||||||
|
// 設置響應頭
|
||||||
|
reply.header('Content-Type', 'application/pdf');
|
||||||
|
reply.header('Content-Disposition', `attachment; filename=${settings.filename || 'figma-export.pdf'}`);
|
||||||
|
|
||||||
|
return reply.send(Buffer.from(pdfBytes));
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
fastify.log.error(error);
|
||||||
|
reply.code(500).send({
|
||||||
|
error: 'Failed to generate PDF',
|
||||||
|
message: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 啟動服務器
|
||||||
|
const start = async () => {
|
||||||
|
try {
|
||||||
|
await fastify.listen({ port: 3000, host: '0.0.0.0' });
|
||||||
|
console.log('Server running at http://localhost:3000');
|
||||||
|
} catch (err) {
|
||||||
|
fastify.log.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
start();
|
||||||
230
src/code.ts
Normal file
230
src/code.ts
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
// 定义默认尺寸
|
||||||
|
const DEFAULT_SIZE = {
|
||||||
|
width: 450,
|
||||||
|
height: 600
|
||||||
|
};
|
||||||
|
let currentExportScale = 0.5;
|
||||||
|
|
||||||
|
async function initializePlugin() {
|
||||||
|
const savedSize = await figma.clientStorage.getAsync('size').catch(() => DEFAULT_SIZE);
|
||||||
|
|
||||||
|
figma.showUI(__html__, {
|
||||||
|
width: savedSize?.width || DEFAULT_SIZE.width,
|
||||||
|
height: savedSize?.height || DEFAULT_SIZE.height,
|
||||||
|
themeColors: true
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
initializePlugin();
|
||||||
|
function getSelectedFrames(nodes: readonly SceneNode[]): SceneNode[] {
|
||||||
|
return nodes.filter(node => {
|
||||||
|
if (node.type === "FRAME" ||
|
||||||
|
node.type === "COMPONENT" ||
|
||||||
|
node.type === "INSTANCE") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (node.type === "GROUP") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function formatFileSize(bytes: number): string {
|
||||||
|
if (bytes === 0) return '0 B';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Frame {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
imageUrl: string;
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
figma.on('selectionchange', async () => {
|
||||||
|
const selection = figma.currentPage.selection;
|
||||||
|
const frames = getSelectedFrames(selection);
|
||||||
|
|
||||||
|
const frameData = await Promise.all(frames.map(async frame => {
|
||||||
|
const imageBytes = await frame.exportAsync({
|
||||||
|
format: 'PNG',
|
||||||
|
constraint: { type: 'SCALE', value: currentExportScale }
|
||||||
|
});
|
||||||
|
|
||||||
|
const imageBase64 = figma.base64Encode(imageBytes);
|
||||||
|
return {
|
||||||
|
id: frame.id,
|
||||||
|
name: frame.name,
|
||||||
|
imageUrl: imageBase64,
|
||||||
|
size: imageBytes.length
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
|
||||||
|
figma.ui.postMessage({
|
||||||
|
type: 'frame-selection-change',
|
||||||
|
data: {
|
||||||
|
frames: frameData
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
interface ExportOptions {
|
||||||
|
margin: number; // 页边距
|
||||||
|
orientation: 'portrait' | 'landscape'; // 纸张方向
|
||||||
|
}
|
||||||
|
|
||||||
|
// A4 尺寸常量 (单位: 像素, 300dpi)
|
||||||
|
const A4_SIZE = {
|
||||||
|
width: 2480, // 210mm
|
||||||
|
height: 3508 // 297mm
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ExportOptions {
|
||||||
|
margin: number;
|
||||||
|
orientation: 'portrait' | 'landscape';
|
||||||
|
}
|
||||||
|
// 监听主题变化
|
||||||
|
// figma.on('themechange', () => {
|
||||||
|
// const newTheme = figma.root.getPluginData('theme') || 'dark';
|
||||||
|
// figma.ui.postMessage({
|
||||||
|
// type: 'theme-change',
|
||||||
|
// theme: newTheme
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
|
||||||
|
figma.ui.onmessage = async (msg: any) => {
|
||||||
|
switch (msg.type) {
|
||||||
|
|
||||||
|
|
||||||
|
case 'resize':
|
||||||
|
figma.ui.resize(msg.width, msg.height);
|
||||||
|
await figma.clientStorage.setAsync('size', {
|
||||||
|
width: msg.width,
|
||||||
|
height: msg.height
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'setStorage':
|
||||||
|
try {
|
||||||
|
await figma.clientStorage.setAsync(msg.key, msg.value);
|
||||||
|
figma.ui.postMessage({
|
||||||
|
type: 'storageSet',
|
||||||
|
success: true
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
figma.ui.postMessage({
|
||||||
|
type: 'storageSet',
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'getStorage':
|
||||||
|
try {
|
||||||
|
const value = await figma.clientStorage.getAsync(msg.key);
|
||||||
|
figma.ui.postMessage({
|
||||||
|
type: 'storageGet',
|
||||||
|
value
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
figma.ui.postMessage({
|
||||||
|
type: 'storageGet',
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'removeStorage':
|
||||||
|
try {
|
||||||
|
await figma.clientStorage.deleteAsync(msg.key);
|
||||||
|
figma.ui.postMessage({
|
||||||
|
type: 'storageRemoved',
|
||||||
|
success: true
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
figma.ui.postMessage({
|
||||||
|
type: 'storageRemoved',
|
||||||
|
success: false,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "OPEN_OAUTH":
|
||||||
|
figma.openExternal(msg.data);
|
||||||
|
break;
|
||||||
|
case "OPEN_WEB":
|
||||||
|
figma.openExternal(msg.data);
|
||||||
|
break;
|
||||||
|
case 'update-export-scale':
|
||||||
|
const selection = figma.currentPage.selection;
|
||||||
|
const frames = getSelectedFrames(selection);
|
||||||
|
const frameData = await Promise.all(frames.map(async frame => {
|
||||||
|
const imageBytes = await frame.exportAsync({
|
||||||
|
format: 'PNG',
|
||||||
|
constraint: { type: 'SCALE', value: msg.scale }
|
||||||
|
});
|
||||||
|
const imageBase64 = figma.base64Encode(imageBytes);
|
||||||
|
return {
|
||||||
|
id: frame.id,
|
||||||
|
name: frame.name,
|
||||||
|
imageUrl: imageBase64
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
|
||||||
|
figma.ui.postMessage({
|
||||||
|
type: 'export-frame',
|
||||||
|
frames: frameData,
|
||||||
|
currentScale: msg.scale
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'export-to-file':
|
||||||
|
const _selection = figma.currentPage.selection;
|
||||||
|
console.log(msg,'msg');
|
||||||
|
|
||||||
|
// 通过 id 获取 Figma 节点
|
||||||
|
const framesToExport = msg.options.list.map((id: string) =>
|
||||||
|
figma.getNodeById(id)
|
||||||
|
).filter((node:any): node is FrameNode | ComponentNode | InstanceNode | GroupNode =>
|
||||||
|
node !== null &&
|
||||||
|
(node.type === "FRAME" ||
|
||||||
|
node.type === "COMPONENT" ||
|
||||||
|
node.type === "INSTANCE" ||
|
||||||
|
node.type === "GROUP")
|
||||||
|
);
|
||||||
|
|
||||||
|
const _previewList = await Promise.all(framesToExport.map(async (frame:any) => {
|
||||||
|
const imageBytes = await frame.exportAsync({
|
||||||
|
format: 'PNG',
|
||||||
|
constraint: { type: 'SCALE', value: msg.options.scale }
|
||||||
|
});
|
||||||
|
|
||||||
|
const imageBase64 = figma.base64Encode(imageBytes);
|
||||||
|
return {
|
||||||
|
id: frame.id,
|
||||||
|
name: frame.name,
|
||||||
|
imageUrl: imageBase64
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
const sortedPreviewList = msg.options.list
|
||||||
|
.map((id:string) => _previewList.find((item:any) => item.id === id))
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
figma.ui.postMessage({
|
||||||
|
type: 'generated',
|
||||||
|
success: true,
|
||||||
|
data: sortedPreviewList
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.warn('Unknown message type:', msg.type);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
86
src/components/resize.tsx
Normal file
86
src/components/resize.tsx
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import React, { useRef } from "react";
|
||||||
|
const pluginDefaultHieght = 400;
|
||||||
|
const pluginDefaultWidth = 600;
|
||||||
|
const pluginMaxHieght = 1200;
|
||||||
|
const pluginMaxWidth = 750;
|
||||||
|
const ResizableCorner = () => {
|
||||||
|
const cornerRef = useRef<any>(null);
|
||||||
|
|
||||||
|
const resizeWindow = async(e: any) => {
|
||||||
|
let size = {
|
||||||
|
w: Math.max(50, Math.floor(e.clientX + 5)),
|
||||||
|
h: Math.max(50, Math.floor(e.clientY + 5)),
|
||||||
|
};
|
||||||
|
if (size.h > pluginMaxHieght) {
|
||||||
|
size.h = pluginMaxHieght;
|
||||||
|
} else if (size.h < pluginDefaultHieght) {
|
||||||
|
size.h = pluginDefaultHieght;
|
||||||
|
}
|
||||||
|
if (size.w > pluginMaxWidth) {
|
||||||
|
size.w = pluginMaxWidth;
|
||||||
|
} else if (size.w < pluginDefaultWidth) {
|
||||||
|
size.w = pluginDefaultWidth;
|
||||||
|
}
|
||||||
|
parent.postMessage(
|
||||||
|
{
|
||||||
|
pluginMessage: {
|
||||||
|
type: 'resize',
|
||||||
|
width: size.w,
|
||||||
|
height: size.h
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'*'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const handlePointerDown = (e: any) => {
|
||||||
|
const corner = cornerRef.current;
|
||||||
|
corner.onpointermove = resizeWindow;
|
||||||
|
corner.setPointerCapture(e.pointerId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePointerUp = (e: any) => {
|
||||||
|
const corner = cornerRef.current;
|
||||||
|
corner.onpointermove = null;
|
||||||
|
corner.releasePointerCapture(e.pointerId);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: "auto",
|
||||||
|
padding: "16px",
|
||||||
|
boxSizing: "border-box",
|
||||||
|
fontFamily: "sans-serif",
|
||||||
|
fontSize: "12px",
|
||||||
|
margin: "0",
|
||||||
|
zIndex:'999'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
ref={cornerRef}
|
||||||
|
id="corner"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
right: "1px",
|
||||||
|
bottom: "2px",
|
||||||
|
cursor: "nwse-resize",
|
||||||
|
}}
|
||||||
|
onPointerDown={handlePointerDown}
|
||||||
|
onPointerUp={handlePointerUp}
|
||||||
|
>
|
||||||
|
<path d="M16 0V16H0L16 0Z" fill="var(--figma-color-bg)" />
|
||||||
|
<path d="M6.22577 16H3L16 3V6.22576L6.22577 16Z" fill="var(--figma-color-border)" />
|
||||||
|
<path
|
||||||
|
d="M11.8602 16H8.63441L16 8.63441V11.8602L11.8602 16Z"
|
||||||
|
fill="var(--figma-color-border)"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default ResizableCorner;
|
||||||
48
src/hook/fetchFn.tsx
Normal file
48
src/hook/fetchFn.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { useCallback, useState } from "react";
|
||||||
|
const useHttpClient = (baseurl:string) => {
|
||||||
|
interface ApiError extends Error {
|
||||||
|
status?: number;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
const httpClient = useCallback(async (url: string, options: any = {},token:string='') => {
|
||||||
|
try {
|
||||||
|
const defaultOptions: any = {
|
||||||
|
method: "GET",
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
...options.headers,
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (options.body instanceof FormData) {
|
||||||
|
delete defaultOptions.headers['content-type'];
|
||||||
|
} else {
|
||||||
|
defaultOptions.headers['content-type'] = 'application/json';
|
||||||
|
}
|
||||||
|
const response = await fetch(`${baseurl}${url}`, defaultOptions);
|
||||||
|
const responseBody = await response.text() as any;
|
||||||
|
if (response.status === 401) {
|
||||||
|
const errorData: any = JSON.parse(responseBody);
|
||||||
|
const error: ApiError = new Error(errorData.message);
|
||||||
|
error.status = response.status;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
if ([400, 403, 500].includes(response.status)) {
|
||||||
|
const errorData: any = JSON.parse(responseBody);
|
||||||
|
const error = new Error(errorData.message);
|
||||||
|
(error as any).status = response.status;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return JSON.parse(responseBody);
|
||||||
|
} catch (error) {
|
||||||
|
if (!(error instanceof Error) || !(error as any).status) {
|
||||||
|
console.error("Request failed:", error);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { httpClient };
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useHttpClient;
|
||||||
29
src/hook/i18n.ts
Normal file
29
src/hook/i18n.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import i18next from 'i18next';
|
||||||
|
import { initReactI18next } from 'react-i18next';
|
||||||
|
import { translations } from '../locales/translations';
|
||||||
|
import { storage } from './useStore';
|
||||||
|
const i18n = i18next;
|
||||||
|
i18n.use(initReactI18next).init({
|
||||||
|
lng: "en",
|
||||||
|
fallbackLng: "tw",
|
||||||
|
debug: false,
|
||||||
|
interpolation: {
|
||||||
|
escapeValue: false
|
||||||
|
},
|
||||||
|
resources: {
|
||||||
|
'zh-CN': { translation: translations['zh-CN'] },
|
||||||
|
'zh-TW': { translation: translations['zh-TW'] },
|
||||||
|
'en': { translation: translations['en'] },
|
||||||
|
'ja': { translation: translations['ja'] },
|
||||||
|
'ko': { translation: translations['ko'] }
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
storage.get('language').then((res:any)=>{
|
||||||
|
if(res){
|
||||||
|
i18n.changeLanguage(res)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
export default i18n;
|
||||||
62
src/hook/supabase.ts
Normal file
62
src/hook/supabase.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { createClient, SupabaseClient } from "@supabase/supabase-js";
|
||||||
|
import { storage } from "./useStore";
|
||||||
|
import { onlyStandalone } from '../utils/getbaseurl'
|
||||||
|
|
||||||
|
// 定义环境变量的默认值
|
||||||
|
const SUPABASE_URL = process.env.REACT_SUPABASE_URL || '';
|
||||||
|
const SUPABASE_ANON_KEY = process.env.REACT_SUPABASE_ANON_KEY || '';
|
||||||
|
|
||||||
|
let supabase: any = null;
|
||||||
|
export const getSupabaseClient = (): SupabaseClient => {
|
||||||
|
if (!supabase&&!onlyStandalone) {
|
||||||
|
supabase = createClient(
|
||||||
|
SUPABASE_URL,
|
||||||
|
SUPABASE_ANON_KEY,
|
||||||
|
{
|
||||||
|
db: { schema: "limq" },
|
||||||
|
auth: {
|
||||||
|
storage: storageAdapter,
|
||||||
|
persistSession: true,
|
||||||
|
autoRefreshToken: true,
|
||||||
|
},
|
||||||
|
realtime: {
|
||||||
|
params: {
|
||||||
|
eventsPerSecond: 10,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return supabase;
|
||||||
|
};
|
||||||
|
|
||||||
|
const storageAdapter = {
|
||||||
|
getItem: async (key: string) => {
|
||||||
|
try {
|
||||||
|
const result: any = await storage.get(key);
|
||||||
|
return result || null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Storage get error:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setItem: async (key: string, value: string) => {
|
||||||
|
try {
|
||||||
|
await storage.set(key, value);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Storage set error:", error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
removeItem: async (key: string) => {
|
||||||
|
try {
|
||||||
|
await storage.remove(key);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Storage remove error:", error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export const clearSupabaseInstance = () => {
|
||||||
|
supabase = null;
|
||||||
|
};
|
||||||
60
src/hook/useFile.tsx
Normal file
60
src/hook/useFile.tsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { message } from 'antd';
|
||||||
|
|
||||||
|
export const useUpload = (supabaseClient: any) => {
|
||||||
|
const upload = async (file: any, bucket: string, name: string) => {
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
|
||||||
|
// 处理文件名
|
||||||
|
const cleanFileName = name?.replace(/[^a-zA-Z0-9.-]/g, '_'); // 清理特殊字符
|
||||||
|
|
||||||
|
// 确保文件名不为空且包含扩展名
|
||||||
|
const fileExtension = file.type === 'application/pdf' ? '.pdf' :
|
||||||
|
file.type === 'image/gif' ? '.gif' :
|
||||||
|
'.file'; // 默认扩展名
|
||||||
|
|
||||||
|
// 组合最终的文件名
|
||||||
|
const uniqueFileName = cleanFileName
|
||||||
|
? `${timestamp}_${cleanFileName}${cleanFileName.includes('.') ? '' : fileExtension}`
|
||||||
|
: `${timestamp}${fileExtension}`;
|
||||||
|
|
||||||
|
const { data, error } = await supabaseClient
|
||||||
|
.storage
|
||||||
|
.from(bucket)
|
||||||
|
.upload(uniqueFileName, file, {
|
||||||
|
cacheControl: '3600',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw new Error(error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
message.success('文件上传成功');
|
||||||
|
return data?.path;
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { upload };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetPublicUrl = (supabaseClient:any) => {
|
||||||
|
const getPublicUrl = async (filePath: string, bucket: string) => {
|
||||||
|
const { data: urlData, error: urlError } = supabaseClient
|
||||||
|
.storage
|
||||||
|
.from(bucket) // 指定存储桶名称
|
||||||
|
.getPublicUrl(filePath);
|
||||||
|
|
||||||
|
if (urlError) {
|
||||||
|
message.error(`获取公共 URL 失败: ${urlError.message}`);
|
||||||
|
return null; // 如果获取失败,返回 null
|
||||||
|
} else {
|
||||||
|
return urlData?.publicUrl; // 成功时返回公共 URL
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { getPublicUrl };
|
||||||
|
};
|
||||||
85
src/hook/useStore.tsx
Normal file
85
src/hook/useStore.tsx
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
interface StorageMessage {
|
||||||
|
type: "setStorage" | "getStorage" | "removeStorage";
|
||||||
|
key: string;
|
||||||
|
value?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统一的发送消息函数
|
||||||
|
const sendMessageToFigma = (type: string, data?: any) => {
|
||||||
|
parent.postMessage({ pluginMessage: { type, ...data } }, '*');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const storage = {
|
||||||
|
set: async (key: string, value: any) => {
|
||||||
|
try {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const messageHandler = (event: MessageEvent) => {
|
||||||
|
const msg = event.data.pluginMessage;
|
||||||
|
if (msg?.type === 'storageSet') {
|
||||||
|
window.removeEventListener('message', messageHandler);
|
||||||
|
if (msg.success) {
|
||||||
|
resolve(true);
|
||||||
|
} else {
|
||||||
|
reject(msg.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('message', messageHandler);
|
||||||
|
|
||||||
|
// 发送设置存储的消息
|
||||||
|
sendMessageToFigma('setStorage', { key, value });
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error, '222222');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
get: async (key: string) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const messageHandler = (event: MessageEvent) => {
|
||||||
|
const msg = event.data.pluginMessage;
|
||||||
|
if (msg?.type === 'storageGet') {
|
||||||
|
window.removeEventListener('message', messageHandler);
|
||||||
|
if (msg.value !== undefined) {
|
||||||
|
resolve(msg.value);
|
||||||
|
} else {
|
||||||
|
reject(msg.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('message', messageHandler);
|
||||||
|
|
||||||
|
// 发送获取存储的消息
|
||||||
|
sendMessageToFigma('getStorage', { key });
|
||||||
|
} catch (error) {
|
||||||
|
reject(null);
|
||||||
|
console.log(error, 'getgetgetget4444');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
remove: async (key: string) => {
|
||||||
|
try {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const messageHandler = (event: MessageEvent) => {
|
||||||
|
const msg = event.data.pluginMessage;
|
||||||
|
if (msg?.type === 'storageRemoved') {
|
||||||
|
window.removeEventListener('message', messageHandler);
|
||||||
|
if (msg.success) {
|
||||||
|
resolve(true);
|
||||||
|
} else {
|
||||||
|
reject(msg.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('message', messageHandler);
|
||||||
|
|
||||||
|
// 发送删除存储的消息
|
||||||
|
sendMessageToFigma('removeStorage', { key });
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to remove storage:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
238
src/locales/translations.ts
Normal file
238
src/locales/translations.ts
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
// i18n/translations.ts
|
||||||
|
export const translations = {
|
||||||
|
'zh-CN': {
|
||||||
|
t1: '导出格式',
|
||||||
|
t2: '导出质量',
|
||||||
|
t3: '高品质',
|
||||||
|
t4: '普通',
|
||||||
|
t5: '低品质',
|
||||||
|
t6: '页面大小',
|
||||||
|
t7: '自适应',
|
||||||
|
t8: 'A4',
|
||||||
|
t9: '自定义',
|
||||||
|
t10: '宽度',
|
||||||
|
t11: '高度',
|
||||||
|
t12: '毫米',
|
||||||
|
t13: '文件名称',
|
||||||
|
t14: '请输入文件名称',
|
||||||
|
t15: '输入文件标题',
|
||||||
|
t16: '文档标题',
|
||||||
|
t17: '输入文档标题',
|
||||||
|
t18: '页面方向',
|
||||||
|
t19: '直式',
|
||||||
|
t20: '横式',
|
||||||
|
t21: '页面边距',
|
||||||
|
t22: '无边框',
|
||||||
|
t23: '窄边框',
|
||||||
|
t24: '大边框',
|
||||||
|
t25: '作者',
|
||||||
|
t26: '输入作者名称',
|
||||||
|
t27: '关键词标签',
|
||||||
|
t28: '使用逗号分隔多个标签',
|
||||||
|
t29: '播放间隔(秒)',
|
||||||
|
t30: '退出登录',
|
||||||
|
t31: '生成并上传',
|
||||||
|
t32: '生成到本地',
|
||||||
|
t33: '登录',
|
||||||
|
t34: '加载中...',
|
||||||
|
t35: 'PDF 导出成功!',
|
||||||
|
t36: '正在合并 PDF...',
|
||||||
|
t37: 'PDF/GIF 生成器',
|
||||||
|
t38: '选择一个或多个画板',
|
||||||
|
t39: '开始使用!',
|
||||||
|
t40: 'GIF 导出成功!',
|
||||||
|
t41:'请填写文件名称!',
|
||||||
|
t42:'语言',
|
||||||
|
t43:'导出失败',
|
||||||
|
t44:'正在处理',
|
||||||
|
t45: '导出中'
|
||||||
|
},
|
||||||
|
'zh-TW': {
|
||||||
|
t1: '匯出格式',
|
||||||
|
t2: '匯出品質',
|
||||||
|
t3: '高品質',
|
||||||
|
t4: '普通',
|
||||||
|
t5: '低品質',
|
||||||
|
t6: '頁面大小',
|
||||||
|
t7: '自適應',
|
||||||
|
t8: 'A4',
|
||||||
|
t9: '自定義',
|
||||||
|
t10: '寬度',
|
||||||
|
t11: '高度',
|
||||||
|
t12: '毫米',
|
||||||
|
t13: '檔案名稱',
|
||||||
|
t14: '請輸入檔案名稱',
|
||||||
|
t15: '輸入檔案標題',
|
||||||
|
t16: '文件標題',
|
||||||
|
t17: '輸入文件標題',
|
||||||
|
t18: '頁面方向',
|
||||||
|
t19: '直式',
|
||||||
|
t20: '橫式',
|
||||||
|
t21: '頁面邊距',
|
||||||
|
t22: '無邊框',
|
||||||
|
t23: '窄邊框',
|
||||||
|
t24: '寬邊框',
|
||||||
|
t25: '作者',
|
||||||
|
t26: '輸入作者名稱',
|
||||||
|
t27: '關鍵詞標籤',
|
||||||
|
t28: '使用逗號分隔多個標籤',
|
||||||
|
t29: '播放間隔(秒)',
|
||||||
|
t30: '登出',
|
||||||
|
t31: '生成並上傳',
|
||||||
|
t32: '生成到本地',
|
||||||
|
t33: '登入',
|
||||||
|
t34: '載入中...',
|
||||||
|
t35: 'PDF 匯出成功!',
|
||||||
|
t36: '正在合併 PDF...',
|
||||||
|
t37: 'PDF/GIF 生成器',
|
||||||
|
t38: '選擇一個或多個畫板',
|
||||||
|
t39: '開始使用!',
|
||||||
|
t40: 'GIF 匯出成功!',
|
||||||
|
t41: '請填寫檔案名稱!',
|
||||||
|
t42: '語言',
|
||||||
|
t43: '匯出失敗',
|
||||||
|
t44: '處理中',
|
||||||
|
t45: '匯出中'
|
||||||
|
},
|
||||||
|
'ja': {
|
||||||
|
t1: 'エクスポート形式',
|
||||||
|
t2: '出力品質',
|
||||||
|
t3: '高品質',
|
||||||
|
t4: '標準',
|
||||||
|
t5: '低品質',
|
||||||
|
t6: 'ページサイズ',
|
||||||
|
t7: '自動調整',
|
||||||
|
t8: 'A4',
|
||||||
|
t9: 'カスタム',
|
||||||
|
t10: '幅',
|
||||||
|
t11: '高さ',
|
||||||
|
t12: 'ミリメートル',
|
||||||
|
t13: 'ファイル名',
|
||||||
|
t14: 'ファイル名を入力してください',
|
||||||
|
t15: 'ファイルタイトルを入力',
|
||||||
|
t16: 'ドキュメントタイトル',
|
||||||
|
t17: 'ドキュメントタイトルを入力',
|
||||||
|
t18: 'ページの向き',
|
||||||
|
t19: '縦向き',
|
||||||
|
t20: '横向き',
|
||||||
|
t21: 'ページ余白',
|
||||||
|
t22: '余白なし',
|
||||||
|
t23: '狭い余白',
|
||||||
|
t24: '広い余白',
|
||||||
|
t25: '作成者',
|
||||||
|
t26: '作成者名を入力',
|
||||||
|
t27: 'キーワード',
|
||||||
|
t28: 'カンマで区切って入力',
|
||||||
|
t29: 'フレーム間隔(秒)',
|
||||||
|
t30: 'ログアウト',
|
||||||
|
t31: '生成してアップロード',
|
||||||
|
t32: 'ローカルに生成',
|
||||||
|
t33: 'ログイン',
|
||||||
|
t34: '読み込み中...',
|
||||||
|
t35: 'PDFのエクスポートが完了しました!',
|
||||||
|
t36: 'PDFを結合中...',
|
||||||
|
t37: 'PDF/GIF ジェネレーター',
|
||||||
|
t38: '1つ以上のフレームを選択',
|
||||||
|
t39: '始めましょう!',
|
||||||
|
t40: 'GIFのエクスポートが完了しました!',
|
||||||
|
t41: 'ファイル名を入力してください!',
|
||||||
|
t42: '言語',
|
||||||
|
t43: 'エクスポート失敗',
|
||||||
|
t44: '処理中',
|
||||||
|
t45: 'エクスポート中'
|
||||||
|
},
|
||||||
|
'ko': {
|
||||||
|
t1: '내보내기 형식',
|
||||||
|
t2: '출력 품질',
|
||||||
|
t3: '고품질',
|
||||||
|
t4: '보통',
|
||||||
|
t5: '저품질',
|
||||||
|
t6: '페이지 크기',
|
||||||
|
t7: '자동 조정',
|
||||||
|
t8: 'A4',
|
||||||
|
t9: '사용자 정의',
|
||||||
|
t10: '너비',
|
||||||
|
t11: '높이',
|
||||||
|
t12: '밀리미터',
|
||||||
|
t13: '파일 이름',
|
||||||
|
t14: '파일 이름을 입력하세요',
|
||||||
|
t15: '파일 제목 입력',
|
||||||
|
t16: '문서 제목',
|
||||||
|
t17: '문서 제목 입력',
|
||||||
|
t18: '페이지 방향',
|
||||||
|
t19: '세로',
|
||||||
|
t20: '가로',
|
||||||
|
t21: '페이지 여백',
|
||||||
|
t22: '여백 없음',
|
||||||
|
t23: '좁은 여백',
|
||||||
|
t24: '넓은 여백',
|
||||||
|
t25: '작성자',
|
||||||
|
t26: '작성자 이름 입력',
|
||||||
|
t27: '키워드 태그',
|
||||||
|
t28: '쉼표로 구분하여 입력',
|
||||||
|
t29: '재생 간격(초)',
|
||||||
|
t30: '로그아웃',
|
||||||
|
t31: '생성 및 업로드',
|
||||||
|
t32: '로컬에 생성',
|
||||||
|
t33: '로그인',
|
||||||
|
t34: '로딩 중...',
|
||||||
|
t35: 'PDF 내보내기 성공!',
|
||||||
|
t36: 'PDF 병합 중...',
|
||||||
|
t37: 'PDF/GIF 생성기',
|
||||||
|
t38: '하나 이상의 프레임 선택',
|
||||||
|
t39: '시작하기!',
|
||||||
|
t40: 'GIF 내보내기 성공!',
|
||||||
|
t41: '파일 이름을 입력하세요!',
|
||||||
|
t42: '언어',
|
||||||
|
t43: '내보내기 실패',
|
||||||
|
t44: '처리 중',
|
||||||
|
t45: '내보내는 중'
|
||||||
|
},
|
||||||
|
'en': {
|
||||||
|
t1: 'Export Format',
|
||||||
|
t2: 'Export Quality',
|
||||||
|
t3: 'High Quality',
|
||||||
|
t4: 'Medium',
|
||||||
|
t5: 'Low Quality',
|
||||||
|
t6: 'Page Size',
|
||||||
|
t7: 'Auto',
|
||||||
|
t8: 'A4',
|
||||||
|
t9: 'Custom',
|
||||||
|
t10: 'Width',
|
||||||
|
t11: 'Height',
|
||||||
|
t12: 'mm',
|
||||||
|
t13: 'File Name',
|
||||||
|
t14: 'Please enter file name',
|
||||||
|
t15: 'Enter file title',
|
||||||
|
t16: 'Document Title',
|
||||||
|
t17: 'Enter document title',
|
||||||
|
t18: 'Page Orientation',
|
||||||
|
t19: 'Portrait',
|
||||||
|
t20: 'Landscape',
|
||||||
|
t21: 'Page Margin',
|
||||||
|
t22: 'No Margin',
|
||||||
|
t23: 'Narrow',
|
||||||
|
t24: 'Wide',
|
||||||
|
t25: 'Author',
|
||||||
|
t26: 'Enter author name',
|
||||||
|
t27: 'Keywords',
|
||||||
|
t28: 'Separate with commas',
|
||||||
|
t29: 'Frame Duration (s)',
|
||||||
|
t30: 'Logout',
|
||||||
|
t31: 'Generate to Cloud',
|
||||||
|
t32: 'Generate Locally',
|
||||||
|
t33: 'Login',
|
||||||
|
t34: 'Loading...',
|
||||||
|
t35: 'PDF exported successfully!',
|
||||||
|
t36: 'Merging PDF...',
|
||||||
|
t37: 'PDF/GIF Generator',
|
||||||
|
t38: 'Select one or more frames',
|
||||||
|
t39: 'to get started!',
|
||||||
|
t40: 'GIF exported successfully!',
|
||||||
|
t41: 'Please enter file name!',
|
||||||
|
t42: 'Language',
|
||||||
|
t43: 'Export failed',
|
||||||
|
t44: 'Processing',
|
||||||
|
t45: 'Exporting'
|
||||||
|
}
|
||||||
|
};
|
||||||
3
src/logo.svg
Normal file
3
src/logo.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M27 5V27H5M31 9V31H9M1 1H23V23H1V1Z" stroke="black" stroke-width="2"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 185 B |
99
src/theme.ts
Normal file
99
src/theme.ts
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
// theme.ts
|
||||||
|
import { theme } from 'antd';
|
||||||
|
|
||||||
|
export const antdTheme = {
|
||||||
|
dark: {
|
||||||
|
token: {
|
||||||
|
colorPrimaryActive: "#303032",
|
||||||
|
colorPrimaryHover: '#303032',
|
||||||
|
colorPrimary: '#303032',
|
||||||
|
colorText: '#808081',
|
||||||
|
colorBgBase: '#171619',
|
||||||
|
colorBorder: '#262628',
|
||||||
|
colorTextSecondary: '#808081',
|
||||||
|
controlItemBgActive: '#303032',
|
||||||
|
controlItemBgHover: '#262628',
|
||||||
|
},
|
||||||
|
algorithm: theme.darkAlgorithm,
|
||||||
|
components: {
|
||||||
|
Radio: {
|
||||||
|
buttonSolidCheckedActiveBg: '#303032',
|
||||||
|
buttonSolidCheckedBg: "#303032",
|
||||||
|
buttonSolidCheckedHoverBg: '#303032',
|
||||||
|
buttonBg: '#171619',
|
||||||
|
buttonColor: '#808081',
|
||||||
|
},
|
||||||
|
Form: {
|
||||||
|
labelColor: '#808081',
|
||||||
|
fontSize:10
|
||||||
|
},
|
||||||
|
Input: {
|
||||||
|
colorBgContainer: '#171619',
|
||||||
|
colorBorder: '#262628',
|
||||||
|
},
|
||||||
|
Select: {
|
||||||
|
colorBgContainer: '#171619',
|
||||||
|
colorBorder: '#262628',
|
||||||
|
colorTextPlaceholder: '#808081',
|
||||||
|
},
|
||||||
|
Slider: {
|
||||||
|
railBg: '#262628',
|
||||||
|
trackBg: '#303032',
|
||||||
|
handleColor: '#303032',
|
||||||
|
},
|
||||||
|
Button: {
|
||||||
|
defaultBg: '#171619',
|
||||||
|
defaultColor: '#808081',
|
||||||
|
defaultBorderColor: '#262628',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
light: {
|
||||||
|
token: {
|
||||||
|
// 加深主色调
|
||||||
|
colorPrimaryActive: "#4c5767",
|
||||||
|
colorPrimaryHover: '#4c5767',
|
||||||
|
colorPrimary: '#4c5767', // 加深主要文字颜色
|
||||||
|
colorText: '#86909c', // 保持文字主色
|
||||||
|
colorBgBase: '#ffffff', // 保持背景色为白色
|
||||||
|
colorBorder: '#d9d9d9', // 加深边框颜色
|
||||||
|
colorTextSecondary: '#595959', // 加深次要文字颜色
|
||||||
|
controlItemBgActive: '#f2f3f5',
|
||||||
|
controlItemBgHover: '#f5f5f5',
|
||||||
|
},
|
||||||
|
algorithm: theme.defaultAlgorithm,
|
||||||
|
components: {
|
||||||
|
Radio: {
|
||||||
|
buttonSolidCheckedColor:'#4c5767', //选中文字颜色
|
||||||
|
buttonSolidCheckedHoverBg:'#f2f3f5', //hover颜色
|
||||||
|
buttonSolidCheckedActiveBg: '#f2f3f5',//选中背景色
|
||||||
|
buttonSolidCheckedBg: "#f2f3f5",
|
||||||
|
buttonBg: '#ffffff',
|
||||||
|
buttonColor: '#86909c',
|
||||||
|
},
|
||||||
|
Form: {
|
||||||
|
labelColor: '#86909c', // 加深表单标签颜色
|
||||||
|
fontSize:10
|
||||||
|
},
|
||||||
|
Input: {
|
||||||
|
colorBgContainer: '#ffffff',
|
||||||
|
colorBorder: '#d9d9d9', // 加深输入框边框
|
||||||
|
},
|
||||||
|
Select: {
|
||||||
|
colorBgContainer: '#ffffff',
|
||||||
|
colorBorder: '#d9d9d9',
|
||||||
|
colorTextPlaceholder: '#8c8c8c', // 加深占位符文字颜色
|
||||||
|
},
|
||||||
|
Slider: {
|
||||||
|
railBg: '#e8e8e8',
|
||||||
|
trackBg: '#d9d9d9',
|
||||||
|
handleColor: '#d9d9d9',
|
||||||
|
},
|
||||||
|
Button: {
|
||||||
|
defaultBg: '#ffffff',
|
||||||
|
defaultColor: '#434343', // 加深按钮文字颜色
|
||||||
|
defaultBorderColor: '#d9d9d9',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
885
src/ui.css
Normal file
885
src/ui.css
Normal file
@@ -0,0 +1,885 @@
|
|||||||
|
/* 基础样式 */
|
||||||
|
:root {
|
||||||
|
--primary-color: #0d99ff;
|
||||||
|
/* --bg-color: var(--figma-color-bg); */
|
||||||
|
--text-color: var(--figma-color-text);
|
||||||
|
/* --border-color: rgba(255, 255, 255, 0.1); */
|
||||||
|
--card-width: 100px;
|
||||||
|
--card-height: 141.4px;
|
||||||
|
}
|
||||||
|
:root[data-theme="dark"] {
|
||||||
|
--primary-color: #0d99ff;
|
||||||
|
--bg-color: #171619;
|
||||||
|
--text-color: #ffffff;
|
||||||
|
--border-color: rgba(255, 255, 255, 0.1);
|
||||||
|
--text-color-secondary: #ffffff;
|
||||||
|
--button-text-color: #ffffff;
|
||||||
|
--button-bg-color: #0e101c;
|
||||||
|
--border-big: #262628;
|
||||||
|
--number: #efefef;
|
||||||
|
--btn-border:#484849;
|
||||||
|
--btn-shadom:rgba(16, 24, 40, 0.05);
|
||||||
|
--btn-bg:#373739;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 浅色主题变量 */
|
||||||
|
:root[data-theme="light"] {
|
||||||
|
--primary-color: #1677ff;
|
||||||
|
--bg-color: #ffffff;
|
||||||
|
--text-color: #1d2129;
|
||||||
|
--border-color: rgba(0, 0, 0, 0.1);
|
||||||
|
--text-color-secondary: #595959;
|
||||||
|
--button-text-color: #000000;
|
||||||
|
--button-bg-color: #ffffff;
|
||||||
|
--border-big: #f0f1f3;
|
||||||
|
--number: #838a95;
|
||||||
|
--btn-border:#e5e6eb;
|
||||||
|
--btn-shadom:rgba(16, 24, 40, 0.05);
|
||||||
|
--btn-bg:#f2f3f5
|
||||||
|
}
|
||||||
|
@keyframes gradient {
|
||||||
|
0% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
background-position: 100% 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Inter", sans-serif;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
@keyframes gradientAnimation {
|
||||||
|
0% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-position: 100% 50%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo img {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo span {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 内容区域样式 */
|
||||||
|
.content {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
background-color:var(--bg-color) ;
|
||||||
|
}
|
||||||
|
.leftWarp {
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 320px;
|
||||||
|
border-right: 1px solid var(--border-big);
|
||||||
|
padding: 24px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: visible;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.leftWarp.collapsed {
|
||||||
|
flex: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: 0;
|
||||||
|
opacity: 0;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rightWarp {
|
||||||
|
flex: 65%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
padding-top: 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
transition: flex 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leftWarp.collapsed + .toggle-button + .rightWarp {
|
||||||
|
flex: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片样式 */
|
||||||
|
.grid-item {
|
||||||
|
position: relative;
|
||||||
|
width: var(--grid-item-width);
|
||||||
|
min-height: var(--grid-item-height);
|
||||||
|
transition: transform 200ms ease, opacity 200ms ease;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes borderPulse {
|
||||||
|
0% {
|
||||||
|
border-color: rgba(13, 153, 255, 0.4);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
border-color: rgba(13, 153, 255, 0.8);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
border-color: rgba(13, 153, 255, 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame-card {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
box-shadow: 0 2px 4px var(--figma-color-shadow);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frame-preview {
|
||||||
|
width: var(--grid-item-width);
|
||||||
|
min-height: var(--grid-item-height);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid var(--figma-color-border);
|
||||||
|
border-radius: 6px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: move;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 自适应模式的样式 */
|
||||||
|
.frame-preview.auto-size {
|
||||||
|
aspect-ratio: auto;
|
||||||
|
height: auto;
|
||||||
|
min-height: 100px;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frame-preview.auto-size img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 200px; /* 设置最大高度限制 */
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A4 模式的样式 */
|
||||||
|
.frame-preview:not(.auto-size) {
|
||||||
|
aspect-ratio: var(--aspect-ratio); /* 使用动态比例 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 横向模式 */
|
||||||
|
.frame-preview:not(.auto-size)[data-orientation="landscape"] {
|
||||||
|
aspect-ratio: 1.414 / 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 纵向模式 */
|
||||||
|
.frame-preview:not(.auto-size)[data-orientation="portrait"] {
|
||||||
|
aspect-ratio: 1 / 1.414;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frame-preview:not(.auto-size) img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frame-preview:not(.dragging):hover {
|
||||||
|
background-color: #000000;
|
||||||
|
}
|
||||||
|
.frame-preview:hover > img:not(.dragging) {
|
||||||
|
filter: brightness(0.5); /* 或其他你想要的滤镜效果 */
|
||||||
|
}
|
||||||
|
.frame-preview .hover-icon {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
opacity: 0;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.frame-preview:not(.dragging):hover .hover-icon {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.frame-preview > img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 拖拽相关样式 */
|
||||||
|
.frame-card.dragging {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 0.8;
|
||||||
|
z-index: 1000;
|
||||||
|
animation: dragPulse 1s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dragPulse {
|
||||||
|
0% {
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: scale(1.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dragging-over .frame-card:not(.dragging):not(.selected) {
|
||||||
|
opacity: 0.5;
|
||||||
|
transform: scale(0.98);
|
||||||
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 放置动画 */
|
||||||
|
.frame-card.dropping {
|
||||||
|
animation: dropBounce 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dropBounce {
|
||||||
|
0% {
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
75% {
|
||||||
|
transform: scale(1.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 选中状态 */
|
||||||
|
.frame-preview .selected {
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 2px solid #0d99ff;
|
||||||
|
animation: selectedPulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes selectedPulse {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(13, 153, 255, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
70% {
|
||||||
|
box-shadow: 0 0 0 10px rgba(13, 153, 255, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(13, 153, 255, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track,
|
||||||
|
::-webkit-scrollbar-thumb,
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
width: 100%;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 10;
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 35px 0 20px 0;
|
||||||
|
background: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
transparent,
|
||||||
|
var(--bg-color) 45%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
.footer .btn {
|
||||||
|
margin:0 auto;
|
||||||
|
width: 80%;
|
||||||
|
gap: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.footer .size {
|
||||||
|
width: 100%;
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 12px;
|
||||||
|
min-height: 20px;
|
||||||
|
}
|
||||||
|
.version {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 6px;
|
||||||
|
text-align: left;
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 生成按钮样式 */
|
||||||
|
.generate-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--button-text-color);
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 12px 50px;
|
||||||
|
min-width:110px;
|
||||||
|
max-width: 220px;
|
||||||
|
border-radius: 32.587px;
|
||||||
|
border: 1px solid #3c7eff;
|
||||||
|
background: var(--button-bg-color);
|
||||||
|
box-shadow:
|
||||||
|
20px -4px 30px 0px rgba(35, 107, 207, 0.37),
|
||||||
|
-12px 2px 30px 0px rgba(18, 218, 254, 0.22);
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.footer:has(> :is(div, button)) {
|
||||||
|
container-type: inline-size;
|
||||||
|
}
|
||||||
|
|
||||||
|
@container (max-width: 250px) {
|
||||||
|
.generate-button {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.generate-button:hover {
|
||||||
|
box-shadow:
|
||||||
|
25px -6px 35px 0px rgba(35, 107, 207, 0.5),
|
||||||
|
-15px 4px 35px 0px rgba(18, 218, 254, 0.35);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.generate-button:active {
|
||||||
|
box-shadow:
|
||||||
|
15px -2px 25px 0px rgba(35, 107, 207, 0.3),
|
||||||
|
-8px 1px 25px 0px rgba(18, 218, 254, 0.15);
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-count {
|
||||||
|
position: absolute;
|
||||||
|
top: -8px;
|
||||||
|
right: -8px;
|
||||||
|
background: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
border: 2px solid var(--bg-color);
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frame-name {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--figma-color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.frame-name > span {
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Frame 信息样式 */
|
||||||
|
.frame-info {
|
||||||
|
height: 32px;
|
||||||
|
padding-top: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frame-number {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--number);
|
||||||
|
}
|
||||||
|
|
||||||
|
.frames-container {
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding-bottom: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frames-list {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frames-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, var(--grid-item-width));
|
||||||
|
gap: 16px;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 16px;
|
||||||
|
min-height: 200px;
|
||||||
|
place-items: center;
|
||||||
|
contain: layout style paint;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty State 样式 */
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-content {
|
||||||
|
text-align: center;
|
||||||
|
animation: fadeIn 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-content div {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-color);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-content p {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes borderDash {
|
||||||
|
to {
|
||||||
|
stroke-dashoffset: -1000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 拖拽时原位置的样式 */
|
||||||
|
.grid-item.dragging {
|
||||||
|
opacity: 0.3;
|
||||||
|
transform: scale(1);
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dragging-over .grid-item:not(.dragging) {
|
||||||
|
transform: scale(1);
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frame-card.dragging {
|
||||||
|
animation: none;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected {
|
||||||
|
border: 3px solid #1a73e8;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
.imgSelect {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.dragging {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-slider-dot {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-slider-mark-text {
|
||||||
|
color: rgba(255, 255, 255, 0.5) !important;
|
||||||
|
font-size: 11px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-slider-mark-text-active {
|
||||||
|
color: rgba(255, 255, 255, 0.8) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-section {
|
||||||
|
border-top: 1px solid var(--figma-color-border);
|
||||||
|
margin-top: 16px;
|
||||||
|
padding-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metadata-section h3 {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--figma-color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--figma-color-bg);
|
||||||
|
border: 1px solid var(--figma-color-border);
|
||||||
|
color: var(--figma-color-text);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--figma-color-border-selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-input::placeholder {
|
||||||
|
color: var(--figma-color-text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-slider {
|
||||||
|
margin: 8px 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-slider .ant-slider-rail {
|
||||||
|
/* 使用 antd 主题变量 */
|
||||||
|
background-color: var(--figma-color-bg-secondary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-slider .ant-slider-track {
|
||||||
|
/* 使用 antd 主题变量 */
|
||||||
|
background-color: var(--figma-color-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-slider .ant-slider-handle {
|
||||||
|
/* 使用 antd 主题变量 */
|
||||||
|
background-color: var(--figma-color-text) !important;
|
||||||
|
border-color: var(--figma-color-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-slider .ant-slider-handle:hover {
|
||||||
|
border-color: var(--figma-color-border-selected-strong) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-slider .ant-slider-mark-text {
|
||||||
|
color: var(--figma-color-text-secondary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-slider .ant-slider-mark-text-active {
|
||||||
|
color: var(--figma-color-text) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom {
|
||||||
|
margin-top: 5px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
/* 设置面板样式 */
|
||||||
|
.settings-panel {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-input-number .ant-input-number-handler-wrap {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.ant-form-item-required::before {
|
||||||
|
color: #5769fb !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 退出按钮样式 */
|
||||||
|
.logout-button {
|
||||||
|
margin: 16px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 添加切换按钮样式 */
|
||||||
|
.toggle-button {
|
||||||
|
position: fixed;
|
||||||
|
left: 6px;
|
||||||
|
bottom: 12px;
|
||||||
|
width: 20px;
|
||||||
|
height: 45px;
|
||||||
|
border-radius: 0 6px 6px 0;
|
||||||
|
background: var(--figma-color-bg-secondary);
|
||||||
|
color: var(--figma-color-text-secondary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 1000;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-button:hover {
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-button.collapsed {
|
||||||
|
left: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-button.collapsed svg {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-radio-button-wrapper-checked:before {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 拖动预览容器样式 */
|
||||||
|
.drag-preview-container {
|
||||||
|
position: fixed;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-preview-container img {
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-count {
|
||||||
|
position: absolute;
|
||||||
|
top: -8px;
|
||||||
|
right: -8px;
|
||||||
|
background: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主预览项样式 */
|
||||||
|
.primary-preview {
|
||||||
|
position: relative;
|
||||||
|
width: var(--grid-item-width);
|
||||||
|
height: var(--grid-item-height);
|
||||||
|
background: var(--bg-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 次要预览项样式 */
|
||||||
|
.secondary-preview {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: var(--card-width);
|
||||||
|
height: 110px;
|
||||||
|
background: var(--bg-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 拖动数量指示器样式 */
|
||||||
|
.drag-count {
|
||||||
|
position: absolute;
|
||||||
|
top: -8px;
|
||||||
|
right: -8px;
|
||||||
|
background: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
border: 2px solid var(--bg-color);
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 拖动时的占位样式 */
|
||||||
|
.grid-item.placeholder {
|
||||||
|
opacity: 0.2;
|
||||||
|
border: 2px dashed var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 拖动时其他项过渡效果 */
|
||||||
|
.grid-item:not(.dragging) {
|
||||||
|
transition: transform 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-target {
|
||||||
|
border: 2px dashed var(--primary-color);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 优化拖动时的动画效果 */
|
||||||
|
.grid-item:not(.dragging) {
|
||||||
|
transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 优化多选拖动时的视觉效果 */
|
||||||
|
.grid-item.selected:not(.dragging) {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dragging-over .grid-item:not(.dragging) {
|
||||||
|
transition: transform 200ms cubic-bezier(0.2, 0, 0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid-item.dragging {
|
||||||
|
opacity: 0.4;
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 优化拖动时的动画 */
|
||||||
|
.grid-item {
|
||||||
|
transform-origin: 50% 50%;
|
||||||
|
transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 拖动时的占位效果 */
|
||||||
|
.grid-item.placeholder {
|
||||||
|
opacity: 0.2;
|
||||||
|
border: 2px dashed var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 提升拖动时的视觉层级 */
|
||||||
|
.drag-overlay {
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frame-preview:not(.auto-size) {
|
||||||
|
aspect-ratio: var(--aspect-ratio);
|
||||||
|
min-height: 60px; /* 最小高度 */
|
||||||
|
max-height: 300px; /* 最大高度 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-size-inputs {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 添加输入限制提示 */
|
||||||
|
.custom-size-inputs .ant-input-number {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-size-inputs .ant-form-item-extra {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 优化自定义尺寸预览 */
|
||||||
|
.frame-preview[data-size="custom"] {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frame-preview[data-size="custom"] img {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
.ant-segmented{
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
.ant-segmented-item {
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-segmented-item:hover {
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
.ant-segmented-group{
|
||||||
|
border: 1px solid var(--btn-border) !important;
|
||||||
|
border-radius: 4px !important;
|
||||||
|
background: var(--bg-color) !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
|
||||||
|
}
|
||||||
|
.ant-segmented-item-selected {
|
||||||
|
background: var(--btn-bg) !important;
|
||||||
|
color: var(--button-text-color) !important;
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
.ant-segmented-item:first-child.ant-segmented-item-selected {
|
||||||
|
box-shadow: 1px 0 0 0 var(--btn-border) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-segmented-item:last-child.ant-segmented-item-selected {
|
||||||
|
box-shadow: -1px 0 0 0 var(--btn-border) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-segmented-item:not(:first-child):not(:last-child).ant-segmented-item-selected {
|
||||||
|
box-shadow: -1px 0 0 0 var(--btn-border), 1px 0 0 0 var(--btn-border) !important;
|
||||||
|
}
|
||||||
|
.leftWarp.collapsed .toggle-button {
|
||||||
|
right: auto;
|
||||||
|
left: 6px;
|
||||||
|
}
|
||||||
|
.ant-segmented-thumb {
|
||||||
|
background: var(--btn-bg) !important;
|
||||||
|
}
|
||||||
|
/* 当 leftWarp 展开时的 toggle-button 位置 */
|
||||||
|
.leftWarp:not(.collapsed) + .toggle-button {
|
||||||
|
left: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-segmented-item-selected + .ant-segmented-thumb {
|
||||||
|
border: 1px solid var(--btn-border) !important;
|
||||||
|
}
|
||||||
9
src/ui.html
Normal file
9
src/ui.html
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1476
src/ui.tsx
Normal file
1476
src/ui.tsx
Normal file
File diff suppressed because it is too large
Load Diff
4
src/utils/getbaseurl.ts
Normal file
4
src/utils/getbaseurl.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
export const webUrl:string = process.env.REACT_SITE_URL||''
|
||||||
|
export const baseUrl:string = process.env.REACT_API_URL||''
|
||||||
|
export const onlyStandalone:boolean = process.env.REACT_ONLY_STANDALONE==='true'
|
||||||
15
tsconfig.json
Normal file
15
tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es6",
|
||||||
|
"lib": ["es6", "dom"],
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"jsx": "react",
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"strict": true,
|
||||||
|
"typeRoots": [
|
||||||
|
"./node_modules/@types",
|
||||||
|
"./node_modules/@figma"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
78
webpack.config.js
Normal file
78
webpack.config.js
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||||
|
const InlineChunkHtmlPlugin = require('react-dev-utils/InlineChunkHtmlPlugin')
|
||||||
|
const Dotenv = require('dotenv-webpack')
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
module.exports = (env, argv) => ({
|
||||||
|
mode: argv.mode === 'production' ? 'production' : 'development',
|
||||||
|
|
||||||
|
// This is necessary because Figma's 'eval' works differently than normal eval
|
||||||
|
devtool: argv.mode === 'production' ? false : 'inline-source-map',
|
||||||
|
|
||||||
|
entry: {
|
||||||
|
ui: './src/ui.tsx', // The entry point for your UI code
|
||||||
|
code: './src/code.ts', // The entry point for your plugin code
|
||||||
|
},
|
||||||
|
performance: {
|
||||||
|
hints: false, // 完全禁用警告
|
||||||
|
// 或者调整限制
|
||||||
|
maxEntrypointSize: 700000, // 增加入口点大小限制到 700KB
|
||||||
|
maxAssetSize: 700000, // 增加资源大小限制到 700KB
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
// Converts TypeScript code to JavaScript
|
||||||
|
{ test: /\.tsx?$/, use: 'ts-loader', exclude: /node_modules/ },
|
||||||
|
|
||||||
|
// Enables including CSS by doing "import './file.css'" in your TypeScript code
|
||||||
|
{ test: /\.css$/, use: ['style-loader', { loader: 'css-loader' }] },
|
||||||
|
|
||||||
|
// Allows you to use "<%= require('./file.svg') %>" in your HTML code to get a data URI
|
||||||
|
{ test: /\.(png|jpg|gif|webp|svg|zip)$/, loader: 'url-loader' },
|
||||||
|
|
||||||
|
// 添加 wasm 文件的处理规则
|
||||||
|
{
|
||||||
|
test: /\.wasm$/,
|
||||||
|
type: 'asset/resource',
|
||||||
|
generator: {
|
||||||
|
filename: '[name][ext]'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// Webpack tries these extensions for you if you omit the extension like "import './file'"
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.tsx', '.ts', '.jsx', '.js'],
|
||||||
|
fallback: {
|
||||||
|
fs: false,
|
||||||
|
path: false,
|
||||||
|
crypto: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
output: {
|
||||||
|
filename: '[name].js',
|
||||||
|
path: path.resolve(__dirname, 'dist'), // Compile into a folder called "dist"
|
||||||
|
publicPath: '/',
|
||||||
|
crossOriginLoading: 'anonymous'
|
||||||
|
},
|
||||||
|
|
||||||
|
// Tells Webpack to generate "ui.html" and to inline "ui.ts" into it
|
||||||
|
plugins: [
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
template: './src/ui.html',
|
||||||
|
filename: 'ui.html',
|
||||||
|
inlineSource: '.(js)$',
|
||||||
|
chunks: ['ui'],
|
||||||
|
}),
|
||||||
|
new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/ui/]),
|
||||||
|
new Dotenv({
|
||||||
|
defaults: false, // 首先加载默认值
|
||||||
|
path: '.env', // 默认的 .env 文件
|
||||||
|
safe: false,
|
||||||
|
systemvars: false,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user