AspNetForge.WebCore
1.0.3
See the version list below for details.
dotnet add package AspNetForge.WebCore --version 1.0.3
NuGet\Install-Package AspNetForge.WebCore -Version 1.0.3
<PackageReference Include="AspNetForge.WebCore" Version="1.0.3" />
<PackageVersion Include="AspNetForge.WebCore" Version="1.0.3" />
<PackageReference Include="AspNetForge.WebCore" />
paket add AspNetForge.WebCore --version 1.0.3
#r "nuget: AspNetForge.WebCore, 1.0.3"
#:package AspNetForge.WebCore@1.0.3
#addin nuget:?package=AspNetForge.WebCore&version=1.0.3
#tool nuget:?package=AspNetForge.WebCore&version=1.0.3
AspNetForge.WebCore
Class Library + стартовый шаблон для ASP.NET Core Minimal API (.NET 8).
Содержит generic CRUD-эндпоинты, cookie-авторизацию, встроенные HTML/CSS/JS и готовый пример проекта.
Быстрый старт
dotnet new web -n MyWebApp
cd MyWebApp
dotnet add package AspNetForge.WebCore --version 1.0.3
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL --version 8.0.11
dotnet build
dotnet run
Важно: пакет рассчитан на чистый проект
dotnet new web. Он заменяетProgram.csи копируетwwwroot/.
Что происходит после установки
Models.csдобавляется в проект как linked-файл (компилируется, но не копируется на диск).Program.csзаменяется шаблонным примером.wwwroot/копируется с HTML/CSS/JS.appsettings.jsonне перезаписывается, если уже существует. Добавьте строку подключения вручную.
Настройка PostgreSQL
Откройте appsettings.json и добавьте:
{
"ConnectionStrings": {
"DefaultConnection": "Host=localhost;Port=5432;Database=examdb;Username=postgres;Password=postgres"
}
}
Создайте базу:
CREATE DATABASE examdb;
Встроенные учётные записи
| Логин | Пароль | Роль |
|---|---|---|
| admin | admin | Admin |
| manager | manager | Manager |
| client | client | Client |
Возможности библиотеки
using AspNetForge.Web.Extensions;
app.MapCrud<Product, AppDbContext>("/api/products");
app.MapCrud<Order, AppDbContext>("/api/orders");
app.MapImport<Product, AppDbContext>("/api/products/import").RequireAuthorization("ManagerPlus");
app.UseForgeResources(overwrite: true);
Если файлы не появились
Закройте Visual Studio, удалите папку obj/, откройте решение заново и нажмите Ctrl+Shift+B.
Возможные ошибки
"Строка подключения 'DefaultConnection' не найдена"
Добавьте строку подключения в appsettings.json (см. выше).
Visual Studio предлагает запустить прошлый вариант
- Ctrl+Shift+B — собрать проект.
- Правый клик по проекту → Перезагрузить проект.
- Запустить снова.
CS0234: EntityFrameworkCore не существует
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL --version 8.0.11
404 Not Found
Убедитесь, что в Program.cs есть:
app.UseDefaultFiles();
app.UseStaticFiles();
wwwroot пустой
При старте UseForgeResources(overwrite: true) копирует встроенные файлы. Убедитесь, что метод вызван до UseStaticFiles().
Fallback: код всех файлов
Если автоматическое копирование не сработало, создайте файлы вручную.
Models.cs
using Microsoft.EntityFrameworkCore;
namespace ExamProject.Models;
public enum UserRole
{
Client,
Manager,
Admin
}
public class User
{
public int Id { get; set; }
public required string Login { get; set; }
public required string Password { get; set; }
public required string FullName { get; set; }
public UserRole Role { get; set; } = UserRole.Client;
}
public class PickupPoint
{
public int Id { get; set; }
public required string Address { get; set; }
public required string City { get; set; }
}
public class Product
{
public int Id { get; set; }
public required string Article { get; set; }
public required string Name { get; set; }
public required string Category { get; set; }
public required string Manufacturer { get; set; }
public required string Supplier { get; set; }
public decimal Price { get; set; }
public int Discount { get; set; }
public int Quantity { get; set; }
public required string Description { get; set; }
public required string ImagePath { get; set; }
}
public class Order
{
public int Id { get; set; }
public int OrderNumber { get; set; }
public DateTime OrderDate { get; set; }
public required string ClientFullName { get; set; }
public int ProductId { get; set; }
public Product Product { get; set; } = null!;
public int Quantity { get; set; }
public int PickupPointId { get; set; }
public PickupPoint PickupPoint { get; set; } = null!;
public int Discount { get; set; }
public required string Status { get; set; }
public DateTime DeliveryDate { get; set; }
public int Code { get; set; }
}
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
public DbSet<User> Users { get; set; }
public DbSet<PickupPoint> PickupPoints { get; set; }
public DbSet<Product> Products { get; set; }
public DbSet<Order> Orders { get; set; }
}
Program.cs
using AspNetForge.Web.Extensions;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore;
using ExamProject.Models;
using System.Security.Claims;
var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
if (string.IsNullOrWhiteSpace(connectionString))
throw new InvalidOperationException(
"Строка подключения 'DefaultConnection' не найдена. " +
"Добавьте её в appsettings.json.");
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(connectionString));
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("AdminOnly", policy => policy.RequireRole("Admin"));
options.AddPolicy("ManagerPlus", policy => policy.RequireRole("Manager", "Admin"));
});
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = "/login.html";
});
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.UseForgeResources(overwrite: true);
app.UseDefaultFiles();
app.UseStaticFiles();
app.MapGet("/", () => Results.Redirect("/index.html"));
app.MapCrud<Product, AppDbContext>("/api/products");
app.MapCrud<Order, AppDbContext>("/api/orders");
app.MapCrud<PickupPoint, AppDbContext>("/api/pickuppoints");
app.MapCrud<User, AppDbContext>("/api/users");
app.MapImport<Product, AppDbContext>("/api/products/import").RequireAuthorization("ManagerPlus");
app.MapGet("/api/products/search", async (AppDbContext db, string? q, string? category, string? sort, bool? desc) =>
{
var query = db.Products.AsQueryable();
if (!string.IsNullOrWhiteSpace(q))
query = query.Where(p =>
p.Name.Contains(q) ||
p.Article.Contains(q) ||
p.Manufacturer.Contains(q) ||
p.Supplier.Contains(q));
if (!string.IsNullOrWhiteSpace(category))
query = query.Where(p => p.Category == category);
query = sort?.ToLower() switch
{
"price" => (desc == true) ? query.OrderByDescending(p => p.Price) : query.OrderBy(p => p.Price),
"discount" => (desc == true) ? query.OrderByDescending(p => p.Discount) : query.OrderBy(p => p.Discount),
"name" => (desc == true) ? query.OrderByDescending(p => p.Name) : query.OrderBy(p => p.Name),
_ => query.OrderBy(p => p.Id)
};
return Results.Ok(await query.ToListAsync());
});
app.MapGet("/api/categories", async (AppDbContext db) =>
await db.Products.Select(p => p.Category).Distinct().ToListAsync());
app.MapPost("/login", async (AppDbContext db, HttpContext context) =>
{
var form = context.Request.Form;
var login = form["login"].ToString();
var password = form["password"].ToString();
var user = await db.Users.FirstOrDefaultAsync(u =>
u.Login == login && u.Password == password);
if (user is null)
return Results.BadRequest("Логин или пароль неверный");
var claims = new ClaimsIdentity(new[]
{
new Claim(ClaimsIdentity.DefaultNameClaimType, user.Login),
new Claim(ClaimsIdentity.DefaultRoleClaimType, user.Role.ToString()),
new Claim("FullName", user.FullName)
}, "Cookies");
await context.SignInAsync(new ClaimsPrincipal(claims));
return Results.Redirect("/index.html");
});
app.MapPost("/register", async (AppDbContext db, HttpContext context) =>
{
var form = context.Request.Form;
var login = form["login"].ToString();
var password = form["password"].ToString();
if (password != form["confirmPassword"].ToString())
return Results.BadRequest("Пароли не совпадают");
if (await db.Users.AnyAsync(u => u.Login == login))
return Results.BadRequest("Пользователь уже существует");
await db.Users.AddAsync(new User
{
Login = login,
Password = password,
FullName = form["fullName"].ToString(),
Role = UserRole.Client
});
await db.SaveChangesAsync();
return Results.Redirect("/login.html");
});
app.MapGet("/logout", async (HttpContext context) =>
{
await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return Results.Redirect("/login.html");
});
app.MapGet("/api/me", (HttpContext context) =>
{
if (!context.User.Identity!.IsAuthenticated)
return Results.Json(new { role = "Guest", name = "" });
return Results.Json(new
{
role = context.User.FindFirst(ClaimsIdentity.DefaultRoleClaimType)?.Value ?? "User",
name = context.User.Identity.Name,
fullName = context.User.FindFirst("FullName")?.Value ?? ""
});
});
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await db.Database.MigrateAsync();
if (!db.Users.Any())
{
db.Users.AddRange(
new User { Login = "admin", Password = "admin", FullName = "Администратор", Role = UserRole.Admin },
new User { Login = "manager", Password = "manager", FullName = "Менеджер", Role = UserRole.Manager },
new User { Login = "client", Password = "client", FullName = "Клиент", Role = UserRole.Client });
}
if (!db.PickupPoints.Any())
{
db.PickupPoints.AddRange(
new PickupPoint { Address = "ул. Ленина, 1", City = "Москва" },
new PickupPoint { Address = "ул. Гагарина, 5", City = "Санкт-Петербург" });
}
if (!db.Products.Any())
{
db.Products.AddRange(
new Product
{
Article = "B001",
Name = "Горный велосипед",
Category = "Горный",
Manufacturer = "Stels",
Supplier = "ООО ВелоСнаб",
Price = 25000,
Discount = 10,
Quantity = 5,
Description = "Надёжный горный велосипед",
ImagePath = "images/bike1.jpg"
},
new Product
{
Article = "B002",
Name = "Городской велосипед",
Category = "Городской",
Manufacturer = "Forward",
Supplier = "ООО ВелоСнаб",
Price = 18000,
Discount = 0,
Quantity = 12,
Description = "Удобный городской велосипед",
ImagePath = "images/bike2.jpg"
});
}
await db.SaveChangesAsync();
}
app.Run();
appsettings.json
{
"ConnectionStrings": {
"DefaultConnection": "Host=localhost;Port=5432;Database=examdb;Username=postgres;Password=postgres"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
wwwroot/styles.css
:root {
--bg: #ffffff;
--accent: #6A5ACD;
--accent-dark: #4B0082;
--highlight: #483D8B;
--text: #222222;
--muted: #666666;
--danger: #dc2626;
--success: #16a34a;
--card: #ffffff;
--border: #e5e7eb;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: Arial, Helvetica, sans-serif;
background: var(--bg);
color: var(--text);
}
header {
background: var(--accent);
color: white;
border-bottom: 4px solid var(--accent-dark);
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
header h1 { margin: 0; font-size: 1.5rem; }
.logo {
height: 48px;
margin-right: 1rem;
}
nav a {
margin-left: 1rem;
color: white;
text-decoration: none;
font-weight: bold;
}
nav a:hover { text-decoration: underline; }
main {
max-width: 1300px;
margin: 2rem auto;
padding: 0 1rem;
}
.card {
background: var(--card);
border: 1px solid var(--border);
border-radius: .5rem;
padding: 1.5rem;
margin-bottom: 1.5rem;
}
.card h3 { margin-top: 0; color: var(--accent-dark); }
input, select, button, textarea {
padding: .5rem .75rem;
border: 1px solid var(--border);
border-radius: .25rem;
font-family: Arial, Helvetica, sans-serif;
font-size: 1rem;
}
button {
background: var(--accent);
color: white;
border: none;
cursor: pointer;
font-weight: bold;
}
button:hover { background: var(--accent-dark); }
button.secondary { background: var(--muted); }
button.danger { background: var(--danger); }
button.highlight { background: var(--highlight); }
table {
width: 100%;
border-collapse: collapse;
margin-top: 1rem;
}
td, th {
padding: .75rem;
border-bottom: 1px solid var(--border);
text-align: left;
}
th {
background: var(--accent);
color: white;
}
tr:hover { background: #f3f4f6; }
.form-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: .75rem;
margin-bottom: 1rem;
}
.filter-bar {
display: flex;
flex-wrap: wrap;
gap: .75rem;
align-items: end;
margin-bottom: 1rem;
}
.auth-form {
max-width: 360px;
margin: 5rem auto;
}
.auth-form input {
width: 100%;
margin-bottom: .75rem;
}
.badge {
display: inline-block;
padding: .25rem .5rem;
border-radius: .25rem;
background: var(--accent);
color: white;
font-size: .875rem;
}
.hidden { display: none; }
.readonly-row td { background: #f9fafb; }
wwwroot/js/crud.js
const api = {
async get(url) {
const r = await fetch(url);
if (!r.ok) throw new Error('GET ' + url + ' failed: ' + r.status);
return r.json();
},
async post(url, data) {
const r = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!r.ok) throw new Error('POST failed: ' + r.status);
return r.json().catch(() => null);
},
async put(url, data) {
const r = await fetch(url, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!r.ok) throw new Error('PUT failed: ' + r.status);
return r.json().catch(() => null);
},
async delete(url) {
const r = await fetch(url, { method: 'DELETE' });
if (!r.ok) throw new Error('DELETE failed: ' + r.status);
},
async postForm(url, formData) {
const r = await fetch(url, { method: 'POST', body: formData });
if (!r.ok) throw new Error('POST form failed: ' + r.status);
return r.json().catch(() => null);
}
};
async function renderHeader() {
try {
const me = await api.get('/api/me');
const authLink = document.getElementById('authLink');
const userInfo = document.getElementById('userInfo');
if (me.role !== 'Guest' && authLink) {
authLink.textContent = 'Выйти';
authLink.href = '/logout';
if (userInfo) userInfo.textContent = (me.fullName || me.name) + ' (' + me.role + ')';
}
return me.role;
} catch {
return 'Guest';
}
}
function renderTable(rows, displayKeys, onEdit, onDelete, options = {}) {
const tbody = document.getElementById('tableBody');
tbody.innerHTML = '';
for (const row of rows) {
const tr = document.createElement('tr');
if (options.readonly) tr.className = 'readonly-row';
for (const key of displayKeys) {
const td = document.createElement('td');
td.textContent = row[key] ?? '';
tr.appendChild(td);
}
const actions = document.createElement('td');
if (onEdit) {
const editBtn = document.createElement('button');
editBtn.textContent = '✎';
editBtn.onclick = () => onEdit(row);
actions.appendChild(editBtn);
}
if (onDelete) {
const delBtn = document.createElement('button');
delBtn.textContent = '✕';
delBtn.className = 'danger';
delBtn.onclick = () => onDelete(row.id);
actions.appendChild(delBtn);
}
tr.appendChild(actions);
tbody.appendChild(tr);
}
}
function applyFilters(rows, { search, category, sort, desc }) {
let result = [...rows];
if (search) {
const q = search.toLowerCase();
result = result.filter(r => Object.values(r).some(v =>
v != null && v.toString().toLowerCase().includes(q)));
}
if (category) {
result = result.filter(r => r.category === category || r.Category === category);
}
if (sort) {
const key = sort;
result.sort((a, b) => {
const av = a[key] ?? '';
const bv = b[key] ?? '';
if (typeof av === 'number' && typeof bv === 'number')
return desc ? bv - av : av - bv;
return desc ? String(bv).localeCompare(String(av)) : String(av).localeCompare(String(bv));
});
}
return result;
}
wwwroot/index.html
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Главная — ВелосипедДрайв</title>
<link rel="stylesheet" href="/styles.css">
</head>
<body>
<header>
<div style="display:flex;align-items:center;">
<img src="/images/logo.png" alt="Логотип" class="logo" onerror="this.style.display='none'">
<h1>ВелосипедДрайв</h1>
</div>
<nav>
<a href="/products.html">Товары</a>
<a href="/orders.html">Заказы</a>
<span id="userInfo"></span>
<a id="authLink" href="/login.html">Войти</a>
</nav>
</header>
<main>
<div class="card">
<h2>Добро пожаловать</h2>
<p>Это стартовый шаблон на базе <strong>AspNetForge.WebCore</strong>.</p>
<p>Для входа используйте тестовые учётные записи:</p>
<ul>
<li><code>admin / admin</code> — администратор (полный CRUD)</li>
<li><code>manager / manager</code> — менеджер (товары + фильтры + заказы)</li>
<li><code>client / client</code> — клиент (только просмотр)</li>
</ul>
<p><strong>Важно:</strong> перед запуском создайте базу данных PostgreSQL и проверьте строку подключения в <code>appsettings.json</code>.</p>
</div>
</main>
<script src="/js/crud.js"></script>
<script>renderHeader();</script>
</body>
</html>
wwwroot/login.html
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Вход</title>
<link rel="stylesheet" href="/styles.css">
</head>
<body>
<main>
<div class="card auth-form">
<h2>Вход</h2>
<form action="/login" method="post">
<input name="login" placeholder="Логин" required>
<input name="password" type="password" placeholder="Пароль" required>
<button type="submit">Войти</button>
</form>
<p>Нет аккаунта? <a href="/register.html">Зарегистрироваться</a></p>
</div>
</main>
</body>
</html>
wwwroot/register.html
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Регистрация</title>
<link rel="stylesheet" href="/styles.css">
</head>
<body>
<main>
<div class="card auth-form">
<h2>Регистрация</h2>
<form action="/register" method="post">
<input name="login" placeholder="Логин" required>
<input name="fullName" placeholder="ФИО" required>
<input name="password" type="password" placeholder="Пароль" required>
<input name="confirmPassword" type="password" placeholder="Повторите пароль" required>
<button type="submit">Создать аккаунт</button>
</form>
<p>Уже есть аккаунт? <a href="/login.html">Войти</a></p>
</div>
</main>
</body>
</html>
wwwroot/products.html
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Товары</title>
<link rel="stylesheet" href="/styles.css">
</head>
<body>
<header>
<div style="display:flex;align-items:center;">
<img src="/images/logo.png" alt="Логотип" class="logo" onerror="this.style.display='none'">
<h1>Товары</h1>
</div>
<nav>
<a href="/index.html">Главная</a>
<a href="/orders.html">Заказы</a>
<span id="userInfo"></span>
<a id="authLink" href="/login.html">Войти</a>
</nav>
</header>
<main>
<div id="editorCard" class="card hidden">
<h3>Добавить / изменить</h3>
<form id="productForm">
<input type="hidden" id="id">
<div class="form-row">
<input id="article" name="article" placeholder="Артикул" required>
<input id="name" name="name" placeholder="Название" required>
<input id="category" name="category" placeholder="Категория" required>
<input id="manufacturer" name="manufacturer" placeholder="Производитель" required>
<input id="supplier" name="supplier" placeholder="Поставщик" required>
<input id="price" name="price" type="number" step="0.01" placeholder="Цена" required>
<input id="discount" name="discount" type="number" placeholder="Скидка %" required>
<input id="quantity" name="quantity" type="number" placeholder="Количество" required>
<input id="description" name="description" placeholder="Описание" required>
<input id="imagePath" name="imagePath" placeholder="Путь к изображению" required>
</div>
<button type="submit">Сохранить</button>
<button type="button" class="secondary" onclick="resetForm()">Отмена</button>
</form>
</div>
<div class="card">
<h3>Список товаров</h3>
<div class="filter-bar">
<input id="searchInput" placeholder="Поиск...">
<select id="categoryFilter"><option value="">Все категории</option></select>
<select id="sortInput">
<option value="">Сортировка</option>
<option value="price">Цена</option>
<option value="discount">Скидка</option>
<option value="name">Название</option>
<option value="quantity">Количество</option>
</select>
<label><input type="checkbox" id="descInput"> По убыванию</label>
<button onclick="applyClientFilters()">Применить</button>
<button class="secondary" onclick="resetFilters()">Сбросить</button>
</div>
<table>
<thead>
<tr>
<th>Артикул</th><th>Название</th><th>Категория</th><th>Производитель</th>
<th>Поставщик</th><th>Цена</th><th>Скидка</th><th>Кол-во</th><th></th>
</tr>
</thead>
<tbody id="tableBody"></tbody>
</table>
</div>
<div id="importCard" class="card hidden">
<h3>Импорт товаров из CSV</h3>
<form id="importForm" enctype="multipart/form-data">
<input type="file" name="file" accept=".csv" required>
<button type="submit">Импортировать</button>
</form>
</div>
</main>
<script src="/js/crud.js"></script>
<script>
const endpoint = '/api/products';
const displayKeys = ['article','name','category','manufacturer','supplier','price','discount','quantity'];
const allKeys = ['id','article','name','category','manufacturer','supplier','price','discount','quantity','description','imagePath'];
let allRows = [];
let role = 'Guest';
async function init() {
role = await renderHeader();
if (role === 'Admin' || role === 'Manager') {
document.getElementById('editorCard').classList.remove('hidden');
}
if (role === 'Admin') {
document.getElementById('importCard').classList.remove('hidden');
}
await loadCategories();
await load();
}
async function load() {
allRows = await api.get(endpoint);
applyClientFilters();
}
async function loadCategories() {
const cats = await api.get('/api/categories');
const sel = document.getElementById('categoryFilter');
cats.forEach(c => {
const opt = document.createElement('option');
opt.value = c;
opt.textContent = c;
sel.appendChild(opt);
});
}
function applyClientFilters() {
const search = document.getElementById('searchInput').value;
const category = document.getElementById('categoryFilter').value;
const sort = document.getElementById('sortInput').value;
const desc = document.getElementById('descInput').checked;
const rows = applyFilters(allRows, { search, category, sort, desc });
const canEdit = role === 'Admin';
renderTable(rows, displayKeys, canEdit ? editRow : null, canEdit ? deleteRow : null, { readonly: !canEdit });
}
function resetFilters() {
document.getElementById('searchInput').value = '';
document.getElementById('categoryFilter').value = '';
document.getElementById('sortInput').value = '';
document.getElementById('descInput').checked = false;
applyClientFilters();
}
function editRow(row) {
allKeys.forEach(k => {
document.getElementById(k).value = row[k] ?? '';
});
}
function resetForm() { document.getElementById('productForm').reset(); }
document.getElementById('productForm').addEventListener('submit', async (e) => {
e.preventDefault();
const payload = {};
allKeys.forEach(k => {
let v = document.getElementById(k).value;
if (['id','discount','quantity'].includes(k)) v = Number(v) || 0;
else if (k === 'price') v = Number(v) || 0;
payload[k] = v;
});
if (payload.id) await api.put(`${endpoint}/${payload.id}`, payload);
else await api.post(endpoint, payload);
resetForm();
await load();
});
async function deleteRow(id) {
if (!confirm('Удалить товар?')) return;
await api.delete(`${endpoint}/${id}`);
await load();
}
document.getElementById('importForm').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
await api.postForm('/api/products/import', formData);
e.target.reset();
await load();
});
init();
</script>
</body>
</html>
wwwroot/orders.html
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Заказы</title>
<link rel="stylesheet" href="/styles.css">
</head>
<body>
<header>
<div style="display:flex;align-items:center;">
<img src="/images/logo.png" alt="Логотип" class="logo" onerror="this.style.display='none'">
<h1>Заказы</h1>
</div>
<nav>
<a href="/index.html">Главная</a>
<a href="/products.html">Товары</a>
<span id="userInfo"></span>
<a id="authLink" href="/login.html">Войти</a>
</nav>
</header>
<main>
<div id="editorCard" class="card hidden">
<h3>Добавить / изменить</h3>
<form id="orderForm">
<input type="hidden" id="id">
<div class="form-row">
<input id="orderNumber" name="orderNumber" type="number" placeholder="Номер заказа" required>
<input id="orderDate" name="orderDate" type="date" required>
<input id="clientFullName" name="clientFullName" placeholder="ФИО клиента" required>
<input id="productId" name="productId" type="number" placeholder="ID товара" required>
<input id="quantity" name="quantity" type="number" placeholder="Количество" required>
<input id="pickupPointId" name="pickupPointId" type="number" placeholder="ID пункта выдачи" required>
<input id="discount" name="discount" type="number" placeholder="Скидка %" required>
<input id="status" name="status" placeholder="Статус" required>
<input id="deliveryDate" name="deliveryDate" type="date" required>
<input id="code" name="code" type="number" placeholder="Код получения" required>
</div>
<button type="submit">Сохранить</button>
<button type="button" class="secondary" onclick="resetForm()">Отмена</button>
</form>
</div>
<div class="card">
<h3>Список заказов</h3>
<table>
<thead>
<tr>
<th>№</th><th>Дата</th><th>Клиент</th><th>Товар ID</th><th>Кол-во</th>
<th>Пункт выдачи ID</th><th>Скидка</th><th>Статус</th><th>Доставка</th><th>Код</th><th></th>
</tr>
</thead>
<tbody id="tableBody"></tbody>
</table>
</div>
</main>
<script src="/js/crud.js"></script>
<script>
const endpoint = '/api/orders';
const displayKeys = ['orderNumber','orderDate','clientFullName','productId','quantity','pickupPointId','discount','status','deliveryDate','code'];
const allKeys = ['id','orderNumber','orderDate','clientFullName','productId','quantity','pickupPointId','discount','status','deliveryDate','code'];
let role = 'Guest';
async function init() {
role = await renderHeader();
if (role === 'Admin') {
document.getElementById('editorCard').classList.remove('hidden');
}
await load();
}
async function load() {
const rows = await api.get(endpoint);
const canEdit = role === 'Admin';
renderTable(rows, displayKeys, canEdit ? editRow : null, canEdit ? deleteRow : null, { readonly: !canEdit });
}
function editRow(row) {
allKeys.forEach(k => {
document.getElementById(k).value = row[k] ? row[k].toString().split('T')[0] : '';
});
}
function resetForm() { document.getElementById('orderForm').reset(); }
document.getElementById('orderForm').addEventListener('submit', async (e) => {
e.preventDefault();
const payload = {};
allKeys.forEach(k => {
let v = document.getElementById(k).value;
if (['id','orderNumber','productId','quantity','pickupPointId','discount','code'].includes(k)) v = Number(v) || 0;
else if (['orderDate','deliveryDate'].includes(k)) v = new Date(v).toISOString();
payload[k] = v;
});
if (payload.id) await api.put(`${endpoint}/${payload.id}`, payload);
else await api.post(endpoint, payload);
resetForm();
await load();
});
async function deleteRow(id) {
if (!confirm('Удалить заказ?')) return;
await api.delete(`${endpoint}/${id}`);
await load();
}
init();
</script>
</body>
</html>
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.