feat(preview): 添加PC端预览页面和发货单预览功能
- 添加了PC端预览页面,支持多种设备模拟预览 - 在采购审核页面集成了发货单预览按钮 - 重构了发货表单组件,移除ref暴露机制 - 更新订单转换器以支持发货单数据转换 - 在发票上传页面添加供应商选择器组件 - 添加初始车次号相关字段到API类型定义 - 将发货表单中的Input组件替换为TextArea组件 - 修复了订单项ID匹配逻辑错误
This commit is contained in:
parent
d6e3afd100
commit
fffb0c7269
@ -4,6 +4,7 @@ import {
|
||||
DatePicker,
|
||||
Input,
|
||||
PickerOption,
|
||||
TextArea,
|
||||
Toast,
|
||||
} from "@nutui/nutui-react-taro";
|
||||
import dayjs from "dayjs";
|
||||
@ -22,7 +23,6 @@ export interface Step1FormRef {
|
||||
|
||||
const Step1Form = forwardRef<Step1FormRef, Step1FormProps>((props, ref) => {
|
||||
const { moduleList, orderShip, setOrderShip, readOnly = false } = props;
|
||||
console.log("moduleList", moduleList, orderShip);
|
||||
|
||||
// 当天和未来10天
|
||||
const startDate = new Date();
|
||||
@ -288,7 +288,7 @@ const Step1Form = forwardRef<Step1FormRef, Step1FormProps>((props, ref) => {
|
||||
<View
|
||||
className={`flex h-10 w-full items-center rounded-md ${formErrors.remark ? "border-4 border-red-500" : "border-4 border-gray-300"}`}
|
||||
>
|
||||
<Input
|
||||
<TextArea
|
||||
disabled={readOnly}
|
||||
value={orderShip?.remark || ""}
|
||||
onChange={(value) => {
|
||||
@ -323,7 +323,6 @@ const Step1Form = forwardRef<Step1FormRef, Step1FormProps>((props, ref) => {
|
||||
placeholder={
|
||||
column.fieldProps?.placeholder || `请${column.title}`
|
||||
}
|
||||
type="text"
|
||||
className="flex-1"
|
||||
/>
|
||||
</View>
|
||||
@ -385,7 +384,7 @@ const Step1Form = forwardRef<Step1FormRef, Step1FormProps>((props, ref) => {
|
||||
orderShip?.orderShipItemList?.map(
|
||||
(item: any) => {
|
||||
if (
|
||||
item.itemId ===
|
||||
item.orderShipItemId ===
|
||||
orderShipItem.orderShipItemId
|
||||
) {
|
||||
return {
|
||||
|
||||
@ -52,7 +52,6 @@ export { default as MaterialCostSection } from "./section/MaterialCostSection";
|
||||
export { default as ProductionAdvanceSection } from "./section/ProductionAdvanceSection";
|
||||
export { default as WorkerAdvanceSection } from "./section/WorkerAdvanceSection";
|
||||
export { default as DeliveryFormSection } from "./section/DeliveryFormSection";
|
||||
export type { DeliveryFormSectionRef } from "./section/DeliveryFormSection";
|
||||
export { default as PurchaseFormSection } from "./section/PurchaseFormSection";
|
||||
|
||||
export { default as PurchaseStep1Form } from "./document/Step1Form";
|
||||
|
||||
@ -1,52 +1,40 @@
|
||||
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
|
||||
import { DeliveryStep1Form, DeliveryStep2Preview } from "@/components";
|
||||
import { useEffect, useState } from "react";
|
||||
import { DeliveryStep1Form, DeliveryStep2Preview, Icon } from "@/components";
|
||||
import { convertOrderShipVOToExamplesFormat } from "@/utils";
|
||||
import { business } from "@/services";
|
||||
import { Popup } from "@nutui/nutui-react-taro";
|
||||
import { Button, Popup } from "@nutui/nutui-react-taro";
|
||||
import { View } from "@tarojs/components";
|
||||
|
||||
export interface DeliveryFormSectionRef {
|
||||
handlePreview: () => void;
|
||||
}
|
||||
|
||||
export default forwardRef<
|
||||
DeliveryFormSectionRef,
|
||||
{
|
||||
orderVO: BusinessAPI.OrderVO;
|
||||
onChange?: (orderDealer: BusinessAPI.OrderVO) => void;
|
||||
readOnly?: boolean;
|
||||
}
|
||||
>((props, ref) => {
|
||||
export default function DeliveryFormSection(props: {
|
||||
orderVO: BusinessAPI.OrderVO;
|
||||
onChange?: (orderDealer: BusinessAPI.OrderVO) => void;
|
||||
readOnly?: boolean;
|
||||
}) {
|
||||
const { orderVO, onChange, readOnly } = props;
|
||||
const [moduleList, setModuleList] = useState<any[]>([]);
|
||||
const [template, setTemplate] = useState<any[]>([]);
|
||||
|
||||
const [previewVisible, setPreviewVisible] = useState(false);
|
||||
|
||||
const orderShip = orderVO.orderShipList[0];
|
||||
|
||||
const init = async (orderVO: BusinessAPI.OrderVO) => {
|
||||
const init = async (orderId: BusinessAPI.OrderVO["orderId"]) => {
|
||||
const { data } = await business.dealer.showDealer({
|
||||
dealerShowQry: {
|
||||
dealerId: orderVO.orderDealer.dealerId,
|
||||
dealerId: orderId,
|
||||
},
|
||||
});
|
||||
|
||||
if (data.data?.deliveryTemplate!) {
|
||||
const template = JSON.parse(data.data?.deliveryTemplate!);
|
||||
// 将 orderShipVO 转换为 examples 的数据格式,然后再替换 moduleList 里面的 config 数据
|
||||
const convertedData = convertOrderShipVOToExamplesFormat(orderVO);
|
||||
const updatedTemplate = await updateTemplateConfig(
|
||||
template,
|
||||
convertedData,
|
||||
);
|
||||
setModuleList(updatedTemplate);
|
||||
} else {
|
||||
setModuleList([]);
|
||||
setTemplate(template);
|
||||
}
|
||||
};
|
||||
|
||||
// 将 orderShipVO 转换为 examples 的数据格式,然后再替换 moduleList 里面的 config 数据
|
||||
const convertedData = convertOrderShipVOToExamplesFormat(orderVO);
|
||||
|
||||
// 更新模板配置
|
||||
const updateTemplateConfig = async (template: any[], data: any) => {
|
||||
const updateTemplateConfig = (template: any[], data: any) => {
|
||||
let templateList: any[] = [];
|
||||
template.map(async (module: any) => {
|
||||
let newModule = { ...module };
|
||||
@ -59,26 +47,26 @@ export default forwardRef<
|
||||
return templateList;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (orderVO.orderDealer.dealerId) {
|
||||
init(orderVO.orderDealer.dealerId);
|
||||
}
|
||||
}, [orderVO.orderDealer.dealerId]);
|
||||
|
||||
// 预览发货单操作
|
||||
const handlePreview = () => {
|
||||
setPreviewVisible(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (orderShip) {
|
||||
init(orderVO);
|
||||
}
|
||||
}, [orderVO.orderDealer.dealerId, orderShip]);
|
||||
|
||||
// 暴露方法给父组件
|
||||
useImperativeHandle(ref, () => ({
|
||||
handlePreview,
|
||||
}));
|
||||
|
||||
if (!orderShip) {
|
||||
if (!template) {
|
||||
return;
|
||||
}
|
||||
|
||||
const moduleList = updateTemplateConfig(template, convertedData);
|
||||
|
||||
console.log("moduleList", moduleList);
|
||||
console.log("orderShip", orderShip);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DeliveryStep1Form
|
||||
@ -93,6 +81,20 @@ export default forwardRef<
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 预览发货单 */}
|
||||
<View className={"flex-1"}>
|
||||
<Button
|
||||
icon={<Icon name="eye" size={18} />}
|
||||
size={"large"}
|
||||
type="primary"
|
||||
fill="outline"
|
||||
block
|
||||
onClick={handlePreview}
|
||||
>
|
||||
预览发货单
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
{/* 预览发货单 */}
|
||||
<Popup
|
||||
duration={150}
|
||||
@ -115,4 +117,4 @@ export default forwardRef<
|
||||
</Popup>
|
||||
</>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@ import hocAuth from "@/hocs/auth";
|
||||
import { CommonComponent } from "@/types/typings";
|
||||
import Taro from "@tarojs/taro";
|
||||
import { business } from "@/services";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { View } from "@tarojs/components";
|
||||
import {
|
||||
ActionSheet,
|
||||
@ -19,7 +19,6 @@ import {
|
||||
CostSummarySection,
|
||||
DealerInfoSection,
|
||||
DeliveryFormSection,
|
||||
DeliveryFormSectionRef,
|
||||
EmptyBoxInfoSection,
|
||||
Icon,
|
||||
MarketPriceSection,
|
||||
@ -243,23 +242,6 @@ export default hocAuth(function Page(props: CommonComponent) {
|
||||
|
||||
console.log("orderVO", orderVO);
|
||||
|
||||
// DeliveryFormSection ref
|
||||
const deliveryFormRef = useRef<DeliveryFormSectionRef>(null);
|
||||
|
||||
const handlePreview = () => {
|
||||
// 调用 DeliveryFormSection 组件的 handlePreview 方法
|
||||
if (deliveryFormRef.current) {
|
||||
deliveryFormRef.current.handlePreview();
|
||||
} else {
|
||||
// 如果 ref 不存在,提示用户
|
||||
Toast.show("toast", {
|
||||
icon: "fail",
|
||||
title: "提示",
|
||||
content: "发货单组件未加载,请稍后再试",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const [originPrincipal, setOriginPrincipal] = useState("");
|
||||
|
||||
// 暂存和提交审核的Dialog状态
|
||||
@ -848,13 +830,15 @@ export default hocAuth(function Page(props: CommonComponent) {
|
||||
)
|
||||
}
|
||||
orderVO={orderVO}
|
||||
onChange={setOrderVO}
|
||||
onChange={(orderVO: BusinessAPI.OrderVO) => {
|
||||
setOrderVO({
|
||||
...orderVO,
|
||||
orderShipList: [convertOrderToOrderShip(orderVO)],
|
||||
});
|
||||
}}
|
||||
// @ts-ignore
|
||||
costList={costList}
|
||||
calculator={calculator}
|
||||
ref={
|
||||
section.name === "deliveryForm" ? deliveryFormRef : null
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
@ -973,9 +957,6 @@ export default hocAuth(function Page(props: CommonComponent) {
|
||||
title="更多操作"
|
||||
visible={moreActionVisible}
|
||||
options={[
|
||||
{
|
||||
name: "预览发货单据",
|
||||
},
|
||||
{
|
||||
name: "暂存审核",
|
||||
},
|
||||
@ -989,9 +970,6 @@ export default hocAuth(function Page(props: CommonComponent) {
|
||||
if (item.name === "暂存审核") {
|
||||
// 暂存操作
|
||||
handleSave();
|
||||
} else if (item.name === "预览发货单据") {
|
||||
// 预览发货单据
|
||||
handlePreview();
|
||||
} else if (item.name === "返回首页") {
|
||||
// 返回首页
|
||||
Taro.redirectTo({ url: "/pages/main/index/index" });
|
||||
|
||||
@ -133,13 +133,11 @@ export default hocAuth(function Page(props: CommonComponent) {
|
||||
const template = JSON.parse(deliveryTemplate);
|
||||
// 将 orderShipVO 转换为 examples 的数据格式,然后再替换 moduleList 里面的 config 数据
|
||||
const convertedData = convertOrderShipVOToExamplesFormat(orderVO);
|
||||
console.log("convertedData", convertedData);
|
||||
const updatedTemplate = await updateTemplateConfig(
|
||||
template,
|
||||
convertedData,
|
||||
orderVO,
|
||||
);
|
||||
console.log("updatedTemplate", updatedTemplate);
|
||||
setModuleList(updatedTemplate);
|
||||
};
|
||||
|
||||
@ -241,6 +239,7 @@ export default hocAuth(function Page(props: CommonComponent) {
|
||||
data: { data: picData },
|
||||
} = await poster.poster.postApiV1Poster({
|
||||
html: template.generateHtmlString(),
|
||||
//@ts-ignore
|
||||
format: "a4",
|
||||
});
|
||||
|
||||
|
||||
@ -231,6 +231,33 @@ export default hocAuth(function Page(props: CommonComponent) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<View className="p-2.5">
|
||||
<SupplierPicker
|
||||
type={"FARMER"}
|
||||
onFinish={(supplierVO) => {
|
||||
setSupplierVO(supplierVO);
|
||||
actionRef.current?.reload();
|
||||
}}
|
||||
trigger={
|
||||
<View className={"flex flex-1 flex-row gap-2.5"}>
|
||||
<View
|
||||
className={`flex h-10 w-full items-center rounded-md border-4 border-gray-300`}
|
||||
>
|
||||
<View
|
||||
className={
|
||||
"flex flex-1 flex-row items-center justify-between px-5"
|
||||
}
|
||||
>
|
||||
<View className={"text-sm"}>
|
||||
{supplierVO?.name || "请选择瓜农"}
|
||||
</View>
|
||||
<Icon name={"chevron-down"} />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
<PageList<BusinessAPI.OrderSupplierVO, BusinessAPI.OrderSupplierPageQry>
|
||||
rowId={"orderSupplierId"}
|
||||
itemHeight={100}
|
||||
|
||||
@ -1336,6 +1336,10 @@ declare namespace BusinessAPI {
|
||||
enableLoss?: boolean;
|
||||
/** 损耗金额 */
|
||||
lossAmount?: number;
|
||||
/** 是否启用初始车次号 */
|
||||
enableInitialTrainNo?: boolean;
|
||||
/** 初始车次号 */
|
||||
initialTrainNo?: number;
|
||||
};
|
||||
|
||||
type DealerDestroyCmd = {
|
||||
@ -1660,6 +1664,10 @@ declare namespace BusinessAPI {
|
||||
enableLoss?: boolean;
|
||||
/** 损耗金额 */
|
||||
lossAmount?: number;
|
||||
/** 是否启用初始车次号 */
|
||||
enableInitialTrainNo?: boolean;
|
||||
/** 初始车次号 */
|
||||
initialTrainNo?: number;
|
||||
/** 发货单模板 */
|
||||
deliveryTemplate?: string;
|
||||
};
|
||||
@ -1711,6 +1719,10 @@ declare namespace BusinessAPI {
|
||||
enableLoss?: boolean;
|
||||
/** 损耗金额 */
|
||||
lossAmount?: number;
|
||||
/** 是否启用初始车次号 */
|
||||
enableInitialTrainNo?: boolean;
|
||||
/** 初始车次号 */
|
||||
initialTrainNo?: number;
|
||||
};
|
||||
|
||||
type DealerWarehouseCreateCmd = {
|
||||
|
||||
@ -10,6 +10,8 @@ import { DecimalUtils } from "@/utils/classes/calculators/core/DecimalUtils";
|
||||
export const convertOrderToOrderShip = (
|
||||
orderVO: BusinessAPI.OrderVO,
|
||||
): BusinessAPI.OrderShip => {
|
||||
const oldOrderShip = orderVO.orderShipList[0];
|
||||
|
||||
// 添加一个辅助函数用于分组
|
||||
const groupBy = <T>(
|
||||
array: T[],
|
||||
@ -34,6 +36,8 @@ export const convertOrderToOrderShip = (
|
||||
(supplier) => String(supplier.purchasePrice),
|
||||
);
|
||||
|
||||
const orderShipId = oldOrderShip?.orderShipId || generateShortId();
|
||||
|
||||
const orderShipItemList: BusinessAPI.OrderShipItem[] = Object.entries(
|
||||
suppliersByPrice,
|
||||
).map(([price, suppliers]) => {
|
||||
@ -59,35 +63,55 @@ export const convertOrderToOrderShip = (
|
||||
),
|
||||
);
|
||||
|
||||
const totalBoxCount = DecimalUtils.toDecimalPlaces(
|
||||
suppliers.reduce(
|
||||
(sum, supplier) =>
|
||||
DecimalUtils.add(
|
||||
sum,
|
||||
supplier.orderPackageList?.reduce(
|
||||
(sum, item) => DecimalUtils.add(sum, item.boxCount || 0),
|
||||
0,
|
||||
) || 0,
|
||||
),
|
||||
0,
|
||||
),
|
||||
);
|
||||
|
||||
const oldOrderShipItem = oldOrderShip?.orderShipItemList?.find(
|
||||
(item) => item.unitPrice == parseFloat(price),
|
||||
);
|
||||
|
||||
return {
|
||||
itemId: generateShortId(),
|
||||
orderShipId: "", // 将在创建发货单时填充
|
||||
orderShipItemId: oldOrderShipItem?.orderShipItemId || generateShortId(),
|
||||
orderShipId: orderShipId, // 将在创建发货单时填充
|
||||
orderId: orderVO.orderId,
|
||||
boxCount: totalBoxCount,
|
||||
grossWeight: totalGrossWeight,
|
||||
boxWeight: totalGrossWeight - totalNetWeight,
|
||||
netWeight: totalNetWeight,
|
||||
unitPrice: parseFloat(price),
|
||||
totalAmount: totalAmount,
|
||||
watermelonGrade: "", // 需要手动填写
|
||||
watermelonGrade: oldOrderShipItem?.watermelonGrade || "", // 需要手动填写
|
||||
};
|
||||
});
|
||||
|
||||
// 构建orderShip对象,不转换费用信息
|
||||
return {
|
||||
orderShipId: generateShortId(),
|
||||
orderShipId: orderShipId,
|
||||
orderId: orderVO.orderId,
|
||||
orderSn: "",
|
||||
watermelonGrade: "",
|
||||
orderSn: oldOrderShip?.orderSn,
|
||||
watermelonGrade: oldOrderShip?.watermelonGrade || "",
|
||||
dealerId: orderVO.orderVehicle?.dealerId!,
|
||||
dealerName: orderVO.orderVehicle?.dealerName!,
|
||||
companyId: orderVO.orderCompany?.companyId,
|
||||
companyName: orderVO.orderCompany?.fullName,
|
||||
shippingAddress: orderVO.orderVehicle?.origin,
|
||||
shippingAddress:
|
||||
oldOrderShip?.shippingAddress || orderVO.orderVehicle?.origin,
|
||||
receivingAddress: orderVO.orderVehicle?.destination,
|
||||
shippingDate: orderVO.orderVehicle?.deliveryTime,
|
||||
estimatedArrivalDate: "",
|
||||
pdfUrl: "",
|
||||
picUrl: "",
|
||||
estimatedArrivalDate: oldOrderShip?.estimatedArrivalDate || "",
|
||||
pdfUrl: oldOrderShip?.pdfUrl || "",
|
||||
picUrl: oldOrderShip?.picUrl || "",
|
||||
state: "WAIT_PAYMENT",
|
||||
orderShipItemList,
|
||||
};
|
||||
|
||||
751
pc-wrapper.html
Normal file
751
pc-wrapper.html
Normal file
@ -0,0 +1,751 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>新发雷盛西瓜管理系统</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1600px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 20px 30px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.header p {
|
||||
margin: 0;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
display: flex;
|
||||
padding: 20px;
|
||||
gap: 30px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
/* 左侧设备选择器 */
|
||||
.device-selector {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 12px;
|
||||
flex-shrink: 0;
|
||||
width: 140px;
|
||||
height: fit-content;
|
||||
position: sticky;
|
||||
top: 20px;
|
||||
}
|
||||
|
||||
.device-selector-label {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 2px solid #dee2e6;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.device-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.device-btn {
|
||||
padding: 10px 12px;
|
||||
border: 2px solid #dee2e6;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #495057;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.device-btn:hover {
|
||||
border-color: #667eea;
|
||||
color: #667eea;
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
.device-btn.active {
|
||||
background: #667eea;
|
||||
border-color: #667eea;
|
||||
color: white;
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.device-btn .device-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.device-btn span:last-child {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 右侧内容区 */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 40px;
|
||||
justify-content: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.frame-section {
|
||||
flex: 1;
|
||||
min-width: 400px;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.qrcode-section {
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
max-width: 400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 通用设备框架样式 */
|
||||
.frame-wrapper {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background: white;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
|
||||
margin: 0 auto;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* iPhone X */
|
||||
.frame-wrapper.iphone-x {
|
||||
width: 375px;
|
||||
height: 812px;
|
||||
border: 12px solid #333;
|
||||
border-radius: 40px;
|
||||
}
|
||||
|
||||
.frame-wrapper.iphone-x:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 60%;
|
||||
height: 25px;
|
||||
background: #333;
|
||||
border-bottom-left-radius: 15px;
|
||||
border-bottom-right-radius: 15px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.frame-wrapper.iphone-x:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 134px;
|
||||
height: 5px;
|
||||
background: #333;
|
||||
border-radius: 3px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* iPhone 14 Pro Max */
|
||||
.frame-wrapper.iphone-14-pro {
|
||||
width: 430px;
|
||||
height: 932px;
|
||||
border: 8px solid #1c1c1e;
|
||||
border-radius: 55px;
|
||||
}
|
||||
|
||||
.frame-wrapper.iphone-14-pro:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 140px;
|
||||
height: 35px;
|
||||
background: #1c1c1e;
|
||||
border-radius: 20px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.frame-wrapper.iphone-14-pro:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 134px;
|
||||
height: 5px;
|
||||
background: #333;
|
||||
border-radius: 3px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* Samsung Galaxy S21 */
|
||||
.frame-wrapper.galaxy-s21 {
|
||||
width: 384px;
|
||||
height: 854px;
|
||||
border: 10px solid #1a1a1a;
|
||||
border-radius: 32px;
|
||||
}
|
||||
|
||||
.frame-wrapper.galaxy-s21:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: #1a1a1a;
|
||||
border-radius: 50%;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* iPad */
|
||||
.frame-wrapper.ipad {
|
||||
width: 768px;
|
||||
height: 1024px;
|
||||
border: 18px solid #b4b4b4;
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.frame-wrapper.ipad:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: #333;
|
||||
border-radius: 50%;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* iPad Pro 11 */
|
||||
.frame-wrapper.ipad-pro {
|
||||
width: 834px;
|
||||
height: 1194px;
|
||||
border: 14px solid #b4b4b4;
|
||||
border-radius: 18px;
|
||||
}
|
||||
|
||||
/* Android 通用 */
|
||||
.frame-wrapper.android {
|
||||
width: 360px;
|
||||
height: 760px;
|
||||
border: 10px solid #2c2c2c;
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.frame-wrapper.android:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 60px;
|
||||
height: 8px;
|
||||
background: #2c2c2c;
|
||||
border-radius: 4px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* 屏幕容器 */
|
||||
.frame-wrapper .screen-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
overflow: hidden;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.frame-wrapper.iphone-x .screen-container,
|
||||
.frame-wrapper.iphone-14-pro .screen-container,
|
||||
.frame-wrapper.galaxy-s21 .screen-container,
|
||||
.frame-wrapper.android .screen-container {
|
||||
border-radius: 28px;
|
||||
}
|
||||
|
||||
.frame-wrapper.ipad .screen-container,
|
||||
.frame-wrapper.ipad-pro .screen-container {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
iframe {
|
||||
width: 100%;
|
||||
height: calc(100% - 84px); /* 减去TabBar高度 */
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* 顶部TabBar */
|
||||
.tabbar {
|
||||
height: 50px;
|
||||
background: rgba(248, 248, 248, 0.95);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.tabbar-time {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #000;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.tabbar-icons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.tabbar-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tabbar-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
fill: #000;
|
||||
}
|
||||
|
||||
/* 底部安全区域占位 */
|
||||
.safe-area-bottom {
|
||||
height: 34px;
|
||||
background: transparent;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.qrcode-container {
|
||||
background: white;
|
||||
padding: 25px;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.1);
|
||||
text-align: center;
|
||||
margin-bottom: 25px;
|
||||
border: 1px solid #eee;
|
||||
}
|
||||
|
||||
#qrcode {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
margin: 15px auto;
|
||||
padding: 10px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.qrcode-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.qrcode-desc {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 20px;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.url-display {
|
||||
background: #f8f9fa;
|
||||
padding: 12px 15px;
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
color: #555;
|
||||
word-break: break-all;
|
||||
margin-top: 15px;
|
||||
border: 1px solid #e9ecef;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.mobile-tips {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.mobile-tips svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
fill: #667eea;
|
||||
}
|
||||
|
||||
.tips {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
background: #f8f9fa;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.content-wrapper {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.device-selector {
|
||||
width: 100%;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
position: static;
|
||||
}
|
||||
|
||||
.device-buttons {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.device-btn {
|
||||
flex-direction: row;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
.device-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.device-btn.active {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.device-btn .device-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.device-btn span:last-child {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.frame-section, .qrcode-section {
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.qrcode-section {
|
||||
order: -1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 设备信息提示 */
|
||||
.device-info {
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 6px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.device-info .device-size {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
</style>
|
||||
<script src="qrcode.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
|
||||
<div class="content-wrapper">
|
||||
<!-- 左侧设备选择器 -->
|
||||
<div class="device-selector">
|
||||
<span class="device-selector-label">📱 设备</span>
|
||||
<div class="device-buttons">
|
||||
<button class="device-btn active" data-device="iphone-x">
|
||||
<span class="device-icon">📱</span>
|
||||
<span>iPhone X</span>
|
||||
</button>
|
||||
<button class="device-btn" data-device="iphone-14-pro">
|
||||
<span class="device-icon">📲</span>
|
||||
<span>iPhone 14 Pro</span>
|
||||
</button>
|
||||
<button class="device-btn" data-device="galaxy-s21">
|
||||
<span class="device-icon">🤖</span>
|
||||
<span>Galaxy S21</span>
|
||||
</button>
|
||||
<button class="device-btn" data-device="android">
|
||||
<span class="device-icon">📱</span>
|
||||
<span>Android</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧内容区 -->
|
||||
<div class="main-content">
|
||||
<div class="frame-section">
|
||||
<div class="frame-wrapper iphone-x" id="device-frame">
|
||||
<div class="screen-container">
|
||||
<!-- 顶部TabBar -->
|
||||
<div class="tabbar">
|
||||
<div class="tabbar-time" id="current-time">9:41</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- iframe内容区域 -->
|
||||
<iframe id="mobile-frame" src="/"
|
||||
title="移动端页面"></iframe>
|
||||
|
||||
<!-- 底部安全区域占位 -->
|
||||
<div class="safe-area-bottom"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="device-info">
|
||||
当前设备: <span class="device-size" id="device-name">iPhone X (375×812)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="qrcode-section">
|
||||
<div class="qrcode-container">
|
||||
<div class="qrcode-title">手机扫码预览</div>
|
||||
<div class="qrcode-desc">
|
||||
使用手机扫描下方二维码,可直接在手机上访问此页面,获得真实的移动端体验
|
||||
</div>
|
||||
|
||||
<div id="qrcode"></div>
|
||||
|
||||
<div class="url-display" id="current-url">
|
||||
<!-- URL 将在这里显示 -->
|
||||
</div>
|
||||
|
||||
<div class="mobile-tips">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M17 1.01L7 1c-1.1 0-2 .9-2 2v18c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2V3c0-1.1-.9-1.99-2-1.99zM17 19H7V5h10v14z"/>
|
||||
</svg>
|
||||
<span>建议使用手机扫描体验最佳效果</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 设备配置信息
|
||||
const deviceConfigs = {
|
||||
'iphone-x': {
|
||||
name: 'iPhone X',
|
||||
size: '375×812',
|
||||
className: 'iphone-x'
|
||||
},
|
||||
'iphone-14-pro': {
|
||||
name: 'iPhone 14 Pro Max',
|
||||
size: '430×932',
|
||||
className: 'iphone-14-pro'
|
||||
},
|
||||
'galaxy-s21': {
|
||||
name: 'Samsung Galaxy S21',
|
||||
size: '384×854',
|
||||
className: 'galaxy-s21'
|
||||
},
|
||||
'android': {
|
||||
name: 'Android (通用)',
|
||||
size: '360×760',
|
||||
className: 'android'
|
||||
},
|
||||
};
|
||||
|
||||
// 当前选择的设备
|
||||
let currentDevice = 'iphone-x';
|
||||
|
||||
// 切换设备
|
||||
function switchDevice(deviceId) {
|
||||
const frame = document.getElementById('device-frame');
|
||||
const deviceName = document.getElementById('device-name');
|
||||
const config = deviceConfigs[deviceId];
|
||||
|
||||
// 移除所有设备类名
|
||||
frame.className = 'frame-wrapper';
|
||||
|
||||
// 添加新设备类名
|
||||
frame.classList.add(config.className);
|
||||
|
||||
// 更新设备信息显示
|
||||
deviceName.textContent = `${config.name} (${config.size})`;
|
||||
|
||||
// 保存到localStorage
|
||||
localStorage.setItem('previewDevice', deviceId);
|
||||
|
||||
// 更新按钮状态
|
||||
document.querySelectorAll('.device-btn').forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
if (btn.dataset.device === deviceId) {
|
||||
btn.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
currentDevice = deviceId;
|
||||
}
|
||||
|
||||
// 初始化设备选择器
|
||||
function initDeviceSelector() {
|
||||
// 从localStorage读取上次选择的设备
|
||||
const savedDevice = localStorage.getItem('previewDevice');
|
||||
if (savedDevice && deviceConfigs[savedDevice]) {
|
||||
switchDevice(savedDevice);
|
||||
}
|
||||
|
||||
// 绑定按钮点击事件
|
||||
document.querySelectorAll('.device-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const deviceId = this.dataset.device;
|
||||
switchDevice(deviceId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 更新当前时间
|
||||
function updateCurrentTime() {
|
||||
const now = new Date();
|
||||
const hours = now.getHours().toString().padStart(2, '0');
|
||||
const minutes = now.getMinutes().toString().padStart(2, '0');
|
||||
document.getElementById('current-time').textContent = `${hours}:${minutes}`;
|
||||
}
|
||||
|
||||
// 获取当前页面URL
|
||||
function getCurrentUrl() {
|
||||
// 如果 $request_uri 是占位符,则使用实际URL
|
||||
let iframeSrc = document.getElementById('mobile-frame').src;
|
||||
if (iframeSrc.includes('$request_uri')) {
|
||||
// 使用当前页面URL(去掉预览页面的路径)
|
||||
let currentUrl = window.location.href;
|
||||
// 移除预览页面的查询参数或路径
|
||||
return currentUrl.split('?')[0].replace(/\/preview\/?$/, '');
|
||||
}
|
||||
return iframeSrc;
|
||||
}
|
||||
|
||||
// 生成二维码
|
||||
function generateQRCode() {
|
||||
const url = getCurrentUrl();
|
||||
const qrcodeElement = document.getElementById('qrcode');
|
||||
const urlDisplay = document.getElementById('current-url');
|
||||
|
||||
// 显示URL
|
||||
urlDisplay.textContent = url.length > 50 ? url.substring(0, 50) + '...' : url;
|
||||
|
||||
// 清空之前的二维码
|
||||
qrcodeElement.innerHTML = '';
|
||||
|
||||
// 生成当前页面的二维码
|
||||
new QRCode(document.getElementById('qrcode'), {
|
||||
text: window.location.href,
|
||||
width: 200,
|
||||
height: 200,
|
||||
margin: 1,
|
||||
color: {
|
||||
dark: '#333333',
|
||||
light: '#ffffff'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 处理iframe内部链接
|
||||
var iframe = document.getElementById("mobile-frame");
|
||||
iframe.onload = function () {
|
||||
try {
|
||||
var iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
|
||||
var links = iframeDoc.querySelectorAll("a");
|
||||
links.forEach(function (link) {
|
||||
link.onclick = function (e) {
|
||||
e.preventDefault();
|
||||
var href = this.getAttribute("href");
|
||||
if (href && !href.match(/^(http|https|#|javascript:)/)) {
|
||||
iframe.src = href;
|
||||
// 更新二维码
|
||||
setTimeout(generateQRCode, 100);
|
||||
}
|
||||
};
|
||||
});
|
||||
} catch (e) {
|
||||
// 跨域限制
|
||||
}
|
||||
|
||||
// 每次iframe加载后更新二维码
|
||||
generateQRCode();
|
||||
};
|
||||
|
||||
// 页面加载时初始化
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// 初始化设备选择器
|
||||
initDeviceSelector();
|
||||
|
||||
// 更新时间
|
||||
updateCurrentTime();
|
||||
setInterval(updateCurrentTime, 60000); // 每分钟更新一次
|
||||
|
||||
// 生成二维码
|
||||
generateQRCode();
|
||||
});
|
||||
|
||||
// 监听URL变化(如果使用History API)
|
||||
window.addEventListener('popstate', generateQRCode);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
1
qrcode.min.js
vendored
Normal file
1
qrcode.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user