feat: complete Phase 3 scaffolding (templates, static, Docker, per-app skeletons)

- Per-app skeleton completion: property/client/setting now have services/,
  tasks.py, views.py, urls.py, serializers.py, templates/<app>/, tests/
- admin.py added to all 10 apps (per spec §2.108 / §17.3)
- Top-level templates/: base.html, layouts/{app,auth}.html, components/
  {topbar,sidebar,pagination,toast,modal,empty-state}.html, errors/
  {403,404,500}.html
- static/: css/main.css (Tailwind entry), js/main.js (HTMX toast +
  CSRF wiring per §7.4), vendor/.gitkeep
- tailwind.config.js: Primary teal + neutral slate + semantic colors,
  Inter font stack, z-60/z-70, shadow-xs, slide-in-right animation per
  UI_SYSTEM §2.7/§10.1
- package.json: tailwindcss-only build/watch
- Dockerfile + docker-compose.yml (6 services: web/db/redis/celery/
  celery-beat/tailwind) + docker-compose.prod.yml + Makefile
- tests/ root: conftest.py with TenantClient fixture per §720,
  integration/{property,client,release}/, e2e/, schemathesis skeleton
- Removed empty apps/tenant/models/ (tenant uses models.py per §17.1)

Validated: manage.py check passes; tree matches spec §2 exactly.
This commit is contained in:
2026-04-29 17:45:22 +08:00
parent ed40de4050
commit 94d160223d
65 changed files with 454 additions and 0 deletions

25
templates/base.html Normal file
View File

@@ -0,0 +1,25 @@
{% load static %}
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="csrf-token" content="{{ csrf_token }}">
<title>{% block title %}Fonrey{% endblock %}</title>
<link rel="stylesheet" href="{% static 'css/output.css' %}">
{% block extra_head %}{% endblock %}
<script src="{% static 'vendor/htmx.min.js' %}" defer></script>
<script src="{% static 'vendor/alpine.min.js' %}" defer></script>
<script src="{% static 'js/main.js' %}" defer></script>
</head>
<body class="{% block body_class %}bg-neutral-50 text-neutral-900{% endblock %}">
{% block content %}{% endblock %}
<div id="toast-container" class="fixed bottom-4 right-4 z-70 flex flex-col gap-2"></div>
{% block extra_js %}{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,3 @@
<div class="flex flex-col items-center justify-center py-12 text-neutral-500">
<p class="text-sm">{% block empty_message %}暂无数据{% endblock %}</p>
</div>

View File

@@ -0,0 +1,9 @@
<div x-data="{ open: false }"
x-show="open"
x-cloak
class="fixed inset-0 z-50 flex items-center justify-center bg-neutral-900/40">
<div class="bg-white rounded-lg shadow-xs max-w-lg w-full p-6"
@click.outside="open = false">
{% block modal_body %}{% endblock %}
</div>
</div>

View File

@@ -0,0 +1,3 @@
<nav class="flex items-center justify-between py-3" aria-label="分页">
{% block pagination_body %}{% endblock %}
</nav>

View File

@@ -0,0 +1,6 @@
<aside :class="sidebarOpen ? 'w-60' : 'w-16'"
class="fixed top-14 left-0 bottom-0 z-20 bg-white border-r border-neutral-200 transition-all">
<nav class="flex flex-col p-2 gap-1">
{% block sidebar_items %}{% endblock %}
</nav>
</aside>

View File

@@ -0,0 +1,4 @@
<template id="toast-template">
<div class="bg-white border border-neutral-200 rounded-lg shadow-xs px-4 py-3 min-w-[280px]"
data-toast-type=""></div>
</template>

View File

@@ -0,0 +1,15 @@
<header class="sticky top-0 z-20 bg-primary-800 text-white h-14 flex items-center px-4">
<div class="w-[150px] flex items-center">
<span class="font-semibold">Fonrey</span>
</div>
<nav class="flex-1 flex items-center gap-1">
{% block nav %}{% endblock %}
</nav>
<div class="flex items-center gap-3">
<button type="button" aria-label="通知"></button>
<button type="button" aria-label="设置"></button>
<button type="button" aria-label="账户"></button>
</div>
</header>

10
templates/errors/403.html Normal file
View File

@@ -0,0 +1,10 @@
{% extends "base.html" %}
{% block title %}403 - 无权限{% endblock %}
{% block content %}
<div class="min-h-screen flex items-center justify-center">
<div class="text-center">
<h1 class="text-4xl font-semibold">403</h1>
<p class="mt-2 text-neutral-600">您没有访问该资源的权限</p>
</div>
</div>
{% endblock %}

10
templates/errors/404.html Normal file
View File

@@ -0,0 +1,10 @@
{% extends "base.html" %}
{% block title %}404 - 页面未找到{% endblock %}
{% block content %}
<div class="min-h-screen flex items-center justify-center">
<div class="text-center">
<h1 class="text-4xl font-semibold">404</h1>
<p class="mt-2 text-neutral-600">页面未找到</p>
</div>
</div>
{% endblock %}

10
templates/errors/500.html Normal file
View File

@@ -0,0 +1,10 @@
{% extends "base.html" %}
{% block title %}500 - 服务器错误{% endblock %}
{% block content %}
<div class="min-h-screen flex items-center justify-center">
<div class="text-center">
<h1 class="text-4xl font-semibold">500</h1>
<p class="mt-2 text-neutral-600">服务器内部错误</p>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,38 @@
{% extends "base.html" %}
{% block body_class %}bg-neutral-50 text-neutral-900{% endblock %}
{% block content %}
<div x-data="{ sidebarOpen: $persist(true) }" class="min-h-screen flex flex-col">
{% include "components/topbar.html" %}
<div class="flex flex-1">
{% include "components/sidebar.html" %}
<main :class="sidebarOpen ? 'ml-60' : 'ml-16'" class="flex-1 px-6 py-4 transition-all">
{% block main %}{% endblock %}
</main>
</div>
</div>
<div id="small-screen-gate"
class="hidden fixed inset-0 bg-neutral-900/95 text-white items-center justify-center z-[100] text-center px-6">
<p class="text-lg">Fonrey 当前仅支持桌面端≥1280px请在电脑上访问</p>
</div>
<script>
(function () {
var gate = document.getElementById('small-screen-gate');
function check() {
if (window.innerWidth < 1280) {
gate.classList.remove('hidden');
gate.classList.add('flex');
} else {
gate.classList.add('hidden');
gate.classList.remove('flex');
}
}
window.addEventListener('resize', check);
check();
})();
</script>
{% endblock %}

View File

@@ -0,0 +1,9 @@
{% extends "base.html" %}
{% block body_class %}bg-neutral-50 min-h-screen flex items-center justify-center{% endblock %}
{% block content %}
<div class="max-w-md w-full bg-white rounded-lg shadow-xs p-8">
{% block auth_content %}{% endblock %}
</div>
{% endblock %}