Align specification prototypes with screens
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 6.1 KiB |
491
docs/scripts/generate-prototypes.mjs
Normal file
@@ -0,0 +1,491 @@
|
||||
import { mkdirSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const outDir = join(__dirname, '..', 'public', 'prototypes');
|
||||
|
||||
const W = 1440;
|
||||
const C = {
|
||||
page: '#f4f4f4',
|
||||
paper: '#ffffff',
|
||||
panel: '#ffffff',
|
||||
soft: '#f7f7f7',
|
||||
line: '#d5d5d5',
|
||||
dark: '#181818',
|
||||
mid: '#545454',
|
||||
muted: '#777777',
|
||||
fill: '#e8e8e8',
|
||||
fill2: '#f0f0f0',
|
||||
};
|
||||
const font = '"Times New Roman", Times, serif';
|
||||
|
||||
function esc(value) {
|
||||
return String(value)
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"');
|
||||
}
|
||||
|
||||
function attrs(values) {
|
||||
return Object.entries(values)
|
||||
.filter(([, value]) => value !== undefined && value !== null)
|
||||
.map(([key, value]) => `${key}="${esc(value)}"`)
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
function rect(x, y, width, height, options = {}) {
|
||||
const {
|
||||
rx = 18,
|
||||
fill = C.panel,
|
||||
stroke = C.line,
|
||||
sw = 1.5,
|
||||
} = options;
|
||||
|
||||
return `<rect ${attrs({ x, y, width, height, rx, fill, stroke, 'stroke-width': sw })} />`;
|
||||
}
|
||||
|
||||
function line(x1, y1, x2, y2, options = {}) {
|
||||
return `<line ${attrs({
|
||||
x1,
|
||||
y1,
|
||||
x2,
|
||||
y2,
|
||||
stroke: options.stroke ?? C.line,
|
||||
'stroke-width': options.sw ?? 1.5,
|
||||
})} />`;
|
||||
}
|
||||
|
||||
function text(x, y, value, options = {}) {
|
||||
const {
|
||||
size = 16,
|
||||
weight = 500,
|
||||
fill = C.dark,
|
||||
anchor = 'start',
|
||||
} = options;
|
||||
|
||||
return `<text ${attrs({
|
||||
x,
|
||||
y,
|
||||
'font-family': font,
|
||||
'font-size': size,
|
||||
'font-weight': weight,
|
||||
fill,
|
||||
'text-anchor': anchor,
|
||||
})}>${esc(value)}</text>`;
|
||||
}
|
||||
|
||||
function circle(cx, cy, r, options = {}) {
|
||||
return `<circle ${attrs({
|
||||
cx,
|
||||
cy,
|
||||
r,
|
||||
fill: options.fill ?? C.fill,
|
||||
stroke: options.stroke,
|
||||
'stroke-width': options.sw,
|
||||
})} />`;
|
||||
}
|
||||
|
||||
function chip(x, y, value, options = {}) {
|
||||
const width = options.width ?? Math.max(76, value.length * 9 + 28);
|
||||
const selected = options.selected ?? false;
|
||||
return [
|
||||
rect(x, y, width, 34, {
|
||||
rx: 17,
|
||||
fill: selected ? C.dark : C.soft,
|
||||
stroke: selected ? C.dark : C.line,
|
||||
}),
|
||||
text(x + width / 2, y + 22, value, {
|
||||
size: 13,
|
||||
weight: 700,
|
||||
fill: selected ? '#ffffff' : C.mid,
|
||||
anchor: 'middle',
|
||||
}),
|
||||
].join('');
|
||||
}
|
||||
|
||||
function input(x, y, width, label) {
|
||||
return [
|
||||
text(x, y - 12, label, { size: 13, weight: 700, fill: C.mid }),
|
||||
rect(x, y, width, 48, { rx: 16, fill: C.paper }),
|
||||
].join('');
|
||||
}
|
||||
|
||||
function button(x, y, width, label, options = {}) {
|
||||
const dark = options.dark ?? false;
|
||||
return [
|
||||
rect(x, y, width, 44, {
|
||||
rx: 22,
|
||||
fill: dark ? C.dark : C.fill,
|
||||
stroke: dark ? C.dark : C.line,
|
||||
}),
|
||||
text(x + width / 2, y + 28, label, {
|
||||
size: 14,
|
||||
weight: 700,
|
||||
fill: dark ? '#ffffff' : C.mid,
|
||||
anchor: 'middle',
|
||||
}),
|
||||
].join('');
|
||||
}
|
||||
|
||||
function topShell(label, nav = [], active = '') {
|
||||
const parts = [
|
||||
rect(24, 24, 1392, 56, { rx: 28, fill: '#fafafa' }),
|
||||
rect(24, 52, 1392, 28, { rx: 0, fill: '#fafafa', stroke: '#fafafa' }),
|
||||
circle(58, 52, 7, { fill: '#b7b7b7' }),
|
||||
circle(82, 52, 7, { fill: '#d2d2d2' }),
|
||||
circle(106, 52, 7, { fill: '#d4d4d4' }),
|
||||
text(136, 58, label, { size: 17, weight: 700 }),
|
||||
];
|
||||
|
||||
let x = 820;
|
||||
for (const item of nav) {
|
||||
parts.push(chip(x, 36, item, { width: Math.max(88, item.length * 10 + 34), selected: item === active }));
|
||||
x += Math.max(88, item.length * 10 + 34) + 12;
|
||||
}
|
||||
|
||||
return parts.join('');
|
||||
}
|
||||
|
||||
function page(label, height, body, options = {}) {
|
||||
const nav = options.nav ?? ['Каталог', 'Мои заказы', 'Корзина', 'Профиль'];
|
||||
const active = options.active ?? '';
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${height}" viewBox="0 0 ${W} ${height}" fill="none"><rect width="${W}" height="${height}" fill="${C.page}" />${rect(24, 24, 1392, height - 48, { rx: 28, fill: C.paper })}${topShell(label, nav, active)}${body.join('')}</svg>`;
|
||||
}
|
||||
|
||||
function titleBlock(title, y = 132, x = 72) {
|
||||
return text(x, y, title, { size: 32, weight: 800 });
|
||||
}
|
||||
|
||||
function searchHero(title, placeholder, controls = []) {
|
||||
const parts = [
|
||||
titleBlock(title),
|
||||
rect(72, 168, 600, 54, { rx: 27, fill: C.paper }),
|
||||
text(98, 201, placeholder, { size: 15, weight: 500, fill: C.muted }),
|
||||
];
|
||||
let x = 700;
|
||||
for (const control of controls) {
|
||||
parts.push(chip(x, 178, control, { width: Math.max(120, control.length * 9 + 34), selected: control === controls[0] }));
|
||||
x += Math.max(120, control.length * 9 + 34) + 12;
|
||||
}
|
||||
return parts.join('');
|
||||
}
|
||||
|
||||
function catalogCards(y = 260) {
|
||||
const cards = ['Стретч-пленка', 'Скотч', 'Пакеты', 'Пленка ПВД', 'Воздушно-пузырьковая', 'Картон'];
|
||||
const parts = [];
|
||||
cards.forEach((name, index) => {
|
||||
const col = index % 3;
|
||||
const row = Math.floor(index / 3);
|
||||
const x = 72 + col * 432;
|
||||
const yy = y + row * 238;
|
||||
parts.push(rect(x, yy, 396, 202, { rx: 26 }));
|
||||
parts.push(rect(x + 20, yy + 20, 356, 118, { rx: 22, fill: C.fill2 }));
|
||||
parts.push(text(x + 28, yy + 170, name, { size: 22, weight: 800 }));
|
||||
});
|
||||
return parts.join('');
|
||||
}
|
||||
|
||||
function orderRows(x, y, width, rows, options = {}) {
|
||||
const parts = [];
|
||||
const rowH = options.rowH ?? 72;
|
||||
rows.forEach((row, index) => {
|
||||
const yy = y + index * (rowH + 12);
|
||||
parts.push(rect(x, yy, width, rowH, { rx: 20, fill: index % 2 ? '#fbfbfb' : C.paper }));
|
||||
parts.push(text(x + 24, yy + 30, row[0], { size: 17, weight: 800 }));
|
||||
parts.push(text(x + 24, yy + 54, row[1], { size: 14, weight: 500, fill: C.mid }));
|
||||
if (row[2]) {
|
||||
parts.push(chip(x + width - 210, yy + 18, row[2], { width: 150 }));
|
||||
}
|
||||
});
|
||||
return parts.join('');
|
||||
}
|
||||
|
||||
function cardGrid(x, y, labels, columns = 3) {
|
||||
const parts = [];
|
||||
labels.forEach((label, index) => {
|
||||
const col = index % columns;
|
||||
const row = Math.floor(index / columns);
|
||||
const w = columns === 4 ? 300 : 388;
|
||||
const xx = x + col * (w + 24);
|
||||
const yy = y + row * 128;
|
||||
parts.push(rect(xx, yy, w, 104, { rx: 24 }));
|
||||
parts.push(circle(xx + 44, yy + 52, 24, { fill: C.fill2 }));
|
||||
parts.push(text(xx + 82, yy + 48, label[0], { size: 17, weight: 800 }));
|
||||
if (label[1]) {
|
||||
parts.push(text(xx + 82, yy + 72, label[1], { size: 14, weight: 500, fill: C.mid }));
|
||||
}
|
||||
});
|
||||
return parts.join('');
|
||||
}
|
||||
|
||||
const pages = {
|
||||
'dashboard.svg': page('Главная страница клиента', 900, [
|
||||
searchHero('Каталог', 'Поиск по типу товара', []),
|
||||
catalogCards(260),
|
||||
], { active: 'Каталог' }),
|
||||
|
||||
'catalog-grid.svg': page('Каталог продукции', 900, [
|
||||
searchHero('Каталог', 'Поиск по типу товара', []),
|
||||
catalogCards(260),
|
||||
], { active: 'Каталог' }),
|
||||
|
||||
'product-card.svg': page('Карточка товара', 1040, [
|
||||
button(72, 116, 110, 'Назад'),
|
||||
titleBlock('Алюминиевый скотч', 166),
|
||||
rect(72, 220, 400, 330, { rx: 32 }),
|
||||
rect(102, 252, 340, 228, { rx: 26, fill: C.fill2 }),
|
||||
text(272, 510, 'Изображение товара', { size: 16, weight: 700, fill: C.mid, anchor: 'middle' }),
|
||||
rect(504, 220, 536, 330, { rx: 32 }),
|
||||
text(536, 258, 'Параметры', { size: 22, weight: 800 }),
|
||||
text(536, 304, 'Ширина', { size: 14, weight: 700, fill: C.mid }),
|
||||
chip(536, 320, '48 мм', { selected: true }),
|
||||
chip(628, 320, '75 мм'),
|
||||
text(780, 304, 'Длина', { size: 14, weight: 700, fill: C.mid }),
|
||||
chip(780, 320, '25 м', { selected: true }),
|
||||
chip(862, 320, '50 м'),
|
||||
chip(944, 320, '100 м'),
|
||||
text(536, 386, 'Толщина', { size: 14, weight: 700, fill: C.mid }),
|
||||
chip(536, 402, '43 мкм', { selected: true }),
|
||||
chip(638, 402, '45 мкм'),
|
||||
text(780, 386, 'Втулка', { size: 14, weight: 700, fill: C.mid }),
|
||||
chip(780, 402, 'Стандарт', { selected: true, width: 112 }),
|
||||
chip(904, 402, 'Логотип', { width: 104 }),
|
||||
text(536, 468, 'Цвет', { size: 14, weight: 700, fill: C.mid }),
|
||||
chip(536, 484, 'Серебристый', { selected: true, width: 126 }),
|
||||
text(780, 468, 'Надпись', { size: 14, weight: 700, fill: C.mid }),
|
||||
chip(780, 484, 'Без надписи', { selected: true, width: 136 }),
|
||||
rect(1072, 220, 296, 330, { rx: 32 }),
|
||||
text(1100, 262, 'FRG-ALU-48-50', { size: 20, weight: 800 }),
|
||||
text(1100, 310, 'В наличии', { size: 16, weight: 700, fill: C.mid }),
|
||||
text(1100, 342, '2 140', { size: 38, weight: 800 }),
|
||||
button(1100, 394, 220, 'В корзину', { dark: true }),
|
||||
text(72, 624, 'Доступные варианты', { size: 24, weight: 800 }),
|
||||
rect(72, 652, 1296, 258, { rx: 24 }),
|
||||
text(104, 698, 'SKU', { size: 14, weight: 800, fill: C.mid }),
|
||||
text(312, 698, 'Ширина', { size: 14, weight: 800, fill: C.mid }),
|
||||
text(470, 698, 'Длина', { size: 14, weight: 800, fill: C.mid }),
|
||||
text(620, 698, 'Толщина', { size: 14, weight: 800, fill: C.mid }),
|
||||
text(790, 698, 'Втулка', { size: 14, weight: 800, fill: C.mid }),
|
||||
text(970, 698, 'Остаток', { size: 14, weight: 800, fill: C.mid }),
|
||||
text(1160, 698, 'Действие', { size: 14, weight: 800, fill: C.mid }),
|
||||
line(96, 716, 1344, 716),
|
||||
orderRows(96, 738, 1248, [
|
||||
['FRG-ALU-48-50', '48 мм · 50 м · 43 мкм · стандарт', 'В корзину'],
|
||||
['FRG-ALU-75-50', '75 мм · 50 м · 45 мкм · стандарт', 'В корзину'],
|
||||
], { rowH: 64 }),
|
||||
], { active: 'Каталог' }),
|
||||
|
||||
'cart.svg': page('Корзина', 900, [
|
||||
titleBlock('Корзина'),
|
||||
rect(72, 168, 1296, 68, { rx: 24, fill: C.soft }),
|
||||
text(102, 210, 'Заполните карточку контрагента перед оформлением заявки', { size: 16, weight: 700, fill: C.mid }),
|
||||
text(72, 284, 'Состав заказа', { size: 24, weight: 800 }),
|
||||
rect(72, 312, 760, 330, { rx: 28 }),
|
||||
orderRows(104, 344, 696, [
|
||||
['Стретч-пленка', '48 мм · 50 м · 43 мкм', '2 шт'],
|
||||
['Скотч упаковочный', '75 мм · 66 м', '4 шт'],
|
||||
['Пакет ПВД', '300 x 400 мм', '1 шт'],
|
||||
], { rowH: 72 }),
|
||||
rect(872, 312, 496, 330, { rx: 28 }),
|
||||
text(904, 354, 'Информация о доставке', { size: 22, weight: 800 }),
|
||||
chip(904, 390, 'Склад клиента', { selected: true, width: 160 }),
|
||||
chip(904, 444, 'Новый адрес', { width: 148 }),
|
||||
input(904, 532, 380, 'Комментарий'),
|
||||
button(904, 610, 260, 'Оформить заявку', { dark: true }),
|
||||
], { active: 'Корзина' }),
|
||||
|
||||
'client-order.svg': page('Карточка заказа клиента', 860, [
|
||||
button(72, 116, 190, 'Назад к моим заказам'),
|
||||
titleBlock('Заказ FRG-1024', 170),
|
||||
rect(72, 220, 1296, 118, { rx: 28 }),
|
||||
text(104, 262, 'Статус заказа', { size: 16, weight: 700, fill: C.mid }),
|
||||
chip(104, 282, 'Предложение', { selected: true, width: 148 }),
|
||||
chip(282, 282, 'Подтвердить', { width: 150 }),
|
||||
chip(456, 282, 'Отклонить', { width: 130 }),
|
||||
text(72, 394, 'Состав заказа', { size: 24, weight: 800 }),
|
||||
rect(72, 426, 1296, 260, { rx: 28 }),
|
||||
orderRows(104, 458, 1232, [
|
||||
['Стретч-пленка', '48 мм · 50 м · количество 2', 'Цена задана'],
|
||||
['Скотч упаковочный', '75 мм · 66 м · количество 4', 'Цена задана'],
|
||||
], { rowH: 76 }),
|
||||
rect(72, 720, 1296, 80, { rx: 24, fill: C.soft }),
|
||||
text(104, 754, 'Доставка', { size: 17, weight: 800 }),
|
||||
text(260, 754, 'Адрес, срок и стоимость доставки показываются в одной строке', { size: 15, weight: 500, fill: C.mid }),
|
||||
], { active: 'Мои заказы' }),
|
||||
|
||||
'login.svg': page('Логин', 760, [
|
||||
rect(450, 130, 540, 520, { rx: 32 }),
|
||||
text(720, 196, 'Фрегат', { size: 14, weight: 800, fill: C.mid, anchor: 'middle' }),
|
||||
text(720, 244, 'Вход', { size: 36, weight: 800, anchor: 'middle' }),
|
||||
input(510, 304, 420, 'E-mail'),
|
||||
button(510, 386, 420, 'Получить код', { dark: true }),
|
||||
line(510, 464, 930, 464),
|
||||
text(720, 492, 'или войти через', { size: 13, weight: 700, fill: C.mid, anchor: 'middle' }),
|
||||
button(510, 526, 196, 'Telegram'),
|
||||
button(734, 526, 196, 'Max'),
|
||||
], { nav: [] }),
|
||||
|
||||
'bonus-cabinet.svg': page('Бонусный кабинет', 940, [
|
||||
titleBlock('Чёрный кабинет бонусной программы'),
|
||||
rect(72, 178, 820, 250, { rx: 30 }),
|
||||
text(112, 230, 'Аккаунт', { size: 15, weight: 700, fill: C.mid }),
|
||||
text(112, 280, 'Клиент бонусной программы', { size: 32, weight: 800, fill: C.dark }),
|
||||
text(112, 354, 'Доступный баланс', { size: 15, weight: 700, fill: C.mid }),
|
||||
text(112, 398, '12 400', { size: 48, weight: 800, fill: C.dark }),
|
||||
rect(928, 178, 440, 250, { rx: 30 }),
|
||||
text(968, 230, 'Вывод бонусов', { size: 20, weight: 800, fill: C.dark }),
|
||||
input(968, 292, 320, 'Сумма заявки'),
|
||||
button(968, 370, 280, 'Подать заявку', { dark: false }),
|
||||
rect(72, 472, 620, 300, { rx: 30 }),
|
||||
text(112, 522, 'История бонусов', { size: 24, weight: 800, fill: C.dark }),
|
||||
orderRows(112, 552, 540, [
|
||||
['+1 500', 'Начисление по заказу', ''],
|
||||
['+900', 'Реферальное начисление', ''],
|
||||
], { rowH: 68 }),
|
||||
rect(748, 472, 620, 300, { rx: 30 }),
|
||||
text(788, 522, 'Вознаграждения', { size: 24, weight: 800, fill: C.dark }),
|
||||
button(788, 566, 170, 'Ozon 3000'),
|
||||
button(980, 566, 210, 'Wildberries 4000'),
|
||||
button(788, 634, 190, 'М.Видео 5000'),
|
||||
], { active: 'Профиль' }),
|
||||
|
||||
'client-list.svg': page('Клиенты', 900, [
|
||||
searchHero('Клиенты', 'Имя, компания или email', ['Пригласить']),
|
||||
cardGrid(72, 270, [
|
||||
['Иван Петров', 'ООО Альфа'],
|
||||
['Мария Соколова', 'ИП Соколова'],
|
||||
['Дмитрий Иванов', 'ООО Север'],
|
||||
['Анна Смирнова', 'ООО Вектор'],
|
||||
['Павел Морозов', 'Завод Мир'],
|
||||
['Елена Орлова', 'ТД Орлова'],
|
||||
], 3),
|
||||
], { nav: ['Заказы', 'Бонусы', 'Настройки'], active: 'Заказы' }),
|
||||
|
||||
'client-card.svg': page('Карточка клиента', 880, [
|
||||
button(72, 116, 170, 'Назад к клиентам'),
|
||||
titleBlock('Клиент Иван Петров', 170),
|
||||
cardGrid(72, 224, [
|
||||
['Email', 'client@company.ru'],
|
||||
['Telegram', 'Подключен'],
|
||||
['Компания', 'ООО Альфа'],
|
||||
['ИНН', '7700000000'],
|
||||
], 4),
|
||||
text(72, 500, 'Заказы пользователя', { size: 24, weight: 800 }),
|
||||
rect(72, 532, 1296, 240, { rx: 28 }),
|
||||
orderRows(104, 564, 1232, [
|
||||
['FRG-1024', 'Стретч-пленка · Москва', 'В работе'],
|
||||
['FRG-1017', 'Скотч · Санкт-Петербург', 'Завершен'],
|
||||
], { rowH: 72 }),
|
||||
], { nav: ['Заказы', 'Бонусы', 'Настройки'], active: 'Заказы' }),
|
||||
|
||||
'manager-order.svg': page('Обработка заявки', 900, [
|
||||
button(72, 116, 170, 'Назад к заказам'),
|
||||
titleBlock('Заказ FRG-1024', 170),
|
||||
rect(72, 220, 1296, 118, { rx: 28 }),
|
||||
text(104, 262, 'Статус заказа', { size: 16, weight: 700, fill: C.mid }),
|
||||
chip(104, 282, 'В обработке', { selected: true, width: 148 }),
|
||||
chip(282, 282, 'Предложение', { width: 150 }),
|
||||
rect(72, 382, 920, 300, { rx: 28 }),
|
||||
text(104, 426, 'Состав заказа', { size: 24, weight: 800 }),
|
||||
orderRows(104, 460, 856, [
|
||||
['Стретч-пленка', 'Количество 2 · цена редактируется', 'Цена'],
|
||||
['Скотч упаковочный', 'Количество 4 · цена редактируется', 'Цена'],
|
||||
], { rowH: 76 }),
|
||||
rect(1028, 382, 340, 300, { rx: 28 }),
|
||||
text(1060, 426, 'Условия', { size: 24, weight: 800 }),
|
||||
input(1060, 484, 250, 'Срок доставки'),
|
||||
input(1060, 578, 250, 'Стоимость доставки'),
|
||||
button(1060, 650, 230, 'Сохранить', { dark: true }),
|
||||
], { nav: ['Заказы', 'Бонусы', 'Настройки'], active: 'Заказы' }),
|
||||
|
||||
'manager-orders.svg': page('Заказы менеджера', 920, [
|
||||
searchHero('Заказы', 'Номер заказа, клиент, адрес или товар', ['Список', 'Календарь']),
|
||||
chip(72, 250, 'Все', { selected: true, width: 88 }),
|
||||
chip(174, 250, 'Заявки', { width: 112 }),
|
||||
chip(300, 250, 'Предложения', { width: 150 }),
|
||||
chip(464, 250, 'В работе', { width: 126 }),
|
||||
chip(604, 250, 'Закрытые', { width: 126 }),
|
||||
rect(72, 320, 1296, 430, { rx: 30 }),
|
||||
orderRows(104, 356, 1232, [
|
||||
['FRG-1024', 'Иван Петров · Стретч-пленка · Москва', 'Заявка'],
|
||||
['FRG-1025', 'Мария Соколова · Скотч · Казань', 'Предложение'],
|
||||
['FRG-1026', 'Дмитрий Иванов · Пакеты · СПб', 'В работе'],
|
||||
['FRG-1027', 'Анна Смирнова · Пленка ПВД · Москва', 'Закрыт'],
|
||||
], { rowH: 76 }),
|
||||
], { nav: ['Заказы', 'Бонусы', 'Настройки'], active: 'Заказы' }),
|
||||
|
||||
'catalog-settings.svg': page('Настройки каталога', 980, [
|
||||
titleBlock('Каталог'),
|
||||
rect(72, 184, 1296, 104, { rx: 28 }),
|
||||
text(104, 226, 'Стретч-пленка', { size: 22, weight: 800 }),
|
||||
text(104, 256, '6 параметров, 3 кастомные возможности', { size: 15, weight: 500, fill: C.mid }),
|
||||
rect(72, 318, 1296, 446, { rx: 28 }),
|
||||
text(104, 362, 'Кастомные возможности', { size: 22, weight: 800 }),
|
||||
chip(104, 390, 'Любая длина', { selected: true, width: 140 }),
|
||||
chip(262, 390, 'Логотип на втулке', { width: 190 }),
|
||||
chip(470, 390, 'Нанесение надписи', { width: 200 }),
|
||||
text(104, 478, 'Диапазон длины', { size: 18, weight: 800 }),
|
||||
input(104, 520, 240, 'Мин. длина, м'),
|
||||
input(378, 520, 240, 'Макс. длина, м'),
|
||||
input(652, 520, 240, 'Шаг, м'),
|
||||
text(104, 638, 'Параметры', { size: 18, weight: 800 }),
|
||||
chip(104, 664, 'Ширина', { width: 110 }),
|
||||
chip(232, 664, 'Длина', { width: 100 }),
|
||||
chip(350, 664, 'Толщина', { width: 120 }),
|
||||
chip(488, 664, 'Втулка', { width: 108 }),
|
||||
chip(614, 664, 'Цвет', { width: 96 }),
|
||||
chip(728, 664, 'Надпись', { width: 120 }),
|
||||
button(1100, 804, 190, 'Сохранить', { dark: true }),
|
||||
], { nav: ['Заказы', 'Бонусы', 'Настройки'], active: 'Настройки' }),
|
||||
|
||||
'sync-settings.svg': page('Настройки синхронизации', 900, [
|
||||
titleBlock('1С'),
|
||||
text(72, 168, 'Статус загрузки файлов обмена', { size: 16, weight: 500, fill: C.mid }),
|
||||
cardGrid(72, 230, [
|
||||
['catalog_snapshot', 'Каталог и остатки'],
|
||||
['balances_snapshot', 'Задолженность клиентов'],
|
||||
['orders_snapshot', 'Заказы клиентов'],
|
||||
], 3),
|
||||
rect(72, 450, 1296, 250, { rx: 28 }),
|
||||
text(104, 494, 'Последние загрузки', { size: 24, weight: 800 }),
|
||||
orderRows(104, 530, 1232, [
|
||||
['Каталог и остатки', 'Загружено 2 418 записей · последний run сегодня', 'Работает'],
|
||||
['Задолженность клиентов', 'Баланс по клиентам с личным кабинетом', 'Работает'],
|
||||
['Заказы клиентов', 'Статусы заказов за рабочий период', 'Работает'],
|
||||
], { rowH: 62 }),
|
||||
], { nav: ['Заказы', 'Бонусы', 'Настройки'], active: 'Настройки' }),
|
||||
|
||||
'profile.svg': page('Профиль клиента', 820, [
|
||||
titleBlock('Профиль'),
|
||||
cardGrid(72, 210, [
|
||||
['Карточка контрагента', 'Реквизиты и ИНН'],
|
||||
['Уведомления', 'Telegram и Max'],
|
||||
['Адреса доставки', 'Список адресов'],
|
||||
], 3),
|
||||
], { active: 'Профиль' }),
|
||||
|
||||
'bonus-manager.svg': page('Бонусная система менеджера', 920, [
|
||||
searchHero('Бонусы', 'Клиент, связанный клиент или email', ['Добавить']),
|
||||
chip(72, 250, 'Балансы', { selected: true, width: 120 }),
|
||||
chip(208, 250, 'Заявки', { width: 110 }),
|
||||
chip(334, 250, 'Награды', { width: 116 }),
|
||||
cardGrid(72, 320, [
|
||||
['Иван Петров', '12 400 ₽'],
|
||||
['Мария Соколова', '8 250 ₽'],
|
||||
['Дмитрий Иванов', '5 100 ₽'],
|
||||
['Анна Смирнова', '2 900 ₽'],
|
||||
], 4),
|
||||
rect(72, 610, 1296, 170, { rx: 28 }),
|
||||
text(104, 654, 'Заявки на выплату', { size: 24, weight: 800 }),
|
||||
orderRows(104, 686, 1232, [
|
||||
['WD-01A23F', 'Иван Петров · на проверке', '12 000 ₽'],
|
||||
], { rowH: 68 }),
|
||||
], { nav: ['Заказы', 'Бонусы', 'Настройки'], active: 'Бонусы' }),
|
||||
};
|
||||
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
for (const [fileName, content] of Object.entries(pages)) {
|
||||
writeFileSync(join(outDir, fileName), `${content}\n`, 'utf8');
|
||||
}
|
||||
@@ -1064,7 +1064,7 @@
|
||||
[Главная страница],
|
||||
[/],
|
||||
[клиент],
|
||||
[стартовая точка, быстрые действия, актуальные события],
|
||||
[стартовый экран каталога],
|
||||
[Каталог],
|
||||
[/products],
|
||||
[клиент],
|
||||
@@ -1330,17 +1330,16 @@
|
||||
|
||||
Назначение страницы:
|
||||
|
||||
- вход в основные разделы личного кабинета
|
||||
- отображение актуальных действий и событий
|
||||
- отображение каталога товарных направлений
|
||||
- переход к карточке выбранного типа товара
|
||||
|
||||
Состав страницы:
|
||||
|
||||
- верхняя навигация
|
||||
- быстрые переходы по основным разделам
|
||||
- блок актуальных заказов и заявок
|
||||
- блок последних уведомлений
|
||||
- блок бонусной информации при наличии подключенного бонусного контура
|
||||
- индикатор статуса заполненности профиля клиента
|
||||
- заголовок раздела
|
||||
- поиск по типу товара
|
||||
- сетка карточек товарных направлений
|
||||
- переход в карточку выбранного товарного направления
|
||||
|
||||
Wireframe-прототип:
|
||||
|
||||
@@ -1393,7 +1392,6 @@ Wireframe-прототип:
|
||||
- блок индивидуальных возможностей, если они разрешены
|
||||
- блок добавления в корзину
|
||||
- таблица доступных вариантов
|
||||
- блок навигации к соседним товарным направлениям
|
||||
|
||||
Маршрут страницы:
|
||||
|
||||
@@ -1485,15 +1483,14 @@ Wireframe-прототип:
|
||||
Назначение страницы:
|
||||
|
||||
- запуск входа в систему
|
||||
- запуск самостоятельной заявки на подключение
|
||||
- запуск сценариев входа через мессенджеры
|
||||
|
||||
Состав страницы:
|
||||
|
||||
- форма запроса кода входа
|
||||
- выбор канала входа
|
||||
- ссылка на самостоятельную заявку на подключение
|
||||
- блок пояснения по дальнейшему сценарию доступа
|
||||
- форма проверки кода из письма
|
||||
- кнопки входа через доступные мессенджеры
|
||||
- служебное сообщение о результате входа либо привязки канала
|
||||
|
||||
Wireframe-прототип:
|
||||
|
||||
@@ -1514,7 +1511,7 @@ Wireframe-прототип:
|
||||
Состав страницы:
|
||||
|
||||
- фильтры
|
||||
- таблица заказов
|
||||
- карточки заказов
|
||||
- переход в карточку конкретного заказа
|
||||
|
||||
=== Уведомления
|
||||
@@ -1539,7 +1536,7 @@ Wireframe-прототип:
|
||||
- история начислений и списаний
|
||||
- связанные реферальные сведения
|
||||
- форма подачи заявки на использование либо вывод бонусов
|
||||
- правила бонусной программы
|
||||
- магазин вознаграждений
|
||||
|
||||
Wireframe-прототип:
|
||||
|
||||
@@ -1563,8 +1560,7 @@ Wireframe-прототип:
|
||||
Состав страницы:
|
||||
|
||||
- поиск и фильтры
|
||||
- таблица клиентов
|
||||
- индикаторы активности и количества заказов
|
||||
- сетка карточек клиентов
|
||||
- действие приглашения нового клиента
|
||||
|
||||
Wireframe-прототип:
|
||||
@@ -1582,15 +1578,12 @@ Wireframe-прототип:
|
||||
|
||||
- просмотр сведений о компании
|
||||
- просмотр истории заявок и заказов
|
||||
- просмотр бонусных и реферальных данных
|
||||
|
||||
Состав страницы:
|
||||
|
||||
- карточка компании и контактных данных
|
||||
- реквизиты контрагента
|
||||
- список заказов клиента
|
||||
- список бонусных операций
|
||||
- связанные рефералы
|
||||
|
||||
Wireframe-прототип:
|
||||
|
||||
@@ -1653,7 +1646,6 @@ Wireframe-прототип:
|
||||
- управление параметрами товарных направлений
|
||||
- управление стандартными значениями параметров
|
||||
- управление возможностями кастомизации
|
||||
- управление описаниями параметров
|
||||
|
||||
Состав страницы:
|
||||
|
||||
@@ -1671,20 +1663,20 @@ Wireframe-прототип:
|
||||
)
|
||||
|
||||
|
||||
=== Настройки синхронизации и уведомлений
|
||||
=== Настройки синхронизации
|
||||
|
||||
|
||||
Назначение страницы:
|
||||
|
||||
- управление шаблонами уведомлений
|
||||
- управление параметрами интеграционного обмена
|
||||
- просмотр состояния интеграционного обмена
|
||||
- контроль последних загрузок данных
|
||||
|
||||
Состав страницы:
|
||||
|
||||
- список шаблонов уведомлений
|
||||
- каналы отправки
|
||||
- статусы последних синхронизаций
|
||||
- диагностическая информация по обмену
|
||||
- количество загруженных записей
|
||||
- дата последней загрузки
|
||||
- примечание по каждому потоку обмена
|
||||
|
||||
Wireframe-прототип:
|
||||
|
||||
|
||||