diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8de136a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.12-slim + +WORKDIR /app + +RUN apt-get update && apt-get install -y \ + libpq-dev gcc \ + && rm -rf /var/lib/apt/lists/* + +COPY requirements/base.txt requirements/base.txt +RUN pip install --no-cache-dir -r requirements/base.txt + +COPY . . + +EXPOSE 8000 + +CMD ["uvicorn", "config.asgi:application", "--host", "0.0.0.0", "--port", "8000"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..28b2e43 --- /dev/null +++ b/Makefile @@ -0,0 +1,23 @@ +.PHONY: dev migrate shell createsuperuser test lint tailwind-build + +dev: + docker compose up + +migrate: + docker compose exec web python manage.py migrate_schemas --shared + docker compose exec web python manage.py migrate_schemas + +shell: + docker compose exec web python manage.py shell_plus + +test: + docker compose exec web pytest apps/ -v + +lint: + ruff check . && black --check . + +tailwind-build: + npm run build + +createsuperuser: + docker compose exec web python manage.py create_tenant_superuser diff --git a/apps/account/admin.py b/apps/account/admin.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/client/admin.py b/apps/client/admin.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/client/serializers.py b/apps/client/serializers.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/client/services/__init__.py b/apps/client/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/client/tasks.py b/apps/client/tasks.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/client/templates/client/.gitkeep b/apps/client/templates/client/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/apps/client/tests/__init__.py b/apps/client/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/client/urls.py b/apps/client/urls.py new file mode 100644 index 0000000..a5f04de --- /dev/null +++ b/apps/client/urls.py @@ -0,0 +1,5 @@ +from django.urls import path + +app_name = "client" + +urlpatterns: list = [] diff --git a/apps/client/views.py b/apps/client/views.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/complex/admin.py b/apps/complex/admin.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/org/admin.py b/apps/org/admin.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/permission/admin.py b/apps/permission/admin.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/property/admin.py b/apps/property/admin.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/property/serializers.py b/apps/property/serializers.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/property/services/__init__.py b/apps/property/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/property/tasks.py b/apps/property/tasks.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/property/templates/property/.gitkeep b/apps/property/templates/property/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/apps/property/tests/__init__.py b/apps/property/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/property/urls.py b/apps/property/urls.py new file mode 100644 index 0000000..c2e559d --- /dev/null +++ b/apps/property/urls.py @@ -0,0 +1,5 @@ +from django.urls import path + +app_name = "property" + +urlpatterns: list = [] diff --git a/apps/property/views.py b/apps/property/views.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/region/admin.py b/apps/region/admin.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/release/admin.py b/apps/release/admin.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/release/tests/__init__.py b/apps/release/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/setting/admin.py b/apps/setting/admin.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/setting/serializers.py b/apps/setting/serializers.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/setting/services/__init__.py b/apps/setting/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/setting/tasks.py b/apps/setting/tasks.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/setting/templates/setting/.gitkeep b/apps/setting/templates/setting/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/apps/setting/tests/__init__.py b/apps/setting/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/setting/urls.py b/apps/setting/urls.py new file mode 100644 index 0000000..69d71a1 --- /dev/null +++ b/apps/setting/urls.py @@ -0,0 +1,5 @@ +from django.urls import path + +app_name = "setting" + +urlpatterns: list = [] diff --git a/apps/setting/views.py b/apps/setting/views.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/tenant/admin.py b/apps/tenant/admin.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/tenant/tests/__init__.py b/apps/tenant/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..9033e28 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,57 @@ +services: + web: + build: . + command: gunicorn config.asgi:application -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000 --workers 4 + env_file: .env + depends_on: + - db + - redis + networks: + - fonrey_net + + db: + image: postgres:16-alpine + env_file: .env + environment: + POSTGRES_DB: ${DB_NAME} + POSTGRES_USER: ${DB_USER} + POSTGRES_PASSWORD: ${DB_PASSWORD} + volumes: + - fonrey_db_data:/var/lib/postgresql/data + networks: + - fonrey_net + + redis: + image: redis:7-alpine + volumes: + - fonrey_redis_data:/data + networks: + - fonrey_net + + celery: + build: . + command: celery -A config worker -l info --concurrency 4 + env_file: .env + depends_on: + - db + - redis + networks: + - fonrey_net + + celery-beat: + build: . + command: celery -A config beat -l info + env_file: .env + depends_on: + - db + - redis + networks: + - fonrey_net + +volumes: + fonrey_db_data: + fonrey_redis_data: + +networks: + fonrey_net: + driver: bridge diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..38c86bb --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,78 @@ +services: + web: + build: . + command: uvicorn config.asgi:application --host 0.0.0.0 --port 8000 --reload + ports: + - "8000:8000" + env_file: .env + volumes: + - .:/app + depends_on: + - db + - redis + networks: + - fonrey_net + + db: + image: postgres:16-alpine + ports: + - "5432:5432" + env_file: .env + environment: + POSTGRES_DB: ${DB_NAME} + POSTGRES_USER: ${DB_USER} + POSTGRES_PASSWORD: ${DB_PASSWORD} + volumes: + - fonrey_db_data:/var/lib/postgresql/data + networks: + - fonrey_net + + redis: + image: redis:7-alpine + ports: + - "6379:6379" + volumes: + - fonrey_redis_data:/data + networks: + - fonrey_net + + celery: + build: . + command: celery -A config worker -l info + env_file: .env + volumes: + - .:/app + depends_on: + - db + - redis + networks: + - fonrey_net + + celery-beat: + build: . + command: celery -A config beat -l info + env_file: .env + volumes: + - .:/app + depends_on: + - db + - redis + networks: + - fonrey_net + + tailwind: + image: node:20-alpine + working_dir: /app + command: sh -c "npm install && npm run watch" + volumes: + - .:/app + networks: + - fonrey_net + +volumes: + fonrey_db_data: + fonrey_redis_data: + +networks: + fonrey_net: + driver: bridge diff --git a/package.json b/package.json new file mode 100644 index 0000000..7ac8260 --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "name": "fonrey-frontend", + "version": "1.0.0", + "private": true, + "scripts": { + "build": "tailwindcss -i ./static/css/main.css -o ./static/css/output.css --minify", + "watch": "tailwindcss -i ./static/css/main.css -o ./static/css/output.css --watch" + }, + "devDependencies": { + "tailwindcss": "^3.4.0" + } +} diff --git a/static/css/main.css b/static/css/main.css new file mode 100644 index 0000000..b5c61c9 --- /dev/null +++ b/static/css/main.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/static/js/main.js b/static/js/main.js new file mode 100644 index 0000000..2893674 --- /dev/null +++ b/static/js/main.js @@ -0,0 +1,32 @@ +document.body.addEventListener("htmx:afterRequest", function (event) { + var trigger = event.detail.xhr.getResponseHeader("HX-Trigger"); + if (!trigger) return; + + try { + var payload = JSON.parse(trigger); + var toast = payload["fonrey:toast"]; + if (!toast) return; + + var container = document.getElementById("toast-container"); + if (!container) return; + + var node = document.createElement("div"); + node.className = + "bg-white border border-neutral-200 rounded-lg shadow-xs px-4 py-3 min-w-[280px]"; + node.setAttribute("data-toast-type", toast.type || "info"); + node.textContent = toast.message || ""; + container.appendChild(node); + + setTimeout(function () { + node.remove(); + }, 4000); + } catch (e) { + } +}); + +document.body.addEventListener("htmx:configRequest", function (event) { + var meta = document.querySelector('meta[name="csrf-token"]'); + if (meta) { + event.detail.headers["X-CSRFToken"] = meta.getAttribute("content"); + } +}); diff --git a/static/vendor/.gitkeep b/static/vendor/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..255f3a3 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,61 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + "./templates/**/*.html", + "./apps/**/templates/**/*.html", + "./static/js/**/*.js", + ], + theme: { + extend: { + colors: { + primary: { + 50: "#F0FDFA", + 100: "#CCFBF1", + 200: "#99F6E4", + 300: "#5EEAD4", + 400: "#2DD4BF", + 500: "#14B8A6", + 600: "#0F766E", + 700: "#115E59", + 800: "#134E4A", + }, + neutral: { + 50: "#F8FAFC", + 100: "#F1F5F9", + 200: "#E2E8F0", + 300: "#CBD5E1", + 400: "#94A3B8", + 500: "#64748B", + 600: "#475569", + 700: "#334155", + 800: "#1E293B", + 900: "#0F172A", + }, + success: { 600: "#16A34A" }, + warning: { 600: "#D97706" }, + danger: { 600: "#DC2626" }, + info: { 600: "#2563EB" }, + }, + fontFamily: { + sans: ["Inter", "PingFang SC", "Microsoft YaHei", "sans-serif"], + }, + zIndex: { + 60: "60", + 70: "70", + }, + boxShadow: { + xs: "0 1px 2px 0 rgb(0 0 0 / 0.05)", + }, + keyframes: { + "slide-in-right": { + "0%": { transform: "translateX(100%)", opacity: "0" }, + "100%": { transform: "translateX(0)", opacity: "1" }, + }, + }, + animation: { + "slide-in-right": "slide-in-right 0.2s ease-out", + }, + }, + }, + plugins: [], +}; diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..e324496 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,25 @@ +{% load static %} + + +
+ + + +{% block empty_message %}暂无数据{% endblock %}
+您没有访问该资源的权限
+页面未找到
+服务器内部错误
+Fonrey 当前仅支持桌面端(≥1280px),请在电脑上访问
+