Ver código fonte

no message

MergeProblem1
pai
commit
5c07df417f
4 arquivos alterados com 133 adições e 2 exclusões
  1. +2
    -1
      src/app/(main)/testing/page.tsx
  2. +28
    -0
      src/app/api/laserPrint/actions.ts
  3. +85
    -1
      src/components/LaserPrint/LaserPrintSearch.tsx
  4. +18
    -0
      src/utils/formatHongKongDateTime.ts

+ 2
- 1
src/app/(main)/testing/page.tsx Ver arquivo

@@ -20,6 +20,7 @@ import {
} from "@mui/material";
import { FileDownload } from "@mui/icons-material";
import dayjs from "dayjs";
import { formatHongKongDateTime } from "@/utils/formatHongKongDateTime";
import { NEXT_PUBLIC_API_URL } from "@/config/api";
import { clientAuthFetch } from "@/app/utils/clientAuthFetch";
import LotLabelPrintModal from "@/components/InventorySearch/LotLabelPrintModal";
@@ -502,7 +503,7 @@ export default function TestingPage() {
display="block"
sx={{ mt: 0.5 }}
>
{laserLastReceive.sentAt ?? ""} {laserLastReceive.source ?? ""}
{formatHongKongDateTime(laserLastReceive.sentAt)} {laserLastReceive.source ?? ""}
</Typography>
</Alert>
) : null}


+ 28
- 0
src/app/api/laserPrint/actions.ts Ver arquivo

@@ -38,6 +38,20 @@ export interface LaserBag2Settings {
lastReceiveSuccess?: LaserLastReceiveSuccess | null;
}

/** Live TCP queries GetMarkData / GetMarkStatus / GetMarkedCount (same host/port as laser send). */
export interface LaserBag2MarkInfo {
host: string;
port: number;
markData?: string | null;
/** 0 idle, 1 marking, 2 other — when the device returns a parseable digit */
markStatus?: number | null;
markStatusLabel?: string | null;
markedCount?: number | null;
rawMarkStatus?: string | null;
rawMarkedCount?: string | null;
error?: string | null;
}

export interface LaserBag2SendRequest {
itemId: number | null;
stockInLineId: number | null;
@@ -109,6 +123,20 @@ export async function fetchLaserBag2Settings(): Promise<LaserBag2Settings> {
return res.json() as Promise<LaserBag2Settings>;
}

export async function fetchLaserBag2MarkInfo(): Promise<LaserBag2MarkInfo> {
const base = (NEXT_PUBLIC_API_URL ?? "").replace(/\/$/, "");
if (!base) {
throw new Error("NEXT_PUBLIC_API_URL is not set.");
}
const url = `${base}/plastic/laser-bag2-mark-info`;
const res = await clientAuthFetch(url, { method: "GET" });
if (!res.ok) {
const body = await res.text().catch(() => "");
throw new Error(`讀取打標狀態失敗(${res.status})${body ? `:${body.slice(0, 200)}` : ""}`);
}
return res.json() as Promise<LaserBag2MarkInfo>;
}

export async function sendLaserBag2Job(body: LaserBag2SendRequest): Promise<LaserBag2SendResponse> {
const url = `${NEXT_PUBLIC_API_URL}/plastic/print-laser-bag2`;
const res = await clientAuthFetch(url, {


+ 85
- 1
src/components/LaserPrint/LaserPrintSearch.tsx Ver arquivo

@@ -21,14 +21,17 @@ import ChevronRight from "@mui/icons-material/ChevronRight";
import Settings from "@mui/icons-material/Settings";
import {
checkPrinterStatus,
fetchLaserBag2MarkInfo,
fetchLaserJobOrders,
fetchLaserBag2Settings,
type LaserBag2MarkInfo,
type LaserLastReceiveSuccess,
JobOrderListItem,
patchSetting,
sendLaserBag2Job,
} from "@/app/api/laserPrint/actions";
import dayjs from "dayjs";
import { formatHongKongDateTime } from "@/utils/formatHongKongDateTime";

const BG_TOP = "#E8F4FC";
const BG_LIST = "#D4E8F7";
@@ -45,6 +48,8 @@ const PRINTER_RETRY_MS = 30 * 1000;
const LASER_SEND_COUNT = 3;
const BETWEEN_SEND_MS = 3000;
const SUCCESS_SIGNAL_MS = 3500;
/** Poll laser TCP GetMarkData / GetMarkStatus / GetMarkedCount */
const MARK_INFO_MS = 5000;

function formatQty(val: number | null | undefined): string {
if (val == null) return "—";
@@ -63,6 +68,11 @@ function getBatch(jo: JobOrderListItem): string {
return (jo.lotNo || "—").trim() || "—";
}

function fmtMarkText(s: string | null | undefined): string {
if (s == null || s === "") return "—";
return s;
}

function delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
@@ -88,6 +98,8 @@ const LaserPrintSearch: React.FC = () => {
const [settingsLoaded, setSettingsLoaded] = useState(false);
const [printerConnected, setPrinterConnected] = useState(false);
const [printerMessage, setPrinterMessage] = useState("檸檬機(激光機)未連接");
const [markInfo, setMarkInfo] = useState<LaserBag2MarkInfo | null>(null);
const [markInfoError, setMarkInfoError] = useState<string | null>(null);

const loadSystemSettings = useCallback(async () => {
try {
@@ -173,6 +185,23 @@ const LaserPrintSearch: React.FC = () => {
return () => clearInterval(id);
}, [printerConnected, checkLaser, settingsLoaded]);

const loadMarkInfo = useCallback(async () => {
try {
const m = await fetchLaserBag2MarkInfo();
setMarkInfo(m);
setMarkInfoError(null);
} catch (e) {
setMarkInfoError(e instanceof Error ? e.message : "無法讀取打標狀態");
}
}, []);

useEffect(() => {
if (!settingsLoaded) return;
void loadMarkInfo();
const id = setInterval(() => void loadMarkInfo(), MARK_INFO_MS);
return () => clearInterval(id);
}, [settingsLoaded, loadMarkInfo]);

const goPrevDay = () => {
setPlanDate((d) => dayjs(d).subtract(1, "day").format("YYYY-MM-DD"));
};
@@ -229,6 +258,7 @@ const LaserPrintSearch: React.FC = () => {
: "";
setSuccessSignal(`已送出 ${LASER_SEND_COUNT} 次至檸檬機(激光機)${ackHint}`);
await loadSystemSettings();
void loadMarkInfo();
} catch (e) {
setErrorSnackbar({
open: true,
@@ -249,6 +279,7 @@ const LaserPrintSearch: React.FC = () => {
void checkLaser();
await loadSystemSettings();
void loadJobOrders(false);
void loadMarkInfo();
} catch (e) {
setErrorSnackbar({
open: true,
@@ -284,12 +315,65 @@ const LaserPrintSearch: React.FC = () => {
JSON:{lastReceiveJson ?? "—"}
</Typography>
<Typography variant="caption" color="text.secondary" display="block" sx={{ mt: 0.5 }}>
時間:{lastLaserReceive.sentAt ?? "—"} 來源:{lastLaserReceive.source ?? "—"}
時間:{formatHongKongDateTime(lastLaserReceive.sentAt)} 來源:{lastLaserReceive.source ?? "—"}
{lastLaserReceive.printerAck ? ` 回覆:${lastLaserReceive.printerAck}` : ""}
</Typography>
</Alert>
)}

{settingsLoaded && (
<Paper sx={{ p: 2, mb: 2, backgroundColor: BG_TOP }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, mb: 1 }}>
目前激光機打標(TCP:GetMarkData/GetMarkStatus/GetMarkedCount)
{markInfo ? ` ${markInfo.host}:${markInfo.port}` : ""}
</Typography>
{markInfoError && (
<Typography variant="body2" color="error" sx={{ mb: 1 }}>
{markInfoError}
</Typography>
)}
{!markInfo && !markInfoError && (
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<CircularProgress size={22} />
<Typography variant="body2" color="text.secondary">
讀取中…
</Typography>
</Box>
)}
{markInfo && (
<Stack spacing={0.75}>
<Typography variant="body2">
<strong>GetMarkStatus</strong>(0 待機/1 打標中/2 其他):
{markInfo.markStatusLabel ?? "—"}
{markInfo.markStatus != null ? `(${markInfo.markStatus})` : ""}
{markInfo.rawMarkStatus ? ` 原始:${markInfo.rawMarkStatus}` : ""}
</Typography>
<Typography variant="body2" sx={{ wordBreak: "break-word" }}>
<strong>GetMarkData</strong>(目前標記字串):
{fmtMarkText(markInfo.markData)}
</Typography>
<Typography variant="body2">
<strong>GetMarkedCount</strong>:
{markInfo.markedCount != null
? markInfo.markedCount.toLocaleString()
: fmtMarkText(markInfo.rawMarkedCount)}
{markInfo.markedCount != null &&
markInfo.rawMarkedCount != null &&
markInfo.rawMarkedCount !== "" &&
String(markInfo.markedCount) !== markInfo.rawMarkedCount.trim()
? ` 原始:${markInfo.rawMarkedCount}`
: ""}
</Typography>
{markInfo.error ? (
<Typography variant="caption" color="warning.main" display="block">
部分查詢:{markInfo.error}
</Typography>
) : null}
</Stack>
)}
</Paper>
)}

<Paper sx={{ p: 2, mb: 2, backgroundColor: BG_TOP }}>
<Stack direction="row" alignItems="center" justifyContent="space-between" flexWrap="wrap" gap={2}>
<Stack direction="row" alignItems="center" spacing={2}>


+ 18
- 0
src/utils/formatHongKongDateTime.ts Ver arquivo

@@ -0,0 +1,18 @@
import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";
import timezone from "dayjs/plugin/timezone";

dayjs.extend(utc);
dayjs.extend(timezone);

const HK = "Asia/Hong_Kong";

/**
* Formats an ISO-8601 instant (e.g. Java `Instant.toString()`) for Hong Kong (GMT+8).
*/
export function formatHongKongDateTime(value: string | null | undefined): string {
if (value == null || value === "") return "—";
const ms = Date.parse(value);
if (Number.isNaN(ms)) return value;
return dayjs.utc(ms).tz(HK).format("YYYY-MM-DD HH:mm:ss [HKT]");
}

Carregando…
Cancelar
Salvar