first commit

This commit is contained in:
liamzi
2025-01-21 10:57:59 +08:00
commit 83096f7c9e
22 changed files with 8799 additions and 0 deletions

8
.env Normal file
View 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
View File

@@ -0,0 +1,2 @@
node_modules/
dist/

18
manifest.json Normal file
View 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
View 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

File diff suppressed because it is too large Load Diff

144
server.js Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

4
src/utils/getbaseurl.ts Normal file
View 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
View 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
View 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,
}),
],
})