Merge branch 'main' of https://git.ambigrat.com/hom/yuanhe-checkin-electron
This commit is contained in:
114
electron/main.js
114
electron/main.js
@@ -1,5 +1,9 @@
|
|||||||
const { app, BrowserWindow, ipcMain } = require("electron");
|
const { app, BrowserWindow, ipcMain } = require("electron");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
const https = require("https");
|
||||||
|
const http = require("http");
|
||||||
|
|
||||||
|
let mainWindow = null;
|
||||||
const { Worker } = require("worker_threads");
|
const { Worker } = require("worker_threads");
|
||||||
const log = require("electron-log");
|
const log = require("electron-log");
|
||||||
|
|
||||||
@@ -19,7 +23,6 @@ ipcMain.on("log-message", (event, { level, message }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let idCardWorker = null;
|
let idCardWorker = null;
|
||||||
let mainWindow = null;
|
|
||||||
|
|
||||||
function createIdCardWorker() {
|
function createIdCardWorker() {
|
||||||
if (idCardWorker) return;
|
if (idCardWorker) return;
|
||||||
@@ -53,7 +56,7 @@ function createIdCardWorker() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createWindow() {
|
function createWindow() {
|
||||||
const win = new BrowserWindow({
|
mainWindow = new BrowserWindow({
|
||||||
width: 1080,
|
width: 1080,
|
||||||
height: 1920,
|
height: 1920,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
@@ -63,21 +66,112 @@ function createWindow() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
mainWindow = win;
|
|
||||||
|
|
||||||
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>¼<EFBFBD><C2BC><EFBFBD> Vite <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ַ
|
|
||||||
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>¼<EFBFBD><C2BC>ش<EFBFBD><D8B4><EFBFBD><EFBFBD><EFBFBD> index.html
|
|
||||||
const isDev = !app.isPackaged;
|
const isDev = !app.isPackaged;
|
||||||
|
|
||||||
if (isDev) {
|
if (isDev) {
|
||||||
win.loadURL("http://localhost:5173");
|
mainWindow.loadURL("http://localhost:5173");
|
||||||
// <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>߹<EFBFBD><EFBFBD><EFBFBD>
|
// 打开开发者工具
|
||||||
win.webContents.openDevTools();
|
mainWindow.webContents.openDevTools();
|
||||||
} else {
|
} else {
|
||||||
win.loadFile(path.join(__dirname, "../dist/index.html"));
|
mainWindow.loadFile(path.join(__dirname, "../dist/index.html"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理PDF获取请求(绕过CORS)
|
||||||
|
ipcMain.handle("fetch-pdf", async (event, pdfUrl) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const protocol = pdfUrl.startsWith("https") ? https : http;
|
||||||
|
|
||||||
|
protocol
|
||||||
|
.get(pdfUrl, (response) => {
|
||||||
|
// 处理重定向
|
||||||
|
if (response.statusCode === 301 || response.statusCode === 302) {
|
||||||
|
const redirectUrl = response.headers.location;
|
||||||
|
protocol
|
||||||
|
.get(redirectUrl, (redirectResponse) => {
|
||||||
|
const chunks = [];
|
||||||
|
redirectResponse.on("data", (chunk) => chunks.push(chunk));
|
||||||
|
redirectResponse.on("end", () => {
|
||||||
|
const buffer = Buffer.concat(chunks);
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: buffer.toString("base64"),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
redirectResponse.on("error", (error) => {
|
||||||
|
reject({ success: false, error: error.message });
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.on("error", (error) => {
|
||||||
|
reject({ success: false, error: error.message });
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const chunks = [];
|
||||||
|
response.on("data", (chunk) => chunks.push(chunk));
|
||||||
|
response.on("end", () => {
|
||||||
|
const buffer = Buffer.concat(chunks);
|
||||||
|
resolve({
|
||||||
|
success: true,
|
||||||
|
data: buffer.toString("base64"),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
response.on("error", (error) => {
|
||||||
|
reject({ success: false, error: error.message });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on("error", (error) => {
|
||||||
|
reject({ success: false, error: error.message });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理PDF打印请求
|
||||||
|
ipcMain.handle("print-pdf", async (event, pdfUrl) => {
|
||||||
|
try {
|
||||||
|
// 创建一个隐藏的窗口用于加载PDF
|
||||||
|
const printWindow = new BrowserWindow({
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
show: false,
|
||||||
|
webPreferences: {
|
||||||
|
nodeIntegration: false,
|
||||||
|
contextIsolation: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 加载PDF
|
||||||
|
await printWindow.loadURL(pdfUrl);
|
||||||
|
|
||||||
|
// 等待页面加载完成
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
printWindow.webContents.on("did-finish-load", resolve);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 静默打印(直接调用系统打印对话框)
|
||||||
|
printWindow.webContents.print(
|
||||||
|
{
|
||||||
|
silent: false, // 显示打印对话框
|
||||||
|
printBackground: true,
|
||||||
|
margins: {
|
||||||
|
marginType: "none",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
(success, errorType) => {
|
||||||
|
printWindow.close();
|
||||||
|
if (!success) {
|
||||||
|
console.error("Print failed:", errorType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Print error:", error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
createWindow();
|
createWindow();
|
||||||
createIdCardWorker();
|
createIdCardWorker();
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
const { contextBridge, ipcRenderer } = require("electron");
|
const { contextBridge, ipcRenderer } = require("electron");
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld("electronAPI", {
|
contextBridge.exposeInMainWorld("electronAPI", {
|
||||||
|
// 获取PDF(绕过CORS)
|
||||||
|
fetchPdf: (pdfUrl) => ipcRenderer.invoke("fetch-pdf", pdfUrl),
|
||||||
|
// 打印PDF
|
||||||
|
printPdf: (pdfUrl) => ipcRenderer.invoke("print-pdf", pdfUrl),
|
||||||
startIdCardListen: () => ipcRenderer.invoke("start_idcard_listen"),
|
startIdCardListen: () => ipcRenderer.invoke("start_idcard_listen"),
|
||||||
stopIdCardListen: () => ipcRenderer.invoke("stop_idcard_listen"),
|
stopIdCardListen: () => ipcRenderer.invoke("stop_idcard_listen"),
|
||||||
onIdCardData: (callback) =>
|
onIdCardData: (callback) =>
|
||||||
|
|||||||
1812
package-lock.json
generated
1812
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -21,11 +21,13 @@
|
|||||||
"koffi": "^2.14.1",
|
"koffi": "^2.14.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-router-dom": "^7.9.6"
|
"react-pdf": "5.7.2",
|
||||||
|
"react-router-dom": "6.11.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^19.2.6",
|
"@types/react": "^19.2.6",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"@types/react-pdf": "^6.2.0",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"concurrently": "^8.2.2",
|
"concurrently": "^8.2.2",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import U4 from "./pages/U4/u4";
|
|||||||
|
|
||||||
import UI6 from "./pages/UI6/UI6";
|
import UI6 from "./pages/UI6/UI6";
|
||||||
import UI7 from "./pages/UI7/UI7";
|
import UI7 from "./pages/UI7/UI7";
|
||||||
|
import UI8 from "./pages/UI8/UI8";
|
||||||
|
import UI9 from "./pages/UI9/UI9";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [time, setTime] = useState<string>(() => formatDate(new Date()));
|
const [time, setTime] = useState<string>(() => formatDate(new Date()));
|
||||||
@@ -54,6 +56,8 @@ function App() {
|
|||||||
<Route path="/u4" element={<U4 />} />
|
<Route path="/u4" element={<U4 />} />
|
||||||
<Route path="/UI6" element={<UI6 />} />
|
<Route path="/UI6" element={<UI6 />} />
|
||||||
<Route path="/UI7" element={<UI7 />} />
|
<Route path="/UI7" element={<UI7 />} />
|
||||||
|
<Route path="/UI8" element={<UI8 />} />
|
||||||
|
<Route path="/UI9" element={<UI9 />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
BIN
src/assets/UI9A.png
Normal file
BIN
src/assets/UI9A.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
BIN
src/assets/UI9B.png
Normal file
BIN
src/assets/UI9B.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 104 KiB |
BIN
src/assets/ui8A.png
Normal file
BIN
src/assets/ui8A.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
BIN
src/assets/ui8B.png
Normal file
BIN
src/assets/ui8B.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 57 KiB |
10
src/electron.d.ts
vendored
Normal file
10
src/electron.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
// Electron API 类型声明
|
||||||
|
interface ElectronAPI {
|
||||||
|
fetchPdf: (pdfUrl: string) => Promise<{ success: boolean; data?: string; error?: string }>;
|
||||||
|
printPdf: (pdfUrl: string) => Promise<{ success: boolean; error?: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Window {
|
||||||
|
electronAPI?: ElectronAPI;
|
||||||
|
}
|
||||||
|
|
||||||
64
src/main.tsx
64
src/main.tsx
@@ -1,3 +1,67 @@
|
|||||||
|
// Polyfill for Promise.withResolvers (ES2024) - needed for Electron 22
|
||||||
|
if (!(Promise as any).withResolvers) {
|
||||||
|
(Promise as any).withResolvers = function <T>() {
|
||||||
|
let resolve!: (value: T | PromiseLike<T>) => void;
|
||||||
|
let reject!: (reason?: any) => void;
|
||||||
|
const promise = new Promise<T>((res, rej) => {
|
||||||
|
resolve = res;
|
||||||
|
reject = rej;
|
||||||
|
});
|
||||||
|
return { promise, resolve, reject };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Polyfill for URL.parse (Node.js url.parse compatibility) - needed for Electron 22
|
||||||
|
// This creates a global url object with parse method for pdfjs-dist compatibility
|
||||||
|
if (typeof URL !== "undefined" && !(URL as any).parse) {
|
||||||
|
(URL as any).parse = function (urlString: string) {
|
||||||
|
try {
|
||||||
|
const urlObj = new URL(urlString, typeof window !== "undefined" ? window.location.href : undefined);
|
||||||
|
const params: Record<string, string> = {};
|
||||||
|
urlObj.searchParams.forEach((value, key) => {
|
||||||
|
params[key] = value;
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
protocol: urlObj.protocol.replace(":", ""),
|
||||||
|
slashes: true,
|
||||||
|
auth: urlObj.username && urlObj.password ? `${urlObj.username}:${urlObj.password}` : null,
|
||||||
|
host: urlObj.host,
|
||||||
|
hostname: urlObj.hostname,
|
||||||
|
hash: urlObj.hash || null,
|
||||||
|
search: urlObj.search || null,
|
||||||
|
query: params,
|
||||||
|
pathname: urlObj.pathname,
|
||||||
|
path: urlObj.pathname + urlObj.search,
|
||||||
|
href: urlObj.href,
|
||||||
|
port: urlObj.port || null,
|
||||||
|
};
|
||||||
|
} catch (_err) {
|
||||||
|
const match = urlString.match(/^(([^:/?#]+):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/);
|
||||||
|
if (!match) return null;
|
||||||
|
const params: Record<string, string> = {};
|
||||||
|
if (match[7]) {
|
||||||
|
new URLSearchParams(match[7]).forEach((value, key) => {
|
||||||
|
params[key] = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
protocol: match[2] || null,
|
||||||
|
slashes: !!match[3],
|
||||||
|
auth: null,
|
||||||
|
host: match[4] || null,
|
||||||
|
hostname: match[4] ? match[4].split(":")[0] : null,
|
||||||
|
hash: match[8] || null,
|
||||||
|
search: match[6] || null,
|
||||||
|
query: params,
|
||||||
|
pathname: match[5] || null,
|
||||||
|
path: (match[5] || "") + (match[6] || ""),
|
||||||
|
href: urlString,
|
||||||
|
port: match[4] && match[4].includes(":") ? match[4].split(":")[1] : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import { HashRouter } from "react-router-dom";
|
import { HashRouter } from "react-router-dom";
|
||||||
|
|||||||
223
src/pages/UI8/UI8.css
Normal file
223
src/pages/UI8/UI8.css
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
.ui8-table-container {
|
||||||
|
width: 754px;
|
||||||
|
max-height: 881px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
/* 隐藏滚动条但保持滚动功能 */
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
-ms-overflow-style: none; /* IE 和 Edge */
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui8-table-container::-webkit-scrollbar {
|
||||||
|
display: none; /* Chrome, Safari, Opera */
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui8-table {
|
||||||
|
width: 754px;
|
||||||
|
border-collapse: collapse;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui8-table-header {
|
||||||
|
color: rgba(0, 45, 93, 1);
|
||||||
|
font-size: 32px;
|
||||||
|
font-family: NotoSansCJKsc-Bold;
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: center;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid rgba(0, 45, 93, 0.2);
|
||||||
|
background-color: rgba(233, 242, 245, 1);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui8-table-dept {
|
||||||
|
width: 200px;
|
||||||
|
background-color: #b12651;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui8-table-project {
|
||||||
|
width: 554px;
|
||||||
|
background-color: #053875;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui8-table-row {
|
||||||
|
border-bottom: 2px solid #d3d3d3;
|
||||||
|
border-right: 2px solid #d3d3d3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui8-table-dept-cell {
|
||||||
|
color: black;
|
||||||
|
background-color: #daeef2;
|
||||||
|
font-size: 24px;
|
||||||
|
font-family: NotoSansCJKsc-Medium;
|
||||||
|
font-weight: 500;
|
||||||
|
padding-left: 20px;
|
||||||
|
border-right: 2px solid #d3d3d3;
|
||||||
|
border-left: 2px solid #d3d3d3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui8-table-project-cell {
|
||||||
|
color: black;
|
||||||
|
font-size: 24px;
|
||||||
|
padding: 0;
|
||||||
|
font-family: NotoSansCJKsc-Regular;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui8-project-item {
|
||||||
|
border-bottom: 2px solid #d3d3d3;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui8-project-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* PDF 展示容器 */
|
||||||
|
.ui8-pdf-container {
|
||||||
|
height: 1000px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
user-select: none;
|
||||||
|
touch-action: pan-y; /* 允许垂直触摸滑动 */
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui8-pdf-page-wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 1200px;
|
||||||
|
transition: transform 0.3s ease-out, opacity 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 向上滑动动画(下一页) */
|
||||||
|
.ui8-pdf-page-wrapper.slide-up {
|
||||||
|
animation: slideUpOut 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUpOut {
|
||||||
|
0% {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateY(-10px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 向下滑动动画(上一页) */
|
||||||
|
.ui8-pdf-page-wrapper.slide-down {
|
||||||
|
animation: slideDownOut 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideDownOut {
|
||||||
|
0% {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateY(10px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui8-pdf-page {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui8-pdf-page canvas {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes swipeHint {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 0.65;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(-5px) scale(1.02);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* PDF 翻页控制 */
|
||||||
|
.ui8-pdf-controls {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 30px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 0;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui8-page-info {
|
||||||
|
font-size: 26px;
|
||||||
|
font-family: NotoSansCJKsc-Bold;
|
||||||
|
font-weight: 700;
|
||||||
|
color: rgba(0, 45, 93, 1);
|
||||||
|
min-width: 140px;
|
||||||
|
text-align: center;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
animation: fadeIn 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui8-loading,
|
||||||
|
.ui8-error {
|
||||||
|
padding: 40px;
|
||||||
|
font-size: 28px;
|
||||||
|
font-family: NotoSansCJKsc-Medium;
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: center;
|
||||||
|
color: rgba(0, 45, 93, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui8-error {
|
||||||
|
color: #b12651;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui8-right-section {
|
||||||
|
z-index: 9999;
|
||||||
|
position: fixed;
|
||||||
|
right: 0;
|
||||||
|
bottom: 250px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui8-right-section :last-child {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-left: 100px;
|
||||||
|
}
|
||||||
229
src/pages/UI8/UI8.tsx
Normal file
229
src/pages/UI8/UI8.tsx
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
import React, { useState, useRef, useEffect } from "react";
|
||||||
|
import { Document, Page, pdfjs } from "react-pdf";
|
||||||
|
import "react-pdf/dist/esm/Page/AnnotationLayer.css";
|
||||||
|
import "./UI8.css";
|
||||||
|
import "../../assets/css/basic.css";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import BackButton from "../../components/BackButton";
|
||||||
|
import ConfirmButton from "../../components/ConfirmButton";
|
||||||
|
import ui8A from "../../assets/ui8A.png";
|
||||||
|
import ui8B from "../../assets/ui8B.png";
|
||||||
|
|
||||||
|
|
||||||
|
// 设置 PDF.js worker(react-pdf 5.7.2 使用 pdfjs-dist 2.12.313)
|
||||||
|
pdfjs.GlobalWorkerOptions.workerSrc = `//cdnjs.cloudflare.com/ajax/libs/pdf.js/2.12.313/pdf.worker.min.js`;
|
||||||
|
|
||||||
|
const PDF_URL = "https://alist.ambigrat.com/d/cos/test/testPdf.pdf?sign=mELe-vb-ShXHDCtZrP2Hw5nlOvEMEsNkJzaGUUyqDg4=:0";
|
||||||
|
|
||||||
|
const UI8: React.FC = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [numPages, setNumPages] = useState<number>(0);
|
||||||
|
const [pageNumber, setPageNumber] = useState<number>(1);
|
||||||
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
|
const [error, setError] = useState<string>("");
|
||||||
|
const [isPrinting, setIsPrinting] = useState<boolean>(false);
|
||||||
|
const [pdfData, setPdfData] = useState<string | null>(null);
|
||||||
|
const [isAnimating, setIsAnimating] = useState<boolean>(false);
|
||||||
|
const [animationDirection, setAnimationDirection] = useState<"up" | "down" | null>(null);
|
||||||
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const touchStartRef = useRef<{ x: number; y: number } | null>(null);
|
||||||
|
|
||||||
|
// 加载PDF数据(绕过CORS)
|
||||||
|
useEffect(() => {
|
||||||
|
const loadPdf = async () => {
|
||||||
|
if (!window.electronAPI?.fetchPdf) {
|
||||||
|
// 非Electron环境,直接使用URL
|
||||||
|
setPdfData(PDF_URL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const result = await window.electronAPI.fetchPdf(PDF_URL);
|
||||||
|
if (result.success && result.data) {
|
||||||
|
// 将base64转换为data URL
|
||||||
|
setPdfData(`data:application/pdf;base64,${result.data}`);
|
||||||
|
} else {
|
||||||
|
setError(`PDF加载失败: ${result.error || "未知错误"}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("PDF fetch error:", err);
|
||||||
|
setError("PDF加载失败,请检查网络连接");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadPdf();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
navigate(-1);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 打印PDF功能
|
||||||
|
const handleConfirm = async () => {
|
||||||
|
if (!window.electronAPI?.printPdf) {
|
||||||
|
alert("打印功能不可用,请在 Electron 环境中运行");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsPrinting(true);
|
||||||
|
try {
|
||||||
|
const result = await window.electronAPI.printPdf(PDF_URL);
|
||||||
|
if (!result.success) {
|
||||||
|
alert(`打印失败: ${result.error || "未知错误"}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Print error:", error);
|
||||||
|
alert("打印失败,请重试");
|
||||||
|
} finally {
|
||||||
|
setIsPrinting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDocumentLoadSuccess = ({ numPages }: { numPages: number }) => {
|
||||||
|
setNumPages(numPages);
|
||||||
|
setLoading(false);
|
||||||
|
setError("");
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDocumentLoadError = (error: Error) => {
|
||||||
|
setError("PDF 加载失败,请检查网络连接");
|
||||||
|
setLoading(false);
|
||||||
|
console.error("PDF load error:", error);
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToPrevPage = () => {
|
||||||
|
if (isAnimating || pageNumber <= 1) return;
|
||||||
|
setIsAnimating(true);
|
||||||
|
setAnimationDirection("down");
|
||||||
|
setTimeout(() => {
|
||||||
|
setPageNumber((prev) => Math.max(1, prev - 1));
|
||||||
|
setIsAnimating(false);
|
||||||
|
setAnimationDirection(null);
|
||||||
|
}, 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToNextPage = () => {
|
||||||
|
if (isAnimating || pageNumber >= numPages) return;
|
||||||
|
setIsAnimating(true);
|
||||||
|
setAnimationDirection("up");
|
||||||
|
setTimeout(() => {
|
||||||
|
setPageNumber((prev) => Math.min(numPages, prev + 1));
|
||||||
|
setIsAnimating(false);
|
||||||
|
setAnimationDirection(null);
|
||||||
|
}, 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听触摸事件实现上下滑动翻页
|
||||||
|
useEffect(() => {
|
||||||
|
const container = scrollContainerRef.current;
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const handleTouchStart = (e: TouchEvent) => {
|
||||||
|
const touch = e.touches[0];
|
||||||
|
touchStartRef.current = {
|
||||||
|
x: touch.clientX,
|
||||||
|
y: touch.clientY,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTouchEnd = (e: TouchEvent) => {
|
||||||
|
if (!touchStartRef.current) return;
|
||||||
|
|
||||||
|
const touch = e.changedTouches[0];
|
||||||
|
const deltaX = touch.clientX - touchStartRef.current.x;
|
||||||
|
const deltaY = touch.clientY - touchStartRef.current.y;
|
||||||
|
|
||||||
|
// 判断是否为有效滑动(至少50px,且垂直滑动大于水平滑动)
|
||||||
|
const minSwipeDistance = 50;
|
||||||
|
if (Math.abs(deltaY) > minSwipeDistance && Math.abs(deltaY) > Math.abs(deltaX)) {
|
||||||
|
if (deltaY < 0) {
|
||||||
|
// 向上滑动 = 下一页
|
||||||
|
goToNextPage();
|
||||||
|
} else if (deltaY > 0) {
|
||||||
|
// 向下滑动 = 上一页
|
||||||
|
goToPrevPage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
touchStartRef.current = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTouchCancel = () => {
|
||||||
|
touchStartRef.current = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
container.addEventListener("touchstart", handleTouchStart, { passive: true });
|
||||||
|
container.addEventListener("touchend", handleTouchEnd, { passive: true });
|
||||||
|
container.addEventListener("touchcancel", handleTouchCancel, { passive: true });
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
container.removeEventListener("touchstart", handleTouchStart);
|
||||||
|
container.removeEventListener("touchend", handleTouchEnd);
|
||||||
|
container.removeEventListener("touchcancel", handleTouchCancel);
|
||||||
|
};
|
||||||
|
}, [numPages, pageNumber]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="basic-root">
|
||||||
|
<div className="basic-white-block">
|
||||||
|
<div className="basic-content">
|
||||||
|
<div className="ui8-pdf-container" ref={scrollContainerRef}>
|
||||||
|
{/* {loading && <div className="ui8-loading">加载中...</div>} */}
|
||||||
|
{error && <div className="ui8-error">{error}</div>}
|
||||||
|
|
||||||
|
{pdfData && (
|
||||||
|
<Document
|
||||||
|
file={pdfData}
|
||||||
|
onLoadSuccess={onDocumentLoadSuccess}
|
||||||
|
onLoadError={onDocumentLoadError}
|
||||||
|
loading=""
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`ui8-pdf-page-wrapper ${
|
||||||
|
animationDirection === "up" ? "slide-up" :
|
||||||
|
animationDirection === "down" ? "slide-down" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Page
|
||||||
|
pageNumber={pageNumber}
|
||||||
|
renderTextLayer={false}
|
||||||
|
renderAnnotationLayer={false}
|
||||||
|
className="ui8-pdf-page"
|
||||||
|
width={920}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Document>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* {numPages > 0 && (
|
||||||
|
<>
|
||||||
|
|
||||||
|
<div className="ui8-pdf-controls">
|
||||||
|
<span className="ui8-page-info">
|
||||||
|
{pageNumber} / {numPages}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)} */}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="ui8-right-section">
|
||||||
|
<img src={ui8A} alt="" />
|
||||||
|
<img src={ui8B} alt="" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="basic-confirm-section">
|
||||||
|
<BackButton text="返回" onClick={handleBack} />
|
||||||
|
<ConfirmButton
|
||||||
|
text={isPrinting ? "打印中..." : "打印"}
|
||||||
|
onClick={handleConfirm}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UI8;
|
||||||
68
src/pages/UI9/UI9.css
Normal file
68
src/pages/UI9/UI9.css
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
.ui9-root {
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 45%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.ui9-title {
|
||||||
|
width: 690px;
|
||||||
|
height: 88px;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
color: rgba(0, 45, 93, 1);
|
||||||
|
font-size: 92px;
|
||||||
|
font-family: NotoSansCJKsc-Bold;
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: left;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.ui9-text {
|
||||||
|
width: 628px;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
color: rgba(0, 45, 93, 1);
|
||||||
|
font-size: 57px;
|
||||||
|
font-family: NotoSansCJKsc-Bold;
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: left;
|
||||||
|
line-height: 80px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.ui9-instruction {
|
||||||
|
height: 34px;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
color: rgba(0, 45, 93, 1);
|
||||||
|
font-size: 35px;
|
||||||
|
font-family: NotoSansCJKsc-Medium;
|
||||||
|
font-weight: Medium;
|
||||||
|
text-align: left;
|
||||||
|
white-space: nowrap;
|
||||||
|
line-height: 46px;
|
||||||
|
margin-bottom: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui9-confirm-section {
|
||||||
|
width: 896px;
|
||||||
|
margin: 32px 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui9-vip-img {
|
||||||
|
margin: 20px 0 0px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui9-qrcode {
|
||||||
|
width: 500px;
|
||||||
|
height: 500px;
|
||||||
|
margin: 40px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui9-success-img {
|
||||||
|
width: 358px;
|
||||||
|
height: 358px;
|
||||||
|
margin: 110px 0;
|
||||||
|
}
|
||||||
55
src/pages/UI9/UI9.tsx
Normal file
55
src/pages/UI9/UI9.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import React from "react";
|
||||||
|
import "./UI9.css";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import DecorLine from "../../components/DecorLine";
|
||||||
|
|
||||||
|
import BackButton from "../../components/BackButton";
|
||||||
|
import ConfirmButton from "../../components/ConfirmButton";
|
||||||
|
import success from "../../assets/success.png";
|
||||||
|
import UI9A from "../../assets/ui9A.png";
|
||||||
|
import UI9B from "../../assets/ui9B.png";
|
||||||
|
const UI9: React.FC = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
// 是否认证成功
|
||||||
|
const isAuthenticated = false;
|
||||||
|
const handleBack = () => {
|
||||||
|
navigate(-1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
// 是否套餐待定
|
||||||
|
const isPackageUndecided = true;
|
||||||
|
if (isPackageUndecided) {
|
||||||
|
//navigate("/u4");
|
||||||
|
} else {
|
||||||
|
//navigate("/u5");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="ui9-root">
|
||||||
|
<span className="ui9-title">太平VIP客户认证</span>
|
||||||
|
<DecorLine />
|
||||||
|
{isAuthenticated ? (
|
||||||
|
<>
|
||||||
|
<span className="ui9-text">认证成功</span>
|
||||||
|
<img className="ui9-success-img" src={success} alt="success" />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span className="ui9-text">一对一专属服务</span>
|
||||||
|
<img className="ui9-vip-img" src={UI9A} alt="vip" />
|
||||||
|
{/* 认证二维码 */}
|
||||||
|
<img className="ui9-qrcode" src={UI9B} alt="二维码位置" />
|
||||||
|
<span className="ui9-instruction">如您体检方面有任何问题,可以联系咨询企业微信客服</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<div className="ui9-confirm-section">
|
||||||
|
<BackButton text="返回" onClick={handleBack} />
|
||||||
|
<ConfirmButton text="确认" onClick={handleConfirm} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UI9;
|
||||||
Reference in New Issue
Block a user