This commit is contained in:
yuchenglong
2025-11-20 18:07:38 +08:00
15 changed files with 2427 additions and 160 deletions

View File

@@ -10,6 +10,8 @@ import U4 from "./pages/U4/u4";
import UI6 from "./pages/UI6/UI6";
import UI7 from "./pages/UI7/UI7";
import UI8 from "./pages/UI8/UI8";
import UI9 from "./pages/UI9/UI9";
function App() {
const [time, setTime] = useState<string>(() => formatDate(new Date()));
@@ -54,6 +56,8 @@ function App() {
<Route path="/u4" element={<U4 />} />
<Route path="/UI6" element={<UI6 />} />
<Route path="/UI7" element={<UI7 />} />
<Route path="/UI8" element={<UI8 />} />
<Route path="/UI9" element={<UI9 />} />
</Routes>
</div>
</div>

BIN
src/assets/UI9A.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
src/assets/UI9B.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

BIN
src/assets/ui8A.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

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
View 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;
}

View File

@@ -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 ReactDOM from "react-dom/client";
import { HashRouter } from "react-router-dom";

223
src/pages/UI8/UI8.css Normal file
View 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
View 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 workerreact-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
View 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
View 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;