fixed complex strings
This commit is contained in:
@@ -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) =>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user