阶段 0 起步:图标与环境自检脚手架
- assets/icons 全套图标:SVG 源文件 + maskable + 12 档 PNG - favicon.ico / apple-touch-icon.png / site.webmanifest - index.php 环境自检页(PHP 版本 / 必需扩展 / 配置 / 数据库连接), 详情页默认不打印连接错误原文,避免泄露数据库账号名 - config.example.php:数据库与 app_key 的配置模板(config.php 不入库) - README 补充项目定位与六阶段路线图;.gitignore 沿用既有约定
@@ -0,0 +1,15 @@
|
||||
# ── 本地配置(含数据库口令与加密密钥),不入库 ──────────────────────────────
|
||||
# 部署时由服务器上的 config.php 提供;新环境请复制 config.example.php 再改。
|
||||
config.php
|
||||
|
||||
# ── 用户上传内容(头像 / 照片 / 附件),不入库 ──────────────────────────────
|
||||
# 只保留「禁止执行脚本」的保护规则,否则上传的 .php 能被直接访问。
|
||||
uploads/*
|
||||
!uploads/.htaccess
|
||||
|
||||
# ── 临时产物 ────────────────────────────────────────────────────────────────
|
||||
*.bak
|
||||
*.bak.*
|
||||
*~
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
@@ -1,3 +1,64 @@
|
||||
# FamilyBook
|
||||
|
||||
家书,记录家庭的全部生活,一本用岁月写成的家谱。
|
||||
**家书,记录家庭的全部生活,一本用岁月写成的家谱。**
|
||||
|
||||
> 把一个家的「人」「日子」「去过的地方、看过的电影」收进同一本书里。
|
||||
|
||||
---
|
||||
|
||||
## 它是什么
|
||||
|
||||
一个自托管的家庭生活系统。它不是又一个待办清单,而是**这个家的长期档案** ——
|
||||
十年之后还能翻出孩子第一次观影的票根、那年暑假走过的路线。
|
||||
|
||||
围绕三个圈展开:
|
||||
|
||||
- **人**:家庭成员、亲友通讯录,以及他们之间的共享关系
|
||||
- **时间**:日历、日程、提醒,以及任务的期限
|
||||
- **事件**:观影、旅行、其他活动 —— 一切都落在同一条时间轴上
|
||||
|
||||
## 它在做什么
|
||||
|
||||
| 阶段 | 主题 | 内容 |
|
||||
| --- | --- | --- |
|
||||
| 0 | 地基 | 名册、账号、登录、权限模型、安装向导 |
|
||||
| 1 | 通信 | 通讯录、共享给某个成员、CardDAV 服务端 |
|
||||
| 2 | 时间 | 日历本、重复日程、提醒、CalDAV 服务端 |
|
||||
| 3 | 归一 | 观影与旅行迁入统一事件模型 |
|
||||
| 4 | 任务 | 任务管理,期限自动映射成日历事件 |
|
||||
| 5 | 回看 | 统计、年度家书、时间轴 |
|
||||
|
||||
当前进度:**阶段 0 起步**,仓库里只有图标与脚手架。
|
||||
|
||||
## 技术选型
|
||||
|
||||
- PHP 7.4+(无框架、无 Composer,上传到站点目录即可运行)
|
||||
- MySQL 5.7+ / 8.0(`utf8mb4`)
|
||||
- 前端为原生 HTML / CSS / JavaScript,无构建步骤
|
||||
|
||||
⚠️ 注意 CalDAV / CardDAV 是**双向协议**,和单纯的 `.ics` 文件导出不是一个量级的东西。
|
||||
|
||||
## 部署
|
||||
|
||||
1. 把代码放到站点根目录;
|
||||
2. 复制 `config.example.php` 为 `config.php`,填写数据库与 `app_key`;
|
||||
3. 访问站点根,按引导页确认 PHP 版本、扩展、配置、数据库连接四项都通过。
|
||||
|
||||
`config.php` 不入库(已在 `.gitignore` 中忽略),其中包含数据库口令与加密密钥。
|
||||
|
||||
⚠️ **`app_key` 一旦开始记录数据就不能再改**(换掉即解不开已有数据),
|
||||
并且它必须单独备份 —— 密钥丢失等同于数据永久丢失。
|
||||
|
||||
## 隐私
|
||||
|
||||
身份证号、手机号属于敏感个人信息,一律加密存储。
|
||||
(就家庭自用而言,身份证号其实没有多少必须存的场景,启用前请三思。)
|
||||
|
||||
## 图标
|
||||
|
||||
见 `assets/icons/`。概念是「**这本书就是家**」:屋顶即一本摊开的书,
|
||||
屋顶下纸做的墙上,三行横线是这个家写下的日子。
|
||||
|
||||
## 开源协议
|
||||
|
||||
[Apache License 2.0](LICENSE)
|
||||
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
@@ -0,0 +1,29 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="家书 FamilyBook">
|
||||
<!--
|
||||
maskable 版本:用于 Android 自适应图标 / PWA。
|
||||
背景铺满整个方形(不裁圆角),主体按 0.7 缩放居中。
|
||||
|
||||
⚠️ 0.7 是按主体包围盒算出来的:主体 55×48.2,缩放后角点到中心约 25.6,
|
||||
正好落在直径 80% 的圆形安全区边界上,再放大会被裁掉屋檐。
|
||||
|
||||
⚠️ 绘制顺序与主图标一致:墙压住屋顶底边 1px,避免出现抗锯齿缝隙。
|
||||
-->
|
||||
<rect width="64" height="64" fill="#F2E9D8"/>
|
||||
|
||||
<g transform="translate(32 32) scale(0.7) translate(-32 -32)">
|
||||
<path d="M32 7 L4.5 34 H32 Z" fill="#A84A28"/>
|
||||
<path d="M32 7 L59.5 34 H32 Z" fill="#C96F45"/>
|
||||
<path d="M28 34 L32 25 L36 34 Z" fill="#F2E9D8"/>
|
||||
|
||||
<path d="M32 7 V25" stroke="#6E3820" stroke-width="2.4" fill="none"/>
|
||||
|
||||
<rect x="13" y="33" width="38" height="19" rx="3" fill="#FDFBF5"/>
|
||||
<g stroke="#BD5C39" stroke-width="2.4" stroke-linecap="round" fill="none">
|
||||
<path d="M18 40 H46"/>
|
||||
<path d="M18 44.5 H46"/>
|
||||
<path d="M18 48.5 H37"/>
|
||||
</g>
|
||||
|
||||
<rect x="10.5" y="52" width="43" height="3.2" rx="1.6" fill="#7A3F2A"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,45 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="家书 FamilyBook">
|
||||
<!--
|
||||
家书 FamilyBook 图标
|
||||
概念:这本书就是家。
|
||||
· 屋顶 = 摊开的书:两片封面(左暗右亮),顶端的深色短脊线点明书脊,
|
||||
底部中央一道 V 形沟口露出背景,是书页分开的地方
|
||||
· 屋顶下是纸做的墙,墙上三行横线是这个家写下的日子
|
||||
· 深褐地基收底
|
||||
构图:主体横向占约 86%,四边留白收紧,小尺寸下更饱满。
|
||||
|
||||
⚠️ 绘制顺序不能改:墙在屋顶之后绘制,由墙压住屋顶底边 1px。
|
||||
这样既不会出现抗锯齿缝隙,也不会让沟口在墙顶切出一道米色横缝。
|
||||
|
||||
配色:宣纸米 #F2E9D8 / 封面 #A84A28·#C96F45 / 书脊·地基 #6E3820·#7A3F2A / 纸白 #FDFBF5
|
||||
-->
|
||||
<defs>
|
||||
<clipPath id="fb-clip">
|
||||
<rect width="64" height="64" rx="14"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
|
||||
<g clip-path="url(#fb-clip)">
|
||||
<!-- 宣纸底 -->
|
||||
<rect width="64" height="64" fill="#F2E9D8"/>
|
||||
|
||||
<!-- 屋顶 = 摊开的书 -->
|
||||
<path d="M32 7 L4.5 34 H32 Z" fill="#A84A28"/>
|
||||
<path d="M32 7 L59.5 34 H32 Z" fill="#C96F45"/>
|
||||
<path d="M28 34 L32 25 L36 34 Z" fill="#F2E9D8"/>
|
||||
|
||||
<!-- 书脊 -->
|
||||
<path d="M32 7 V25" stroke="#6E3820" stroke-width="2.4" fill="none"/>
|
||||
|
||||
<!-- 纸做的墙:写下的岁月 -->
|
||||
<rect x="13" y="33" width="38" height="19" rx="3" fill="#FDFBF5"/>
|
||||
<g stroke="#BD5C39" stroke-width="2.4" stroke-linecap="round" fill="none">
|
||||
<path d="M18 40 H46"/>
|
||||
<path d="M18 44.5 H46"/>
|
||||
<path d="M18 48.5 H37"/>
|
||||
</g>
|
||||
|
||||
<!-- 地基 -->
|
||||
<rect x="10.5" y="52" width="43" height="3.2" rx="1.6" fill="#7A3F2A"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 475 B |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 789 B |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
/**
|
||||
* FamilyBook · 家书 —— 本地配置模板
|
||||
*
|
||||
* 使用:复制为 config.php,按注释填写。
|
||||
* config.php 已在 .gitignore 中忽略,不会进 Git —— 换机器、换库都靠它。
|
||||
*
|
||||
* ⚠️ 仓库是公开的:这里不许出现真实域名、真实口令、真实密钥。
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
|
||||
// ---------------- 站点 ----------------
|
||||
'site_name' => '家书',
|
||||
// 站点对外根地址,末尾不要带斜杠。示例值,请替换成你自己的
|
||||
'base_url' => 'https://example.com',
|
||||
|
||||
// ---------------- 数据库(MySQL)----------------
|
||||
'db' => [
|
||||
'host' => '127.0.0.1',
|
||||
'port' => 3306,
|
||||
'name' => 'familybook',
|
||||
'user' => 'familybook',
|
||||
'pass' => '',
|
||||
'charset' => 'utf8mb4',
|
||||
],
|
||||
|
||||
// ---------------- 敏感字段加密密钥 ----------------
|
||||
// 用于身份证号、手机号的对称加密(openssl AES-256-CBC)。
|
||||
// ⚠️ 开始记录数据之后这个值就不能再改:换掉它,已有数据就解不开了。
|
||||
// ⚠️ 请单独备份它。密钥丢失 = 数据永久丢失,比数据库丢了还严重。
|
||||
// 生成方式:openssl rand -hex 16
|
||||
'app_key' => '请填写32位随机十六进制串',
|
||||
|
||||
// ---------------- 调试 ----------------
|
||||
// true 时引导页会打印数据库连接错误的详情。
|
||||
// ⚠️ 线上务必保持 false —— 错误详情里可能带数据库账号名。
|
||||
'debug' => false,
|
||||
];
|
||||
|
After Width: | Height: | Size: 8.1 KiB |
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
/**
|
||||
* FamilyBook · 家书 —— 阶段 0 引导页
|
||||
*
|
||||
* 这一页不是首页。它的职责是:部署之后先看这里,确认
|
||||
* 「PHP 版本 / 扩展 / 配置文件 / 数据库连接」四件事都通了,再往上盖功能。
|
||||
* 正式首页做好之后会替换掉它。
|
||||
*
|
||||
* ⚠️ 线上请把 config.php 里的 debug 设为 false:
|
||||
* 连接报错时,详情里可能含数据库账号名,不该公开展示。
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
define('FB_ROOT', __DIR__);
|
||||
|
||||
$configFile = FB_ROOT . '/config.php';
|
||||
$hasConfig = is_file($configFile);
|
||||
$config = $hasConfig ? require $configFile : null;
|
||||
$debug = is_array($config) && !empty($config['debug']);
|
||||
|
||||
/**
|
||||
* 静态资源带版本号(mtime + size)。
|
||||
* 不带版本号的话,文件换了浏览器还在用旧缓存 —— 详见踩坑记录。
|
||||
*/
|
||||
function fb_asset(string $rel): string
|
||||
{
|
||||
$p = FB_ROOT . $rel;
|
||||
if (!is_file($p)) {
|
||||
return $rel;
|
||||
}
|
||||
return $rel . '?v=' . substr(md5(filemtime($p) . filesize($p)), 0, 8);
|
||||
}
|
||||
|
||||
$checks = [];
|
||||
|
||||
// 1. PHP 版本:以 PHP 7.4 为底线(线上同时存在 7.4 与 8.1 两套)
|
||||
$checks['PHP 版本'] = [
|
||||
'ok' => PHP_VERSION_ID >= 70400,
|
||||
'val' => PHP_VERSION . '(需 7.4 以上)',
|
||||
];
|
||||
|
||||
// 2. 必需扩展
|
||||
$needExt = ['pdo_mysql' => '数据库', 'mbstring' => '中文处理', 'openssl' => '敏感字段加密', 'json' => '配置与接口', 'gd' => '图片裁剪'];
|
||||
foreach ($needExt as $ext => $why) {
|
||||
$ok = extension_loaded($ext);
|
||||
$checks['扩展 ' . $ext] = ['ok' => $ok, 'val' => $ok ? '已加载 · ' . $why : '缺失 · ' . $why];
|
||||
}
|
||||
|
||||
// 3. 配置文件
|
||||
$checks['配置文件 config.php'] = [
|
||||
'ok' => $hasConfig,
|
||||
'val' => $hasConfig ? '已就位' : '未找到(请复制 config.example.php 后填写)',
|
||||
];
|
||||
|
||||
// 4. 数据库连接(只有配置齐全才去连)
|
||||
if (is_array($config) && !empty($config['db'])) {
|
||||
$d = $config['db'];
|
||||
try {
|
||||
$dsn = sprintf(
|
||||
'mysql:host=%s;port=%d;dbname=%s;charset=%s',
|
||||
$d['host'], $d['port'], $d['name'], $d['charset']
|
||||
);
|
||||
$pdo = new PDO($dsn, $d['user'], $d['pass'], [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
]);
|
||||
$ver = $pdo->query('SELECT VERSION()')->fetchColumn();
|
||||
$checks['数据库连接'] = ['ok' => true, 'val' => 'MySQL ' . $ver . ' · 库 ' . $d['name']];
|
||||
} catch (Throwable $e) {
|
||||
$checks['数据库连接'] = [
|
||||
'ok' => false,
|
||||
'val' => $debug ? $e->getMessage() : '连接失败(把 config.php 的 debug 设为 true 可看详情)',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$allOk = true;
|
||||
foreach ($checks as $c) {
|
||||
$allOk = $allOk && $c['ok'];
|
||||
}
|
||||
$siteName = is_array($config) && !empty($config['site_name']) ? $config['site_name'] : '家书';
|
||||
?><!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title><?= htmlspecialchars($siteName, ENT_QUOTES, 'UTF-8') ?> · 环境自检</title>
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
<style>
|
||||
:root { --paper:#F2E9D8; --cover:#A84A28; --cover2:#C96F45; --ink:#6E3820; --white:#FDFBF5; }
|
||||
* { box-sizing:border-box; }
|
||||
body { margin:0; padding:40px 20px; background:var(--paper); color:var(--ink);
|
||||
font:15px/1.7 -apple-system,"PingFang SC","Microsoft YaHei",sans-serif; }
|
||||
.wrap { max-width:560px; margin:0 auto; }
|
||||
.head { display:flex; align-items:center; gap:14px; margin-bottom:6px; }
|
||||
.head img { width:64px; height:64px; border-radius:14px; }
|
||||
h1 { font-size:22px; margin:0; font-weight:600; }
|
||||
.sub { color:#8a6a52; font-size:13px; margin:0 0 24px 78px; }
|
||||
ul { list-style:none; margin:0; padding:0; background:var(--white);
|
||||
border-radius:12px; overflow:hidden; }
|
||||
li { display:flex; align-items:baseline; gap:10px; padding:12px 16px;
|
||||
border-bottom:1px solid rgba(110,56,32,.08); }
|
||||
li:last-child { border-bottom:0; }
|
||||
.dot { width:9px; height:9px; border-radius:50%; flex:0 0 auto; margin-top:8px; }
|
||||
.ok { background:#6f9e5c; } .bad { background:#c0533f; }
|
||||
.k { font-weight:600; flex:0 0 auto; }
|
||||
.v { color:#7a5a42; font-size:13px; word-break:break-all; }
|
||||
.banner { padding:10px 16px; border-radius:10px; margin-bottom:18px; font-size:14px; }
|
||||
.pass { background:#e6f0dc; color:#3d6b1f; }
|
||||
.fail { background:#f8ddda; color:#a1341f; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<div class="head">
|
||||
<img src="<?= htmlspecialchars(fb_asset('/assets/icons/icon-192.png'), ENT_QUOTES, 'UTF-8') ?>" alt="">
|
||||
<div>
|
||||
<h1><?= htmlspecialchars($siteName, ENT_QUOTES, 'UTF-8') ?> · 环境自检</h1>
|
||||
</div>
|
||||
</div>
|
||||
<p class="sub">FamilyBook —— 一本用岁月写成的家谱</p>
|
||||
|
||||
<div class="banner <?= $allOk ? 'pass' : 'fail' ?>">
|
||||
<?= $allOk ? '全部通过,可以开始盖功能了。' : '还有未通过项,处理后请刷新这一页。' ?>
|
||||
</div>
|
||||
|
||||
<ul>
|
||||
<?php foreach ($checks as $name => $c): ?>
|
||||
<li>
|
||||
<span class="dot <?= $c['ok'] ? 'ok' : 'bad' ?>"></span>
|
||||
<span class="k"><?= htmlspecialchars($name, ENT_QUOTES, 'UTF-8') ?></span>
|
||||
<span class="v"><?= htmlspecialchars((string)$c['val'], ENT_QUOTES, 'UTF-8') ?></span>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "家书 FamilyBook",
|
||||
"short_name": "家书",
|
||||
"description": "记录家庭的全部生活,一本用岁月写成的家谱。",
|
||||
"start_url": "/",
|
||||
"scope": "/",
|
||||
"display": "standalone",
|
||||
"lang": "zh-CN",
|
||||
"background_color": "#F2E9D8",
|
||||
"theme_color": "#A84A28",
|
||||
"icons": [
|
||||
{ "src": "/assets/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
|
||||
{ "src": "/assets/icons/icon-512.png", "sizes": "512x512", "type": "image/png" },
|
||||
{ "src": "/assets/icons/icon-192-maskable.png", "sizes": "192x192", "type": "image/png", "purpose": "maskable" },
|
||||
{ "src": "/assets/icons/icon-512-maskable.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
|
||||
]
|
||||
}
|
||||