Files
tfluceecrud/index.cfm
T

280 lines
11 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!---
index.cfm — CRUD-интерфейс для таблицы в PostgreSQL
Логика:
1. Если POST-запрос с FORM.action — выполняем create/update/delete
и редиректим на GET (POST-redirect-GET паттерн)
2. Если GET-запрос с URL.edit — загружаем запись для редактирования
3. Всегда показываем список всех записей + форму добавления/редактирования
Безопасность:
- cfqueryparam для всех входных данных → защита от SQL-инъекций
- cflocation после POST → защита от повторной отправки формы
- cfoutput только где нужно → XSS-защита через автоматическое экранирование Lucee
Переменные:
- request.tableName — имя таблицы (из Application.cfc / json_env TABLE_NAME)
- FORM.action — create | update | delete (POST)
- FORM.value — значение записи (POST)
- FORM.id — ID записи для update/delete (POST)
- URL.edit — ID записи для редактирования (GET)
--->
<cfset tableName = request.tableName />
<!--- ====== ОБРАБОТКА POST-ЗАПРОСОВ (create / update / delete) ====== --->
<!--- isDefined("FORM.action") — true только если страница вызвана через <form method="post"> --->
<cfif isDefined("FORM.action")>
<!--- CREATE: вставка новой записи --->
<cfif FORM.action EQ "create">
<!--- cfqueryparam cfsqltype="cf_sql_varchar" — экранирует значение, защита от SQL-инъекций --->
<cfquery name="insertRow">
INSERT INTO #tableName# (value)
VALUES (<cfqueryparam value="#FORM.value#" cfsqltype="cf_sql_varchar" />)
</cfquery>
</cfif>
<!--- UPDATE: обновление существующей записи по ID --->
<cfif FORM.action EQ "update">
<cfquery name="updateRow">
UPDATE #tableName#
SET value = <cfqueryparam value="#FORM.value#" cfsqltype="cf_sql_varchar" />
WHERE id = <cfqueryparam value="#FORM.id#" cfsqltype="cf_sql_integer" />
</cfquery>
</cfif>
<!--- DELETE: удаление записи по ID --->
<cfif FORM.action EQ "delete">
<cfquery name="deleteRow">
DELETE FROM #tableName#
WHERE id = <cfqueryparam value="#FORM.id#" cfsqltype="cf_sql_integer" />
</cfquery>
</cfif>
<!--- POST-redirect-GET: после любого действия — редирект на чистый URL без POST-данных.
Это предотвращает повторную отправку формы при обновлении страницы (F5). --->
<cflocation url="index.cfm" addtoken="no" />
</cfif>
<!--- ====== РЕЖИМ РЕДАКТИРОВАНИЯ (GET-запрос с ?edit=ID) ====== --->
<!--- editRow всегда инициализируем пустым словарём.
Если URL.edit не задан или запись не найдена — editRow останется пустым. --->
<cfset editRow = {} />
<cfif isDefined("URL.edit")>
<!--- Загружаем запись по ID из URL --->
<cfquery name="editQuery">
SELECT id, value FROM #tableName#
WHERE id = <cfqueryparam value="#URL.edit#" cfsqltype="cf_sql_integer" />
</cfquery>
<!--- Если запись найдена — заполняем editRow данными из запроса --->
<cfif editQuery.recordCount GT 0>
<cfset editRow = { id = editQuery.id, value = editQuery.value } />
</cfif>
</cfif>
<!--- ====== СПИСОК ВСЕХ ЗАПИСЕЙ ====== --->
<cfquery name="rows">
SELECT id, value FROM #tableName# ORDER BY id
</cfquery>
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<title>CRUD Lucee Example — <cfoutput>#tableName#</cfoutput></title>
<link rel="icon" type="image/svg+xml" href="assets/favicon.svg" />
<style>
:root {
--brand-primary: #2563eb;
--brand-primary-dark: #1d4ed8;
--brand-gray: #d1d5db;
--brand-grey-light: #f3f4f6;
--brand-destructive: #ef4444;
--brand-success: #22c55e;
--text-primary: #1a1a1a;
--text-muted: #6b7280;
--bg-card: #ffffff;
--radius: 12px;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
font-size: 14px;
color: var(--text-primary);
background: var(--brand-grey-light);
min-height: 100vh;
padding: 40px 16px;
}
.container { max-width: 720px; margin: 0 auto; }
/* Header */
.header {
display: flex; align-items: center; gap: 12px;
margin-bottom: 24px;
}
.header img { height: 36px; }
.header h1 { font-size: 20px; font-weight: 600; color: #001C34; }
/* Card */
.card {
background: var(--bg-card);
border-radius: var(--radius);
border: 1px solid var(--brand-gray);
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
overflow: hidden;
margin-bottom: 20px;
}
.card-header {
background: var(--brand-grey-light);
padding: 12px 16px;
font-weight: 600;
font-size: 16px;
}
.card-body { padding: 16px; }
/* Table */
table { width: 100%; border-collapse: collapse; }
th {
background: var(--brand-grey-light);
text-transform: uppercase;
font-size: 12px;
font-weight: 600;
color: var(--text-muted);
padding: 8px 12px;
text-align: left;
border-right: 1px solid var(--brand-gray);
}
th:last-child { border-right: none; text-align: center; width: 60px; }
td {
padding: 8px 12px;
border-bottom: 1px solid var(--brand-gray);
border-right: 1px solid var(--brand-gray);
}
td:last-child { border-right: none; text-align: center; width: 60px; }
tr:hover { background: rgba(243,244,246,0.5); }
tr:last-child td { border-bottom: none; }
/* Buttons */
.btn {
display: inline-flex; align-items: center; gap: 4px;
height: 32px; padding: 0 12px;
border: 1px solid var(--brand-gray);
border-radius: 6px;
background: var(--bg-card);
font-size: 14px; font-family: inherit;
cursor: pointer; white-space: nowrap;
}
.btn:hover { background: var(--brand-grey-light); }
.btn-primary {
background: var(--brand-primary); color: #fff; border-color: var(--brand-primary);
}
.btn-primary:hover { background: var(--brand-primary-dark); }
.btn-destructive { background: var(--brand-destructive); color: #fff; border-color: var(--brand-destructive); }
.btn-destructive:hover { opacity: 0.9; }
.btn-sm { height: 28px; padding: 0 8px; font-size: 13px; }
/* Form */
.form-grid {
display: grid;
grid-template-columns: 1fr;
gap: 12px;
}
.form-row {
display: flex; gap: 8px; align-items: flex-end;
}
.form-row input { flex: 1; }
input[type="text"] {
height: 36px; padding: 0 12px;
border: 1px solid var(--brand-gray);
border-radius: 6px; font-size: 14px; font-family: inherit;
}
input[type="text"]:focus { outline: none; border-color: var(--brand-primary); box-shadow: 0 0 0 2px rgba(37,99,235,0.15); }
/* Action links */
/* Action column */
td.act { text-align: center; width: 60px; border-right: none; }
td.act:last-child { border-right: none; }
.empty-cell { color: var(--text-muted); padding: 24px; text-align: center; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<img src="assets/logo.svg" alt="Nubes" />
<h1>CRUD Lucee Example</h1>
</div>
<div class="card">
<div class="card-header">Записи</div>
<div class="card-body" style="padding: 0;">
<table>
<thead>
<tr><th>ID</th><th>VALUE</th><th style="text-align:center;"></th><th style="text-align:center;"></th></tr>
</thead>
<tbody>
<cfoutput query="rows">
<tr>
<td>#id#</td>
<td>#value#</td>
<td style="text-align:center;">
<a href="index.cfm?edit=#id#" class="btn btn-sm">✏️</a>
</td>
<td style="text-align:center;">
<form method="post" style="display:inline">
<input type="hidden" name="action" value="delete" />
<input type="hidden" name="id" value="#id#" />
<button type="submit" class="btn btn-sm" onclick="return confirm('Удалить #id#?')">🗑️</button>
</form>
</td>
</tr>
</cfoutput>
</tbody>
</table>
<cfif rows.recordCount EQ 0>
<div class="empty-cell">Нет записей. Создайте первую.</div>
</cfif>
</div>
</div>
<div class="card">
<cfif NOT structIsEmpty(editRow)>
<cfoutput>
<div class="card-header">Редактировать #editRow.id#</div>
<div class="card-body">
<form method="post" class="form-grid">
<input type="hidden" name="action" value="update" />
<input type="hidden" name="id" value="#editRow.id#" />
<div class="form-row">
<input type="text" name="value" value="#editRow.value#" required placeholder="Значение" />
<button type="submit" class="btn btn-primary">Сохранить</button>
<a href="index.cfm" class="btn">Отмена</a>
</div>
</form>
</div>
</cfoutput>
<cfelse>
<div class="card-header">Добавить</div>
<div class="card-body">
<form method="post" class="form-grid">
<input type="hidden" name="action" value="create" />
<div class="form-row">
<input type="text" name="value" placeholder="Значение" required />
<button type="submit" class="btn btn-primary">Добавить</button>
</div>
</form>
</div>
</cfif>
</div>
</div>
<div style="text-align:center;padding:16px;color:#9ca3af;font-size:12px;">
v1.0.2 &mdash; <cfoutput>#tableName#</cfoutput>
</div>
</body>
</html>