fixed complex strings

This commit is contained in:
2026-09-02 14:35:30 +03:00
parent b7a1609a39
commit f132e83288
8 changed files with 636 additions and 22 deletions
+4
View File
@@ -109,6 +109,10 @@ export const api = {
},
getComponents: (id) => request(`/api/projects/${id}/components`),
getParams: (id) => request(`/api/projects/${id}/params`),
getPropertyNames: (id) => request(`/api/projects/${id}/property-names`),
getTableSettings: (id, type) => request(`/api/projects/${id}/table-settings/${type}`),
patchTableSettings: (id, type, data) =>
request(`/api/projects/${id}/table-settings/${type}`, { method: "PATCH", json: data }),
getPcb: (id) => request(`/api/projects/${id}/pcb`),
getInscriptions: (id) => request(`/api/projects/${id}/inscriptions`),
patchInscriptions: (id, inscriptions) =>
+219 -5
View File
@@ -79,6 +79,78 @@ const STATUS_LABELS = {
error: "ошибка",
};
/** Выпадающий список параметров проекта + свой текст (как в десктопе). */
function ParamCombo({ value, params, onChange, disabled }) {
const known = params.some((p) => p.name === value);
const [custom, setCustom] = useState(value && !known);
useEffect(() => {
setCustom(Boolean(value && !params.some((p) => p.name === value)));
}, [value, params]);
if (custom) {
return (
<div className="row" style={{ gap: "0.35rem" }}>
<input
style={{ flex: 1 }}
value={value || ""}
disabled={disabled}
placeholder="Свой текст"
onChange={(e) => onChange(e.target.value)}
/>
<button
type="button"
className="secondary"
disabled={disabled}
onClick={() => {
setCustom(false);
onChange("");
}}
>
Список
</button>
</div>
);
}
return (
<select
value={value || ""}
disabled={disabled}
onChange={(e) => {
if (e.target.value === "__custom__") {
setCustom(true);
onChange("");
return;
}
onChange(e.target.value);
}}
>
<option value=""> не выбрано </option>
{params.map((p) => (
<option key={p.name} value={p.name}>
{p.value ? `${p.name} (${p.value})` : p.name}
</option>
))}
<option value="__custom__">Свой текст</option>
</select>
);
}
/** Свойство компонента из списка Altium. */
function PropertySelect({ value, properties, onChange, disabled }) {
return (
<select value={value || ""} disabled={disabled} onChange={(e) => onChange(e.target.value)}>
<option value=""> не выбрано </option>
{properties.map((name) => (
<option key={name} value={name}>
{name}
</option>
))}
</select>
);
}
function formatBytes(bytes) {
if (!bytes) return "0 B";
const units = ["B", "KB", "MB", "GB"];
@@ -107,6 +179,8 @@ export default function ProjectPage() {
const [pendingEdits, setPendingEdits] = useState(null);
const [tab, setTab] = useState("tables");
const [uploadState, setUploadState] = useState(null);
const [tableSettings, setTableSettings] = useState(null);
const [settingsDirty, setSettingsDirty] = useState(false);
const uploading = uploadState && uploadState.phase !== "done";
@@ -128,17 +202,36 @@ export default function ProjectPage() {
setRows(data.rows || []);
}, [projectId, tableType]);
const loadTableSettings = useCallback(async () => {
const data = await api.getTableSettings(projectId, tableType);
setTableSettings(data);
setSettingsDirty(false);
}, [projectId, tableType]);
useEffect(() => {
(async () => {
try {
setError("");
await loadProject();
await loadTable();
await loadTableSettings();
} catch (e) {
setError(e.message);
}
})();
}, [loadProject, loadTable]);
}, [loadProject, loadTable, loadTableSettings]);
useEffect(() => {
if (!project) return;
(async () => {
try {
await loadTable();
await loadTableSettings();
} catch (e) {
setError(e.message);
}
})();
}, [tableType, project, loadTable, loadTableSettings]);
const columns = useMemo(() => COLUMNS[tableType] || [], [tableType]);
@@ -158,7 +251,7 @@ export default function ProjectPage() {
try {
const updated = await api.uploadZip(projectId, file, method, true, setUploadState);
setProject(updated);
await Promise.all([loadProject(), loadTable()]);
await Promise.all([loadProject(), loadTable(), loadTableSettings()]);
setUploadState({
phase: "done",
percent: 100,
@@ -222,6 +315,47 @@ export default function ProjectPage() {
}
}
function updateColumnMapping(key, val) {
setTableSettings((prev) => {
if (!prev) return prev;
return {
...prev,
column_mappings: { ...prev.column_mappings, [key]: val },
};
});
setSettingsDirty(true);
}
function updateSimpleListSetting(key, val) {
setTableSettings((prev) => {
if (!prev) return prev;
return {
...prev,
simple_list: { ...prev.simple_list, [key]: val },
};
});
setSettingsDirty(true);
}
async function saveTableSettings() {
if (!tableSettings) return;
setBusy(true);
setError("");
try {
const body = { column_mappings: tableSettings.column_mappings };
if (tableType === "simple_list" && tableSettings.simple_list) {
body.simple_list = tableSettings.simple_list;
}
const data = await api.patchTableSettings(projectId, tableType, body);
setTableSettings(data);
setSettingsDirty(false);
} catch (e) {
setError(e.message);
} finally {
setBusy(false);
}
}
async function sendChat() {
if (!chatInput.trim()) return;
const message = chatInput.trim();
@@ -432,16 +566,22 @@ export default function ProjectPage() {
{tab === "frame" && (
<div className="card stack">
<h3>Поля основной надписи</h3>
<p className="muted">
Выберите параметр проекта из списка или введите свой текст. При экспорте PDF имя параметра
подставится автоматически.
</p>
<div className="grid" style={{ gridTemplateColumns: "repeat(auto-fill,minmax(280px,1fr))" }}>
{FRAME_FIELDS.map(([num, label]) => (
<label key={num} className="stack">
<span className="muted">
{num}. {label}
</span>
<input
<ParamCombo
value={inscriptions[num] || inscriptions[String(num)] || ""}
onChange={(e) =>
setInscriptions((prev) => ({ ...prev, [num]: e.target.value }))
params={params}
disabled={busy}
onChange={(val) =>
setInscriptions((prev) => ({ ...prev, [num]: val }))
}
/>
</label>
@@ -485,6 +625,80 @@ export default function ProjectPage() {
</button>
)}
</div>
{tableSettings?.fields?.length > 0 && (
<div className="settings-panel stack">
<strong>Настройки таблицы</strong>
<p className="muted" style={{ margin: 0 }}>
Сопоставление колонок со свойствами компонентов и параметрами проекта Altium.
</p>
<div className="grid" style={{ gridTemplateColumns: "repeat(auto-fill,minmax(240px,1fr))" }}>
{tableSettings.fields.map((field) => (
<label key={field.key} className="stack">
<span className="muted">{field.label}</span>
{field.source === "project_param" ? (
<ParamCombo
value={tableSettings.column_mappings[field.key] || ""}
params={tableSettings.project_params || params}
disabled={busy}
onChange={(val) => updateColumnMapping(field.key, val)}
/>
) : field.source === "component_field" ? (
<PropertySelect
value={tableSettings.column_mappings[field.key] || ""}
properties={tableSettings.property_names || []}
disabled={busy}
onChange={(val) => updateColumnMapping(field.key, val)}
/>
) : (
<PropertySelect
value={tableSettings.column_mappings[field.key] || ""}
properties={tableSettings.property_names || []}
disabled={busy}
onChange={(val) => updateColumnMapping(field.key, val)}
/>
)}
</label>
))}
{tableType === "simple_list" && tableSettings.simple_list && (
<>
<label className="stack">
<span className="muted">Тех. запас, %</span>
<input
type="number"
min="0"
max="100"
value={tableSettings.simple_list.tech_reserve_percent ?? 10}
disabled={busy}
onChange={(e) =>
updateSimpleListSetting("tech_reserve_percent", Number(e.target.value))
}
/>
</label>
<label className="stack">
<span className="muted">Кол-во плат</span>
<input
type="number"
min="1"
value={tableSettings.simple_list.boards_count ?? 1}
disabled={busy}
onChange={(e) =>
updateSimpleListSetting("boards_count", Number(e.target.value))
}
/>
</label>
</>
)}
</div>
<div className="row">
<button disabled={busy || !settingsDirty} onClick={saveTableSettings}>
Сохранить настройки
</button>
{settingsDirty && <span className="muted">Есть несохранённые изменения</span>}
</div>
</div>
)}
<div className="table-wrap">
<table className="data">
<thead>
+6
View File
@@ -75,6 +75,12 @@ table.data tr.header-row td { font-weight: 700; text-decoration: underline; }
.tab.active { background: var(--accent); color: white; }
.row { display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center; }
.stack { display: flex; flex-direction: column; gap: 0.6rem; }
.settings-panel {
border: 1px dashed var(--line);
border-radius: 8px;
padding: 0.75rem;
background: #faf7f0;
}
.chat {
display: flex; flex-direction: column; gap: 0.5rem; max-height: 50vh; overflow: auto;
border: 1px solid var(--line); border-radius: 8px; padding: 0.6rem; background: #fff;