提供使用 django-unfold (v0.56+) 构建高级 Django 管理界面的专业指导。当用户提及 django-unfold、unfold admin 等内容时使用此技能。
Django Unfold is a modern admin theme for Django built on Tailwind CSS. It is a drop-in
enhancement of django.contrib.admin — fully compatible with all native Django admin
patterns while adding UI, components, and developer-friendly features.
Current stable version: 0.90.x (Django 5.0+ required from v0.90; dropped Django 4.2)
Rule 1: Every ModelAdmin must inherit from unfold.admin.ModelAdmin
python
# ✅ CORRECT
from unfold.admin import ModelAdmin
@admin.register(MyModel)
class MyModelAdmin(ModelAdmin):
pass
# ❌ WRONG — loses all styling and Unfold features
from django.contrib.admin import ModelAdmin # never use this base
Rule 2: INSTALLED_APPS order — "unfold" must precede "django.contrib.admin"
python
INSTALLED_APPS = [
"unfold", # REQUIRED FIRST
"unfold.contrib.filters", # optional: advanced filters
"unfold.contrib.forms", # optional: ArrayWidget, WysiwygWidget, crispy
"unfold.contrib.inlines", # optional: NonrelatedInline
"unfold.contrib.import_export", # optional: django-import-export
"unfold.contrib.simple_history", # optional: django-simple-history
"unfold.contrib.guardian", # optional: django-guardian
"unfold.contrib.constance", # optional: django-constance
"unfold.contrib.location_field", # optional: django-location-field
"django.contrib.admin", # AFTER unfold
]
Rule 3: Inlines also need Unfold base classes
python
from unfold.admin import StackedInline, TabularInline # not django.contrib.admin
Rule 4: User & Group models need manual re-registration — see references/installation.md
python
from unfold.admin import ModelAdmin
from unfold.contrib.forms.widgets import ArrayWidget, WysiwygWidget
from django.db import models
@admin.register(MyModel)
class MyModelAdmin(ModelAdmin):
# ── Changelist layout ────────────────────────────────────────────
list_fullwidth = False # expand to full page width
list_filter_sheet = True # filters as bottom sheet (False = sidebar)
list_filter_submit = False # show Apply button in filter panel
list_horizontal_scrollbar_top = False
list_disable_select_all = False
# ── Change form UX ────────────────────────────────────────────────
compressed_fields = True # compact field display
warn_unsaved_form = True # warn before leaving unsaved
show_add_link = True # show Add button
change_form_show_cancel_button = False
# Custom template injection (HTML snippets, not full templates)
change_form_before_template = "myapp/before_form.html" # inside form, top
change_form_after_template = "myapp/after_form.html" # inside form, bottom
change_form_outer_before_template = "myapp/outer_top.html"
change_form_outer_after_template = "myapp/outer_bottom.html"
# ── Readonly field post-processing ───────────────────────────────
readonly_preprocess_fields = {
"html_field": "html.unescape",
"text_field": lambda content: content.strip(),
}
# ── Widget overrides ─────────────────────────────────────────────
formfield_overrides = {
models.TextField: {"widget": WysiwygWidget},
# ArrayField: {"widget": ArrayWidget}, # for PostgreSQL ArrayField
}
# ── Actions (5 types) — see references/actions.md ────────────────
actions_list = [] # above changelist (global)
actions_row = [] # per-row in changelist table
actions_detail = [] # top of change form header
actions_submit_line = [] # near Save button in change form
# ── Conditional fields — see references/forms-fields.md ──────────
conditional_fields = {
"field_name": "other_field == 'value'", # Alpine.js expression
}
# ── Expandable row sections — see references/forms-fields.md ─────
list_sections = [] # [SectionClass, ...]
# ── Datasets on change form — see references/forms-fields.md ─────
change_form_datasets = [] # [DatasetClass, ...]
Always use unfold.decorators.display, not Django's built-in.
python
from unfold.decorators import display
class OrderAdmin(ModelAdmin):
list_display = ["show_customer", "show_status", "show_priority"]
# Two-line cell: main heading + subtitle
@display(header=True)
def show_customer(self, obj):
return obj.full_name, obj.email # tuple: (main, subtitle)
# Colored status badge mapped from field values
@display(
description="Status",
ordering="status",
label={
"PENDING": "warning", # orange
"ACTIVE": "info", # blue
"COMPLETED": "success", # green
"FAILED": "danger", # red
},
)
def show_status(self, obj):
return obj.status
# Boolean label with default color
@display(description="VIP", label=True)
def show_priority(self, obj):
return obj.is_vip
→ Full @display and @action decorator docs: references/decorators.md
Read the relevant file before implementing any feature in that area. Each reference file contains explicit pointers to related files.
Reference FileRead When You Need To...references/installation.mdSet up from scratch, configure User/Group, run parallel adminreferences/configuration.mdUnderstand any UNFOLD = {...} key: title, login, callbacks, favicons, envreferences/theming.mdChange colors (OKLCH), sidebar nav, icons/badges, Tailwind custom CSSreferences/actions.mdAdd list/row/detail/submitline/dropdown actions, permissions, form actionsreferences/filters.mdAdd text/date/dropdown/numeric/autocomplete/checkbox/radio/horizontal filtersreferences/tabs.mdAdd fieldset tabs, inline tabs, changelist tabs, changeform tabs, dynamic tabsreferences/inlines.mdConfigure inlines: options, sortable, paginated, nested, nonrelatedreferences/dashboard-components.mdBuild dashboards, use components (card/chart/table/progress/tracker/cohort)references/forms-fields.mdConditional fields, sections (expandable rows), datasets, crispy forms, JsonFieldreferences/decorators.mdFull @display options (header, label, mapping) and @action decorator optionsreferences/integrations.mdAll 10 third-party packages: celery-beat, import-export, guardian, constance, etc.references/advanced.mdCustom pages, custom sites, sortable changelist, command palette, multi-language
collectstaticUNFOLD["COLORS"] or Tailwind 4UNFOLD["SIDEBAR"]["navigation"]unfold.admin.ModelAdminlist_filter on Datasets → not supported; filter via get_queryset() instead