mwy2.0
This commit is contained in:
16
.env.example
Normal file
16
.env.example
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# 复制为 .env 后填写。仅在使用 frpc 内网穿透时需要。
|
||||||
|
# cp .env.example .env
|
||||||
|
|
||||||
|
# frps 地址与端口(公网或局域网中的 FRP 服务端)
|
||||||
|
FRP_SERVER_ADDR=your-frps-host.example.com
|
||||||
|
FRP_SERVER_PORT=7000
|
||||||
|
FRP_TOKEN=your-frp-token
|
||||||
|
|
||||||
|
# HTTP 穿透:在 frps 上配置的域名(须与 frps 中 vhostHTTPPort 等匹配)
|
||||||
|
FRP_CUSTOM_DOMAIN=mwy.example.com
|
||||||
|
|
||||||
|
# 公网访问地址(用于 Django CORS / CSRF;带协议,HTTPS 时写 https://)
|
||||||
|
FRP_PUBLIC_ORIGIN=https://mwy.example.com
|
||||||
|
|
||||||
|
# 可选:TCP 穿透时 frps 对外端口(使用 frpc.toml 中 tcp 配置时填写)
|
||||||
|
# FRP_REMOTE_PORT=18080
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -8,3 +8,5 @@ frontend/node_modules/
|
|||||||
frontend/dist/
|
frontend/dist/
|
||||||
*.log
|
*.log
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.env
|
||||||
|
frpc/frpc.log
|
||||||
|
|||||||
@@ -20,6 +20,6 @@ RUN pip install --no-cache-dir -r requirements.txt
|
|||||||
COPY . .
|
COPY . .
|
||||||
RUN chmod +x /app/docker-entrypoint.sh
|
RUN chmod +x /app/docker-entrypoint.sh
|
||||||
|
|
||||||
EXPOSE 8000
|
EXPOSE 8963
|
||||||
|
|
||||||
ENTRYPOINT ["/app/docker-entrypoint.sh"]
|
ENTRYPOINT ["/app/docker-entrypoint.sh"]
|
||||||
|
|||||||
@@ -3,4 +3,4 @@ set -e
|
|||||||
mkdir -p "$(dirname "${SQLITE_PATH:-/data/db.sqlite3}")" "${MEDIA_ROOT:-/data/media}"
|
mkdir -p "$(dirname "${SQLITE_PATH:-/data/db.sqlite3}")" "${MEDIA_ROOT:-/data/media}"
|
||||||
python manage.py migrate --noinput
|
python manage.py migrate --noinput
|
||||||
python manage.py collectstatic --noinput
|
python manage.py collectstatic --noinput
|
||||||
exec gunicorn ourstory.wsgi:application --bind 0.0.0.0:8000 --workers 2
|
exec gunicorn ourstory.wsgi:application --bind 0.0.0.0:8963 --workers 2
|
||||||
|
|||||||
@@ -145,7 +145,6 @@ class ConfigAdmin(admin.ModelAdmin):
|
|||||||
list_display = (
|
list_display = (
|
||||||
"site_title",
|
"site_title",
|
||||||
"together_since",
|
"together_since",
|
||||||
"bgm_enabled",
|
|
||||||
"unlock_at",
|
"unlock_at",
|
||||||
)
|
)
|
||||||
list_display_links = ("site_title",)
|
list_display_links = ("site_title",)
|
||||||
@@ -155,17 +154,13 @@ class ConfigAdmin(admin.ModelAdmin):
|
|||||||
{"fields": ("site_title",)},
|
{"fields": ("site_title",)},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"时间与音乐",
|
"时间",
|
||||||
{
|
{
|
||||||
"fields": (
|
"fields": (
|
||||||
"together_since",
|
"together_since",
|
||||||
"bgm_enabled",
|
|
||||||
"unlock_at",
|
"unlock_at",
|
||||||
),
|
),
|
||||||
"description": (
|
"description": "「在一起」时间用于首页计数。unlock_at 已不使用,可留空。",
|
||||||
"「在一起」时间用于首页计数。顶部网易云播放器在前端 .env 的 "
|
|
||||||
"VITE_NETEASE_PLAYER_SRC 配置。unlock_at 已不使用,可留空。"
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
15
backend/journal/migrations/0007_remove_config_bgm_enabled.py
Normal file
15
backend/journal/migrations/0007_remove_config_bgm_enabled.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('journal', '0006_memory_photo'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='config',
|
||||||
|
name='bgm_enabled',
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -123,10 +123,6 @@ class Config(models.Model):
|
|||||||
null=True,
|
null=True,
|
||||||
help_text="用于首页「我们在一起多少天多少秒」的起点(建议为正式在一起的时刻,含时区)。",
|
help_text="用于首页「我们在一起多少天多少秒」的起点(建议为正式在一起的时刻,含时区)。",
|
||||||
)
|
)
|
||||||
bgm_enabled = models.BooleanField(
|
|
||||||
default=True,
|
|
||||||
help_text="是否展示顶部音乐区(播放器地址在前端环境变量 VITE_NETEASE_PLAYER_SRC 配置)。",
|
|
||||||
)
|
|
||||||
site_title = models.CharField(
|
site_title = models.CharField(
|
||||||
max_length=120,
|
max_length=120,
|
||||||
default="Nepal Journey: Our Story",
|
default="Nepal Journey: Our Story",
|
||||||
|
|||||||
@@ -35,12 +35,10 @@ class StatusView(APIView):
|
|||||||
|
|
||||||
config = Config.objects.filter(pk=1).first()
|
config = Config.objects.filter(pk=1).first()
|
||||||
if config is None:
|
if config is None:
|
||||||
bgm_enabled = True
|
|
||||||
site_title = "Nepal Journey: Our Story"
|
site_title = "Nepal Journey: Our Story"
|
||||||
together_since = default_together
|
together_since = default_together
|
||||||
unlock_at = None
|
unlock_at = None
|
||||||
else:
|
else:
|
||||||
bgm_enabled = config.bgm_enabled
|
|
||||||
site_title = config.site_title or "Nepal Journey: Our Story"
|
site_title = config.site_title or "Nepal Journey: Our Story"
|
||||||
ts = config.together_since
|
ts = config.together_since
|
||||||
if ts is None:
|
if ts is None:
|
||||||
@@ -63,7 +61,6 @@ class StatusView(APIView):
|
|||||||
"unlocked": True,
|
"unlocked": True,
|
||||||
"unlock_at": unlock_at,
|
"unlock_at": unlock_at,
|
||||||
"together_since": together_since,
|
"together_since": together_since,
|
||||||
"bgm_enabled": bgm_enabled,
|
|
||||||
"site_title": site_title,
|
"site_title": site_title,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -134,6 +134,6 @@ CSRF_TRUSTED_ORIGINS = [
|
|||||||
# 未配置时至少信任本机 Admin,避免部分环境下保存表单被 CSRF 拦截
|
# 未配置时至少信任本机 Admin,避免部分环境下保存表单被 CSRF 拦截
|
||||||
if not CSRF_TRUSTED_ORIGINS:
|
if not CSRF_TRUSTED_ORIGINS:
|
||||||
CSRF_TRUSTED_ORIGINS = [
|
CSRF_TRUSTED_ORIGINS = [
|
||||||
"http://127.0.0.1:8000",
|
"http://127.0.0.1:8963",
|
||||||
"http://localhost:8000",
|
"http://localhost:8963",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
# 一键启动:docker compose up --build
|
# 一键启动:docker compose up --build
|
||||||
# 前端 http://localhost:8080 后端管理 http://localhost:8000/admin/
|
# 前端 http://localhost:8080 后端管理 http://localhost:8963/admin/
|
||||||
|
# 内网穿透:cp .env.example .env 并填写后执行 docker compose --profile frp up -d
|
||||||
services:
|
services:
|
||||||
backend:
|
backend:
|
||||||
build: ./backend
|
build: ./backend
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8963:8963"
|
||||||
volumes:
|
volumes:
|
||||||
- backend_data:/data
|
- backend_data:/data
|
||||||
environment:
|
environment:
|
||||||
DJANGO_SECRET_KEY: ${DJANGO_SECRET_KEY:-dev-secret-change-me}
|
DJANGO_SECRET_KEY: ${DJANGO_SECRET_KEY:-dev-secret-change-me}
|
||||||
DJANGO_DEBUG: "false"
|
DJANGO_DEBUG: "false"
|
||||||
DJANGO_ALLOWED_HOSTS: localhost,127.0.0.1,backend
|
DJANGO_ALLOWED_HOSTS: localhost,127.0.0.1,backend,${FRP_CUSTOM_DOMAIN:-}
|
||||||
CORS_ALLOWED_ORIGINS: http://localhost:8080,http://127.0.0.1:8080
|
CORS_ALLOWED_ORIGINS: http://localhost:8080,http://127.0.0.1:8080,${FRP_PUBLIC_ORIGIN:-}
|
||||||
CSRF_TRUSTED_ORIGINS: http://localhost:8080,http://127.0.0.1:8080
|
CSRF_TRUSTED_ORIGINS: http://localhost:8080,http://127.0.0.1:8080,${FRP_PUBLIC_ORIGIN:-}
|
||||||
SQLITE_PATH: /data/db.sqlite3
|
SQLITE_PATH: /data/db.sqlite3
|
||||||
MEDIA_ROOT: /data/media
|
MEDIA_ROOT: /data/media
|
||||||
|
|
||||||
@@ -23,5 +24,20 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- backend
|
- backend
|
||||||
|
|
||||||
|
frpc:
|
||||||
|
image: snowdreamtech/frpc:0.61
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- frontend
|
||||||
|
volumes:
|
||||||
|
- ./frpc/frpc.toml:/etc/frp/frpc.toml:ro
|
||||||
|
environment:
|
||||||
|
FRP_SERVER_ADDR: ${FRP_SERVER_ADDR:-}
|
||||||
|
FRP_SERVER_PORT: ${FRP_SERVER_PORT:-7000}
|
||||||
|
FRP_TOKEN: ${FRP_TOKEN:-}
|
||||||
|
FRP_CUSTOM_DOMAIN: ${FRP_CUSTOM_DOMAIN:-}
|
||||||
|
profiles:
|
||||||
|
- frp
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
backend_data:
|
backend_data:
|
||||||
|
|||||||
@@ -1,5 +1,2 @@
|
|||||||
# 使用 Vite 代理时保持默认 /api 即可
|
# 使用 Vite 代理时保持默认 /api 即可
|
||||||
VITE_API_BASE_URL=/api
|
VITE_API_BASE_URL=/api
|
||||||
|
|
||||||
# 网易云外链播放器 src(整段 iframe 亦可,会自动提取 src)
|
|
||||||
# VITE_NETEASE_PLAYER_SRC=https://music.163.com/outchain/player?type=2&id=2054353432&auto=1&height=66
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ server {
|
|||||||
gzip_types text/plain text/css application/javascript application/json image/svg+xml;
|
gzip_types text/plain text/css application/javascript application/json image/svg+xml;
|
||||||
|
|
||||||
location /api/ {
|
location /api/ {
|
||||||
proxy_pass http://backend:8000;
|
proxy_pass http://backend:8963;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
@@ -17,7 +17,7 @@ server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
location /media/ {
|
location /media/ {
|
||||||
proxy_pass http://backend:8000;
|
proxy_pass http://backend:8963;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ export interface StatusPayload {
|
|||||||
unlocked: boolean
|
unlocked: boolean
|
||||||
unlock_at: string | null
|
unlock_at: string | null
|
||||||
together_since: string
|
together_since: string
|
||||||
bgm_enabled: boolean
|
|
||||||
site_title: string
|
site_title: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ async function submit() {
|
|||||||
<div class="flex items-center gap-2 text-rose-deep">
|
<div class="flex items-center gap-2 text-rose-deep">
|
||||||
<Heart class="h-5 w-5" fill="currentColor" aria-hidden="true" />
|
<Heart class="h-5 w-5" fill="currentColor" aria-hidden="true" />
|
||||||
<h2 id="guestbook-title" class="font-brush text-3xl text-lokta-deep sm:text-4xl">
|
<h2 id="guestbook-title" class="font-brush text-3xl text-lokta-deep sm:text-4xl">
|
||||||
给她的一句话
|
给他的一句话
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<p class="mt-2 text-sm text-ink-muted">
|
<p class="mt-2 text-sm text-ink-muted">
|
||||||
@@ -69,7 +69,7 @@ async function submit() {
|
|||||||
rows="5"
|
rows="5"
|
||||||
maxlength="2000"
|
maxlength="2000"
|
||||||
class="mt-1 w-full resize-y rounded-xl border border-paper-shadow bg-paper/80 px-4 py-3 text-ink shadow-inner outline-none transition focus:border-rose-soft/60 focus:ring-2 focus:ring-rose-soft/25"
|
class="mt-1 w-full resize-y rounded-xl border border-paper-shadow bg-paper/80 px-4 py-3 text-ink shadow-inner outline-none transition focus:border-rose-soft/60 focus:ring-2 focus:ring-rose-soft/25"
|
||||||
placeholder="写下你想说的话…"
|
placeholder="写下你想说的话…,例如:谢谢你陪伴我度过了这么多美好的时光!"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="error" class="text-sm text-rose-deep">
|
<p v-if="error" class="text-sm text-rose-deep">
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { computed } from 'vue'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
/** 后端已规范为 https://music.163.com/... 的播放器地址 */
|
|
||||||
src: string
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const safeSrc = computed(() => {
|
|
||||||
let u = (props.src || '').trim()
|
|
||||||
if (!u) return ''
|
|
||||||
if (u.startsWith('//')) u = `https:${u}`
|
|
||||||
try {
|
|
||||||
const url = new URL(u)
|
|
||||||
if (url.protocol !== 'https:') return ''
|
|
||||||
const h = url.hostname.toLowerCase()
|
|
||||||
if (h !== 'music.163.com') return ''
|
|
||||||
return url.href.split('#')[0]
|
|
||||||
} catch {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
v-if="safeSrc"
|
|
||||||
class="overflow-hidden rounded-xl border border-lokta-deep/15 bg-white/80 shadow-sm"
|
|
||||||
>
|
|
||||||
<iframe
|
|
||||||
:src="safeSrc"
|
|
||||||
title="网易云音乐"
|
|
||||||
frameborder="no"
|
|
||||||
border="0"
|
|
||||||
marginwidth="0"
|
|
||||||
marginheight="0"
|
|
||||||
width="330"
|
|
||||||
height="86"
|
|
||||||
class="mx-auto block border-0"
|
|
||||||
allow="autoplay; encrypted-media; fullscreen"
|
|
||||||
referrerpolicy="strict-origin-when-cross-origin"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
/**
|
|
||||||
* 网易云外链播放器:在前端配置,不经过 Django。
|
|
||||||
*
|
|
||||||
* 在 `frontend/.env` / `.env.development` 中设置:
|
|
||||||
* VITE_NETEASE_PLAYER_SRC=https://music.163.com/outchain/player?type=2&id=…&auto=1&height=66
|
|
||||||
* 也可粘贴整段 `<iframe … src="…">`,会自动取出 src。
|
|
||||||
*/
|
|
||||||
function extractIframeSrc(raw: string): string {
|
|
||||||
const t = raw.trim()
|
|
||||||
if (!t) return ''
|
|
||||||
const m = /\bsrc\s*=\s*["']([^"']+)["']/i.exec(t)
|
|
||||||
return (m ? m[1] : t).trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
const fromEnv = (import.meta.env.VITE_NETEASE_PLAYER_SRC as string | undefined)?.trim() ?? ''
|
|
||||||
|
|
||||||
export const NETEASE_PLAYER_SRC = extractIframeSrc(fromEnv)
|
|
||||||
@@ -2,11 +2,9 @@
|
|||||||
import { RefreshCw } from 'lucide-vue-next'
|
import { RefreshCw } from 'lucide-vue-next'
|
||||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||||
import { fetchMemories, fetchStatus, type Memory, type StatusPayload } from '../api/client'
|
import { fetchMemories, fetchStatus, type Memory, type StatusPayload } from '../api/client'
|
||||||
import { NETEASE_PLAYER_SRC } from '../config/neteasePlayer'
|
|
||||||
import MessageForm from '../components/MessageForm.vue'
|
import MessageForm from '../components/MessageForm.vue'
|
||||||
import NepalChapterBand from '../components/NepalChapterBand.vue'
|
import NepalChapterBand from '../components/NepalChapterBand.vue'
|
||||||
import NepalPageContainer from '../components/NepalPageContainer.vue'
|
import NepalPageContainer from '../components/NepalPageContainer.vue'
|
||||||
import NeteaseEmbed from '../components/NeteaseEmbed.vue'
|
|
||||||
import TogetherCounter from '../components/TogetherCounter.vue'
|
import TogetherCounter from '../components/TogetherCounter.vue'
|
||||||
|
|
||||||
const status = ref<StatusPayload | null>(null)
|
const status = ref<StatusPayload | null>(null)
|
||||||
@@ -20,19 +18,6 @@ const serverOffsetMs = ref(0)
|
|||||||
let statusPoll: ReturnType<typeof setInterval> | undefined
|
let statusPoll: ReturnType<typeof setInterval> | undefined
|
||||||
|
|
||||||
const siteTitle = computed(() => status.value?.site_title ?? 'Nepal Journey: Our Story')
|
const siteTitle = computed(() => status.value?.site_title ?? 'Nepal Journey: Our Story')
|
||||||
const bgmEnabled = computed(() => status.value?.bgm_enabled ?? true)
|
|
||||||
const neteaseSrc = NETEASE_PLAYER_SRC
|
|
||||||
const showNetease = computed(() => {
|
|
||||||
if (!bgmEnabled.value || !neteaseSrc) return false
|
|
||||||
try {
|
|
||||||
let u = neteaseSrc.trim()
|
|
||||||
if (u.startsWith('//')) u = `https:${u}`
|
|
||||||
const url = new URL(u)
|
|
||||||
return url.protocol === 'https:' && url.hostname.toLowerCase() === 'music.163.com'
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const showAppChrome = computed(() => !busy.value && status.value !== null)
|
const showAppChrome = computed(() => !busy.value && status.value !== null)
|
||||||
|
|
||||||
@@ -99,7 +84,7 @@ const counterMilestones = computed(() =>
|
|||||||
v-if="showAppChrome && status"
|
v-if="showAppChrome && status"
|
||||||
class="sticky top-0 z-40 border-b border-lokta-deep/10 bg-gradient-to-r from-lokta-cream/90 via-paper/90 to-lokta-teal/15 backdrop-blur-md"
|
class="sticky top-0 z-40 border-b border-lokta-deep/10 bg-gradient-to-r from-lokta-cream/90 via-paper/90 to-lokta-teal/15 backdrop-blur-md"
|
||||||
>
|
>
|
||||||
<div class="mx-auto max-w-3xl space-y-3 px-4 py-3 sm:px-6">
|
<div class="mx-auto max-w-3xl px-4 py-3 sm:px-6">
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<p class="truncate font-brush text-2xl text-lokta-deep sm:text-3xl">
|
<p class="truncate font-brush text-2xl text-lokta-deep sm:text-3xl">
|
||||||
{{ siteTitle }}
|
{{ siteTitle }}
|
||||||
@@ -109,15 +94,7 @@ const counterMilestones = computed(() =>
|
|||||||
:server-offset-ms="serverOffsetMs"
|
:server-offset-ms="serverOffsetMs"
|
||||||
:milestones="counterMilestones"
|
:milestones="counterMilestones"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<NeteaseEmbed v-if="showNetease" :src="neteaseSrc" />
|
|
||||||
<p
|
|
||||||
v-else-if="bgmEnabled && !neteaseSrc"
|
|
||||||
class="rounded-lg border border-dashed border-lokta-deep/20 bg-white/50 px-3 py-2 text-xs text-ink-muted"
|
|
||||||
>
|
|
||||||
<iframe frameborder="no" border="0" marginwidth="0" marginheight="0" width=330 height=86 src="//music.163.com/outchain/player?type=2&id=2054353432&auto=1&height=66"></iframe>
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ export default defineConfig({
|
|||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://127.0.0.1:8000',
|
target: 'http://127.0.0.1:8963',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
'/media': {
|
'/media': {
|
||||||
target: 'http://127.0.0.1:8000',
|
target: 'http://127.0.0.1:8963',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
28
frpc/frpc.toml
Normal file
28
frpc/frpc.toml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
|
||||||
|
# ---------------- 基本连接配置 ----------------
|
||||||
|
# 【重要】请填写你 VPS/服务器 的公网 IP
|
||||||
|
serverAddr = "47.86.50.107"
|
||||||
|
|
||||||
|
# 对应服务端的 bindPort
|
||||||
|
serverPort = 7000
|
||||||
|
|
||||||
|
# ---------------- 身份验证 ----------------
|
||||||
|
# 对应服务端的 auth.token
|
||||||
|
auth.method = "token"
|
||||||
|
auth.token = "f4HXwoNK15RjBK6Q"
|
||||||
|
|
||||||
|
# ---------------- 穿透规则 (你可以根据需要添加多个) ----------------
|
||||||
|
|
||||||
|
|
||||||
|
[[proxies]]
|
||||||
|
name = "mwy" # 名字必须唯一,这里改了个名
|
||||||
|
type = "tcp"
|
||||||
|
localIP = "127.0.0.1" # 本机 frpc 直连;Docker 内运行则改为 host.docker.internal 或宿主机局域网 IP
|
||||||
|
localPort = 5174
|
||||||
|
remotePort = 5200
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -4,14 +4,14 @@
|
|||||||
|
|
||||||
| 环境 | 手账站 | Django Admin |
|
| 环境 | 手账站 | Django Admin |
|
||||||
|------|--------|----------------|
|
|------|--------|----------------|
|
||||||
| 本地开发 | http://localhost:5173 | http://localhost:8000/admin/ |
|
| 本地开发 | http://localhost:5173 | http://localhost:8963/admin/ |
|
||||||
| Docker | http://localhost:8080 | http://localhost:8000/admin/ |
|
| Docker | http://localhost:8080 | http://localhost:8963/admin/ |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 本地开发(推荐)
|
## 本地开发(推荐)
|
||||||
|
|
||||||
前后端各开一个终端。前端 Vite 会把 `/api`、`/media` 代理到 `http://127.0.0.1:8000`,需先启动后端。
|
前后端各开一个终端。前端 Vite 会把 `/api`、`/media` 代理到 `http://127.0.0.1:8963`,需先启动后端。
|
||||||
|
|
||||||
### 后端(Django)
|
### 后端(Django)
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ python -m venv .venv
|
|||||||
.\.venv\Scripts\Activate.ps1
|
.\.venv\Scripts\Activate.ps1
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
python manage.py migrate
|
python manage.py migrate
|
||||||
python manage.py runserver
|
python manage.py runserver 8963
|
||||||
```
|
```
|
||||||
|
|
||||||
首次使用 Admin 可创建超级用户:
|
首次使用 Admin 可创建超级用户:
|
||||||
@@ -54,7 +54,7 @@ npm run dev
|
|||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# .env 或 .env.local
|
# .env 或 .env.local
|
||||||
# VITE_API_BASE_URL=http://127.0.0.1:8000/api
|
# VITE_API_BASE_URL=http://127.0.0.1:8963/api
|
||||||
```
|
```
|
||||||
|
|
||||||
### 生产构建预览(可选)
|
### 生产构建预览(可选)
|
||||||
@@ -76,18 +76,42 @@ docker compose up --build
|
|||||||
```
|
```
|
||||||
|
|
||||||
- 手账站:http://localhost:8080
|
- 手账站:http://localhost:8080
|
||||||
- Admin:http://localhost:8000/admin/
|
- Admin:http://localhost:8963/admin/
|
||||||
|
|
||||||
数据与媒体挂在 Docker 卷 `backend_data`(SQLite、`media`)。
|
数据与媒体挂在 Docker 卷 `backend_data`(SQLite、`media`)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## FRP 内网穿透(frpc)
|
||||||
|
|
||||||
|
通过 [frp](https://github.com/fatedier/frp) 把本机 Docker 里的手账站暴露到公网(需你已有 **frps** 服务端)。
|
||||||
|
|
||||||
|
1. 复制环境变量模板并填写 frps 信息:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cp .env.example .env
|
||||||
|
# 编辑 .env:FRP_SERVER_ADDR、FRP_SERVER_PORT、FRP_TOKEN、FRP_CUSTOM_DOMAIN、FRP_PUBLIC_ORIGIN
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 先启动应用,再启用 frpc(`frp` profile,默认不启动):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
docker compose up --build -d
|
||||||
|
docker compose --profile frp up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 在 frps 上为域名 `FRP_CUSTOM_DOMAIN` 配置 HTTP 入口后,用 `FRP_PUBLIC_ORIGIN` 访问手账站;Admin 为 `{FRP_PUBLIC_ORIGIN 的 host}/admin/` 需经同一 HTTP 代理(当前穿透的是 frontend:80,已包含 `/api`、`/media`)。
|
||||||
|
|
||||||
|
配置文件:`frpc/frpc.toml`。无域名时可在该文件中改用 TCP 代理(见文件内注释),并设置 `FRP_REMOTE_PORT`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 常用命令速查
|
## 常用命令速查
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# 后端目录 backend/ 下,且已激活虚拟环境
|
# 后端目录 backend/ 下,且已激活虚拟环境
|
||||||
python manage.py migrate
|
python manage.py migrate
|
||||||
python manage.py runserver
|
python manage.py runserver 8963
|
||||||
python manage.py seed_nepal_story
|
python manage.py seed_nepal_story
|
||||||
python manage.py createsuperuser
|
python manage.py createsuperuser
|
||||||
|
|
||||||
BIN
照片/机电院.jpg
Normal file
BIN
照片/机电院.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 754 KiB |
BIN
照片/见面花.jpg
Normal file
BIN
照片/见面花.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 805 KiB |
Reference in New Issue
Block a user