feat(web): 补齐官网协议页,并按包名控制会员价格下发
商店审核需要独立的隐私、用户协议和注销页;运营后台也要按应用包名启用或停用价格。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
5d05eb0804
commit
f941bf4454
@ -50,6 +50,45 @@ server {
|
|||||||
try_files $uri =404;
|
try_files $uri =404;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location = /legal.json {
|
||||||
|
default_type application/json;
|
||||||
|
charset utf-8;
|
||||||
|
try_files /legal.json =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Independent pages. Do not fall through to the marketing homepage.
|
||||||
|
location = /privacy {
|
||||||
|
try_files /privacy/index.html =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /terms {
|
||||||
|
try_files /terms/index.html =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /delete {
|
||||||
|
try_files /delete/index.html =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /about {
|
||||||
|
try_files /about/index.html =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /privacy/ {
|
||||||
|
return 301 /privacy$is_args$args;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /terms/ {
|
||||||
|
return 301 /terms$is_args$args;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /delete/ {
|
||||||
|
return 301 /delete$is_args$args;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /about/ {
|
||||||
|
return 301 /about$is_args$args;
|
||||||
|
}
|
||||||
|
|
||||||
# Marketing site only — never expose API / OpenAPI / architecture docs here.
|
# Marketing site only — never expose API / OpenAPI / architecture docs here.
|
||||||
location ~* ^/(docs|openapi|swagger|architecture)(/|\.|$) {
|
location ~* ^/(docs|openapi|swagger|architecture)(/|\.|$) {
|
||||||
return 404;
|
return 404;
|
||||||
|
|||||||
@ -2394,16 +2394,63 @@ const pages = {
|
|||||||
|
|
||||||
|
|
||||||
async prices() {
|
async prices() {
|
||||||
const [rows, apps] = await Promise.all([
|
// 包名与客户端 X-Wyrs-Package 及工程 bundle 对齐;保存时写入对应 appCode,客户端按 appCode + 包名取价。
|
||||||
api(`/prices?appCode=${APP}`),
|
// FullStoryNovel:下发包名 wf.wyrs.com(Android applicationId,iOS 请求同样发这个);iOS Bundle com.wyrs.fullstorynovel;注册表旧包名 com.wyrs.novelreader。
|
||||||
|
// StepsWith:bundle 与下发包名均为 com.wyrs.runsteps。
|
||||||
|
const PRICE_APPS = [
|
||||||
|
{ appCode: 'spark', name: 'Spark', packages: ['com.jspp.pop', 'spark.wyrs.com', 'pop.wyrs.com'] },
|
||||||
|
{ appCode: 'novel-reader', name: 'FullStoryNovel', packages: ['wf.wyrs.com', 'com.wyrs.fullstorynovel', 'com.wyrs.novelreader'] },
|
||||||
|
{ appCode: 'runsteps', name: 'StepsWith', packages: ['com.wyrs.runsteps'] },
|
||||||
|
];
|
||||||
|
let apps = [];
|
||||||
|
const priceGroups = [];
|
||||||
|
try {
|
||||||
|
const fetched = await Promise.all([
|
||||||
api('/apps').catch(() => []),
|
api('/apps').catch(() => []),
|
||||||
|
...PRICE_APPS.map((d) => api('/prices?appCode=' + encodeURIComponent(d.appCode)).catch(() => [])),
|
||||||
]);
|
]);
|
||||||
const all = Array.isArray(rows) ? rows : [];
|
apps = Array.isArray(fetched[0]) ? fetched[0] : [];
|
||||||
const popApp = (Array.isArray(apps) ? apps : []).find((a) => a.code === APP) || {};
|
for (let i = 0; i < PRICE_APPS.length; i++) {
|
||||||
const pkgOptions = Array.isArray(popApp.packageNames) ? popApp.packageNames.slice() : [];
|
priceGroups.push(Array.isArray(fetched[i + 1]) ? fetched[i + 1] : []);
|
||||||
// 常见火花包名兜底(注册表缺省时仍可手填)
|
}
|
||||||
['com.jspp.pop', 'spark.wyrs.com', 'pop.wyrs.com'].forEach((p) => {
|
} catch (e) {
|
||||||
if (!pkgOptions.includes(p)) pkgOptions.push(p);
|
$('content').innerHTML = `<div class="note error">会员价格 API:${esc(e.message)}</div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const pkgMeta = new Map();
|
||||||
|
PRICE_APPS.forEach((def) => {
|
||||||
|
const fromApi = apps.find((a) => a && a.code === def.appCode);
|
||||||
|
const names = def.packages.slice();
|
||||||
|
const extra = fromApi && Array.isArray(fromApi.packageNames) ? fromApi.packageNames : [];
|
||||||
|
extra.forEach((p) => {
|
||||||
|
const v = String(p || '').trim();
|
||||||
|
if (v && !names.includes(v)) names.push(v);
|
||||||
|
});
|
||||||
|
names.forEach((p) => {
|
||||||
|
if (!pkgMeta.has(p)) pkgMeta.set(p, { appCode: def.appCode, name: def.name });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const pkgOptions = [...pkgMeta.keys()];
|
||||||
|
const pkgOptionLabel = (p) => {
|
||||||
|
const m = pkgMeta.get(p);
|
||||||
|
return m ? (p + ' · ' + m.name) : p;
|
||||||
|
};
|
||||||
|
const appCodeForPackage = (pkg, row) => {
|
||||||
|
const m = pkgMeta.get(String(pkg || '').trim());
|
||||||
|
if (m) return m.appCode;
|
||||||
|
if (row && row.appCode) return row.appCode;
|
||||||
|
return APP;
|
||||||
|
};
|
||||||
|
const all = [];
|
||||||
|
const seenIds = new Set();
|
||||||
|
priceGroups.forEach((group, i) => {
|
||||||
|
const code = PRICE_APPS[i].appCode;
|
||||||
|
group.forEach((p) => {
|
||||||
|
if (!p || p.id == null || seenIds.has(String(p.id))) return;
|
||||||
|
seenIds.add(String(p.id));
|
||||||
|
if (!p.appCode) p.appCode = code;
|
||||||
|
all.push(p);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
const savedFilter = (typeof window.__pricePkgFilter === 'string') ? window.__pricePkgFilter : '';
|
const savedFilter = (typeof window.__pricePkgFilter === 'string') ? window.__pricePkgFilter : '';
|
||||||
const list = (() => {
|
const list = (() => {
|
||||||
@ -2412,7 +2459,7 @@ const pages = {
|
|||||||
return all.filter((p) => (p.packageName || '') === savedFilter);
|
return all.filter((p) => (p.packageName || '') === savedFilter);
|
||||||
})();
|
})();
|
||||||
const CHANNELS = ['apple','google','paypal','pingpong','wechat','alipay','huawei','xiaomi','vivo','oppo','honor'];
|
const CHANNELS = ['apple','google','paypal','pingpong','wechat','alipay','huawei','xiaomi','vivo','oppo','honor'];
|
||||||
const skuInputs = CHANNELS.map((k) => `<input id="pfSku_${k}" placeholder="${k} 商品码" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:#0b1220;color:var(--text);width:140px">`).join('');
|
const skuInputs = CHANNELS.map((k) => `<input id="pfSku_${k}" placeholder="${k} 商品码">`).join('');
|
||||||
const billingLabel = (bt) => bt === 'subscription' ? '订阅' : (bt === 'one_time' ? '一次性' : (bt || '—'));
|
const billingLabel = (bt) => bt === 'subscription' ? '订阅' : (bt === 'one_time' ? '一次性' : (bt || '—'));
|
||||||
const skuSummary = (m) => {
|
const skuSummary = (m) => {
|
||||||
if (!m || typeof m !== 'object') return '—';
|
if (!m || typeof m !== 'object') return '—';
|
||||||
@ -2421,64 +2468,242 @@ const pages = {
|
|||||||
};
|
};
|
||||||
const pkgLabel = (p) => (p && String(p).trim()) ? p : '(默认·全包兜底)';
|
const pkgLabel = (p) => (p && String(p).trim()) ? p : '(默认·全包兜底)';
|
||||||
$('content').innerHTML = `
|
$('content').innerHTML = `
|
||||||
<div class="note">会员价格(火花)· 可按 <strong>applicationId / 包名</strong> 配置不同价格;包名为空表示该 App 默认兜底。客户端 <code>GET /api/client/spark/prices?packageName=...</code> 或 Header <code>X-Wyrs-Package</code> · 有精确包名价则用之,否则回退默认。</div>
|
<style>
|
||||||
<div class="toolbar" style="flex-wrap:wrap;gap:8px;align-items:center">
|
.price-page { width: 100%; max-width: 100%; min-width: 0; }
|
||||||
|
.price-page .note { overflow-wrap: anywhere; word-break: break-word; }
|
||||||
|
.price-page .price-grid,
|
||||||
|
.price-page .price-skus {
|
||||||
|
display: grid;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.price-page .price-grid { grid-template-columns: repeat(auto-fit, minmax(min(100%, 168px), 1fr)); align-items: center; }
|
||||||
|
.price-page .price-skus { grid-template-columns: repeat(auto-fill, minmax(min(100%, 148px), 1fr)); }
|
||||||
|
.price-page .price-grid input,
|
||||||
|
.price-page .price-grid select,
|
||||||
|
.price-page .price-skus input,
|
||||||
|
.price-page .price-filter select {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
min-width: 0 !important;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: #0b1220;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.price-page .price-filter { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-bottom: 10px; max-width: 100%; }
|
||||||
|
.price-page .price-filter label { display: flex; flex-direction: column; gap: 4px; min-width: min(100%, 220px); flex: 1 1 220px; font-size: 12px; color: var(--muted); }
|
||||||
|
.price-page .price-actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; grid-column: 1 / -1; }
|
||||||
|
.price-page .price-actions label { white-space: nowrap; flex: 0 0 auto; }
|
||||||
|
.price-page button,
|
||||||
|
.price-page button.secondary,
|
||||||
|
.price-page .btn.secondary { width: auto; padding: 8px 14px; }
|
||||||
|
.price-page .price-scroll { width: 100%; max-width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch; }
|
||||||
|
.price-page table { width: 100%; min-width: 860px; }
|
||||||
|
.price-page th, .price-page td { white-space: normal !important; overflow-wrap: break-word; }
|
||||||
|
.price-page td.price-sku { max-width: 240px; }
|
||||||
|
.price-page td.price-switch { width: 64px; white-space: nowrap !important; }
|
||||||
|
.price-page button.pill-switch {
|
||||||
|
position: relative;
|
||||||
|
width: 44px;
|
||||||
|
height: 26px;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #64748b;
|
||||||
|
cursor: pointer;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.price-page button.pill-switch::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 3px;
|
||||||
|
left: 3px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #fff;
|
||||||
|
transition: transform 0.15s ease;
|
||||||
|
}
|
||||||
|
.price-page button.pill-switch[aria-checked="true"] { background: var(--ok, #34d399); }
|
||||||
|
.price-page button.pill-switch[aria-checked="true"]::after { transform: translateX(18px); }
|
||||||
|
.price-page button.pill-switch:disabled { opacity: 0.55; cursor: progress; }
|
||||||
|
.price-page button.pill-switch:focus-visible { outline: 2px solid var(--ok, #34d399); outline-offset: 2px; }
|
||||||
|
</style>
|
||||||
|
<div class="price-page">
|
||||||
|
<div class="note">会员价格 · 按 <strong>applicationId / 包名</strong> 下发。Spark <code>GET /api/client/spark/prices</code>;FullStoryNovel <code>GET /api/client/novel-reader/prices</code>(下发包名 <code>wf.wyrs.com</code>,iOS Bundle <code>com.wyrs.fullstorynovel</code>);StepsWith <code>GET /api/client/runsteps/prices</code>(<code>com.wyrs.runsteps</code>)。Header <code>X-Wyrs-Package</code> · 有精确包名价则用之,否则回退该 App 的空包名默认。行首胶囊即 <code>enabled</code>:开=启用下发,关=停用不下发。</div>
|
||||||
|
<div class="price-filter">
|
||||||
<label>筛选包名
|
<label>筛选包名
|
||||||
<select id="pfPkgFilter" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:#0b1220;color:var(--text)">
|
<select id="pfPkgFilter">
|
||||||
<option value="">全部</option>
|
<option value="">全部</option>
|
||||||
<option value="__default__"${savedFilter==='__default__'?' selected':''}>仅默认兜底</option>
|
<option value="__default__"${savedFilter==='__default__'?' selected':''}>仅默认兜底</option>
|
||||||
${pkgOptions.map((p) => `<option value="${esc(p)}"${savedFilter===p?' selected':''}>${esc(p)}</option>`).join('')}
|
${pkgOptions.map((p) => `<option value="${esc(p)}"${savedFilter===p?' selected':''}>${esc(pkgOptionLabel(p))}</option>`).join('')}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<span class="note" style="margin:0">共 ${esc(list.length)} / ${esc(all.length)} 条</span>
|
<span class="note" style="margin:0">共 ${esc(list.length)} / ${esc(all.length)} 条</span>
|
||||||
<button type="button" class="secondary" id="pfReload">刷新</button>
|
<button type="button" class="secondary" id="pfReload">刷新</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar" style="flex-wrap:wrap;gap:8px">
|
<div class="price-grid">
|
||||||
<select id="pfPkg" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:#0b1220;color:var(--text);min-width:200px">
|
<select id="pfPkg">
|
||||||
<option value="">默认兜底(空包名)</option>
|
<option value="">默认兜底(空包名)</option>
|
||||||
${pkgOptions.map((p) => `<option value="${esc(p)}">${esc(p)}</option>`).join('')}
|
${pkgOptions.map((p) => `<option value="${esc(p)}">${esc(pkgOptionLabel(p))}</option>`).join('')}
|
||||||
</select>
|
</select>
|
||||||
<input id="pfPkgCustom" placeholder="或手写包名 applicationId" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:#0b1220;color:var(--text);width:200px">
|
<input id="pfPkgCustom" placeholder="或手写包名 applicationId">
|
||||||
<input id="pfName" placeholder="套餐名称" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:#0b1220;color:var(--text)">
|
<input id="pfName" placeholder="套餐名称">
|
||||||
<select id="pfBilling" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:#0b1220;color:var(--text)">
|
<select id="pfBilling">
|
||||||
<option value="subscription">订阅 subscription</option>
|
<option value="subscription">订阅 subscription</option>
|
||||||
<option value="one_time">一次性 one_time</option>
|
<option value="one_time">一次性 one_time</option>
|
||||||
</select>
|
</select>
|
||||||
<input id="pfPrice" placeholder="价格" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:#0b1220;color:var(--text);width:100px">
|
<input id="pfPrice" placeholder="价格">
|
||||||
<input id="pfOrig" placeholder="原价" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:#0b1220;color:var(--text);width:100px">
|
<input id="pfOrig" placeholder="原价">
|
||||||
<input id="pfDays" type="number" placeholder="天数" style="padding:8px;border-radius:8px;border:1px solid var(--border);background:#0b1220;color:var(--text);width:90px">
|
<input id="pfDays" type="number" placeholder="天数">
|
||||||
|
<div class="price-actions">
|
||||||
|
<label style="display:flex;align-items:center;gap:6px"><input type="checkbox" id="pfEnabled" checked>启用</label>
|
||||||
<button type="button" id="pfSave">新增价格</button>
|
<button type="button" id="pfSave">新增价格</button>
|
||||||
|
<button type="button" class="secondary" id="pfCancel" style="display:none">取消修改</button>
|
||||||
|
<span class="note" id="pfEditHint" style="margin:0;display:none"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar" style="flex-wrap:wrap;gap:8px;margin-top:8px">${skuInputs}</div>
|
</div>
|
||||||
|
<div class="price-skus">${skuInputs}</div>
|
||||||
|
<div class="price-scroll">
|
||||||
<table>
|
<table>
|
||||||
<thead><tr><th>ID</th><th>包名</th><th>名称</th><th>类型</th><th>价格</th><th>原价</th><th>天数</th><th>渠道码</th><th>启用</th><th></th></tr></thead>
|
<thead><tr><th>下发</th><th>ID</th><th>包名</th><th>名称</th><th>类型</th><th>价格</th><th>原价</th><th>天数</th><th>渠道码</th><th></th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
${list.length ? list.map((p) => `<tr><td>${esc(p.id)}</td><td>${esc(pkgLabel(p.packageName))}</td><td>${esc(p.name)}</td><td>${esc(billingLabel(p.billingType))}</td><td>${esc(p.price)}</td><td>${esc(p.originalPrice)}</td><td>${esc(p.durationDays)}</td><td style="max-width:220px;word-break:break-all">${esc(skuSummary(p.channelProductCodes))}</td><td>${esc(p.enabled)}</td><td><button type="button" class="secondary btn-sm" data-pdel="${esc(p.id)}">删</button></td></tr>`).join('') : emptyRows(10, '暂无价格')}
|
${list.length ? list.map((p) => {
|
||||||
|
const on = p.enabled !== false && p.enabled !== 0;
|
||||||
|
return `<tr><td class="price-switch"><button type="button" class="pill-switch" role="switch" aria-checked="${on ? 'true' : 'false'}" aria-label="${on ? '停用下发' : '启用下发'} ${esc(p.name || '')}" data-penable="${esc(p.id)}"></button></td><td>${esc(p.id)}</td><td>${esc(pkgLabel(p.packageName))}</td><td>${esc(p.name)}</td><td>${esc(billingLabel(p.billingType))}</td><td>${esc(p.price)}</td><td>${esc(p.originalPrice)}</td><td>${esc(p.durationDays)}</td><td class="price-sku">${esc(skuSummary(p.channelProductCodes))}</td><td class="price-ops"><button type="button" class="secondary btn-sm" data-pedit="${esc(p.id)}">修改</button> <button type="button" class="secondary btn-sm" data-pdel="${esc(p.id)}">删</button></td></tr>`;
|
||||||
|
}).join('') : emptyRows(10, '暂无价格')}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>`;
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
let editing = null;
|
||||||
|
const setEditMode = (on, id) => {
|
||||||
|
$('pfSave').textContent = on ? '保存修改' : '新增价格';
|
||||||
|
$('pfCancel').style.display = on ? '' : 'none';
|
||||||
|
$('pfEditHint').style.display = on ? '' : 'none';
|
||||||
|
$('pfEditHint').textContent = on ? ('正在修改 #' + id) : '';
|
||||||
|
};
|
||||||
|
const clearPriceForm = () => {
|
||||||
|
editing = null;
|
||||||
|
$('pfPkg').value = '';
|
||||||
|
$('pfPkgCustom').value = '';
|
||||||
|
$('pfName').value = '';
|
||||||
|
$('pfBilling').value = 'subscription';
|
||||||
|
$('pfPrice').value = '';
|
||||||
|
$('pfOrig').value = '';
|
||||||
|
$('pfDays').value = '';
|
||||||
|
$('pfEnabled').checked = true;
|
||||||
|
CHANNELS.forEach((k) => { const el = $('pfSku_' + k); if (el) el.value = ''; });
|
||||||
|
setEditMode(false);
|
||||||
|
};
|
||||||
$('pfPkgFilter').onchange = () => { window.__pricePkgFilter = $('pfPkgFilter').value; pages.prices(); };
|
$('pfPkgFilter').onchange = () => { window.__pricePkgFilter = $('pfPkgFilter').value; pages.prices(); };
|
||||||
$('pfReload').onclick = () => pages.prices();
|
$('pfReload').onclick = () => pages.prices();
|
||||||
|
$('pfCancel').onclick = clearPriceForm;
|
||||||
$('pfSave').onclick = async () => {
|
$('pfSave').onclick = async () => {
|
||||||
const channelProductCodes = {};
|
const channelProductCodes = {};
|
||||||
CHANNELS.forEach((k) => { const v = $('pfSku_' + k)?.value?.trim(); if (v) channelProductCodes[k] = v; });
|
CHANNELS.forEach((k) => { const v = $('pfSku_' + k)?.value?.trim(); if (v) channelProductCodes[k] = v; });
|
||||||
const custom = ($('pfPkgCustom')?.value || '').trim();
|
const custom = ($('pfPkgCustom')?.value || '').trim();
|
||||||
const packageName = custom || ($('pfPkg')?.value || '').trim();
|
const packageName = custom || ($('pfPkg')?.value || '').trim();
|
||||||
await api('/prices', {
|
const name = ($('pfName').value || '').trim();
|
||||||
method: 'POST',
|
if (!name) return alert('请填写套餐名称');
|
||||||
body: JSON.stringify({
|
const priceNum = Number($('pfPrice').value);
|
||||||
appCode: APP,
|
if (!Number.isFinite(priceNum)) return alert('请填写有效价格');
|
||||||
|
const origRaw = ($('pfOrig').value || '').trim();
|
||||||
|
const origNum = origRaw === '' ? null : Number(origRaw);
|
||||||
|
if (origRaw !== '' && !Number.isFinite(origNum)) return alert('原价格式无效');
|
||||||
|
const body = {
|
||||||
|
appCode: appCodeForPackage(packageName, editing),
|
||||||
packageName,
|
packageName,
|
||||||
name: $('pfName').value,
|
name,
|
||||||
billingType: $('pfBilling').value,
|
billingType: $('pfBilling').value,
|
||||||
price: $('pfPrice').value,
|
price: priceNum,
|
||||||
originalPrice: $('pfOrig').value,
|
originalPrice: origNum,
|
||||||
durationDays: parseInt($('pfDays').value, 10) || 0,
|
durationDays: parseInt($('pfDays').value, 10) || 0,
|
||||||
channelProductCodes,
|
channelProductCodes,
|
||||||
enabled: true,
|
enabled: !!$('pfEnabled').checked,
|
||||||
}),
|
};
|
||||||
});
|
if (editing && editing.id != null) {
|
||||||
|
body.id = editing.id;
|
||||||
|
if (editing.productId) body.productId = editing.productId;
|
||||||
|
if (editing.createdAt) body.createdAt = editing.createdAt;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await api('/prices', { method: 'POST', body: JSON.stringify(body) });
|
||||||
|
} catch (e) {
|
||||||
|
alert((editing ? '修改失败:' : '新增失败:') + e.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
pages.prices();
|
pages.prices();
|
||||||
};
|
};
|
||||||
|
$('content').querySelectorAll('[data-pedit]').forEach((btn) => {
|
||||||
|
btn.onclick = () => {
|
||||||
|
const row = all.find((p) => String(p.id) === String(btn.dataset.pedit));
|
||||||
|
if (!row) return alert('价格不存在');
|
||||||
|
editing = row;
|
||||||
|
const pkg = (row.packageName || '').trim();
|
||||||
|
const known = [...$('pfPkg').options].some((o) => o.value === pkg);
|
||||||
|
if (pkg && known) {
|
||||||
|
$('pfPkg').value = pkg;
|
||||||
|
$('pfPkgCustom').value = '';
|
||||||
|
} else {
|
||||||
|
$('pfPkg').value = '';
|
||||||
|
$('pfPkgCustom').value = pkg;
|
||||||
|
}
|
||||||
|
$('pfName').value = row.name || '';
|
||||||
|
$('pfBilling').value = row.billingType === 'one_time' ? 'one_time' : 'subscription';
|
||||||
|
$('pfPrice').value = row.price ?? '';
|
||||||
|
$('pfOrig').value = row.originalPrice ?? '';
|
||||||
|
$('pfDays').value = row.durationDays ?? '';
|
||||||
|
$('pfEnabled').checked = row.enabled !== false;
|
||||||
|
CHANNELS.forEach((k) => {
|
||||||
|
const el = $('pfSku_' + k);
|
||||||
|
if (el) el.value = (row.channelProductCodes && row.channelProductCodes[k]) || '';
|
||||||
|
});
|
||||||
|
setEditMode(true, row.id);
|
||||||
|
$('pfName').focus();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
$('content').querySelectorAll('[data-penable]').forEach((btn) => {
|
||||||
|
btn.onclick = async () => {
|
||||||
|
if (btn.disabled) return;
|
||||||
|
const row = all.find((p) => String(p.id) === String(btn.dataset.penable));
|
||||||
|
if (!row) return alert('价格不存在');
|
||||||
|
const prev = btn.getAttribute('aria-checked') === 'true';
|
||||||
|
const next = !prev;
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.setAttribute('aria-checked', next ? 'true' : 'false');
|
||||||
|
btn.setAttribute('aria-label', (next ? '停用下发 ' : '启用下发 ') + (row.name || ''));
|
||||||
|
const body = {
|
||||||
|
id: row.id,
|
||||||
|
appCode: row.appCode || appCodeForPackage(row.packageName || '', row),
|
||||||
|
packageName: row.packageName || '',
|
||||||
|
name: row.name,
|
||||||
|
billingType: row.billingType || 'subscription',
|
||||||
|
price: row.price,
|
||||||
|
originalPrice: row.originalPrice,
|
||||||
|
durationDays: row.durationDays,
|
||||||
|
channelProductCodes: row.channelProductCodes || {},
|
||||||
|
enabled: next,
|
||||||
|
};
|
||||||
|
if (row.productId) body.productId = row.productId;
|
||||||
|
if (row.createdAt) body.createdAt = row.createdAt;
|
||||||
|
try {
|
||||||
|
await api('/prices', { method: 'POST', body: JSON.stringify(body) });
|
||||||
|
} catch (e) {
|
||||||
|
btn.setAttribute('aria-checked', prev ? 'true' : 'false');
|
||||||
|
btn.setAttribute('aria-label', (prev ? '停用下发 ' : '启用下发 ') + (row.name || ''));
|
||||||
|
btn.disabled = false;
|
||||||
|
alert('修改失败:' + e.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pages.prices();
|
||||||
|
};
|
||||||
|
});
|
||||||
$('content').querySelectorAll('[data-pdel]').forEach((btn) => {
|
$('content').querySelectorAll('[data-pdel]').forEach((btn) => {
|
||||||
btn.onclick = async () => {
|
btn.onclick = async () => {
|
||||||
if (!confirm('确认删除?')) return;
|
if (!confirm('确认删除?')) return;
|
||||||
|
|||||||
182
docs/daily/2026-10-11-change-list.md
Normal file
182
docs/daily/2026-10-11-change-list.md
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
# 每日变动 2026-10-11
|
||||||
|
|
||||||
|
## 16:06 — 官网导航增加协议、注销与关于入口
|
||||||
|
|
||||||
|
### 概要
|
||||||
|
|
||||||
|
- 官网顶栏在「能力」「下载」旁增加「隐私协议」「用户协议」「注销」「关于」,桌面并排,窄屏折行。
|
||||||
|
- 隐私协议、用户协议链到已有页面;新建注销与关于页,子页面可回首页,当前页高亮。
|
||||||
|
|
||||||
|
### 变更范围
|
||||||
|
|
||||||
|
| 类型 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 功能 | 官网主导航与两个子页面 |
|
||||||
|
| 配置 | 官网 nginx 为隐私、用户协议、注销、关于提供独立路径,避免落到首页 |
|
||||||
|
|
||||||
|
### 主要文件
|
||||||
|
|
||||||
|
- `website/index.html` — 顶栏四个站内入口
|
||||||
|
- `website/assets/styles.css` — 导航折行、当前页高亮、子页面版式
|
||||||
|
- `website/about/index.html` — 关于页,复用官网介绍
|
||||||
|
- `website/delete/index.html` — 注销说明(App 内邮箱验证码;找不到入口时走意见反馈)
|
||||||
|
- `website/legal/legal.html` — 协议页补同一导航、当前项与返回首页
|
||||||
|
- `deploy/nginx/host/sparkmessenger.conf` — `/privacy`、`/terms` 改指向现有页面,并增加 `/delete`、`/about`
|
||||||
|
|
||||||
|
### 验证
|
||||||
|
|
||||||
|
- `make test`: 未跑(只改官网静态页与 nginx,未改 Go)
|
||||||
|
- 本地静态服务打开首页,点过四个入口:隐私协议、用户协议、注销、关于均可打开;关于页「返回首页」回到首页。桌面 1280 导航同一行且不重叠;320 宽折行后仍可点。
|
||||||
|
|
||||||
|
### 关联
|
||||||
|
|
||||||
|
- 线上站点 sparkmessenger.wyrs365.com / sparkmsg.wyrs365.com
|
||||||
|
|
||||||
|
## 16:07 — 会员价格补 FullStoryNovel / StepsWith 包名,行首胶囊开关下发
|
||||||
|
|
||||||
|
### 概要
|
||||||
|
|
||||||
|
- 会员价格页的包名下拉与筛选补上 FullStoryNovel、StepsWith。保存时按包名写入对应 appCode,客户端按 appCode + `X-Wyrs-Package` 命中精确价,否则回退该 App 的空包名默认。
|
||||||
|
- 每条价格行最左侧改为胶囊开关,对应字段 `enabled`。点击即 `POST /api/admin/prices` 保存;开=启用下发,关=停用不下发。请求中禁止连点,失败回滚并 alert。
|
||||||
|
|
||||||
|
### 变更范围
|
||||||
|
|
||||||
|
| 类型 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 功能 | popadmin 会员价格分包名与单条启用下发 |
|
||||||
|
|
||||||
|
### 主要文件
|
||||||
|
|
||||||
|
- `deploy/popadmin/popadmin.js` — 包名选项与 appCode 映射;行首 `enabled` 胶囊开关
|
||||||
|
|
||||||
|
### 验证
|
||||||
|
|
||||||
|
- `make test`: 未跑(只改 popadmin 静态前端,未改 Go / SQL)
|
||||||
|
- 本地页面演练:下拉含 `wf.wyrs.com`、`com.wyrs.fullstorynovel`、`com.wyrs.runsteps`;保存分别落到 `novel-reader` / `runsteps`。胶囊开为绿、滑块在右,关为灰、滑块在左。点击关闭会 POST `enabled:false` 并在刷新后保持关闭;接口失败时开关回到原状态并提示「修改失败」。
|
||||||
|
|
||||||
|
### 关联
|
||||||
|
|
||||||
|
- 线上页 popadmin.wyrs365.com/popadmin/index.html「会员价格」
|
||||||
|
- FullStoryNovel 客户端下发包名 `wf.wyrs.com`(iOS Bundle `com.wyrs.fullstorynovel`);StepsWith `com.wyrs.runsteps`
|
||||||
|
|
||||||
|
## 19:50 — FullStoryNovel 官网页脚增加隐私政策、用户协议、注销
|
||||||
|
|
||||||
|
### 概要
|
||||||
|
|
||||||
|
- 在新加坡服务器的 fullstorynovel.com 首页页脚增加三个站内入口,并上线对应中文页面。
|
||||||
|
- 本地留了一份不含密钥的副本。Spark 官网和 popadmin 公网已是新版本,这次没有覆盖。
|
||||||
|
|
||||||
|
### 变更范围
|
||||||
|
|
||||||
|
| 类型 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 功能 | FullStoryNovel 官网页脚与三个子页面 |
|
||||||
|
| 配置 | 只增量改该站点 nginx:首页壳、`/privacy`、`/terms`、`/delete`、`site.js` |
|
||||||
|
|
||||||
|
### 主要文件
|
||||||
|
|
||||||
|
- `fullstorynovel-web/public-index.html` — 页脚三个入口;点击时不走站内单页路由
|
||||||
|
- `fullstorynovel-web/privacy/index.html` — 隐私政策
|
||||||
|
- `fullstorynovel-web/terms/index.html` — 用户协议
|
||||||
|
- `fullstorynovel-web/delete/index.html` — 注销说明
|
||||||
|
- `fullstorynovel-web/assets/js/site.js` — 这三个路径改为整页打开
|
||||||
|
- `fullstorynovel-web/nginx-legal.snippet` — 与线上增量配置对应的片段
|
||||||
|
|
||||||
|
### 验证
|
||||||
|
|
||||||
|
- `make test`: 未跑(只新增静态页副本,未改 Go、迁移、OpenAPI 或 i18n)
|
||||||
|
- 公网 curl:首页含三个入口;`/privacy`、`/terms`、`/delete` 均为 200
|
||||||
|
- 浏览器点过三个入口并回到首页。桌面不重叠;390 宽页脚折行且不溢出
|
||||||
|
|
||||||
|
### 关联
|
||||||
|
|
||||||
|
- 服务器站点目录 `/opt/fullstorynovel/static-override`
|
||||||
|
- nginx `/etc/nginx/conf.d/fullstorynovel.conf`(已 `nginx -t` 并 reload,未重启整机)
|
||||||
|
|
||||||
|
## 19:55 — FullStoryNovel 协议对齐 wyrs365,去掉管理入口
|
||||||
|
|
||||||
|
### 概要
|
||||||
|
|
||||||
|
- 隐私政策和用户协议改为与 wyrs365.com 上 FullStoryNovel(novel-reader)政策同一套条款结构和表述,产品名保持 Full Story Novel / FullStoryNovel。
|
||||||
|
- 首页页脚不再放管理入口;隐私政策、用户协议、注销三个入口保留。注销页仍写 iOS 应用里的真实注销流程。
|
||||||
|
|
||||||
|
### 变更范围
|
||||||
|
|
||||||
|
| 类型 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 文案 | `/privacy`、`/terms` 按 wyrs365 的 novel-reader 政策重写 |
|
||||||
|
| 页面 | 首页去掉管理入口;三个法律入口仍在页脚 |
|
||||||
|
|
||||||
|
### 主要文件
|
||||||
|
|
||||||
|
- `fullstorynovel-web/privacy/index.html` — 软件隐私权政策
|
||||||
|
- `fullstorynovel-web/terms/index.html` — 软件用户服务协议
|
||||||
|
- `fullstorynovel-web/public-index.html` — 页脚无管理入口
|
||||||
|
- 服务器 `/opt/fullstorynovel/static-override` 同步上述三个文件
|
||||||
|
|
||||||
|
### 验证
|
||||||
|
|
||||||
|
- `make test`: 未跑(只改静态页,未改 Go、迁移、OpenAPI 或 i18n)
|
||||||
|
- 公网:首页 200,无管理入口,含三个页脚链接;`/privacy`、`/terms`、`/delete` 均为 200。隐私政策含公司名、友盟披露和公开邮箱;用户协议含小说阅读服务条款。
|
||||||
|
|
||||||
|
### 关联
|
||||||
|
|
||||||
|
- 参考 https://www.wyrs365.com/ 、`/novel-reader-privacy`、`/novel-reader-agreement`
|
||||||
|
- 未改 nginx、未重启整机、未提交 git
|
||||||
|
|
||||||
|
## 20:40 — Spark 协议正文对齐 wyrs365.com
|
||||||
|
|
||||||
|
### 概要
|
||||||
|
|
||||||
|
- 官网 `/privacy`、`/terms` 仍进原有协议页。正文从四段短说明改为与 wyrs365.com 各 App 协议同一套条款:引言到联系方式共九部分,用户协议为导言到附则共八部分。
|
||||||
|
- 产品名保持 Spark Messenger(火花)。公司名与公开邮箱沿用 wyrs365.com 已公开信息。简体、繁体为全文,其余语言显示英文全文。顶栏未改。
|
||||||
|
|
||||||
|
### 变更范围
|
||||||
|
|
||||||
|
| 类型 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 文案 | 协议正文对齐 wyrs365.com 的条款范围 |
|
||||||
|
|
||||||
|
### 主要文件
|
||||||
|
|
||||||
|
- `website/legal/legal.html` — 隐私政策与用户协议正文
|
||||||
|
|
||||||
|
### 验证
|
||||||
|
|
||||||
|
- `make test`: 未跑(只改官网静态协议页,未改 Go、迁移、OpenAPI 或 i18n)
|
||||||
|
- 本地打开隐私政策与英文用户协议,条款标题和联系邮箱可见。
|
||||||
|
- 公网 `https://sparkmsg.wyrs365.com/legal/legal.html?doc=privacy` 含「Spark Messenger(火花)软件隐私权政策」和「九、如何联系我们」。
|
||||||
|
|
||||||
|
### 关联
|
||||||
|
|
||||||
|
- 参考 https://www.wyrs365.com/runsteps-privacy 、https://www.wyrs365.com/runsteps-agreement
|
||||||
|
- 未改 nginx、未动 FullStoryNovel、未提交 git
|
||||||
|
|
||||||
|
## 21:35 — Sleep Heart 官网 health.wyrs365.com
|
||||||
|
|
||||||
|
### 概要
|
||||||
|
|
||||||
|
- 阿里云 DNS 为 `health.wyrs365.com` 新增 A 记录到新加坡 `47.236.66.9`。
|
||||||
|
- 新加坡机新增静态站与只含该域名的证书,首页及下载、注销、隐私协议、用户协议可访问。
|
||||||
|
|
||||||
|
### 变更范围
|
||||||
|
|
||||||
|
| 类型 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 配置 | 仅该主机名的 DNS、Nginx 与证书 |
|
||||||
|
| 功能 | Sleep Heart 官网五页 |
|
||||||
|
|
||||||
|
### 主要文件
|
||||||
|
|
||||||
|
- `health-web/` — 不含密钥的官网源码,已同步到 `/var/www/health.wyrs365.com`
|
||||||
|
- `health-web/nginx/health.wyrs365.com.conf` — 对应服务器 `/etc/nginx/conf.d/health-wyrs365.conf`
|
||||||
|
|
||||||
|
### 验证
|
||||||
|
|
||||||
|
- `make test`: 未跑(只增加静态官网,未改 Go、迁移、OpenAPI 或 i18n)
|
||||||
|
- 直连公网 IP:首页、`/download`、`/delete`、`/privacy`、`/terms` 均为 200
|
||||||
|
- 浏览器点过四个入口并回到首页;约 390px 宽无横向溢出
|
||||||
|
|
||||||
|
### 关联
|
||||||
|
|
||||||
|
- 未提交 git,未重启整机,未改安全组,未改其他站点文件
|
||||||
1084
fullstorynovel-web/assets/js/site.js
Normal file
1084
fullstorynovel-web/assets/js/site.js
Normal file
File diff suppressed because it is too large
Load Diff
88
fullstorynovel-web/delete/index.html
Normal file
88
fullstorynovel-web/delete/index.html
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN" data-theme="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||||
|
<title>注销 — Full Story Novel</title>
|
||||||
|
<meta name="description" content="如何在 Fullstorynovel 中注销账号。" />
|
||||||
|
<link rel="stylesheet" href="/assets/css/site.css?v=20260923-dl" />
|
||||||
|
<link rel="icon" href="/assets/favicon-32.png?v=20260920" type="image/png" sizes="32x32" />
|
||||||
|
<style>
|
||||||
|
.legal-page .topbar-inner { flex-wrap: wrap; row-gap: 0.5rem; }
|
||||||
|
.legal-nav { display: flex; flex-wrap: wrap; gap: 0.65rem 1rem; margin-left: auto; }
|
||||||
|
.legal-nav a { color: var(--text); text-decoration: none; white-space: nowrap; padding: 0.4rem 0; }
|
||||||
|
.legal-nav a[aria-current="page"] { color: var(--accent); font-weight: 700; }
|
||||||
|
.legal-doc { max-width: 44rem; background: var(--bg-card); border: 1px solid var(--line); border-radius: var(--radius); padding: 1.25rem 1.25rem 1.5rem; }
|
||||||
|
.legal-doc h1 { font-size: 1.7rem; line-height: 1.3; margin: 0 0 0.35rem; }
|
||||||
|
.legal-doc .updated { color: var(--muted); margin: 0 0 1rem; }
|
||||||
|
.legal-doc h2 { font-size: 1.05rem; margin: 1.35rem 0 0.4rem; }
|
||||||
|
.legal-doc p, .legal-doc li { color: var(--text); }
|
||||||
|
.legal-doc ol, .legal-doc ul { margin: 0.3rem 0 0; padding-left: 1.2rem; }
|
||||||
|
.legal-doc li { margin: 0.35rem 0; }
|
||||||
|
.legal-doc a { color: var(--accent); }
|
||||||
|
.footer-links a { white-space: nowrap; padding: 0.35rem 0; }
|
||||||
|
.footer-links a[aria-current="page"] { color: var(--text); font-weight: 700; text-decoration: underline; text-underline-offset: 0.2em; }
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.legal-nav { width: 100%; margin-left: 0; }
|
||||||
|
.legal-doc { padding: 1rem; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
try {
|
||||||
|
var t = localStorage.getItem("fsn_theme");
|
||||||
|
if (t === "light" || t === "dark") document.documentElement.setAttribute("data-theme", t);
|
||||||
|
else document.documentElement.setAttribute("data-theme", "dark");
|
||||||
|
} catch (e) {}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body class="legal-page">
|
||||||
|
<header class="topbar">
|
||||||
|
<div class="container topbar-inner">
|
||||||
|
<a class="brand" href="/">
|
||||||
|
<img class="brand-mark" src="/assets/logo.png?v=20260920" width="36" height="36" alt="Full Story Novel" />
|
||||||
|
<span class="brand-text">Full Story Novel</span>
|
||||||
|
</a>
|
||||||
|
<nav class="legal-nav" aria-label="页面">
|
||||||
|
<a href="/">首页</a>
|
||||||
|
<a href="/privacy">隐私政策</a>
|
||||||
|
<a href="/terms">用户协议</a>
|
||||||
|
<a href="/delete" aria-current="page">注销</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main class="container">
|
||||||
|
<article class="legal-doc">
|
||||||
|
<h1>注销账号</h1>
|
||||||
|
<p class="updated">在 Fullstorynovel 应用里注销。本网站没有单独的注销表单。</p>
|
||||||
|
<h2>在应用里怎么做</h2>
|
||||||
|
<ol>
|
||||||
|
<li>打开 Fullstorynovel,进入「设置」。</li>
|
||||||
|
<li>在「账号与安全」中点「注销账号」。还没登录时,应用会提示「请先登录后再注销账号」。</li>
|
||||||
|
<li>页面会显示当前绑定的邮箱。先点「获取验证码」,验证码发到这个邮箱,再把验证码填进去。应用内的说明是:请先向绑定邮箱获取验证码,再确认注销;测试账号可以不填写验证码。</li>
|
||||||
|
<li>点「确认注销」。应用会再问一次「确定注销账号?」,并说明此操作不可撤销,注销后将退出登录。</li>
|
||||||
|
</ol>
|
||||||
|
<h2>提交之后会发生什么</h2>
|
||||||
|
<p>普通账号必须先通过发到绑定邮箱的验证码。通过后,服务器会让该账号的登录失效,并清除好友关系、会话以及相关联的账号数据,再删除这个正在使用的账号。应用内的说明是:注销后账号将被永久删除,这些数据一并清除且无法恢复。原来的邮箱可以重新注册,但历史数据不会找回。</p>
|
||||||
|
<p>注销成功后,应用会清掉本机保存的登录状态。保存在本机的书籍和阅读进度不会随这次注销自动删掉。</p>
|
||||||
|
<h2>相关页面</h2>
|
||||||
|
<p><a href="/">返回首页</a> · <a href="/privacy">隐私政策</a> · <a href="/terms">用户协议</a></p>
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="container footer-inner">
|
||||||
|
<div>
|
||||||
|
<strong>Full Story Novel</strong>
|
||||||
|
<p>连载小说阅读。</p>
|
||||||
|
</div>
|
||||||
|
<div class="footer-links">
|
||||||
|
<a href="/">首页</a>
|
||||||
|
<a href="/privacy">隐私政策</a>
|
||||||
|
<a href="/terms">用户协议</a>
|
||||||
|
<a href="/delete" aria-current="page">注销</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
32
fullstorynovel-web/nginx-legal.snippet
Normal file
32
fullstorynovel-web/nginx-legal.snippet
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# Insert inside the fullstorynovel.com :8443 server, before "location /" that proxies to 127.0.0.1:8095.
|
||||||
|
# BEGIN fsn-site-js
|
||||||
|
location = /assets/js/site.js {
|
||||||
|
root /opt/fullstorynovel/static-override;
|
||||||
|
try_files /assets/js/site.js =404;
|
||||||
|
add_header Cache-Control "no-cache";
|
||||||
|
}
|
||||||
|
# END fsn-site-js
|
||||||
|
# BEGIN fsn-home-shell
|
||||||
|
location = / {
|
||||||
|
root /opt/fullstorynovel/static-override;
|
||||||
|
try_files /public-index.html =404;
|
||||||
|
add_header Cache-Control "no-cache";
|
||||||
|
}
|
||||||
|
# END fsn-home-shell
|
||||||
|
# BEGIN fsn-legal-pages
|
||||||
|
location = /privacy {
|
||||||
|
root /opt/fullstorynovel/static-override;
|
||||||
|
try_files /privacy/index.html =404;
|
||||||
|
add_header Cache-Control "no-cache";
|
||||||
|
}
|
||||||
|
location = /terms {
|
||||||
|
root /opt/fullstorynovel/static-override;
|
||||||
|
try_files /terms/index.html =404;
|
||||||
|
add_header Cache-Control "no-cache";
|
||||||
|
}
|
||||||
|
location = /delete {
|
||||||
|
root /opt/fullstorynovel/static-override;
|
||||||
|
try_files /delete/index.html =404;
|
||||||
|
add_header Cache-Control "no-cache";
|
||||||
|
}
|
||||||
|
# END fsn-legal-pages
|
||||||
80
fullstorynovel-web/privacy/index.html
Normal file
80
fullstorynovel-web/privacy/index.html
Normal file
File diff suppressed because one or more lines are too long
86
fullstorynovel-web/public-index.html
Normal file
86
fullstorynovel-web/public-index.html
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" data-theme="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||||
|
<meta name="theme-color" content="#0b0d12" media="(prefers-color-scheme: dark)" />
|
||||||
|
<meta name="theme-color" content="#f4f6fb" media="(prefers-color-scheme: light)" />
|
||||||
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="format-detection" content="telephone=no" />
|
||||||
|
<title>Full Story Novel</title>
|
||||||
|
<meta name="description" content="Read fantasy, xianxia, and serialized web novels on Full Story Novel." />
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link rel="stylesheet" href="/assets/css/site.css?v=20260923-dl" />
|
||||||
|
<link rel="icon" href="/assets/favicon-32.png?v=20260920" type="image/png" sizes="32x32" />
|
||||||
|
<link rel="icon" href="/assets/favicon.png?v=20260920" type="image/png" sizes="180x180" />
|
||||||
|
<link rel="apple-touch-icon" href="/assets/favicon.png?v=20260920" />
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
try {
|
||||||
|
var t = localStorage.getItem("fsn_theme");
|
||||||
|
if (t === "light" || t === "dark") document.documentElement.setAttribute("data-theme", t);
|
||||||
|
else document.documentElement.setAttribute("data-theme", "dark");
|
||||||
|
var l = localStorage.getItem("fsn_lang") || "en";
|
||||||
|
document.documentElement.lang = l;
|
||||||
|
document.documentElement.dir = (l === "ar" || l === "ur") ? "rtl" : "ltr";
|
||||||
|
} catch (e) {}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="topbar">
|
||||||
|
<div class="container topbar-inner">
|
||||||
|
<a class="brand" href="/">
|
||||||
|
<img class="brand-mark" src="/assets/logo.png?v=20260920" width="36" height="36" alt="Full Story Novel" />
|
||||||
|
<span class="brand-text">Full Story Novel</span>
|
||||||
|
</a>
|
||||||
|
<button type="button" class="nav-toggle" id="nav-toggle" aria-expanded="false" aria-controls="site-nav" aria-label="Menu">
|
||||||
|
<span class="nav-toggle-bars" aria-hidden="true"></span>
|
||||||
|
</button>
|
||||||
|
<nav class="nav" id="site-nav">
|
||||||
|
<a href="/" data-i18n="nav.home">Home</a>
|
||||||
|
<a href="/novels" data-i18n="nav.novels">Novels</a>
|
||||||
|
<a href="/genres" data-i18n="nav.genres">Genres</a>
|
||||||
|
</nav>
|
||||||
|
<form class="search" id="search-form">
|
||||||
|
<input id="search-input" type="search" enterkeyhint="search" data-i18n-placeholder="search.placeholder" placeholder="Search novels..." autocomplete="off" />
|
||||||
|
<button type="submit" data-i18n="search.button">Search</button>
|
||||||
|
</form>
|
||||||
|
<a class="app-dl" id="app-dl-link" href="/download" data-i18n="nav.download">Download App</a>
|
||||||
|
<div class="prefs" aria-label="Preferences">
|
||||||
|
<label class="sr-only" for="lang-select" data-i18n="prefs.language">Language</label>
|
||||||
|
<select id="lang-select" title="Language" aria-label="Language"></select>
|
||||||
|
<button type="button" id="theme-toggle" class="prefs-btn" title="Theme" aria-label="Theme">◐</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="container" id="app">
|
||||||
|
<div class="loading" data-i18n="common.loading">Loading…</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="container footer-inner">
|
||||||
|
<div>
|
||||||
|
<strong>Full Story Novel</strong>
|
||||||
|
<p data-i18n="footer.tagline">Serialized fantasy reading inspired by classic web-novel platforms.</p>
|
||||||
|
</div>
|
||||||
|
<div class="footer-links">
|
||||||
|
<a href="/novels" data-i18n="footer.browse">Browse</a>
|
||||||
|
<a href="/genres" data-i18n="nav.genres">Genres</a>
|
||||||
|
<a href="/download" data-i18n="nav.download">Download App</a>
|
||||||
|
<a href="/privacy" onclick="event.stopPropagation()">隐私政策</a>
|
||||||
|
<a href="/terms" onclick="event.stopPropagation()">用户协议</a>
|
||||||
|
<a href="/delete" onclick="event.stopPropagation()">注销</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}
|
||||||
|
.footer-links a{white-space:nowrap;padding:0.35rem 0}
|
||||||
|
</style>
|
||||||
|
<script src="/assets/js/site.js?v=20261011-legal"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
79
fullstorynovel-web/terms/index.html
Normal file
79
fullstorynovel-web/terms/index.html
Normal file
File diff suppressed because one or more lines are too long
BIN
health-web/assets/icon.png
Normal file
BIN
health-web/assets/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 45 KiB |
210
health-web/assets/site.css
Normal file
210
health-web/assets/site.css
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
:root {
|
||||||
|
--bg: #07111f;
|
||||||
|
--bg-2: #10233d;
|
||||||
|
--card: rgba(255, 255, 255, 0.06);
|
||||||
|
--line: rgba(255, 255, 255, 0.14);
|
||||||
|
--text: #f4f7fb;
|
||||||
|
--muted: #b7c4d6;
|
||||||
|
--gold: #f6c453;
|
||||||
|
--heart: #e23d4a;
|
||||||
|
--cyan: #8ec8ff;
|
||||||
|
--shadow: 0 18px 50px rgba(0, 0, 0, 0.28);
|
||||||
|
}
|
||||||
|
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
|
||||||
|
html { scroll-behavior: smooth; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
color: var(--text);
|
||||||
|
background:
|
||||||
|
radial-gradient(900px 420px at 85% -10%, rgba(246, 196, 83, 0.16), transparent 60%),
|
||||||
|
radial-gradient(700px 380px at -10% 20%, rgba(226, 61, 74, 0.16), transparent 55%),
|
||||||
|
linear-gradient(180deg, var(--bg-2), var(--bg) 42%);
|
||||||
|
font-family: "PingFang SC", "Hiragino Sans GB", "Noto Sans SC", "Microsoft YaHei", sans-serif;
|
||||||
|
line-height: 1.65;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
img { max-width: 100%; height: auto; display: block; }
|
||||||
|
|
||||||
|
a { color: var(--cyan); }
|
||||||
|
|
||||||
|
.wrap {
|
||||||
|
width: min(1040px, calc(100% - 32px));
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 2;
|
||||||
|
background: rgba(7, 17, 31, 0.86);
|
||||||
|
backdrop-filter: blur(14px);
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-inner, .foot-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px 18px;
|
||||||
|
padding: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
color: var(--text);
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand img {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 12px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav, .foot-links {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px 14px;
|
||||||
|
margin-left: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav a, .foot-links a {
|
||||||
|
color: var(--text);
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 6px 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav a[aria-current="page"], .foot-links a[aria-current="page"] {
|
||||||
|
color: var(--gold);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
main { padding: 28px 0 48px; }
|
||||||
|
|
||||||
|
.hero, .panel, .card {
|
||||||
|
background: var(--card);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 22px;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 148px minmax(0, 1fr);
|
||||||
|
gap: 22px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero img {
|
||||||
|
width: 148px;
|
||||||
|
height: 148px;
|
||||||
|
border-radius: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3 { line-height: 1.3; margin: 0 0 0.4em; }
|
||||||
|
|
||||||
|
h1 { font-size: clamp(1.7rem, 4vw, 2.4rem); }
|
||||||
|
|
||||||
|
.lead { color: var(--muted); margin: 0 0 1rem; font-size: 1.05rem; }
|
||||||
|
|
||||||
|
.actions { display: flex; flex-wrap: wrap; gap: 10px; }
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 42px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 999px;
|
||||||
|
text-decoration: none;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary { background: var(--heart); color: white; font-weight: 700; }
|
||||||
|
.btn-ghost { color: var(--text); border-color: var(--line); background: rgba(255, 255, 255, 0.04); }
|
||||||
|
|
||||||
|
.note {
|
||||||
|
margin-top: 16px;
|
||||||
|
color: var(--gold);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section { margin-top: 28px; }
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card { padding: 16px 16px 18px; min-width: 0; }
|
||||||
|
.card h2, .card h3 { font-size: 1.05rem; }
|
||||||
|
.card p, .panel p, .panel li { color: var(--muted); }
|
||||||
|
.card p { margin: 0; }
|
||||||
|
|
||||||
|
.panel { padding: 22px 22px 8px; overflow-wrap: anywhere; }
|
||||||
|
.panel h1 { margin-bottom: 0.3em; }
|
||||||
|
.panel .meta { color: var(--muted); margin-top: 0; }
|
||||||
|
.panel h2 { font-size: 1.08rem; margin-top: 1.3rem; }
|
||||||
|
.panel h3 { font-size: 1rem; margin-top: 1rem; }
|
||||||
|
.panel ul, .panel ol { padding-left: 1.2rem; }
|
||||||
|
.steps { counter-reset: step; list-style: none; padding-left: 0; }
|
||||||
|
.steps li {
|
||||||
|
position: relative;
|
||||||
|
padding: 10px 10px 10px 46px;
|
||||||
|
margin: 0 0 10px;
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
border-radius: 14px;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.steps li::before {
|
||||||
|
counter-increment: step;
|
||||||
|
content: counter(step);
|
||||||
|
position: absolute;
|
||||||
|
left: 10px;
|
||||||
|
top: 10px;
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--heart);
|
||||||
|
color: white;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.foot {
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.foot strong { color: var(--text); }
|
||||||
|
.foot p { margin: 4px 0 0; }
|
||||||
|
|
||||||
|
@media (max-width: 800px) {
|
||||||
|
.grid { grid-template-columns: 1fr; }
|
||||||
|
.nav, .foot-links { margin-left: 0; width: 100%; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 420px) {
|
||||||
|
.wrap { width: min(1040px, calc(100% - 24px)); }
|
||||||
|
.hero { grid-template-columns: 1fr; padding: 18px; }
|
||||||
|
.hero img { width: 96px; height: 96px; border-radius: 24px; }
|
||||||
|
.panel { padding: 16px 14px 6px; }
|
||||||
|
.actions .btn { width: 100%; }
|
||||||
|
}
|
||||||
70
health-web/delete/index.html
Normal file
70
health-web/delete/index.html
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||||
|
<title>注销 — Sleep Heart</title>
|
||||||
|
<meta name="description" content="Sleep Heart 应用内注销账号的真实入口和实际效果。" />
|
||||||
|
<link rel="icon" href="/assets/icon.png" type="image/png" />
|
||||||
|
<link rel="stylesheet" href="/assets/site.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="top">
|
||||||
|
<div class="wrap top-inner">
|
||||||
|
<a class="brand" href="/">
|
||||||
|
<img src="/assets/icon.png" width="40" height="40" alt="" />
|
||||||
|
<span>Sleep Heart</span>
|
||||||
|
</a>
|
||||||
|
<nav class="nav" aria-label="页面">
|
||||||
|
<a href="/">首页</a>
|
||||||
|
<a href="/download">下载</a>
|
||||||
|
<a href="/delete" aria-current="page">注销</a>
|
||||||
|
<a href="/privacy">隐私协议</a>
|
||||||
|
<a href="/terms">用户协议</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<div class="wrap">
|
||||||
|
<article class="panel">
|
||||||
|
<h1>注销账号</h1>
|
||||||
|
<p class="lead">注销在 Sleep Heart 应用里完成。本网站不提供在线注销表单。</p>
|
||||||
|
<h2>入口</h2>
|
||||||
|
<p>需要先登录。未登录时,「设置」里不会出现「账号与安全」。</p>
|
||||||
|
<ol class="steps">
|
||||||
|
<li>打开底部「我的」。</li>
|
||||||
|
<li>进入「设置」。</li>
|
||||||
|
<li>进入「账号与安全」。</li>
|
||||||
|
<li>点「注销账号」。</li>
|
||||||
|
</ol>
|
||||||
|
<h2>确认</h2>
|
||||||
|
<p>应用会先询问「确认注销账号?」。说明文字是:注销将清除本机账号数据并退出登录,此操作不可撤销。随后会再询问「再次确认注销」,说明将清除本机昵称和头像并退出登录。两步都会同时提示:退出登录或注销应用账号不会取消 Apple 订阅。</p>
|
||||||
|
<h2>确认之后会发生什么</h2>
|
||||||
|
<ul>
|
||||||
|
<li>清除这台设备上该账号的昵称、头像和会员标记,并退出登录。</li>
|
||||||
|
<li>如果当时还持有登录凭证,应用会尝试通知服务器结束当前登录会话。</li>
|
||||||
|
<li>当前应用代码不会向服务器提交永久删除账号。</li>
|
||||||
|
<li>不会取消 Apple 订阅。若要停止续费,请在 iPhone 的「设置」里打开顶部 Apple 账户,进入「订阅」,选择 SleepHeart 中要停止续费的项目。</li>
|
||||||
|
</ul>
|
||||||
|
<p>也可以在「我的」里提交「意见反馈」,或发邮件到 <a href="mailto:asd272940236@foxmail.com">asd272940236@foxmail.com</a>。</p>
|
||||||
|
<p><a href="/">返回首页</a></p>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<footer class="foot">
|
||||||
|
<div class="wrap foot-inner">
|
||||||
|
<div>
|
||||||
|
<strong>Sleep Heart</strong>
|
||||||
|
<p>上海微雨如酥网络设计有限公司</p>
|
||||||
|
</div>
|
||||||
|
<nav class="foot-links" aria-label="页脚">
|
||||||
|
<a href="/">首页</a>
|
||||||
|
<a href="/download">下载</a>
|
||||||
|
<a href="/delete" aria-current="page">注销</a>
|
||||||
|
<a href="/privacy">隐私协议</a>
|
||||||
|
<a href="/terms">用户协议</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
54
health-web/download/index.html
Normal file
54
health-web/download/index.html
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||||
|
<title>下载 — Sleep Heart</title>
|
||||||
|
<meta name="description" content="Sleep Heart 下载说明。应用即将上线,此处不放置未核实的应用商店地址。" />
|
||||||
|
<link rel="icon" href="/assets/icon.png" type="image/png" />
|
||||||
|
<link rel="stylesheet" href="/assets/site.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="top">
|
||||||
|
<div class="wrap top-inner">
|
||||||
|
<a class="brand" href="/">
|
||||||
|
<img src="/assets/icon.png" width="40" height="40" alt="" />
|
||||||
|
<span>Sleep Heart</span>
|
||||||
|
</a>
|
||||||
|
<nav class="nav" aria-label="页面">
|
||||||
|
<a href="/">首页</a>
|
||||||
|
<a href="/download" aria-current="page">下载</a>
|
||||||
|
<a href="/delete">注销</a>
|
||||||
|
<a href="/privacy">隐私协议</a>
|
||||||
|
<a href="/terms">用户协议</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<div class="wrap">
|
||||||
|
<article class="panel">
|
||||||
|
<h1>下载</h1>
|
||||||
|
<p class="lead">Sleep Heart 是 iPhone 应用,应用内展示名为 SleepHeart。</p>
|
||||||
|
<p class="note">应用即将上线。目前没有已核实、可以公开打开的 App Store 或安卓应用市场地址,本页不放置商店链接。</p>
|
||||||
|
<p>上架之后,请在 App Store 按应用名 SleepHeart 查找。请从本站公布的正式地址或 Apple 的商店页面安装,不要从未经授权的来源安装安装包。</p>
|
||||||
|
<p><a href="/">返回首页</a></p>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<footer class="foot">
|
||||||
|
<div class="wrap foot-inner">
|
||||||
|
<div>
|
||||||
|
<strong>Sleep Heart</strong>
|
||||||
|
<p>上海微雨如酥网络设计有限公司</p>
|
||||||
|
</div>
|
||||||
|
<nav class="foot-links" aria-label="页脚">
|
||||||
|
<a href="/">首页</a>
|
||||||
|
<a href="/download" aria-current="page">下载</a>
|
||||||
|
<a href="/delete">注销</a>
|
||||||
|
<a href="/privacy">隐私协议</a>
|
||||||
|
<a href="/terms">用户协议</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
90
health-web/index.html
Normal file
90
health-web/index.html
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||||
|
<title>Sleep Heart</title>
|
||||||
|
<meta name="description" content="Sleep Heart 官网:睡眠声监测、指尖心率估算、助眠音乐与呼吸训练。测量结果仅供个人参考,不是医疗诊断。" />
|
||||||
|
<link rel="icon" href="/assets/icon.png" type="image/png" />
|
||||||
|
<link rel="stylesheet" href="/assets/site.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="top">
|
||||||
|
<div class="wrap top-inner">
|
||||||
|
<a class="brand" href="/">
|
||||||
|
<img src="/assets/icon.png" width="40" height="40" alt="" />
|
||||||
|
<span>Sleep Heart</span>
|
||||||
|
</a>
|
||||||
|
<nav class="nav" aria-label="页面">
|
||||||
|
<a href="/" aria-current="page">首页</a>
|
||||||
|
<a href="/download">下载</a>
|
||||||
|
<a href="/delete">注销</a>
|
||||||
|
<a href="/privacy">隐私协议</a>
|
||||||
|
<a href="/terms">用户协议</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<div class="wrap">
|
||||||
|
<section class="hero">
|
||||||
|
<img src="/assets/icon.png" width="148" height="148" alt="Sleep Heart 图标" />
|
||||||
|
<div>
|
||||||
|
<h1>Sleep Heart</h1>
|
||||||
|
<p class="lead">在这台 iPhone 上查看夜间声音环境、估算指尖心率,并记下你自己填写的日常健康数字。可以听助眠音乐,按节奏做呼吸练习,登录后也可以在广场发帖。</p>
|
||||||
|
<div class="actions">
|
||||||
|
<a class="btn btn-primary" href="/download">下载</a>
|
||||||
|
<a class="btn btn-ghost" href="/privacy">隐私协议</a>
|
||||||
|
<a class="btn btn-ghost" href="/terms">用户协议</a>
|
||||||
|
</div>
|
||||||
|
<p class="note">测量和监测结果只供个人参考,不是医疗诊断,不能代替血压计、血糖仪、血氧仪、体温计或就医。</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<h2>可以做什么</h2>
|
||||||
|
<div class="grid">
|
||||||
|
<article class="card">
|
||||||
|
<h3>音乐</h3>
|
||||||
|
<p>播放助眠内容。场景包括自然白噪、深度冥想、呼吸疗愈、治愈乐器和脑波音乐。播放方式有顺序、随机、循环和勾选播放。</p>
|
||||||
|
</article>
|
||||||
|
<article class="card">
|
||||||
|
<h3>心率</h3>
|
||||||
|
<p>用后置相机和闪光灯做指尖光学估算。画面在本机处理,不会上传。同一页可以手填体重、步数、血压、血糖、经期、血氧、体温和饮水。</p>
|
||||||
|
</article>
|
||||||
|
<article class="card">
|
||||||
|
<h3>睡眠</h3>
|
||||||
|
<p>打开监测后使用麦克风,在本机识别夜间声音并保存可回放的短片段。录音和识别结果只留在这台 iPhone,不会上传。</p>
|
||||||
|
</article>
|
||||||
|
<article class="card">
|
||||||
|
<h3>发现</h3>
|
||||||
|
<p>进入广场和呼吸训练。呼吸练习有箱式、4-7-8、舒缓吸呼和共振四种节奏,可以静音,也可以用喜欢的音乐做背景。</p>
|
||||||
|
</article>
|
||||||
|
<article class="card">
|
||||||
|
<h3>我的</h3>
|
||||||
|
<p>管理账号、会员、主题、语言和意见反馈。已登录后,注销入口在「设置」里的「账号与安全」。</p>
|
||||||
|
</article>
|
||||||
|
<article class="card">
|
||||||
|
<h3>会员</h3>
|
||||||
|
<p>会员音频可完整播放并收藏,本地音频导入不限数量,指尖心率测量不限次数。不购买也可以使用免费音频、本机健康记录、睡眠声监测和呼吸训练。</p>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<footer class="foot">
|
||||||
|
<div class="wrap foot-inner">
|
||||||
|
<div>
|
||||||
|
<strong>Sleep Heart</strong>
|
||||||
|
<p>上海微雨如酥网络设计有限公司</p>
|
||||||
|
</div>
|
||||||
|
<nav class="foot-links" aria-label="页脚">
|
||||||
|
<a href="/">首页</a>
|
||||||
|
<a href="/download">下载</a>
|
||||||
|
<a href="/delete">注销</a>
|
||||||
|
<a href="/privacy">隐私协议</a>
|
||||||
|
<a href="/terms">用户协议</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
74
health-web/nginx/health.wyrs365.com.conf
Normal file
74
health-web/nginx/health.wyrs365.com.conf
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
# Sleep Heart website — health.wyrs365.com
|
||||||
|
# Public :443 is stream ssl_preread (stream.d/spark-sni.conf, default → 127.0.0.1:8443).
|
||||||
|
# TLS terminates on loopback :8443. Certificate is this name only.
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
server_name health.wyrs365.com;
|
||||||
|
|
||||||
|
location /.well-known/acme-challenge/ {
|
||||||
|
root /var/www/certbot;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 127.0.0.1:8443 ssl proxy_protocol;
|
||||||
|
listen [::1]:8443 ssl proxy_protocol;
|
||||||
|
http2 on;
|
||||||
|
server_name health.wyrs365.com;
|
||||||
|
|
||||||
|
ssl_certificate /etc/letsencrypt/live/health.wyrs365.com/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/health.wyrs365.com/privkey.pem;
|
||||||
|
ssl_session_timeout 1d;
|
||||||
|
ssl_session_cache shared:HealthSSL:10m;
|
||||||
|
|
||||||
|
add_header Strict-Transport-Security "max-age=31536000" always;
|
||||||
|
add_header X-Content-Type-Options nosniff always;
|
||||||
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
|
|
||||||
|
root /var/www/health.wyrs365.com;
|
||||||
|
index index.html;
|
||||||
|
charset utf-8;
|
||||||
|
|
||||||
|
location = /favicon.ico {
|
||||||
|
try_files /assets/icon.png =404;
|
||||||
|
access_log off;
|
||||||
|
expires 7d;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /assets/ {
|
||||||
|
expires 7d;
|
||||||
|
add_header Cache-Control "public";
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /download {
|
||||||
|
try_files /download/index.html =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /delete {
|
||||||
|
try_files /delete/index.html =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /privacy {
|
||||||
|
try_files /privacy/index.html =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /terms {
|
||||||
|
try_files /terms/index.html =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /download/ { return 301 /download$is_args$args; }
|
||||||
|
location = /delete/ { return 301 /delete$is_args$args; }
|
||||||
|
location = /privacy/ { return 301 /privacy$is_args$args; }
|
||||||
|
location = /terms/ { return 301 /terms$is_args$args; }
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
}
|
||||||
141
health-web/privacy/index.html
Normal file
141
health-web/privacy/index.html
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||||
|
<title>隐私协议 — Sleep Heart</title>
|
||||||
|
<meta name="description" content="Sleep Heart 软件隐私权政策。上海微雨如酥网络设计有限公司。" />
|
||||||
|
<link rel="icon" href="/assets/icon.png" type="image/png" />
|
||||||
|
<link rel="stylesheet" href="/assets/site.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="top">
|
||||||
|
<div class="wrap top-inner">
|
||||||
|
<a class="brand" href="/">
|
||||||
|
<img src="/assets/icon.png" width="40" height="40" alt="" />
|
||||||
|
<span>Sleep Heart</span>
|
||||||
|
</a>
|
||||||
|
<nav class="nav" aria-label="页面">
|
||||||
|
<a href="/">首页</a>
|
||||||
|
<a href="/download">下载</a>
|
||||||
|
<a href="/delete">注销</a>
|
||||||
|
<a href="/privacy" aria-current="page">隐私协议</a>
|
||||||
|
<a href="/terms">用户协议</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<div class="wrap">
|
||||||
|
<article class="panel">
|
||||||
|
<h1>Sleep Heart 软件隐私权政策</h1>
|
||||||
|
<p class="meta">适用于 Sleep Heart 网站(health.wyrs365.com)及 Sleep Heart 应用。</p>
|
||||||
|
<p>上海微雨如酥网络设计有限公司(以下简称「本公司」或「我们」)非常重视用户个人信息及隐私权的保护,因此我们制订了涵盖如何收集、存储、使用、共享和保护用户信息以及访问、更新、删除和保护这些信息方式的隐私政策。</p>
|
||||||
|
<p class="meta">本版本发布时间:2026年10月2日</p>
|
||||||
|
|
||||||
|
<h2>引言</h2>
|
||||||
|
<p>本政策包含以下内容:</p>
|
||||||
|
<p>一、我们如何收集和使用您的个人信息及其他信息</p>
|
||||||
|
<p>二、我们如何使用 Cookie 和同类技术</p>
|
||||||
|
<p>三、合法共享、转让、公开披露您个人信息的情形</p>
|
||||||
|
<p>四、我们如何安全地保护和保存您提供的信息</p>
|
||||||
|
<p>五、您如何访问和控制自己提供的个人信息</p>
|
||||||
|
<p>六、未成年人的个人信息保护</p>
|
||||||
|
<p>七、通知和修订</p>
|
||||||
|
<p>八、适用法律与争议解决</p>
|
||||||
|
<p>九、如何联系我们</p>
|
||||||
|
|
||||||
|
<h2>特别提示</h2>
|
||||||
|
<p>1、感谢您对 Sleep Heart 的使用及信任,我们将持续致力于为您提供更加安全可靠的服务。</p>
|
||||||
|
<p>2、Sleep Heart 的产品和/或服务适用本隐私政策。</p>
|
||||||
|
<p>3、若某一功能另有说明,该说明构成本隐私政策的一部分;如有不一致,以该功能的说明为准。</p>
|
||||||
|
<p>4、除非本隐私政策另有明确所指,本隐私政策不适用于通过我们的产品和/或服务而接入的由第三方独立为您提供的产品和/或服务。</p>
|
||||||
|
<p>5、本隐私政策仅适用于 14 周岁以上的用户。如果您是不满 14 周岁的儿童,请立即停止对本隐私政策的阅读和任何其他操作。</p>
|
||||||
|
|
||||||
|
<h2>定义</h2>
|
||||||
|
<p>1、【个人信息】个人信息是指以电子或者其他方式记录的能够单独或者与其他信息结合识别特定自然人身份或者反映特定自然人活动情况的各种信息。</p>
|
||||||
|
<p>2、【个人敏感信息】个人敏感信息是指一旦泄露、非法提供或滥用可能危害人身和财产安全,极易导致个人名誉、身心健康受到损害或歧视性待遇等的个人信息。健康相关记录可能属于此类信息。</p>
|
||||||
|
|
||||||
|
<h2>一、我们如何收集和使用您的个人信息及其他信息</h2>
|
||||||
|
<p>我们仅会出于以下合法目的,收集和使用您的个人信息及其他信息。</p>
|
||||||
|
<h3>(一)您授权我们收集和使用您个人信息及其他信息的情形</h3>
|
||||||
|
<p>Sleep Heart 提供助眠音乐、睡眠声监测、指尖心率估算、呼吸训练、健康指标记录、广场以及账号与意见反馈。测量和监测结果只供个人参考,不是医疗诊断。</p>
|
||||||
|
<p>【会发送到服务器的信息】登录时发送邮箱、验证码或密码、设备标识 IDFV 和设备名称,用于登录和识别设备。意见反馈会发送您填写的正文、可选邮箱和应用版本。登录后在广场发布的帖子、评论、点赞、关注和举报,通过 HTTPS 发送到服务器。</p>
|
||||||
|
<p>【只保存在这台设备上的信息】指尖测心率时的相机画面;睡眠声监测的录音和识别结果;您记录的体重、步数、血压、血糖、经期、血氧、体温和饮水;头像照片以及您导入的音乐。这些内容不会上传给我们。</p>
|
||||||
|
<p>【Apple 健康】您若打开某一指标的「同步 Apple Health」,对应记录写入您的 Apple 健康,不会因此上传给我们。</p>
|
||||||
|
<p>【蓝牙】只在本机连接您绑定的设备,不会因此上传扫描结果。</p>
|
||||||
|
<p>【权限】相机、麦克风、蓝牙、相册和健康权限,只在您使用对应功能时申请。同意本政策之前,不会读取用于登录上报的设备标识,也不会提前申请这些权限。您可以拒绝这些权限,对应功能不可用,其他功能仍可使用。</p>
|
||||||
|
<p>【我们不做的事】不使用 IDFA,不请求跨 App 跟踪,不出售个人信息。</p>
|
||||||
|
<h3>【第三方处理】</h3>
|
||||||
|
<p>会员通过 Apple 的 StoreKit 自动续期订阅处理。价格、续费和取消以 Apple 订阅管理中的显示为准。您主动打开 Apple 健康同步时,由 Apple 按其规则处理写入健康应用的记录。Sleep Heart 当前版本不接入统计类或广告类第三方 SDK,也不使用 IDFA。</p>
|
||||||
|
<h3>(二)我们从第三方获得您个人信息及其他信息的情形</h3>
|
||||||
|
<p>为完成 Apple 订阅状态核对,应用会在本机读取 StoreKit 交易结果。除此之外,我们不会为了扩充档案而向其他公司购买您的个人信息。</p>
|
||||||
|
<h3>(三)您个人信息及其他信息使用的规则</h3>
|
||||||
|
<p>我们会根据法律规定及本隐私政策的约定,为实现登录、意见反馈、广场互动和订阅状态展示而使用所收集的信息。</p>
|
||||||
|
|
||||||
|
<h2>二、我们如何使用 Cookie 和同类技术</h2>
|
||||||
|
<p>1、本网站是静态页面,不使用 Cookie 识别您。</p>
|
||||||
|
<p>2、Sleep Heart 的登录不依赖本网站 Cookie。我们不会把 Cookie 用于本政策所述目的之外的用途。</p>
|
||||||
|
|
||||||
|
<h2>三、合法共享、转让、公开披露您个人信息的情形</h2>
|
||||||
|
<h3>(一)共享</h3>
|
||||||
|
<p>我们不会与 Sleep Heart 及其关联公司以外的任何公司、组织和个人共享您的个人信息,但法律另有规定、取得您同意,或为完成 Apple 订阅与您主动开启的 Apple 健康同步所必需的情形除外。</p>
|
||||||
|
<h3>(二)转让</h3>
|
||||||
|
<p>我们不会将您的个人信息转让给任何公司、组织和个人,但取得同意或法律要求等情形除外。</p>
|
||||||
|
<h3>(三)公开披露</h3>
|
||||||
|
<p>我们仅会在取得您同意或法律要求的情况下,采取符合业界标准的安全防护措施公开披露您的个人信息。您在广场主动发布的内容,会按产品功能向其他用户展示。</p>
|
||||||
|
<h3>(四)对第三方责任的声明</h3>
|
||||||
|
<p>Apple 订阅和 Apple 健康有各自的规则。建议您同时查看 Apple 的隐私说明与服务条款。</p>
|
||||||
|
|
||||||
|
<h2>四、我们如何安全地保护和保存您提供的信息</h2>
|
||||||
|
<h3>(一)信息的保存地点</h3>
|
||||||
|
<p>发送到服务器的账号、意见反馈和广场内容通过 HTTPS 传输并保存在我们的服务器上,用于提供相应功能。只留在设备上的测量画面、录音和手填健康数字,不会因此上传。</p>
|
||||||
|
<h3>(二)信息的保存期限</h3>
|
||||||
|
<p>我们仅为实现服务目的所必需的时间保留已上传的个人信息。本机记录由您在设备上保留或清理。</p>
|
||||||
|
<h3>(三)我们保护您个人信息的技术与措施</h3>
|
||||||
|
<p>我们对服务器通信使用 HTTPS。我们采取合理可行的访问控制措施保护已上传的信息。</p>
|
||||||
|
|
||||||
|
<h2>五、您如何访问和控制自己提供的个人信息</h2>
|
||||||
|
<p>1、您可以在应用内查看账号、反馈和广场相关内容,并在本机查看或清理只保存在设备上的记录。</p>
|
||||||
|
<p>2、应用内注销的实际效果见<a href="/delete">注销</a>页:清除本机昵称、头像和会员标记并退出登录,并尝试结束当前登录会话。当前版本不会向服务器提交永久删除账号。</p>
|
||||||
|
<p>3、您也可以通过本政策文末的邮箱提出访问、更正或删除请求。我们将在验证身份后五个工作日内处理。</p>
|
||||||
|
<p>4、在法律要求保留,或无法验证请求人身份时,我们可能无法回应部分请求。</p>
|
||||||
|
|
||||||
|
<h2>六、未成年人的个人信息保护</h2>
|
||||||
|
<p>1、若您是未满 18 周岁的未成年人,请在监护人监护、指导下并取得监护人同意后使用 Sleep Heart。</p>
|
||||||
|
<p>2、我们重视未成年人个人信息的保护。本政策不面向不满 14 周岁的儿童。</p>
|
||||||
|
<p>3、监护人应指导未成年人合理使用网络。</p>
|
||||||
|
|
||||||
|
<h2>七、通知和修订</h2>
|
||||||
|
<p>1、为提供服务,本隐私政策会不时更新。更新后将在本网站或应用内以适当方式提供。</p>
|
||||||
|
<p>2、如发生重大变更,我们将以更明确的方式告知您。</p>
|
||||||
|
<p>3、若您在更新生效后继续使用产品和/或服务,即表示您同意受修订后的本隐私政策约束。</p>
|
||||||
|
|
||||||
|
<h2>八、适用法律与争议解决</h2>
|
||||||
|
<p>1、【法律适用】本隐私政策的运行、解释和争议的解决适用中华人民共和国法律。</p>
|
||||||
|
<p>2、【争议解决】您和 Sleep Heart 就本隐私政策内容或其执行发生争议的,双方应友好协商解决。</p>
|
||||||
|
|
||||||
|
<h2>九、如何联系我们</h2>
|
||||||
|
<p>如您对本隐私政策或您的个人信息有任何问题、意见或建议,请发送电子邮件至 <a href="mailto:asd272940236@foxmail.com">asd272940236@foxmail.com</a>。</p>
|
||||||
|
<p>账号注销不在本网站提交。应用内入口和实际效果见<a href="/delete">注销</a>页。</p>
|
||||||
|
<p>上海微雨如酥网络设计有限公司</p>
|
||||||
|
<p><a href="/">返回首页</a> · <a href="/terms">用户协议</a> · <a href="/delete">注销</a></p>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<footer class="foot">
|
||||||
|
<div class="wrap foot-inner">
|
||||||
|
<div>
|
||||||
|
<strong>Sleep Heart</strong>
|
||||||
|
<p>上海微雨如酥网络设计有限公司</p>
|
||||||
|
</div>
|
||||||
|
<nav class="foot-links" aria-label="页脚">
|
||||||
|
<a href="/">首页</a>
|
||||||
|
<a href="/download">下载</a>
|
||||||
|
<a href="/delete">注销</a>
|
||||||
|
<a href="/privacy" aria-current="page">隐私协议</a>
|
||||||
|
<a href="/terms">用户协议</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
118
health-web/terms/index.html
Normal file
118
health-web/terms/index.html
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||||
|
<title>用户协议 — Sleep Heart</title>
|
||||||
|
<meta name="description" content="Sleep Heart 软件用户服务协议。上海微雨如酥网络设计有限公司。" />
|
||||||
|
<link rel="icon" href="/assets/icon.png" type="image/png" />
|
||||||
|
<link rel="stylesheet" href="/assets/site.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="top">
|
||||||
|
<div class="wrap top-inner">
|
||||||
|
<a class="brand" href="/">
|
||||||
|
<img src="/assets/icon.png" width="40" height="40" alt="" />
|
||||||
|
<span>Sleep Heart</span>
|
||||||
|
</a>
|
||||||
|
<nav class="nav" aria-label="页面">
|
||||||
|
<a href="/">首页</a>
|
||||||
|
<a href="/download">下载</a>
|
||||||
|
<a href="/delete">注销</a>
|
||||||
|
<a href="/privacy">隐私协议</a>
|
||||||
|
<a href="/terms" aria-current="page">用户协议</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<div class="wrap">
|
||||||
|
<article class="panel">
|
||||||
|
<h1>Sleep Heart 软件用户服务协议</h1>
|
||||||
|
<p class="meta">适用于 Sleep Heart 网站(health.wyrs365.com)及 Sleep Heart 应用。</p>
|
||||||
|
<p class="meta">本版本发布时间:2026年10月2日</p>
|
||||||
|
|
||||||
|
<h2>一、导言</h2>
|
||||||
|
<p>上海微雨如酥网络设计有限公司(以下简称「本公司」或「我们」)在此特别提醒您认真阅读、充分理解本《软件服务协议》(以下简称「本协议」):</p>
|
||||||
|
<p>1、本协议是您与上海微雨如酥网络设计有限公司之间关于您登录、使用「Sleep Heart」(下称「本软件」)所订立的协议。请仔细阅读。</p>
|
||||||
|
<p>2、本协议所称服务,是指本公司现在或将来通过本软件提供的助眠音乐、睡眠声监测、指尖心率估算、呼吸训练、健康指标记录、广场和账号功能(以下简称「服务」)。「用户」是指登录、使用、浏览本服务的个人或组织,更多时候称为「您」。</p>
|
||||||
|
<p>3、您下载或使用 Sleep Heart,即表示您同意接受本协议约束。本协议构成对双方有约束力的文件。</p>
|
||||||
|
|
||||||
|
<h2>二、服务协议的确认及接受</h2>
|
||||||
|
<p>本站电子服务的所有权和运作权归我们所有。您同意本协议及《Sleep Heart 软件隐私权政策》后,可以使用相应服务。本协议是处理双方权利义务的约定,法律另有强制性规定或双方另有约定的,从其规定。</p>
|
||||||
|
<p>1、【审慎阅读】请您审慎阅读并充分理解各条款,特别是免除或者限制责任的条款、法律适用和争议解决条款。</p>
|
||||||
|
<p>2、如您需要本公司对条款作出说明,请立即停止使用服务,并发送邮件至本公司客服邮箱。</p>
|
||||||
|
<p>3、您的登录、使用等行为视为接受本协议。</p>
|
||||||
|
<p>4、如果您未满 18 周岁,请在法定监护人陪同下阅读本协议,并在使用前取得监护人同意。</p>
|
||||||
|
<p>5、在中华人民共和国大陆地区现行法律法规允许的范围内,本公司可以拒绝服务、停止传输违法内容或对违规账号采取限制措施。</p>
|
||||||
|
<p>6、本软件的著作权及我们发布的内容受著作权法保护。上海微雨如酥网络设计有限公司保留本协议未明确授予的权利。</p>
|
||||||
|
<p>7、【协议修改】根据法律法规更新及运营需要,我们有权不时修改本协议。重大变更将在本网站或应用内告知。</p>
|
||||||
|
|
||||||
|
<h2>三、软件的取得与更新</h2>
|
||||||
|
<p>1、请通过本网站公布的下载说明,或通过得到本公司授权的应用商店取得本软件。下载页若写明应用即将上线,则表示当时还没有可公开访问的商店地址。</p>
|
||||||
|
<p>2、【从未授权第三方取得】如果您从未经本公司授权的第三方取得本软件,本公司无法保证该软件能够正常使用,也不对由此产生的损失负责。</p>
|
||||||
|
<p>3、为改善使用体验,本公司可能不时提供软件更新。请及时安装与您的系统兼容的版本。</p>
|
||||||
|
|
||||||
|
<h2>四、服务方式</h2>
|
||||||
|
<p>1、除免费功能外,本软件提供 Apple 自动续期订阅。订阅组名称为 SleepHeart VIP,包含周、月、季、年四档,四档权益相同,区别是周期。收费前,购买页会展示来自 App Store 的名称和价格。</p>
|
||||||
|
<p>2、确认购买后,才会通过 Apple 账户付款。您取消购买单时,不会完成这笔扣款。订阅按所选周期自动续期,除非您在当前周期结束至少 24 小时前取消。续费价格、管理和取消都在 Apple 的订阅管理里。</p>
|
||||||
|
<p>3、在 iPhone 上可以打开「设置」,点顶部的 Apple 账户,进入「订阅」,选择 SleepHeart 里要停止续费的项目并取消订阅。取消续订不影响已经生效的使用时间。</p>
|
||||||
|
<p>4、退出登录或注销应用账号不会取消 Apple 订阅。会员页可以恢复购买。会员状态跟随 Apple 购买账户,不由应用登录账号单独授予。</p>
|
||||||
|
<p>5、四档会员权益为:会员音频可以完整播放并加入收藏;本地音频导入不限数量;指尖心率测量不限次数。不购买也可以使用免费音频、本机健康记录、睡眠声监测和呼吸训练。</p>
|
||||||
|
<p>6、价格以 App Store 显示为准。商品信息可能变化,请以购买当时页面为准。</p>
|
||||||
|
<p>7、若购买或恢复遇到问题,可发送邮件至 <a href="mailto:asd272940236@foxmail.com">asd272940236@foxmail.com</a>。</p>
|
||||||
|
<p>8、您同意遵守法律及本协议,善意使用本软件,不利用本软件从事非法活动或侵犯他人权利。</p>
|
||||||
|
|
||||||
|
<h2>五、服务规则</h2>
|
||||||
|
<p>1、本软件中的广场内容由用户提供。您必须为自己账号下的行为负责。</p>
|
||||||
|
<p>2、您发布的内容并不代表本公司的观点。</p>
|
||||||
|
<p>3、您应对所发送信息的真实性、合法性和有效性负责。请不要发布垃圾信息、骚扰、虚假或违法内容。您可以使用举报功能报告上述内容。未登录不能使用广场互动。</p>
|
||||||
|
<p>4、本公司可能因业务需要变更、暂停、限制或终止全部或部分服务。</p>
|
||||||
|
<p>5、测量和监测结果只供个人参考,不是医疗诊断,不能代替血压计、血糖仪、血氧仪、体温计或就医。呼吸训练是放松节奏,不是治疗。睡眠声里的静音标记不是呼吸暂停诊断。</p>
|
||||||
|
<p>6、您使用本软件应当遵守宪法和法律,遵守公共秩序,尊重社会公德。</p>
|
||||||
|
<p>7、除非法律允许或本公司书面许可,您不得干扰本软件正常运行,或对本软件进行反向工程等破坏行为。</p>
|
||||||
|
<p>8、本公司可依法停止传输违反法律法规或本协议的内容,并采取相应措施。</p>
|
||||||
|
<p>9、本协议未书面明示授权的其他权利仍归本公司所有。</p>
|
||||||
|
<p>10、您使用 Apple 提供的订阅、健康或系统能力时,还应遵守 Apple 的相关条款。</p>
|
||||||
|
|
||||||
|
<h2>六、您的权利与义务</h2>
|
||||||
|
<p>1、本软件账号的所有权归本公司所有,您获得使用权。</p>
|
||||||
|
<p>2、您应妥善保管账号和密码。</p>
|
||||||
|
<p>3、您应合法、正确地使用本服务。</p>
|
||||||
|
<p>4、注销的实际效果以应用内流程和<a href="/delete">注销</a>页为准。当前版本会清除本机昵称、头像和会员标记并退出登录,并尝试结束当前登录会话;不会取消 Apple 订阅,也不会向服务器提交永久删除账号。</p>
|
||||||
|
|
||||||
|
<h2>七、隐私保护政策</h2>
|
||||||
|
<p>1、保护用户个人信息是本公司的基本原则。我们将按照本协议及《<a href="/privacy">Sleep Heart 软件隐私权政策</a>》收集、使用和保存您的个人信息。</p>
|
||||||
|
<p>2、使用登录、意见反馈或广场时,您需要按页面提示提供相应信息。</p>
|
||||||
|
<p>3、只留在本机的测量画面、睡眠录音、手填健康数字、头像和导入音乐,不会因此上传给我们。</p>
|
||||||
|
<p>4、个人信息的具体规则以隐私权政策为准。</p>
|
||||||
|
|
||||||
|
<h2>八、附则</h2>
|
||||||
|
<p>1、请您特别阅读免除本公司责任和加重您义务的条款,并自行考虑风险。</p>
|
||||||
|
<p>2、本协议任何条款若被认定无效,不影响其余条款的效力。</p>
|
||||||
|
<p>3、本协议标题仅为方便阅读,不具有单独法律效力。</p>
|
||||||
|
<p>4、【法律适用】本协议的解释、效力及纠纷的解决,适用中华人民共和国法律。</p>
|
||||||
|
<p>5、【争议解决】若您和本公司发生纠纷,首先应友好协商;协商不成的,提交本公司所在地有管辖权的人民法院诉讼解决。</p>
|
||||||
|
<p>6、如因您违反法律或本协议给我们造成损失,您应赔偿相应损失。</p>
|
||||||
|
<p>若您对本协议有任何意见,请发送电子邮件至 <a href="mailto:asd272940236@foxmail.com">asd272940236@foxmail.com</a>。</p>
|
||||||
|
<p>上海微雨如酥网络设计有限公司</p>
|
||||||
|
<p><a href="/">返回首页</a> · <a href="/privacy">隐私协议</a> · <a href="/delete">注销</a></p>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<footer class="foot">
|
||||||
|
<div class="wrap foot-inner">
|
||||||
|
<div>
|
||||||
|
<strong>Sleep Heart</strong>
|
||||||
|
<p>上海微雨如酥网络设计有限公司</p>
|
||||||
|
</div>
|
||||||
|
<nav class="foot-links" aria-label="页脚">
|
||||||
|
<a href="/">首页</a>
|
||||||
|
<a href="/download">下载</a>
|
||||||
|
<a href="/delete">注销</a>
|
||||||
|
<a href="/privacy">隐私协议</a>
|
||||||
|
<a href="/terms" aria-current="page">用户协议</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
74
website/about/index.html
Normal file
74
website/about/index.html
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>关于 — Spark Messenger</title>
|
||||||
|
<meta name="description" content="Spark Messenger 是面向好友、群聊与朋友圈的即时通讯应用。安全、简洁、跨端同步。" />
|
||||||
|
<link rel="canonical" href="https://sparkmessenger.wyrs365.com/about" />
|
||||||
|
<link rel="icon" href="/assets/favicon.ico?v=20260924-flames" sizes="any" />
|
||||||
|
<link rel="icon" href="/assets/favicon-32.png?v=20260924-flames" type="image/png" sizes="32x32" />
|
||||||
|
<link rel="apple-touch-icon" href="/assets/apple-touch-icon.png?v=20260924-flames" />
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,500;12..96,700;12..96,800&family=Source+Serif+4:opsz,wght@8..60,400;8..60,600&display=swap" rel="stylesheet" />
|
||||||
|
<link rel="stylesheet" href="/assets/styles.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="grain" aria-hidden="true"></div>
|
||||||
|
|
||||||
|
<header class="top">
|
||||||
|
<a class="mark" href="/">
|
||||||
|
<img class="mark-icon" src="/assets/logo.png?v=20260924-flames" width="32" height="32" alt="" />
|
||||||
|
<span>Spark Messenger</span>
|
||||||
|
</a>
|
||||||
|
<nav class="top-nav" aria-label="主导航">
|
||||||
|
<a href="/#features">能力</a>
|
||||||
|
<a href="/#download">下载</a>
|
||||||
|
<a href="/privacy">隐私协议</a>
|
||||||
|
<a href="/terms">用户协议</a>
|
||||||
|
<a href="/delete">注销</a>
|
||||||
|
<a href="/about" aria-current="page">关于</a>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="page">
|
||||||
|
<p class="eyebrow">关于</p>
|
||||||
|
<h1>Spark Messenger</h1>
|
||||||
|
<p class="lede">Spark Messenger 是面向好友、群聊与朋友圈的即时通讯应用。安全、简洁、跨端同步。</p>
|
||||||
|
<div class="prose">
|
||||||
|
<p>把对话、群聊与朋友圈收进同一束火花——安静、可靠、随处可达。</p>
|
||||||
|
<p>少一层噪音,多一点连接。消息直达,媒体直传,跨设备保持同一条时间线。</p>
|
||||||
|
</div>
|
||||||
|
<ul class="feature-rail">
|
||||||
|
<li>
|
||||||
|
<h3>一对一与群聊</h3>
|
||||||
|
<p>长连接实时推送,撤回与删除按会话清晰落地。</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<h3>朋友圈</h3>
|
||||||
|
<p>图片与短文按可见范围发布,不挤占会话列表。</p>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<h3>媒体直传</h3>
|
||||||
|
<p>对象存储签名上传,聊天里只留引用,加载更快。</p>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div class="prose">
|
||||||
|
<p>Android 与 iOS 客户端陆续开放。运营说明见站内<a href="/privacy">隐私协议</a>与<a href="/terms">用户协议</a>。</p>
|
||||||
|
<a class="page-back" href="/">返回首页</a>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="foot">
|
||||||
|
<p><strong>Spark Messenger</strong> · wyrs365</p>
|
||||||
|
<p class="foot-links">
|
||||||
|
<a href="/">首页</a>
|
||||||
|
<span aria-hidden="true">·</span>
|
||||||
|
<a href="/privacy">隐私协议</a>
|
||||||
|
<span aria-hidden="true">·</span>
|
||||||
|
<a href="/terms">用户协议</a>
|
||||||
|
</p>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
229
website/assets/legal.css
Normal file
229
website/assets/legal.css
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
:root {
|
||||||
|
color-scheme: light;
|
||||||
|
--ink: #1a221c;
|
||||||
|
--ink-soft: #3d4a40;
|
||||||
|
--mist: #f3efe6;
|
||||||
|
--paper: #faf7f0;
|
||||||
|
--card: rgba(255, 255, 255, 0.62);
|
||||||
|
--ember: #e35a1c;
|
||||||
|
--leaf: #2f6b4f;
|
||||||
|
--line: rgba(26, 34, 28, 0.14);
|
||||||
|
--font-display: "Bricolage Grotesque", "PingFang SC", "Hiragino Sans", "Noto Sans", "Segoe UI", sans-serif;
|
||||||
|
--font-body: "Source Serif 4", "Songti SC", "PingFang SC", "Hiragino Mincho ProN", "Yu Mincho", "Noto Serif CJK SC", "Geeza Pro", "Noto Naskh Arabic", "Thonburi", "Noto Sans Thai", Georgia, serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-theme="dark"] {
|
||||||
|
color-scheme: dark;
|
||||||
|
--ink: #f4f0e6;
|
||||||
|
--ink-soft: #c9d2c8;
|
||||||
|
--mist: #101512;
|
||||||
|
--paper: #1a221c;
|
||||||
|
--card: rgba(26, 34, 28, 0.72);
|
||||||
|
--ember: #ff8a4a;
|
||||||
|
--leaf: #8fceb0;
|
||||||
|
--line: rgba(244, 240, 230, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root:not([data-theme="light"]) {
|
||||||
|
color-scheme: dark;
|
||||||
|
--ink: #f4f0e6;
|
||||||
|
--ink-soft: #c9d2c8;
|
||||||
|
--mist: #101512;
|
||||||
|
--paper: #1a221c;
|
||||||
|
--card: rgba(26, 34, 28, 0.72);
|
||||||
|
--ember: #ff8a4a;
|
||||||
|
--leaf: #8fceb0;
|
||||||
|
--line: rgba(244, 240, 230, 0.16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
color: var(--ink);
|
||||||
|
background:
|
||||||
|
radial-gradient(900px 480px at 0% -10%, rgba(227, 90, 28, 0.16), transparent 55%),
|
||||||
|
linear-gradient(180deg, var(--paper), var(--mist));
|
||||||
|
font-family: var(--font-body);
|
||||||
|
font-size: 1.0625rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--ember);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sr-only {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
padding: 0;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.75rem;
|
||||||
|
max-width: 46rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 1rem 1.15rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mark {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
color: var(--ink);
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-weight: 700;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mark img {
|
||||||
|
width: 1.75rem;
|
||||||
|
height: 1.75rem;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tools {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tools select,
|
||||||
|
.tools button {
|
||||||
|
min-height: 2.75rem;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 0.55rem;
|
||||||
|
background: var(--card);
|
||||||
|
color: var(--ink);
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tools select {
|
||||||
|
max-width: 9.5rem;
|
||||||
|
padding: 0 0.55rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tools button {
|
||||||
|
padding: 0 0.85rem;
|
||||||
|
font-weight: 650;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tools select:focus,
|
||||||
|
.tools button:focus {
|
||||||
|
outline: 2px solid var(--ember);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
max-width: 46rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 1.25rem 1.15rem 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
article {
|
||||||
|
background: var(--card);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 1rem;
|
||||||
|
padding: 1.25rem 1.15rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
article h1 {
|
||||||
|
margin: 0.35rem 0 0.75rem;
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: clamp(1.8rem, 5vw, 2.4rem);
|
||||||
|
line-height: 1.15;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
}
|
||||||
|
|
||||||
|
article h2 {
|
||||||
|
margin: 1.35rem 0 0.4rem;
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: 1.15rem;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
article p,
|
||||||
|
article li {
|
||||||
|
margin: 0.45rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
article ul {
|
||||||
|
margin: 0.35rem 0 0.6rem;
|
||||||
|
padding-inline-start: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eyebrow {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--leaf);
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
font-weight: 650;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta {
|
||||||
|
color: var(--ink-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.foot {
|
||||||
|
max-width: 46rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 1.15rem 2.25rem;
|
||||||
|
color: var(--ink-soft);
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: 0.92rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.foot p {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.55rem 0.75rem;
|
||||||
|
margin: 0.35rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.foot a {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
[hidden] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.top {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tools,
|
||||||
|
.tools select {
|
||||||
|
width: 100%;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tools button {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
166
website/assets/legal.js
Normal file
166
website/assets/legal.js
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
(() => {
|
||||||
|
const LANGS = [
|
||||||
|
"zh-Hans", "zh-Hant", "en-US", "ja", "ko", "es-MX", "fr-FR",
|
||||||
|
"de-DE", "pt-BR", "ru", "ar-SA", "it", "th", "vi", "id",
|
||||||
|
];
|
||||||
|
|
||||||
|
const UI = {
|
||||||
|
"zh-Hans": { home: "首页", privacy: "隐私政策", terms: "用户协议", toDark: "暗黑", toLight: "日间" },
|
||||||
|
"zh-Hant": { home: "首頁", privacy: "隱私政策", terms: "使用者協議", toDark: "深色", toLight: "日間" },
|
||||||
|
"en-US": { home: "Home", privacy: "Privacy Policy", terms: "Terms of Use", toDark: "Dark", toLight: "Light" },
|
||||||
|
ja: { home: "ホーム", privacy: "プライバシー", terms: "利用規約", toDark: "ダーク", toLight: "ライト" },
|
||||||
|
ko: { home: "홈", privacy: "개인정보", terms: "이용약관", toDark: "어둡게", toLight: "밝게" },
|
||||||
|
"es-MX": { home: "Inicio", privacy: "Privacidad", terms: "Condiciones", toDark: "Oscuro", toLight: "Claro" },
|
||||||
|
"fr-FR": { home: "Accueil", privacy: "Confidentialité", terms: "Conditions", toDark: "Sombre", toLight: "Clair" },
|
||||||
|
"de-DE": { home: "Start", privacy: "Datenschutz", terms: "Nutzung", toDark: "Dunkel", toLight: "Hell" },
|
||||||
|
"pt-BR": { home: "Início", privacy: "Privacidade", terms: "Termos", toDark: "Escuro", toLight: "Claro" },
|
||||||
|
ru: { home: "Главная", privacy: "Конфиденциальность", terms: "Условия", toDark: "Тёмная", toLight: "Светлая" },
|
||||||
|
"ar-SA": { home: "الرئيسية", privacy: "الخصوصية", terms: "الشروط", toDark: "داكن", toLight: "فاتح" },
|
||||||
|
it: { home: "Home", privacy: "Privacy", terms: "Condizioni", toDark: "Scuro", toLight: "Chiaro" },
|
||||||
|
th: { home: "หน้าแรก", privacy: "ความเป็นส่วนตัว", terms: "ข้อกำหนด", toDark: "มืด", toLight: "สว่าง" },
|
||||||
|
vi: { home: "Trang chủ", privacy: "Quyền riêng tư", terms: "Điều khoản", toDark: "Tối", toLight: "Sáng" },
|
||||||
|
id: { home: "Beranda", privacy: "Privasi", terms: "Ketentuan", toDark: "Gelap", toLight: "Terang" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const HTML_LANG = {
|
||||||
|
"zh-Hans": "zh-CN",
|
||||||
|
"zh-Hant": "zh-TW",
|
||||||
|
"en-US": "en",
|
||||||
|
ja: "ja",
|
||||||
|
ko: "ko",
|
||||||
|
"es-MX": "es-MX",
|
||||||
|
"fr-FR": "fr",
|
||||||
|
"de-DE": "de",
|
||||||
|
"pt-BR": "pt-BR",
|
||||||
|
ru: "ru",
|
||||||
|
"ar-SA": "ar",
|
||||||
|
it: "it",
|
||||||
|
th: "th",
|
||||||
|
vi: "vi",
|
||||||
|
id: "id",
|
||||||
|
};
|
||||||
|
|
||||||
|
function norm(tag) {
|
||||||
|
if (!tag) return "";
|
||||||
|
const raw = String(tag).trim().replace(/_/g, "-");
|
||||||
|
if (LANGS.includes(raw)) return raw;
|
||||||
|
const lower = raw.toLowerCase();
|
||||||
|
if (lower === "zh" || lower === "zh-cn" || lower === "zh-sg" || lower === "zh-hans") return "zh-Hans";
|
||||||
|
if (lower === "zh-tw" || lower === "zh-hk" || lower === "zh-mo" || lower === "zh-hant") return "zh-Hant";
|
||||||
|
if (lower.startsWith("en")) return "en-US";
|
||||||
|
if (lower.startsWith("ja")) return "ja";
|
||||||
|
if (lower.startsWith("ko")) return "ko";
|
||||||
|
if (lower.startsWith("es")) return "es-MX";
|
||||||
|
if (lower.startsWith("fr")) return "fr-FR";
|
||||||
|
if (lower.startsWith("de")) return "de-DE";
|
||||||
|
if (lower.startsWith("pt")) return "pt-BR";
|
||||||
|
if (lower.startsWith("ru")) return "ru";
|
||||||
|
if (lower.startsWith("ar")) return "ar-SA";
|
||||||
|
if (lower.startsWith("it")) return "it";
|
||||||
|
if (lower.startsWith("th")) return "th";
|
||||||
|
if (lower.startsWith("vi")) return "vi";
|
||||||
|
if (lower === "id" || lower.startsWith("id-") || lower.startsWith("in")) return "id";
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectLang() {
|
||||||
|
const query = new URLSearchParams(location.search).get("lang");
|
||||||
|
if (query) return norm(query) || "zh-Hans";
|
||||||
|
try {
|
||||||
|
const saved = norm(localStorage.getItem("spark-legal-lang"));
|
||||||
|
if (saved) return saved;
|
||||||
|
} catch (_) { /* ignore */ }
|
||||||
|
const list = navigator.languages && navigator.languages.length
|
||||||
|
? navigator.languages
|
||||||
|
: [navigator.language || ""];
|
||||||
|
for (const item of list) {
|
||||||
|
const code = norm(item);
|
||||||
|
if (code) return code;
|
||||||
|
}
|
||||||
|
return "zh-Hans";
|
||||||
|
}
|
||||||
|
|
||||||
|
function effectiveTheme() {
|
||||||
|
const chosen = document.documentElement.getAttribute("data-theme");
|
||||||
|
if (chosen === "dark" || chosen === "light") return chosen;
|
||||||
|
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTheme(mode) {
|
||||||
|
if (mode === "light" || mode === "dark") {
|
||||||
|
document.documentElement.setAttribute("data-theme", mode);
|
||||||
|
} else {
|
||||||
|
document.documentElement.removeAttribute("data-theme");
|
||||||
|
}
|
||||||
|
paintThemeButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
function paintThemeButton() {
|
||||||
|
const button = document.getElementById("theme");
|
||||||
|
const lang = document.documentElement.getAttribute("data-lang") || "zh-Hans";
|
||||||
|
const ui = UI[lang] || UI["zh-Hans"];
|
||||||
|
if (!button) return;
|
||||||
|
const dark = effectiveTheme() === "dark";
|
||||||
|
button.textContent = dark ? ui.toLight : ui.toDark;
|
||||||
|
button.setAttribute("aria-pressed", dark ? "true" : "false");
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyLang(code) {
|
||||||
|
const safe = LANGS.includes(code) ? code : "zh-Hans";
|
||||||
|
document.querySelectorAll("article[data-lang]").forEach((node) => {
|
||||||
|
node.hidden = node.getAttribute("data-lang") !== safe;
|
||||||
|
});
|
||||||
|
document.documentElement.lang = HTML_LANG[safe] || "zh-CN";
|
||||||
|
document.documentElement.dir = safe === "ar-SA" ? "rtl" : "ltr";
|
||||||
|
document.documentElement.setAttribute("data-lang", safe);
|
||||||
|
const heading = document.querySelector(`article[data-lang="${safe}"] h1`);
|
||||||
|
if (heading && heading.textContent) {
|
||||||
|
document.title = `${heading.textContent.trim()} — Spark Messenger`;
|
||||||
|
}
|
||||||
|
const ui = UI[safe];
|
||||||
|
document.querySelectorAll("[data-i18n]").forEach((node) => {
|
||||||
|
const key = node.getAttribute("data-i18n");
|
||||||
|
if (ui[key]) node.textContent = ui[key];
|
||||||
|
});
|
||||||
|
const select = document.getElementById("lang");
|
||||||
|
if (select) select.value = safe;
|
||||||
|
paintThemeButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
applyLang(detectLang());
|
||||||
|
|
||||||
|
try {
|
||||||
|
const savedTheme = localStorage.getItem("spark-legal-theme");
|
||||||
|
if (savedTheme === "light" || savedTheme === "dark") applyTheme(savedTheme);
|
||||||
|
else paintThemeButton();
|
||||||
|
} catch (_) {
|
||||||
|
paintThemeButton();
|
||||||
|
}
|
||||||
|
|
||||||
|
const select = document.getElementById("lang");
|
||||||
|
if (select) {
|
||||||
|
select.addEventListener("change", () => {
|
||||||
|
applyLang(select.value);
|
||||||
|
try { localStorage.setItem("spark-legal-lang", select.value); } catch (_) { /* ignore */ }
|
||||||
|
const url = new URL(location.href);
|
||||||
|
url.searchParams.set("lang", select.value);
|
||||||
|
history.replaceState(null, "", url);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const themeButton = document.getElementById("theme");
|
||||||
|
if (themeButton) {
|
||||||
|
themeButton.addEventListener("click", () => {
|
||||||
|
const next = effectiveTheme() === "dark" ? "light" : "dark";
|
||||||
|
applyTheme(next);
|
||||||
|
try { localStorage.setItem("spark-legal-theme", next); } catch (_) { /* ignore */ }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const media = window.matchMedia("(prefers-color-scheme: dark)");
|
||||||
|
const onScheme = () => {
|
||||||
|
if (!document.documentElement.getAttribute("data-theme")) paintThemeButton();
|
||||||
|
};
|
||||||
|
if (typeof media.addEventListener === "function") media.addEventListener("change", onScheme);
|
||||||
|
else if (typeof media.addListener === "function") media.addListener(onScheme);
|
||||||
|
})();
|
||||||
@ -66,9 +66,10 @@ a {
|
|||||||
position: relative;
|
position: relative;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 1rem;
|
gap: 0.75rem 1.25rem;
|
||||||
max-width: 1120px;
|
max-width: 1120px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 1.25rem 1.5rem 0;
|
padding: 1.25rem 1.5rem 0;
|
||||||
@ -96,21 +97,33 @@ a {
|
|||||||
|
|
||||||
.top-nav {
|
.top-nav {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1.25rem;
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 0.15rem 1.15rem;
|
||||||
font-family: var(--font-display);
|
font-family: var(--font-display);
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-nav a {
|
.top-nav a {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 2.25rem;
|
||||||
|
padding: 0.15rem 0.05rem;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
opacity: 0.78;
|
opacity: 0.78;
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-nav a:hover {
|
.top-nav a:hover,
|
||||||
|
.top-nav a[aria-current="page"] {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.top-nav a[aria-current="page"] {
|
||||||
|
color: var(--ember);
|
||||||
|
box-shadow: inset 0 -2px 0 var(--ember);
|
||||||
|
}
|
||||||
|
|
||||||
.hero {
|
.hero {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
@ -512,6 +525,67 @@ a {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
max-width: 1120px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2.5rem 1.5rem 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: clamp(2.4rem, 6vw, 4rem);
|
||||||
|
line-height: 0.98;
|
||||||
|
letter-spacing: -0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page h2 {
|
||||||
|
margin: 2.2rem 0 0.55rem;
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: clamp(1.35rem, 3vw, 1.75rem);
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page .prose {
|
||||||
|
max-width: 40rem;
|
||||||
|
color: var(--ink-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page .prose p,
|
||||||
|
.page .prose li {
|
||||||
|
margin: 0.75rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page .prose ul {
|
||||||
|
margin: 0.35rem 0 0;
|
||||||
|
padding-inline-start: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-back {
|
||||||
|
display: inline-flex;
|
||||||
|
margin-top: 1.75rem;
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-weight: 650;
|
||||||
|
color: var(--ember);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page .feature-rail {
|
||||||
|
margin-top: 1.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.top-nav {
|
||||||
|
flex: 1 1 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
|
column-gap: 0.95rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.hero {
|
.hero {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|||||||
59
website/delete/index.html
Normal file
59
website/delete/index.html
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>注销 — Spark Messenger</title>
|
||||||
|
<meta name="description" content="在 Spark Messenger App 内注销账号。找不到入口时,通过 App 内意见反馈联系我们。" />
|
||||||
|
<link rel="canonical" href="https://sparkmessenger.wyrs365.com/delete" />
|
||||||
|
<link rel="icon" href="/assets/favicon.ico?v=20260924-flames" sizes="any" />
|
||||||
|
<link rel="icon" href="/assets/favicon-32.png?v=20260924-flames" type="image/png" sizes="32x32" />
|
||||||
|
<link rel="apple-touch-icon" href="/assets/apple-touch-icon.png?v=20260924-flames" />
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,500;12..96,700;12..96,800&family=Source+Serif+4:opsz,wght@8..60,400;8..60,600&display=swap" rel="stylesheet" />
|
||||||
|
<link rel="stylesheet" href="/assets/styles.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="grain" aria-hidden="true"></div>
|
||||||
|
|
||||||
|
<header class="top">
|
||||||
|
<a class="mark" href="/">
|
||||||
|
<img class="mark-icon" src="/assets/logo.png?v=20260924-flames" width="32" height="32" alt="" />
|
||||||
|
<span>Spark Messenger</span>
|
||||||
|
</a>
|
||||||
|
<nav class="top-nav" aria-label="主导航">
|
||||||
|
<a href="/#features">能力</a>
|
||||||
|
<a href="/#download">下载</a>
|
||||||
|
<a href="/privacy">隐私协议</a>
|
||||||
|
<a href="/terms">用户协议</a>
|
||||||
|
<a href="/delete" aria-current="page">注销</a>
|
||||||
|
<a href="/about">关于</a>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="page">
|
||||||
|
<p class="eyebrow">账号</p>
|
||||||
|
<h1>注销</h1>
|
||||||
|
<p class="lede">官网不能直接注销账号。请在已登录的 Spark Messenger 里完成。</p>
|
||||||
|
<div class="prose">
|
||||||
|
<h2>在 App 内注销</h2>
|
||||||
|
<p>使用当前账号发起注销后,系统会向这个账号绑定的邮箱发送注销验证码。验证码 5 分钟内有效,只发到绑定邮箱,不会发到手机号。验证通过并提交后,账号会被永久删除,不能再登录。</p>
|
||||||
|
<h2>找不到入口时</h2>
|
||||||
|
<p>如果 App 里暂时没有看到注销入口,请通过 App 内的「意见反馈」联系我们,说明需要注销账号。隐私协议也写明:可以通过意见反馈要求更正或删除与账号相关的信息。</p>
|
||||||
|
<a class="page-back" href="/">返回首页</a>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="foot">
|
||||||
|
<p><strong>Spark Messenger</strong> · wyrs365</p>
|
||||||
|
<p class="foot-links">
|
||||||
|
<a href="/">首页</a>
|
||||||
|
<span aria-hidden="true">·</span>
|
||||||
|
<a href="/privacy">隐私协议</a>
|
||||||
|
<span aria-hidden="true">·</span>
|
||||||
|
<a href="/terms">用户协议</a>
|
||||||
|
</p>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -25,6 +25,10 @@
|
|||||||
<nav class="top-nav" aria-label="主导航">
|
<nav class="top-nav" aria-label="主导航">
|
||||||
<a href="#features">能力</a>
|
<a href="#features">能力</a>
|
||||||
<a href="#download">下载</a>
|
<a href="#download">下载</a>
|
||||||
|
<a href="/privacy">隐私协议</a>
|
||||||
|
<a href="/terms">用户协议</a>
|
||||||
|
<a href="/delete">注销</a>
|
||||||
|
<a href="/about">关于</a>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@ -98,6 +102,10 @@
|
|||||||
<a href="https://sparkmessenger.wyrs365.com/">sparkmessenger.wyrs365.com</a>
|
<a href="https://sparkmessenger.wyrs365.com/">sparkmessenger.wyrs365.com</a>
|
||||||
<span aria-hidden="true">·</span>
|
<span aria-hidden="true">·</span>
|
||||||
<a href="https://sparkmsg.wyrs365.com/">sparkmsg.wyrs365.com</a>
|
<a href="https://sparkmsg.wyrs365.com/">sparkmsg.wyrs365.com</a>
|
||||||
|
<span aria-hidden="true">·</span>
|
||||||
|
<a href="/privacy">隐私政策</a>
|
||||||
|
<span aria-hidden="true">·</span>
|
||||||
|
<a href="/terms">用户协议</a>
|
||||||
</p>
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
|||||||
135
website/legal/legal.html
Normal file
135
website/legal/legal.html
Normal file
File diff suppressed because one or more lines are too long
3
website/privacy/index.html
Normal file
3
website/privacy/index.html
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<script>location.replace("/legal/legal.html?doc=privacy")</script>
|
||||||
1
website/terms/index.html
Normal file
1
website/terms/index.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
<script>location.replace("/legal/legal.html?doc=terms");</script>
|
||||||
Loading…
Reference in New Issue
Block a user