优化
This commit is contained in:
48
src/App.tsx
48
src/App.tsx
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useMemo, useEffect } from 'react';
|
import React, { useState, useMemo, useEffect, useLayoutEffect } from 'react';
|
||||||
import { LabelTemplate, PaperConfig, ImportData, RecordRow, ExportRangeConfig, CutLineConfig } from './types';
|
import { LabelTemplate, PaperConfig, ImportData, RecordRow, ExportRangeConfig, CutLineConfig } from './types';
|
||||||
import {
|
import {
|
||||||
stripTemplateForStorage,
|
stripTemplateForStorage,
|
||||||
@@ -442,6 +442,40 @@ export default function App() {
|
|||||||
return !exportRangeEquals(draftExportRange, appliedExportData.exportRange);
|
return !exportRangeEquals(draftExportRange, appliedExportData.exportRange);
|
||||||
}, [importData, variableColumnMapping, appliedExportData, draftExportRange]);
|
}, [importData, variableColumnMapping, appliedExportData, draftExportRange]);
|
||||||
|
|
||||||
|
// 移动端导出页实时同步数据,无需手动「应用数据」
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!isMobile || workflowStep !== 'print-view') return;
|
||||||
|
if (!importData || activeRows.length === 0) return;
|
||||||
|
|
||||||
|
const nextExportRange = { ...draftExportRange };
|
||||||
|
setAppliedExportData({
|
||||||
|
fileName: importData.fileName,
|
||||||
|
currentSheet: importData.currentSheet,
|
||||||
|
rows: importData.rows,
|
||||||
|
mapping: { ...variableColumnMapping },
|
||||||
|
exportRange: nextExportRange,
|
||||||
|
});
|
||||||
|
setPreviewSnapshot({
|
||||||
|
pages: layoutResult.pages,
|
||||||
|
gridCols: layoutResult.gridCols,
|
||||||
|
labelsPerPage: layoutResult.labelsPerPage,
|
||||||
|
totalPages: layoutResult.totalPages,
|
||||||
|
selectedCount: layoutResult.selectedCount,
|
||||||
|
paper,
|
||||||
|
exportRange: nextExportRange,
|
||||||
|
});
|
||||||
|
setPreviewLayoutStale(false);
|
||||||
|
}, [
|
||||||
|
isMobile,
|
||||||
|
workflowStep,
|
||||||
|
importData,
|
||||||
|
activeRows,
|
||||||
|
variableColumnMapping,
|
||||||
|
draftExportRange,
|
||||||
|
layoutResult,
|
||||||
|
paper,
|
||||||
|
]);
|
||||||
|
|
||||||
const handleApplyData = async () => {
|
const handleApplyData = async () => {
|
||||||
if (!importData || activeRows.length === 0) {
|
if (!importData || activeRows.length === 0) {
|
||||||
await showAlert('请先上传 Excel 或 CSV 数据。', { variant: 'warning' });
|
await showAlert('请先上传 Excel 或 CSV 数据。', { variant: 'warning' });
|
||||||
@@ -884,7 +918,7 @@ export default function App() {
|
|||||||
</span>
|
</span>
|
||||||
<div className="flex-1" />
|
<div className="flex-1" />
|
||||||
<span className="hidden md:inline text-[10px] text-[#666]">
|
<span className="hidden md:inline text-[10px] text-[#666]">
|
||||||
Shift/Ctrl+点击多选 · V 移动 · T 文本 · 图片工具栏
|
Shift/Ctrl+点击多选
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => openPrintView()}
|
onClick={() => openPrintView()}
|
||||||
@@ -937,14 +971,8 @@ export default function App() {
|
|||||||
onChangeDraftExportRange={setDraftExportRange}
|
onChangeDraftExportRange={setDraftExportRange}
|
||||||
cutLine={cutLine}
|
cutLine={cutLine}
|
||||||
onChangeCutLine={handleCutLineChange}
|
onChangeCutLine={handleCutLineChange}
|
||||||
dataStale={dataStale}
|
selectedCount={selectedCount}
|
||||||
dataApplied={appliedExportData !== null}
|
totalPages={totalPages}
|
||||||
onApplyData={handleApplyData}
|
|
||||||
appliedSelectedCount={appliedLayoutResult.selectedCount}
|
|
||||||
appliedTotalPages={appliedLayoutResult.totalPages}
|
|
||||||
appliedGridCols={appliedLayoutResult.gridCols}
|
|
||||||
appliedGridRows={appliedLayoutResult.gridRows}
|
|
||||||
draftSelectedCount={selectedCount}
|
|
||||||
pdfGenerating={pdfGenerating}
|
pdfGenerating={pdfGenerating}
|
||||||
onBack={() => setWorkflowStep('template-select')}
|
onBack={() => setWorkflowStep('template-select')}
|
||||||
onExportPdf={exportPdf}
|
onExportPdf={exportPdf}
|
||||||
|
|||||||
@@ -13,6 +13,50 @@ export function downloadDataTemplate(templateName: string, templateVariables: st
|
|||||||
XLSX.writeFile(wb, `${safeName}_数据模板.xlsx`);
|
XLSX.writeFile(wb, `${safeName}_数据模板.xlsx`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readWorkbook(buffer: ArrayBuffer) {
|
||||||
|
return XLSX.read(new Uint8Array(buffer), { type: 'array' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseWorksheet(
|
||||||
|
workbook: XLSX.WorkBook,
|
||||||
|
sheetName: string,
|
||||||
|
fileName: string
|
||||||
|
): ImportData | null {
|
||||||
|
const worksheet = workbook.Sheets[sheetName];
|
||||||
|
if (!worksheet) return null;
|
||||||
|
|
||||||
|
const jsonData = XLSX.utils.sheet_to_json<unknown[]>(worksheet, { header: 1, defval: '' });
|
||||||
|
if (jsonData.length === 0) return null;
|
||||||
|
|
||||||
|
const rawColumns = jsonData[0] as string[];
|
||||||
|
const columns = rawColumns
|
||||||
|
.map((c, idx) => (c ? String(c).trim() : `列_${idx + 1}`))
|
||||||
|
.filter(Boolean);
|
||||||
|
const rows: RecordRow[] = [];
|
||||||
|
|
||||||
|
for (let i = 1; i < jsonData.length; i++) {
|
||||||
|
const rawRow = jsonData[i] as unknown[];
|
||||||
|
if (
|
||||||
|
rawRow.every((cell) => cell === undefined || cell === null || String(cell).trim() === '')
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const row: RecordRow = {};
|
||||||
|
columns.forEach((col, colIdx) => {
|
||||||
|
row[col] = rawRow[colIdx] !== undefined ? String(rawRow[colIdx]).trim() : '';
|
||||||
|
});
|
||||||
|
rows.push(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
fileName,
|
||||||
|
sheets: workbook.SheetNames,
|
||||||
|
currentSheet: sheetName,
|
||||||
|
columns,
|
||||||
|
rows,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface DataImporterProps {
|
interface DataImporterProps {
|
||||||
importData: ImportData | null;
|
importData: ImportData | null;
|
||||||
onDataLoaded: (data: ImportData) => void;
|
onDataLoaded: (data: ImportData) => void;
|
||||||
@@ -34,7 +78,10 @@ export const DataImporter: React.FC<DataImporterProps> = ({
|
|||||||
const isPs = variant === 'ps';
|
const isPs = variant === 'ps';
|
||||||
const isMobile = variant === 'mobile';
|
const isMobile = variant === 'mobile';
|
||||||
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
/** 缓存已上传文件,切换工作表时无需依赖 input.files(拖拽上传时 input 为空) */
|
||||||
|
const workbookBufferRef = useRef<ArrayBuffer | null>(null);
|
||||||
const [dragActive, setDragActive] = useState<boolean>(false);
|
const [dragActive, setDragActive] = useState<boolean>(false);
|
||||||
|
const [sheetLoading, setSheetLoading] = useState(false);
|
||||||
|
|
||||||
const handleDrag = (e: React.DragEvent) => {
|
const handleDrag = (e: React.DragEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -51,139 +98,109 @@ export const DataImporter: React.FC<DataImporterProps> = ({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setDragActive(false);
|
setDragActive(false);
|
||||||
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
|
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
|
||||||
processFile(e.dataTransfer.files[0]);
|
void processFile(e.dataTransfer.files[0]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
if (e.target.files && e.target.files[0]) {
|
if (e.target.files && e.target.files[0]) {
|
||||||
processFile(e.target.files[0]);
|
void processFile(e.target.files[0]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const processFile = (file: File) => {
|
const processFile = async (file: File) => {
|
||||||
const reader = new FileReader();
|
try {
|
||||||
reader.onload = async (e) => {
|
const buffer = await file.arrayBuffer();
|
||||||
try {
|
workbookBufferRef.current = buffer;
|
||||||
const data = new Uint8Array(e.target?.result as ArrayBuffer);
|
const workbook = readWorkbook(buffer);
|
||||||
const workbook = XLSX.read(data, { type: 'array' });
|
const firstSheetName = workbook.SheetNames[0];
|
||||||
const firstSheetName = workbook.SheetNames[0];
|
const parsed = parseWorksheet(workbook, firstSheetName, file.name);
|
||||||
const worksheet = workbook.Sheets[firstSheetName];
|
|
||||||
const jsonData = XLSX.utils.sheet_to_json<any[]>(worksheet, { header: 1, defval: '' });
|
|
||||||
|
|
||||||
if (jsonData.length === 0) {
|
if (!parsed || parsed.rows.length === 0) {
|
||||||
await showAlert('表格为空或格式不正确', { variant: 'warning' });
|
workbookBufferRef.current = null;
|
||||||
return;
|
await showAlert('表格为空或格式不正确', { variant: 'warning' });
|
||||||
}
|
return;
|
||||||
|
|
||||||
const rawColumns = jsonData[0] as string[];
|
|
||||||
const columns = rawColumns.map((c, idx) => (c ? String(c).trim() : `列_${idx + 1}`)).filter(Boolean);
|
|
||||||
const rows: RecordRow[] = [];
|
|
||||||
for (let i = 1; i < jsonData.length; i++) {
|
|
||||||
const rawRow = jsonData[i] as any[];
|
|
||||||
if (rawRow.every((cell) => cell === undefined || cell === null || String(cell).trim() === '')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const row: RecordRow = {};
|
|
||||||
columns.forEach((col, colIdx) => {
|
|
||||||
row[col] = rawRow[colIdx] !== undefined ? String(rawRow[colIdx]).trim() : '';
|
|
||||||
});
|
|
||||||
rows.push(row);
|
|
||||||
}
|
|
||||||
|
|
||||||
onDataLoaded({
|
|
||||||
fileName: file.name,
|
|
||||||
sheets: workbook.SheetNames,
|
|
||||||
currentSheet: firstSheetName,
|
|
||||||
columns,
|
|
||||||
rows,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
await showAlert('解析文件出错,请确保是标准的 Excel 或 CSV 文件', { variant: 'error' });
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
reader.readAsArrayBuffer(file);
|
onDataLoaded(parsed);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
workbookBufferRef.current = null;
|
||||||
|
await showAlert('解析文件出错,请确保是标准的 Excel 或 CSV 文件', { variant: 'error' });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSheetChange = (sheetName: string) => {
|
const handleSheetChange = async (sheetName: string) => {
|
||||||
if (!fileInputRef.current?.files?.[0]) return;
|
if (!importData || sheetName === importData.currentSheet) return;
|
||||||
const file = fileInputRef.current.files[0];
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = async (e) => {
|
|
||||||
try {
|
|
||||||
const data = new Uint8Array(e.target?.result as ArrayBuffer);
|
|
||||||
const workbook = XLSX.read(data, { type: 'array' });
|
|
||||||
const worksheet = workbook.Sheets[sheetName];
|
|
||||||
const jsonData = XLSX.utils.sheet_to_json<any[]>(worksheet, { header: 1, defval: '' });
|
|
||||||
if (jsonData.length === 0) return;
|
|
||||||
|
|
||||||
const rawColumns = jsonData[0] as string[];
|
const buffer = workbookBufferRef.current;
|
||||||
const columns = rawColumns.map((c, idx) => (c ? String(c).trim() : `列_${idx + 1}`)).filter(Boolean);
|
if (!buffer) {
|
||||||
const rows: RecordRow[] = [];
|
await showAlert('无法切换工作表,请重新上传文件', { variant: 'warning' });
|
||||||
for (let i = 1; i < jsonData.length; i++) {
|
return;
|
||||||
const rawRow = jsonData[i] as any[];
|
}
|
||||||
if (rawRow.every((cell) => cell === undefined || cell === null || String(cell).trim() === '')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const row: RecordRow = {};
|
|
||||||
columns.forEach((col, colIdx) => {
|
|
||||||
row[col] = rawRow[colIdx] !== undefined ? String(rawRow[colIdx]).trim() : '';
|
|
||||||
});
|
|
||||||
rows.push(row);
|
|
||||||
}
|
|
||||||
|
|
||||||
onDataLoaded({
|
setSheetLoading(true);
|
||||||
fileName: file.name,
|
try {
|
||||||
sheets: workbook.SheetNames,
|
const workbook = readWorkbook(buffer);
|
||||||
currentSheet: sheetName,
|
const parsed = parseWorksheet(workbook, sheetName, importData.fileName);
|
||||||
columns,
|
|
||||||
rows,
|
if (!parsed) {
|
||||||
});
|
await showAlert('该工作表为空或无法解析', { variant: 'warning' });
|
||||||
} catch {
|
return;
|
||||||
await showAlert('切换工作表时出错', { variant: 'error' });
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
reader.readAsArrayBuffer(file);
|
onDataLoaded(parsed);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
await showAlert('切换工作表时出错', { variant: 'error' });
|
||||||
|
} finally {
|
||||||
|
setSheetLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClear = () => {
|
||||||
|
workbookBufferRef.current = null;
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.value = '';
|
||||||
|
}
|
||||||
|
onClear();
|
||||||
};
|
};
|
||||||
|
|
||||||
const rows = importData?.rows || [];
|
const rows = importData?.rows || [];
|
||||||
const columns = importData?.columns || [];
|
const columns = importData?.columns || [];
|
||||||
|
const hasMultipleSheets = (importData?.sheets.length ?? 0) > 1;
|
||||||
|
|
||||||
const wrapperClass = isMobile
|
const wrapperClass = isMobile
|
||||||
? ''
|
? 'min-w-0 w-full'
|
||||||
: isPs
|
: isPs
|
||||||
? 'space-y-3'
|
? 'space-y-3 min-w-0 w-full'
|
||||||
: 'bg-white border border-gray-200 rounded-2xl p-6 shadow-sm flex flex-col gap-5';
|
: 'bg-white border border-gray-200 rounded-2xl p-6 shadow-sm flex flex-col gap-5 min-w-0';
|
||||||
|
|
||||||
|
const sheetSelectClass = isMobile
|
||||||
|
? 'mobile-field text-sm py-2 flex-1 min-w-0 max-w-full'
|
||||||
|
: isPs
|
||||||
|
? 'ps-field text-[10px] py-1 flex-1 min-w-0 max-w-full'
|
||||||
|
: 'px-2.5 py-1 text-xs bg-white border border-gray-100 rounded-lg flex-1 min-w-0 max-w-full';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={wrapperClass}>
|
<div className={wrapperClass}>
|
||||||
{/* {!isPs && (
|
|
||||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-sm font-bold text-gray-900 leading-none">批量数据源导入</h3>
|
|
||||||
<p className="text-[11px] text-gray-400 mt-1">上传 Excel 或 CSV,绑定当前模板变量</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)} */}
|
|
||||||
|
|
||||||
{!isMobile && (
|
{!isMobile && (
|
||||||
<div className={`flex flex-wrap gap-2 ${isPs ? '' : 'justify-end'}`}>
|
<div className={`flex flex-wrap gap-2 ${isPs ? '' : 'justify-end'}`}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => downloadDataTemplate(templateName, templateVariables)}
|
onClick={() => downloadDataTemplate(templateName, templateVariables)}
|
||||||
className={
|
className={
|
||||||
isMobile
|
isMobile
|
||||||
? 'mobile-secondary-btn w-full'
|
? 'mobile-secondary-btn w-full'
|
||||||
: isPs
|
: isPs
|
||||||
? 'ps-btn text-[10px] w-full justify-center'
|
? 'ps-btn text-[10px] w-full justify-center'
|
||||||
: 'inline-flex items-center gap-1.5 px-3 py-1.5 border border-gray-200 text-gray-600 hover:bg-gray-50 rounded-lg text-xs font-semibold cursor-pointer'
|
: 'inline-flex items-center gap-1.5 px-3 py-1.5 border border-gray-200 text-gray-600 hover:bg-gray-50 rounded-lg text-xs font-semibold cursor-pointer'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Download className="w-3.5 h-3.5" />
|
<Download className="w-3.5 h-3.5" />
|
||||||
下载数据模板
|
下载数据模板
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!importData ? (
|
{!importData ? (
|
||||||
@@ -197,16 +214,16 @@ export const DataImporter: React.FC<DataImporterProps> = ({
|
|||||||
isMobile
|
isMobile
|
||||||
? `mobile-upload-zone ${dragActive ? 'active' : ''}`
|
? `mobile-upload-zone ${dragActive ? 'active' : ''}`
|
||||||
: isPs
|
: isPs
|
||||||
? `border border-dashed rounded py-8 px-4 text-center cursor-pointer transition flex flex-col items-center gap-2 ${
|
? `border border-dashed rounded py-8 px-4 text-center cursor-pointer transition flex flex-col items-center gap-2 ${
|
||||||
dragActive
|
dragActive
|
||||||
? 'border-[#31a8ff] bg-[#1a3a4f]/30'
|
? 'border-[#31a8ff] bg-[#1a3a4f]/30'
|
||||||
: 'border-[#4a4a4a] hover:border-[#31a8ff]/60 hover:bg-[#1e1e1e]'
|
: 'border-[#4a4a4a] hover:border-[#31a8ff]/60 hover:bg-[#1e1e1e]'
|
||||||
}`
|
}`
|
||||||
: `border-2 border-dashed rounded-2xl py-10 px-6 text-center cursor-pointer transition flex flex-col items-center gap-3 ${
|
: `border-2 border-dashed rounded-2xl py-10 px-6 text-center cursor-pointer transition flex flex-col items-center gap-3 ${
|
||||||
dragActive
|
dragActive
|
||||||
? 'border-indigo-500 bg-indigo-50/20'
|
? 'border-indigo-500 bg-indigo-50/20'
|
||||||
: 'border-gray-200 hover:border-indigo-400 hover:bg-gray-50/50'
|
: 'border-gray-200 hover:border-indigo-400 hover:bg-gray-50/50'
|
||||||
}`
|
}`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@@ -227,16 +244,17 @@ export const DataImporter: React.FC<DataImporterProps> = ({
|
|||||||
isMobile
|
isMobile
|
||||||
? 'text-[15px] font-semibold text-slate-700'
|
? 'text-[15px] font-semibold text-slate-700'
|
||||||
: isPs
|
: isPs
|
||||||
? 'text-xs text-[#ccc]'
|
? 'text-xs text-[#ccc]'
|
||||||
: 'text-xs text-gray-700'
|
: 'text-xs text-gray-700'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{isMobile ? '点击选择 Excel / CSV' : '点击上传或拖拽文件到此处'}
|
{isMobile ? '点击选择 Excel / CSV' : '点击上传或拖拽文件到此处'}
|
||||||
</p>
|
</p>
|
||||||
{isMobile && (
|
{isMobile && (
|
||||||
<p className="text-xs text-slate-400 leading-relaxed">
|
<p className="text-xs text-slate-400 leading-relaxed">支持 .xlsx、.xls、.csv</p>
|
||||||
支持 .xlsx、.xls、.csv
|
)}
|
||||||
</p>
|
{!isMobile && isPs && (
|
||||||
|
<p className="text-[10px] text-[#666] mt-1">多工作表文件可在导入后切换 Sheet</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -246,43 +264,74 @@ export const DataImporter: React.FC<DataImporterProps> = ({
|
|||||||
isMobile
|
isMobile
|
||||||
? 'mobile-file-card'
|
? 'mobile-file-card'
|
||||||
: isPs
|
: isPs
|
||||||
? 'flex flex-wrap items-center justify-between p-2.5 bg-[#1e1e1e] border border-[#3a3a3a] rounded gap-3'
|
? 'p-2.5 bg-[#1e1e1e] border border-[#3a3a3a] rounded space-y-2.5 min-w-0'
|
||||||
: 'flex flex-wrap items-center justify-between p-3.5 bg-gray-50 rounded-xl gap-4 border border-gray-100'
|
: 'p-3.5 bg-gray-50 rounded-xl border border-gray-100 space-y-3 min-w-0'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 overflow-hidden min-w-0">
|
<div className="flex items-start gap-2 w-full min-w-0">
|
||||||
<FileSpreadsheet
|
<div className="flex items-start gap-2 min-w-0 flex-1">
|
||||||
className={`w-5 h-5 shrink-0 ${isPs ? 'text-emerald-400' : 'text-emerald-600'}`}
|
<FileSpreadsheet
|
||||||
/>
|
className={`w-5 h-5 shrink-0 mt-0.5 ${isPs ? 'text-emerald-400' : 'text-emerald-600'}`}
|
||||||
<div className="overflow-hidden min-w-0">
|
/>
|
||||||
<p
|
<div className="min-w-0 flex-1">
|
||||||
className={`truncate ${
|
<p
|
||||||
isMobile ? 'text-sm text-slate-800' : isPs ? 'text-xs text-[#e8e8e8]' : 'text-gray-800'
|
className={`truncate ${
|
||||||
}`}
|
isMobile ? 'text-sm text-slate-800' : isPs ? 'text-xs text-[#e8e8e8]' : 'text-gray-800'
|
||||||
>
|
}`}
|
||||||
{importData.fileName}
|
title={importData.fileName}
|
||||||
</p>
|
>
|
||||||
<p
|
{importData.fileName}
|
||||||
className={`mt-0.5 ${
|
</p>
|
||||||
isMobile ? 'text-xs text-slate-500' : isPs ? 'text-[10px] text-[#777]' : 'text-[10px] text-gray-400'
|
<p
|
||||||
}`}
|
className={`mt-0.5 truncate ${
|
||||||
>
|
isMobile ? 'text-xs text-slate-500' : isPs ? 'text-[10px] text-[#777]' : 'text-[10px] text-gray-400'
|
||||||
{rows.length} 条记录 · {columns.length} 列
|
}`}
|
||||||
</p>
|
title={
|
||||||
|
hasMultipleSheets
|
||||||
|
? `${importData.currentSheet} · ${rows.length} 条记录 · ${columns.length} 列`
|
||||||
|
: `${rows.length} 条记录 · ${columns.length} 列`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{hasMultipleSheets && (
|
||||||
|
<span className={isPs ? 'text-[#31a8ff]' : 'text-indigo-600'}>
|
||||||
|
{importData.currentSheet}
|
||||||
|
{' · '}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{rows.length} 条记录 · {columns.length} 列
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleClear}
|
||||||
|
className={
|
||||||
|
isMobile
|
||||||
|
? 'mobile-text-btn danger shrink-0'
|
||||||
|
: isPs
|
||||||
|
? 'ps-btn text-[10px] text-rose-300 hover:text-rose-200 shrink-0'
|
||||||
|
: 'flex items-center gap-1 text-xs font-semibold text-rose-600 hover:bg-rose-50 px-2.5 py-1.5 rounded-lg cursor-pointer shrink-0'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Trash2 className="w-3.5 h-3.5" />
|
||||||
|
{!isMobile && !isPs && <span>清除</span>}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 shrink-0">
|
|
||||||
{importData.sheets.length > 1 && (
|
{hasMultipleSheets && (
|
||||||
|
<div className="flex items-center gap-2 w-full min-w-0">
|
||||||
|
<label
|
||||||
|
className={`shrink-0 ${
|
||||||
|
isMobile ? 'text-xs text-slate-500' : isPs ? 'text-[10px] text-[#888]' : 'text-[10px] text-gray-500'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
数据源工作表
|
||||||
|
</label>
|
||||||
<select
|
<select
|
||||||
value={importData.currentSheet}
|
value={importData.currentSheet}
|
||||||
onChange={(e) => handleSheetChange(e.target.value)}
|
disabled={sheetLoading}
|
||||||
className={
|
onChange={(e) => void handleSheetChange(e.target.value)}
|
||||||
isMobile
|
className={sheetSelectClass}
|
||||||
? 'mobile-field text-sm py-2'
|
|
||||||
: isPs
|
|
||||||
? 'ps-field text-[10px] py-1'
|
|
||||||
: 'px-2.5 py-1 text-xs bg-white border border-gray-100 rounded-lg'
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{importData.sheets.map((sheet) => (
|
{importData.sheets.map((sheet) => (
|
||||||
<option key={sheet} value={sheet}>
|
<option key={sheet} value={sheet}>
|
||||||
@@ -290,21 +339,13 @@ export const DataImporter: React.FC<DataImporterProps> = ({
|
|||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
)}
|
{sheetLoading && (
|
||||||
<button
|
<span className={`${isPs ? 'text-[10px] text-[#777]' : 'text-[10px] text-gray-400'}`}>
|
||||||
type="button"
|
切换中…
|
||||||
onClick={onClear}
|
</span>
|
||||||
className={
|
)}
|
||||||
isMobile
|
</div>
|
||||||
? 'mobile-text-btn danger'
|
)}
|
||||||
: isPs
|
|
||||||
? 'ps-btn text-[10px] text-rose-300 hover:text-rose-200'
|
|
||||||
: 'flex items-center gap-1 text-xs font-semibold text-rose-600 hover:bg-rose-50 px-2.5 py-1.5 rounded-lg cursor-pointer'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Trash2 className="w-3.5 h-3.5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -33,15 +33,6 @@ export const DataRangeFields: React.FC<DataRangeFieldsProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div className={isMobile ? 'space-y-4' : 'space-y-2'}>
|
<div className={isMobile ? 'space-y-4' : 'space-y-2'}>
|
||||||
{!isMobile && <div className="ps-section-title">数据范围</div>}
|
{!isMobile && <div className="ps-section-title">数据范围</div>}
|
||||||
{isMobile && (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Filter className="w-4 h-4 text-indigo-500 shrink-0" />
|
|
||||||
<span className="text-sm font-semibold text-slate-800">数据范围</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<p className={isMobile ? 'text-xs text-slate-500 leading-relaxed' : 'text-[10px] text-[#888] leading-relaxed'}>
|
|
||||||
序号指数据行顺序(从 1 开始),应用数据后生效。
|
|
||||||
</p>
|
|
||||||
<div>
|
<div>
|
||||||
{isMobile && <label className="mobile-field-label">范围</label>}
|
{isMobile && <label className="mobile-field-label">范围</label>}
|
||||||
{!isMobile && <label className="ps-field-label">范围</label>}
|
{!isMobile && <label className="ps-field-label">范围</label>}
|
||||||
@@ -51,9 +42,9 @@ export const DataRangeFields: React.FC<DataRangeFieldsProps> = ({
|
|||||||
onChange={(e) => patchRange({ mode: e.target.value as ExportRangeConfig['mode'] })}
|
onChange={(e) => patchRange({ mode: e.target.value as ExportRangeConfig['mode'] })}
|
||||||
className={isMobile ? 'mobile-field' : 'ps-field text-[11px]'}
|
className={isMobile ? 'mobile-field' : 'ps-field text-[11px]'}
|
||||||
>
|
>
|
||||||
<option value="all">全部行</option>
|
<option value="all">全部标签</option>
|
||||||
<option value="odd">仅奇数序号</option>
|
<option value="odd">仅限奇数序号的标签</option>
|
||||||
<option value="even">仅偶数序号</option>
|
<option value="even">仅限偶数序号的标签</option>
|
||||||
<option value="custom">自定义序号</option>
|
<option value="custom">自定义序号</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -32,46 +32,35 @@ export const MobileExportPropsPanel: React.FC<MobileExportPropsPanelProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
if (locked) return null;
|
||||||
|
|
||||||
const paperLabel =
|
const paperLabel =
|
||||||
paper.type === 'custom' ? `${paper.width}×${paper.height} mm` : paper.type;
|
paper.type === 'custom' ? `${paper.width}×${paper.height} mm` : paper.type;
|
||||||
const summary = `${paperLabel} · ${paper.orientation === 'portrait' ? '纵向' : '横向'}${
|
const summary = `${paperLabel} · ${paper.orientation === 'portrait' ? '纵向' : '横向'}${
|
||||||
cutLine.enabled ? ' · 裁切线' : ''
|
cutLine.enabled ? ' · 裁切线' : ''
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
const toggleOpen = () => {
|
|
||||||
if (locked) return;
|
|
||||||
setOpen((value) => !value);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section className={`mobile-export-card mobile-export-props-card ${open ? 'is-open' : ''}`}>
|
||||||
className={`mobile-export-card mobile-export-props-card ${open ? 'is-open' : ''} ${
|
|
||||||
locked ? 'mobile-export-card-locked' : ''
|
|
||||||
}`}
|
|
||||||
aria-disabled={locked}
|
|
||||||
>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="mobile-export-card-head mobile-export-props-head"
|
className="mobile-export-card-head mobile-export-props-head"
|
||||||
onClick={toggleOpen}
|
onClick={() => setOpen((value) => !value)}
|
||||||
aria-expanded={open && !locked}
|
aria-expanded={open}
|
||||||
disabled={locked}
|
|
||||||
>
|
>
|
||||||
<Settings className="w-4 h-4 text-indigo-500 shrink-0 mt-0.5" />
|
<Settings className="w-4 h-4 text-indigo-500 shrink-0 mt-0.5" />
|
||||||
<div className="flex-1 min-w-0 text-left">
|
<div className="flex-1 min-w-0 text-left">
|
||||||
<h2>导出属性</h2>
|
<h2>导出属性</h2>
|
||||||
<p>{locked ? '上传数据后可配置纸张与裁切线' : summary}</p>
|
<p>{summary}</p>
|
||||||
</div>
|
</div>
|
||||||
{!locked && (
|
<ChevronDown
|
||||||
<ChevronDown
|
className={`w-5 h-5 text-slate-400 shrink-0 mt-0.5 transition-transform duration-200 ${
|
||||||
className={`w-5 h-5 text-slate-400 shrink-0 mt-0.5 transition-transform duration-200 ${
|
open ? 'rotate-180' : ''
|
||||||
open ? 'rotate-180' : ''
|
}`}
|
||||||
}`}
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{open && !locked && (
|
{open && (
|
||||||
<div className="mobile-export-card-body">
|
<div className="mobile-export-card-body">
|
||||||
<PaperSettingsPanel
|
<PaperSettingsPanel
|
||||||
theme="mobile"
|
theme="mobile"
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ import {
|
|||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
FileDown,
|
FileDown,
|
||||||
Loader2,
|
Loader2,
|
||||||
CheckCircle2,
|
|
||||||
Circle,
|
|
||||||
Database,
|
Database,
|
||||||
Link2,
|
Link2,
|
||||||
Download,
|
Download,
|
||||||
@@ -40,40 +38,13 @@ interface MobileExportViewProps {
|
|||||||
onChangeDraftExportRange: (range: ExportRangeConfig) => void;
|
onChangeDraftExportRange: (range: ExportRangeConfig) => void;
|
||||||
cutLine: CutLineConfig;
|
cutLine: CutLineConfig;
|
||||||
onChangeCutLine: (cutLine: CutLineConfig) => void;
|
onChangeCutLine: (cutLine: CutLineConfig) => void;
|
||||||
dataStale: boolean;
|
selectedCount: number;
|
||||||
dataApplied: boolean;
|
totalPages: number;
|
||||||
onApplyData: () => void;
|
|
||||||
draftSelectedCount: number;
|
|
||||||
appliedSelectedCount: number;
|
|
||||||
appliedTotalPages: number;
|
|
||||||
pdfGenerating: boolean;
|
pdfGenerating: boolean;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
onExportPdf: () => void;
|
onExportPdf: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function StepBadge({
|
|
||||||
done,
|
|
||||||
active,
|
|
||||||
number,
|
|
||||||
}: {
|
|
||||||
done: boolean;
|
|
||||||
active: boolean;
|
|
||||||
number: number;
|
|
||||||
}) {
|
|
||||||
if (done) {
|
|
||||||
return (
|
|
||||||
<span className="mobile-step-badge done">
|
|
||||||
<CheckCircle2 className="w-3.5 h-3.5" />
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<span className={`mobile-step-badge ${active ? 'active' : ''}`}>
|
|
||||||
{active ? number : <Circle className="w-3 h-3 opacity-50" />}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const MobileExportView: React.FC<MobileExportViewProps> = ({
|
export const MobileExportView: React.FC<MobileExportViewProps> = ({
|
||||||
template,
|
template,
|
||||||
templateName,
|
templateName,
|
||||||
@@ -91,12 +62,8 @@ export const MobileExportView: React.FC<MobileExportViewProps> = ({
|
|||||||
onChangeDraftExportRange,
|
onChangeDraftExportRange,
|
||||||
cutLine,
|
cutLine,
|
||||||
onChangeCutLine,
|
onChangeCutLine,
|
||||||
dataStale,
|
selectedCount,
|
||||||
dataApplied,
|
totalPages,
|
||||||
onApplyData,
|
|
||||||
draftSelectedCount,
|
|
||||||
appliedSelectedCount,
|
|
||||||
appliedTotalPages,
|
|
||||||
pdfGenerating,
|
pdfGenerating,
|
||||||
onBack,
|
onBack,
|
||||||
onExportPdf,
|
onExportPdf,
|
||||||
@@ -108,17 +75,13 @@ export const MobileExportView: React.FC<MobileExportViewProps> = ({
|
|||||||
const needsMapping = templateVariables.length > 0;
|
const needsMapping = templateVariables.length > 0;
|
||||||
const mappingComplete = !needsMapping || (hasData && mappedCount === templateVariables.length);
|
const mappingComplete = !needsMapping || (hasData && mappedCount === templateVariables.length);
|
||||||
|
|
||||||
const currentStep = !hasData ? 1 : !mappingComplete ? 2 : 3;
|
|
||||||
|
|
||||||
const exportBlockedReason = !hasData
|
const exportBlockedReason = !hasData
|
||||||
? '请先上传 Excel 数据'
|
? '请先上传 Excel 数据'
|
||||||
: !mappingComplete
|
: !mappingComplete
|
||||||
? `还有 ${templateVariables.length - mappedCount} 个变量未关联`
|
? `还有 ${templateVariables.length - mappedCount} 个变量未关联`
|
||||||
: dataStale || !dataApplied
|
: selectedCount === 0
|
||||||
? '请先应用数据'
|
? '当前数据范围内无标签'
|
||||||
: appliedSelectedCount === 0
|
: null;
|
||||||
? '当前数据范围内无标签'
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mobile-export-view">
|
<div className="mobile-export-view">
|
||||||
@@ -130,34 +93,16 @@ export const MobileExportView: React.FC<MobileExportViewProps> = ({
|
|||||||
<p className="mobile-export-title truncate">{templateName}</p>
|
<p className="mobile-export-title truncate">{templateName}</p>
|
||||||
<p className="mobile-export-subtitle">
|
<p className="mobile-export-subtitle">
|
||||||
{template.width}×{template.height} mm
|
{template.width}×{template.height} mm
|
||||||
{hasData && dataApplied && !dataStale && (
|
{hasData && selectedCount > 0 && (
|
||||||
<span>
|
<span>
|
||||||
{' '}
|
{' '}
|
||||||
· {appliedSelectedCount} 枚 · {appliedTotalPages} 页
|
· {selectedCount} 枚 · {totalPages} 页
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="mobile-export-steps" aria-label="导出步骤">
|
|
||||||
<div className={`mobile-export-step ${currentStep >= 1 ? 'active' : ''} ${hasData ? 'done' : ''}`}>
|
|
||||||
<StepBadge done={hasData} active={currentStep === 1} number={1} />
|
|
||||||
<span>数据</span>
|
|
||||||
</div>
|
|
||||||
<div className="mobile-export-step-line" />
|
|
||||||
<div
|
|
||||||
className={`mobile-export-step ${currentStep >= 2 ? 'active' : ''} ${mappingComplete && hasData ? 'done' : ''}`}
|
|
||||||
>
|
|
||||||
<StepBadge done={mappingComplete && hasData} active={currentStep === 2} number={2} />
|
|
||||||
<span>关联</span>
|
|
||||||
</div>
|
|
||||||
<div className={`mobile-export-step ${currentStep >= 3 ? 'active' : ''}`}>
|
|
||||||
<StepBadge done={dataApplied && !dataStale && mappingComplete} active={currentStep === 3} number={3} />
|
|
||||||
<span>导出</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<main className="mobile-export-content">
|
<main className="mobile-export-content">
|
||||||
<section className="mobile-export-card">
|
<section className="mobile-export-card">
|
||||||
<div className="mobile-export-card-head">
|
<div className="mobile-export-card-head">
|
||||||
@@ -165,7 +110,7 @@ export const MobileExportView: React.FC<MobileExportViewProps> = ({
|
|||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex items-start justify-between gap-2">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<h2>步骤 1 · 数据源</h2>
|
<h2>数据源</h2>
|
||||||
<p>{hasData ? '已上传,可设置数据范围' : '上传 Excel 或 CSV 文件'}</p>
|
<p>{hasData ? '已上传,可设置数据范围' : '上传 Excel 或 CSV 文件'}</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -189,36 +134,14 @@ export const MobileExportView: React.FC<MobileExportViewProps> = ({
|
|||||||
templateVariables={templateVariables}
|
templateVariables={templateVariables}
|
||||||
/>
|
/>
|
||||||
{hasData && (
|
{hasData && (
|
||||||
<>
|
<DataRangeFields
|
||||||
<DataRangeFields
|
variant="mobile"
|
||||||
variant="mobile"
|
exportRange={draftExportRange}
|
||||||
exportRange={draftExportRange}
|
onChangeExportRange={onChangeDraftExportRange}
|
||||||
onChangeExportRange={onChangeDraftExportRange}
|
rows={rows}
|
||||||
rows={rows}
|
paper={paper}
|
||||||
paper={paper}
|
template={template}
|
||||||
template={template}
|
/>
|
||||||
/>
|
|
||||||
<div className="space-y-2 pt-1 border-t border-slate-100">
|
|
||||||
{dataStale && (
|
|
||||||
<p className="text-xs text-amber-600">数据或数据范围已变更,请先应用数据</p>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onApplyData}
|
|
||||||
disabled={draftSelectedCount === 0}
|
|
||||||
className={`w-full inline-flex items-center justify-center gap-2 min-h-[44px] px-4 rounded-xl text-sm font-semibold transition cursor-pointer ${
|
|
||||||
draftSelectedCount > 0
|
|
||||||
? dataStale || !dataApplied
|
|
||||||
? 'bg-indigo-600 text-white active:bg-indigo-700'
|
|
||||||
: 'bg-slate-100 text-slate-600 border border-slate-200'
|
|
||||||
: 'bg-slate-100 text-slate-400 cursor-not-allowed'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<Database className="w-4 h-4 shrink-0" />
|
|
||||||
应用数据
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -228,7 +151,7 @@ export const MobileExportView: React.FC<MobileExportViewProps> = ({
|
|||||||
<div className="mobile-export-card-head">
|
<div className="mobile-export-card-head">
|
||||||
<Link2 className="w-4 h-4 text-indigo-500 shrink-0" />
|
<Link2 className="w-4 h-4 text-indigo-500 shrink-0" />
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<h2>步骤 2 · 关联变量</h2>
|
<h2>关联变量</h2>
|
||||||
<p>
|
<p>
|
||||||
{needsMapping
|
{needsMapping
|
||||||
? `已关联 ${mappedCount}/${templateVariables.length}`
|
? `已关联 ${mappedCount}/${templateVariables.length}`
|
||||||
@@ -281,7 +204,7 @@ export const MobileExportView: React.FC<MobileExportViewProps> = ({
|
|||||||
<FileDown className="w-5 h-5 shrink-0" />
|
<FileDown className="w-5 h-5 shrink-0" />
|
||||||
{exportBlockedReason
|
{exportBlockedReason
|
||||||
? '生成 PDF'
|
? '生成 PDF'
|
||||||
: `生成 PDF(${appliedSelectedCount} 枚 · ${appliedTotalPages} 页)`}
|
: `生成 PDF(${selectedCount} 枚 · ${totalPages} 页)`}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ export const PaperSettingsPanel: React.FC<PaperSettingsPanelProps> = ({
|
|||||||
const isPs = theme === 'ps';
|
const isPs = theme === 'ps';
|
||||||
const isMobile = theme === 'mobile';
|
const isMobile = theme === 'mobile';
|
||||||
const fieldClass = isMobile
|
const fieldClass = isMobile
|
||||||
? 'mobile-field font-mono text-center'
|
? 'mobile-field font-mono'
|
||||||
: isPs
|
: isPs
|
||||||
? 'ps-field font-mono text-center'
|
? 'ps-field font-mono text-center'
|
||||||
: 'w-full px-2.5 py-1.5 bg-gray-50 border border-gray-100 rounded-lg text-xs text-center font-mono';
|
: 'w-full px-2.5 py-1.5 bg-gray-50 border border-gray-100 rounded-lg text-xs text-center font-mono';
|
||||||
@@ -326,20 +326,19 @@ export const PaperSettingsPanel: React.FC<PaperSettingsPanelProps> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<div>
|
<div className="col-span-2 flex justify-end -mt-0.5">
|
||||||
<label className={labelClass}>自动计算边距</label>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={autoCenterMargins}
|
onClick={autoCenterMargins}
|
||||||
className={
|
className={
|
||||||
isPs
|
isPs
|
||||||
? 'ps-btn text-[10px]'
|
? 'text-[10px] text-[#777] hover:text-[#31a8ff] transition cursor-pointer'
|
||||||
: isMobile
|
: isMobile
|
||||||
? 'mobile-secondary-btn w-full'
|
? 'text-xs text-slate-500 hover:text-indigo-600 font-normal py-1 cursor-pointer'
|
||||||
: 'w-full inline-flex items-center justify-center gap-1 bg-white hover:bg-gray-100 border border-gray-200 text-gray-600 py-1.5 px-3 rounded-lg text-[10px] font-bold cursor-pointer'
|
: 'text-[10px] text-gray-500 hover:text-indigo-600 font-normal cursor-pointer'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
计算
|
自动计算边距
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -272,6 +272,7 @@ body,
|
|||||||
.ps-panel-body {
|
.ps-panel-body {
|
||||||
color: var(--color-ps-text);
|
color: var(--color-ps-text);
|
||||||
overscroll-behavior: none;
|
overscroll-behavior: none;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ps-panel-body input[type="number"],
|
.ps-panel-body input[type="number"],
|
||||||
@@ -845,6 +846,7 @@ body,
|
|||||||
|
|
||||||
.mobile-export-card-body {
|
.mobile-export-card-body {
|
||||||
padding: 18px 18px 20px;
|
padding: 18px 18px 20px;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-export-card-locked {
|
.mobile-export-card-locked {
|
||||||
@@ -927,10 +929,11 @@ body,
|
|||||||
|
|
||||||
.mobile-file-card {
|
.mobile-file-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: stretch;
|
||||||
justify-content: space-between;
|
gap: 12px;
|
||||||
gap: 14px;
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
|
|||||||
Reference in New Issue
Block a user