From c90b10cd5a980b51c3a6b1885512381d247d5da8 Mon Sep 17 00:00:00 2001 From: flowerstonezl <125468358+flowerstonezl@users.noreply.github.com> Date: Mon, 11 May 2026 19:34:35 +0800 Subject: [PATCH] first commit --- .gitignore | 10 + backend/.dockerignore | 7 + backend/Dockerfile | 25 + backend/docker-entrypoint.sh | 6 + backend/journal/__init__.py | 0 backend/journal/admin.py | 196 ++ backend/journal/apps.py | 6 + backend/journal/management/__init__.py | 0 .../journal/management/commands/__init__.py | 0 .../management/commands/seed_nepal_story.py | 85 + backend/journal/migrations/0001_initial.py | 56 + .../0002_expand_nepal_art_style_length.py | 18 + ...config_together_netease_unlock_nullable.py | 33 + .../0004_netease_player_textfield.py | 18 + .../0005_remove_netease_from_config.py | 22 + .../journal/migrations/0006_memory_photo.py | 28 + backend/journal/migrations/__init__.py | 0 backend/journal/models.py | 143 ++ backend/journal/serializers.py | 51 + backend/journal/tests.py | 3 + backend/journal/url | 4 + backend/journal/urls.py | 9 + backend/journal/views.py | 69 + backend/manage.py | 22 + backend/ourstory/__init__.py | 0 backend/ourstory/asgi.py | 16 + backend/ourstory/settings.py | 139 + backend/ourstory/urls.py | 14 + backend/ourstory/wsgi.py | 16 + backend/requirements.txt | 6 + docker-compose.yml | 27 + frontend/.dockerignore | 3 + frontend/.env.development | 5 + frontend/.gitignore | 24 + frontend/.vscode/extensions.json | 3 + frontend/Dockerfile | 17 + frontend/README.md | 5 + frontend/index.html | 20 + frontend/nginx.conf | 31 + frontend/package-lock.json | 2263 +++++++++++++++++ frontend/package.json | 34 + frontend/public/audio/.gitkeep | 0 frontend/public/favicon.svg | 1 + frontend/public/icons.svg | 24 + frontend/public/lottie/heart.json | 73 + frontend/src/App.vue | 7 + frontend/src/api/client.ts | 77 + frontend/src/assets/hero.png | Bin 0 -> 13057 bytes frontend/src/assets/vite.svg | 1 + frontend/src/assets/vue.svg | 1 + frontend/src/components/FissurePage.vue | 112 + .../src/components/HeartMangaBackdrop.vue | 25 + frontend/src/components/HeartWindowPage.vue | 177 ++ frontend/src/components/LottieSticker.vue | 64 + frontend/src/components/MessageForm.vue | 93 + frontend/src/components/NepalChapterBand.vue | 22 + frontend/src/components/NepalHero.vue | 46 + .../src/components/NepalPageContainer.vue | 327 +++ frontend/src/components/NeteaseEmbed.vue | 44 + frontend/src/components/PolaroidCard.vue | 209 ++ frontend/src/components/StandardNepalPage.vue | 87 + frontend/src/components/Timeline.vue | 70 + frontend/src/components/TogetherCounter.vue | 174 ++ frontend/src/components/WashiTape.vue | 28 + .../standardScenes/ArgueAnimeScene.vue | 20 + .../standardScenes/BlueAnimeScene.vue | 24 + .../standardScenes/DawnAnimeScene.vue | 25 + .../standardScenes/GoldAnimeScene.vue | 27 + .../standardScenes/HomeAnimeScene.vue | 20 + .../standardScenes/IkeaAnimeScene.vue | 79 + .../standardScenes/LotusAnimeScene.vue | 29 + .../standardScenes/MangaSceneRouter.vue | 43 + .../standardScenes/MintAnimeScene.vue | 24 + .../standardScenes/NightAnimeScene.vue | 23 + .../standardScenes/PaperAnimeScene.vue | 11 + .../standardScenes/RoseAnimeScene.vue | 24 + frontend/src/config/neteasePlayer.ts | 17 + frontend/src/constants/nepalThemes.ts | 82 + frontend/src/main.ts | 5 + frontend/src/style.css | 171 ++ frontend/src/utils/renderMarkdown.ts | 38 + frontend/src/utils/storyPageTransition.ts | 100 + frontend/src/utils/togetherDuration.ts | 48 + frontend/src/views/OurStory.vue | 163 ++ frontend/src/vite-env.d.ts | 9 + frontend/tsconfig.app.json | 14 + frontend/tsconfig.json | 7 + frontend/tsconfig.node.json | 24 + frontend/vite.config.ts | 19 + 89 files changed, 6142 insertions(+) create mode 100644 .gitignore create mode 100644 backend/.dockerignore create mode 100644 backend/Dockerfile create mode 100644 backend/docker-entrypoint.sh create mode 100644 backend/journal/__init__.py create mode 100644 backend/journal/admin.py create mode 100644 backend/journal/apps.py create mode 100644 backend/journal/management/__init__.py create mode 100644 backend/journal/management/commands/__init__.py create mode 100644 backend/journal/management/commands/seed_nepal_story.py create mode 100644 backend/journal/migrations/0001_initial.py create mode 100644 backend/journal/migrations/0002_expand_nepal_art_style_length.py create mode 100644 backend/journal/migrations/0003_config_together_netease_unlock_nullable.py create mode 100644 backend/journal/migrations/0004_netease_player_textfield.py create mode 100644 backend/journal/migrations/0005_remove_netease_from_config.py create mode 100644 backend/journal/migrations/0006_memory_photo.py create mode 100644 backend/journal/migrations/__init__.py create mode 100644 backend/journal/models.py create mode 100644 backend/journal/serializers.py create mode 100644 backend/journal/tests.py create mode 100644 backend/journal/url create mode 100644 backend/journal/urls.py create mode 100644 backend/journal/views.py create mode 100644 backend/manage.py create mode 100644 backend/ourstory/__init__.py create mode 100644 backend/ourstory/asgi.py create mode 100644 backend/ourstory/settings.py create mode 100644 backend/ourstory/urls.py create mode 100644 backend/ourstory/wsgi.py create mode 100644 backend/requirements.txt create mode 100644 docker-compose.yml create mode 100644 frontend/.dockerignore create mode 100644 frontend/.env.development create mode 100644 frontend/.gitignore create mode 100644 frontend/.vscode/extensions.json create mode 100644 frontend/Dockerfile create mode 100644 frontend/README.md create mode 100644 frontend/index.html create mode 100644 frontend/nginx.conf create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/public/audio/.gitkeep create mode 100644 frontend/public/favicon.svg create mode 100644 frontend/public/icons.svg create mode 100644 frontend/public/lottie/heart.json create mode 100644 frontend/src/App.vue create mode 100644 frontend/src/api/client.ts create mode 100644 frontend/src/assets/hero.png create mode 100644 frontend/src/assets/vite.svg create mode 100644 frontend/src/assets/vue.svg create mode 100644 frontend/src/components/FissurePage.vue create mode 100644 frontend/src/components/HeartMangaBackdrop.vue create mode 100644 frontend/src/components/HeartWindowPage.vue create mode 100644 frontend/src/components/LottieSticker.vue create mode 100644 frontend/src/components/MessageForm.vue create mode 100644 frontend/src/components/NepalChapterBand.vue create mode 100644 frontend/src/components/NepalHero.vue create mode 100644 frontend/src/components/NepalPageContainer.vue create mode 100644 frontend/src/components/NeteaseEmbed.vue create mode 100644 frontend/src/components/PolaroidCard.vue create mode 100644 frontend/src/components/StandardNepalPage.vue create mode 100644 frontend/src/components/Timeline.vue create mode 100644 frontend/src/components/TogetherCounter.vue create mode 100644 frontend/src/components/WashiTape.vue create mode 100644 frontend/src/components/standardScenes/ArgueAnimeScene.vue create mode 100644 frontend/src/components/standardScenes/BlueAnimeScene.vue create mode 100644 frontend/src/components/standardScenes/DawnAnimeScene.vue create mode 100644 frontend/src/components/standardScenes/GoldAnimeScene.vue create mode 100644 frontend/src/components/standardScenes/HomeAnimeScene.vue create mode 100644 frontend/src/components/standardScenes/IkeaAnimeScene.vue create mode 100644 frontend/src/components/standardScenes/LotusAnimeScene.vue create mode 100644 frontend/src/components/standardScenes/MangaSceneRouter.vue create mode 100644 frontend/src/components/standardScenes/MintAnimeScene.vue create mode 100644 frontend/src/components/standardScenes/NightAnimeScene.vue create mode 100644 frontend/src/components/standardScenes/PaperAnimeScene.vue create mode 100644 frontend/src/components/standardScenes/RoseAnimeScene.vue create mode 100644 frontend/src/config/neteasePlayer.ts create mode 100644 frontend/src/constants/nepalThemes.ts create mode 100644 frontend/src/main.ts create mode 100644 frontend/src/style.css create mode 100644 frontend/src/utils/renderMarkdown.ts create mode 100644 frontend/src/utils/storyPageTransition.ts create mode 100644 frontend/src/utils/togetherDuration.ts create mode 100644 frontend/src/views/OurStory.vue create mode 100644 frontend/src/vite-env.d.ts create mode 100644 frontend/tsconfig.app.json create mode 100644 frontend/tsconfig.json create mode 100644 frontend/tsconfig.node.json create mode 100644 frontend/vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ab23fa8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +backend/.venv/ +backend/__pycache__/ +backend/**/__pycache__/ +backend/db.sqlite3 +backend/media/ +backend/staticfiles/ +frontend/node_modules/ +frontend/dist/ +*.log +.DS_Store diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..e6e22c3 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,7 @@ +.venv +__pycache__ +*.py[cod] +db.sqlite3 +media +staticfiles +.git diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..ab72762 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,25 @@ +FROM python:3.12-slim-bookworm + +WORKDIR /app + +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 +ENV DJANGO_DEBUG=false +ENV SQLITE_PATH=/data/db.sqlite3 +ENV MEDIA_ROOT=/data/media +ENV DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,backend +ENV CORS_ALLOWED_ORIGINS=http://localhost:8080,http://127.0.0.1:8080 + +RUN apt-get update \ + && apt-get install -y --no-install-recommends libjpeg62-turbo zlib1g \ + && rm -rf /var/lib/apt/lists/* + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . +RUN chmod +x /app/docker-entrypoint.sh + +EXPOSE 8000 + +ENTRYPOINT ["/app/docker-entrypoint.sh"] diff --git a/backend/docker-entrypoint.sh b/backend/docker-entrypoint.sh new file mode 100644 index 0000000..f3abdb0 --- /dev/null +++ b/backend/docker-entrypoint.sh @@ -0,0 +1,6 @@ +#!/bin/sh +set -e +mkdir -p "$(dirname "${SQLITE_PATH:-/data/db.sqlite3}")" "${MEDIA_ROOT:-/data/media}" +python manage.py migrate --noinput +python manage.py collectstatic --noinput +exec gunicorn ourstory.wsgi:application --bind 0.0.0.0:8000 --workers 2 diff --git a/backend/journal/__init__.py b/backend/journal/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/journal/admin.py b/backend/journal/admin.py new file mode 100644 index 0000000..79d9121 --- /dev/null +++ b/backend/journal/admin.py @@ -0,0 +1,196 @@ +from django.contrib import admin +from django.utils.html import format_html +from django.utils.safestring import mark_safe + +from .models import Config, Guestbook, Memory, MemoryPhoto + + +class JournalStaffAccessMixin: + """ + 手账站点多为小项目:凡 is_staff 即可在后台增删改查(无需在「用户」里逐项勾选 journal.* 权限)。 + 生产环境若需细分权限,可移除此 Mixin 并改回 Django 默认的 has_perm 校验。 + """ + + def has_module_permission(self, request): + u = request.user + return bool(getattr(u, "is_active", False) and getattr(u, "is_staff", False)) + + def has_view_permission(self, request, obj=None): + u = request.user + return bool(getattr(u, "is_active", False) and getattr(u, "is_staff", False)) + + def has_change_permission(self, request, obj=None): + u = request.user + return bool(getattr(u, "is_active", False) and getattr(u, "is_staff", False)) + + def has_add_permission(self, request): + u = request.user + return bool(getattr(u, "is_active", False) and getattr(u, "is_staff", False)) + + def has_delete_permission(self, request, obj=None): + u = request.user + return bool(getattr(u, "is_active", False) and getattr(u, "is_staff", False)) + + +class MemoryPhotoInline(admin.TabularInline): + model = MemoryPhoto + extra = 1 + fields = ("image", "sort_order") + + +@admin.register(Memory) +class MemoryAdmin(JournalStaffAccessMixin, admin.ModelAdmin): + inlines = (MemoryPhotoInline,) + list_display = ( + "event_title", + "story_order", + "date", + "unique_feature_type", + "nepal_art_style", + "is_finale", + "thumb_preview", + ) + list_display_links = ("event_title",) + list_filter = ("unique_feature_type", "nepal_art_style", "is_finale", "date") + list_editable = ("story_order",) + search_fields = ("event_title", "main_text") + readonly_fields = ("image_preview",) + ordering = ("story_order", "id") + + fieldsets = ( + ( + "叙事", + { + "fields": ( + "story_order", + "date", + "event_title", + "is_finale", + ) + }, + ), + ( + "版式", + { + "fields": ( + "unique_feature_type", + "nepal_art_style", + "image", + "image_preview", + "main_text", + ) + }, + ), + ) + + @admin.display(description="缩略图") + def thumb_preview(self, obj: Memory) -> str: + url = None + if obj.image: + url = obj.image.url + else: + first = obj.photos.order_by("sort_order", "id").first() + if first: + url = first.image.url + if not url: + return "—" + return format_html( + '', + url, + ) + + @admin.display(description="配图预览") + def image_preview(self, obj: Memory) -> str: + chunks: list[str] = [] + if obj.image: + chunks.append( + format_html( + '
主图
' + '
', + obj.image.url, + ) + ) + for p in obj.photos.order_by("sort_order", "id"): + chunks.append( + format_html( + '', + p.image.url, + ) + ) + if not chunks: + return "—" + return mark_safe("".join(str(c) for c in chunks)) + + +@admin.register(Guestbook) +class GuestbookAdmin(JournalStaffAccessMixin, admin.ModelAdmin): + list_display = ("nickname", "short_message", "created_at") + search_fields = ("nickname", "message") + readonly_fields = ("created_at",) + + @admin.display(description="留言") + def short_message(self, obj: Guestbook) -> str: + return obj.message[:80] + ("…" if len(obj.message) > 80 else "") + + +@admin.register(Config) +class ConfigAdmin(admin.ModelAdmin): + """ + 站点配置单独写权限:不继承 JournalStaffAccessMixin,避免 MRO/多重继承下 + has_change_permission 未按预期生效,导致 change 页全部为只读且 submit_row 不显示「保存」。 + """ + + save_on_top = True + + list_display = ( + "site_title", + "together_since", + "bgm_enabled", + "unlock_at", + ) + list_display_links = ("site_title",) + fieldsets = ( + ( + "站点", + {"fields": ("site_title",)}, + ), + ( + "时间与音乐", + { + "fields": ( + "together_since", + "bgm_enabled", + "unlock_at", + ), + "description": ( + "「在一起」时间用于首页计数。顶部网易云播放器在前端 .env 的 " + "VITE_NETEASE_PLAYER_SRC 配置。unlock_at 已不使用,可留空。" + ), + }, + ), + ) + + def _staff_ok(self, request) -> bool: + u = request.user + return bool( + getattr(u, "is_active", False) + and getattr(u, "is_authenticated", False) + and (getattr(u, "is_staff", False) or getattr(u, "is_superuser", False)) + ) + + def has_module_permission(self, request): + return self._staff_ok(request) + + def has_view_permission(self, request, obj=None): + return self._staff_ok(request) + + def has_change_permission(self, request, obj=None): + return self._staff_ok(request) + + def has_add_permission(self, request): + if not self._staff_ok(request): + return False + return not Config.objects.exists() + + def has_delete_permission(self, request, obj=None): + return False diff --git a/backend/journal/apps.py b/backend/journal/apps.py new file mode 100644 index 0000000..afe76cb --- /dev/null +++ b/backend/journal/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class JournalConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'journal' diff --git a/backend/journal/management/__init__.py b/backend/journal/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/journal/management/commands/__init__.py b/backend/journal/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/journal/management/commands/seed_nepal_story.py b/backend/journal/management/commands/seed_nepal_story.py new file mode 100644 index 0000000..e5b2cf0 --- /dev/null +++ b/backend/journal/management/commands/seed_nepal_story.py @@ -0,0 +1,85 @@ +from datetime import date + +from django.core.management.base import BaseCommand + +from journal.models import Memory + + +class Command(BaseCommand): + help = "Seed 2026 春季时间线:遇见→争执→和好→租房→宜家,终章心窗。" + + def handle(self, *args, **options): + Memory.objects.all().delete() + + Memory.objects.create( + story_order=10, + date=date(2026, 4, 20), + event_title="第一次遇见你", + main_text=( + "**2026 年 4 月 20 日**,故事从这里开始。\n\n" + "相识、相见、相遇——我把这一页留给那天的风与心跳。" + ), + unique_feature_type=Memory.FeatureType.STANDARD_NEPAL, + nepal_art_style=Memory.NepalArtStyle.DAWN_MEET, + is_finale=False, + ) + Memory.objects.create( + story_order=20, + date=date(2026, 5, 3), + event_title="那天我们吵架了", + main_text="旁人皆三千江水,唯你是心上涟漪。", + unique_feature_type=Memory.FeatureType.FISSURE_PAGE, + nepal_art_style=Memory.NepalArtStyle.BLUSH_ARGUE, + is_finale=False, + ) + Memory.objects.create( + story_order=30, + date=date(2026, 5, 4), + event_title="和好了,在一起吧", + main_text=( + "**5 月 4 日**:别扭说开以后,空气又变甜了。\n\n" + "谢谢你愿意和我把话说完。" + ), + unique_feature_type=Memory.FeatureType.STANDARD_NEPAL, + nepal_art_style=Memory.NepalArtStyle.MINT_RECONCILE, + is_finale=False, + ) + Memory.objects.create( + story_order=40, + date=date(2026, 5, 8), + event_title="一起租了房", + main_text=( + "钥匙圈上的小叮当,和阳台晒进来的光。\n\n" + "> 这是我们共同的小宇宙开始成形的日子。" + ), + unique_feature_type=Memory.FeatureType.STANDARD_NEPAL, + nepal_art_style=Memory.NepalArtStyle.SAFFRON_HOME, + is_finale=False, + ) + Memory.objects.create( + story_order=50, + date=date(2026, 5, 10), + event_title="逛宜家的小日子", + main_text=( + "推车里的台灯、地毯和一杯热狗," + "都是以后会说「还记得吗」的碎片。" + ), + unique_feature_type=Memory.FeatureType.STANDARD_NEPAL, + nepal_art_style=Memory.NepalArtStyle.INDIGO_IKEA, + is_finale=False, + ) + Memory.objects.create( + story_order=90, + date=date(2026, 5, 20), + event_title="心墙与窗 · 终章", + main_text=( + "不开心就打开心墙。\n\n" + "**我会在你身边。**\n\n" + "—— *open when you need me*" + ), + unique_feature_type=Memory.FeatureType.HEART_WINDOW, + nepal_art_style=Memory.NepalArtStyle.LOTUS_PETAL, + is_finale=True, + ) + + self.stdout.write(self.style.SUCCESS("Seeded %s memories." % Memory.objects.count())) diff --git a/backend/journal/migrations/0001_initial.py b/backend/journal/migrations/0001_initial.py new file mode 100644 index 0000000..d2f27a6 --- /dev/null +++ b/backend/journal/migrations/0001_initial.py @@ -0,0 +1,56 @@ +# Generated by Django 5.2.14 on 2026-05-11 10:13 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Config', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('unlock_at', models.DateTimeField(help_text='Story unlocks at this time (timezone-aware).')), + ('bgm_enabled', models.BooleanField(default=True)), + ('site_title', models.CharField(blank=True, default='Nepal Journey: Our Story', max_length=120)), + ], + options={ + 'verbose_name_plural': 'Config', + }, + ), + migrations.CreateModel( + name='Guestbook', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('nickname', models.CharField(max_length=80)), + ('message', models.TextField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ], + options={ + 'verbose_name_plural': 'Guestbook entries', + 'ordering': ['-created_at'], + }, + ), + migrations.CreateModel( + name='Memory', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('story_order', models.PositiveIntegerField(db_index=True, default=0, help_text='叙事顺序,数字越小越靠前。')), + ('date', models.DateField()), + ('event_title', models.CharField(max_length=200)), + ('main_text', models.TextField(blank=True, help_text='支持 Markdown。')), + ('unique_feature_type', models.CharField(choices=[('heart_window', '心窗页'), ('fissure_page', '裂缝对开页'), ('standard_nepal', '标准尼泊尔页')], default='standard_nepal', max_length=32)), + ('nepal_art_style', models.CharField(blank=True, choices=[('none', '默认'), ('mandala_pink', '粉曼陀罗'), ('mandala_blue', '蓝曼陀罗'), ('spiral_rose', '玫瑰螺旋'), ('spiral_cyan', '青色螺旋')], default='none', max_length=32)), + ('image', models.ImageField(blank=True, null=True, upload_to='memories/')), + ('is_finale', models.BooleanField(default=False, help_text='终章:在解锁时间前仅可预览占位,到点后需手动打开。')), + ], + options={ + 'ordering': ['story_order', 'id'], + }, + ), + ] diff --git a/backend/journal/migrations/0002_expand_nepal_art_style_length.py b/backend/journal/migrations/0002_expand_nepal_art_style_length.py new file mode 100644 index 0000000..62e13cd --- /dev/null +++ b/backend/journal/migrations/0002_expand_nepal_art_style_length.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.14 on 2026-05-11 10:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('journal', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='memory', + name='nepal_art_style', + field=models.CharField(blank=True, choices=[('none', '默认米黄'), ('mandala_pink', '粉曼陀罗'), ('mandala_blue', '蓝曼陀罗'), ('spiral_rose', '玫瑰螺旋'), ('spiral_cyan', '青色螺旋'), ('dawn_meet', '晨光·遇见'), ('blush_argue', '粉雾·争执'), ('mint_reconcile', '薄荷·和好'), ('saffron_home', '姜黄·小家'), ('indigo_ikea', '靛蓝·漫游'), ('lotus_petal', '莲花瓣'), ('lokta_gold', '金边手工纸'), ('ocean_prayer', '海洋祈愿'), ('desert_rose', '沙漠玫瑰'), ('night_star', '夜空星屑'), ('terra_torn', '赭石撕边')], default='none', help_text='标准页背景/纹理;心窗与裂缝页也可作辅色参考。', max_length=48), + ), + ] diff --git a/backend/journal/migrations/0003_config_together_netease_unlock_nullable.py b/backend/journal/migrations/0003_config_together_netease_unlock_nullable.py new file mode 100644 index 0000000..d34b8ea --- /dev/null +++ b/backend/journal/migrations/0003_config_together_netease_unlock_nullable.py @@ -0,0 +1,33 @@ +# Generated by Django 5.2.14 on 2026-05-11 10:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('journal', '0002_expand_nepal_art_style_length'), + ] + + operations = [ + migrations.AddField( + model_name='config', + name='netease_player_url', + field=models.URLField(blank=True, help_text='网易云「外链播放器」生成的地址,须以 https://music.163.com/ 开头。留空则不显示播放器。', max_length=512), + ), + migrations.AddField( + model_name='config', + name='together_since', + field=models.DateTimeField(blank=True, help_text='用于首页「我们在一起多少天多少秒」的起点(建议为正式在一起的时刻,含时区)。', null=True), + ), + migrations.AlterField( + model_name='config', + name='bgm_enabled', + field=models.BooleanField(default=True, help_text='是否展示顶部音乐区(网易云 iframe)。'), + ), + migrations.AlterField( + model_name='config', + name='unlock_at', + field=models.DateTimeField(blank=True, help_text='已弃用:前端不再按此锁定章节,可留空。', null=True), + ), + ] diff --git a/backend/journal/migrations/0004_netease_player_textfield.py b/backend/journal/migrations/0004_netease_player_textfield.py new file mode 100644 index 0000000..cadc814 --- /dev/null +++ b/backend/journal/migrations/0004_netease_player_textfield.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.14 on 2026-05-11 10:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('journal', '0003_config_together_netease_unlock_nullable'), + ] + + operations = [ + migrations.AlterField( + model_name='config', + name='netease_player_url', + field=models.TextField(blank=True, help_text='粘贴网易云外链播放器的整段 +

+ + + +
+

+ {{ loadError }} +

+ +
+ + + + + diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts new file mode 100644 index 0000000..17aa348 --- /dev/null +++ b/frontend/src/vite-env.d.ts @@ -0,0 +1,9 @@ +/// + +interface ImportMetaEnv { + readonly VITE_API_BASE_URL?: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json new file mode 100644 index 0000000..5c750c5 --- /dev/null +++ b/frontend/tsconfig.app.json @@ -0,0 +1,14 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "types": ["vite/client"], + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 0000000..d3c52ea --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023"], + "module": "esnext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..6575c77 --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,19 @@ +import tailwindcss from '@tailwindcss/vite' +import vue from '@vitejs/plugin-vue' +import { defineConfig } from 'vite' + +export default defineConfig({ + plugins: [vue(), tailwindcss()], + server: { + proxy: { + '/api': { + target: 'http://127.0.0.1:8000', + changeOrigin: true, + }, + '/media': { + target: 'http://127.0.0.1:8000', + changeOrigin: true, + }, + }, + }, +})