Files
jiu/web/register.njk
T
wangjia eda7d37b64 feat: 新增用户自助注册功能
后端新增 POST /api/v1/public/register 接口,支持门店自助注册并自动生成门店编码(S000001 格式);shops 表加 description 字段。营销站新增 /register/ 注册页,含门店信息和管理员账号表单,注册成功后展示门店编码和登录提示。Flutter 登录页底部加「前往官网注册」跳转链接。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 13:56:34 +08:00

281 lines
14 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
layout: base.njk
title: 注册新门店
description: 填写门店信息,创建岩美酒库管理账户,注册完成后获取门店编码用于登录客户端。
pageStyle: |
.reg-page { padding: var(--space-16) 0 var(--space-24); }
.reg-wrap { max-width: 640px; margin: 0 auto; padding: 0 var(--space-4); }
.reg-hero { text-align: center; margin-bottom: var(--space-8); }
.reg-hero h1 { font-size: var(--text-3xl); font-weight: 700; margin-bottom: var(--space-2); }
.reg-hero p { color: var(--neutral-500); font-size: var(--text-md); max-width: 480px; margin: 0 auto; }
.reg-card { background: var(--neutral-0); border: 1px solid var(--neutral-200); border-radius: var(--radius-xl); padding: var(--space-8); }
.reg-fieldset { border: none; padding: 0; margin: 0 0 var(--space-8); }
.reg-legend { font-size: var(--text-sm); font-weight: 600; color: var(--neutral-500); text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: var(--space-5); padding-bottom: var(--space-3); border-bottom: 1px solid var(--neutral-100); display: block; width: 100%; }
.form-row { margin-bottom: var(--space-5); }
.form-label { display: block; font-size: var(--text-sm); font-weight: 500; color: var(--neutral-700); margin-bottom: var(--space-1-5); }
.form-label .req { color: var(--error-500); margin-left: 2px; }
.form-input, .form-textarea { width: 100%; padding: var(--space-2-5) var(--space-3); border: 1px solid var(--neutral-300); border-radius: var(--radius-md); font-size: var(--text-md); font-family: inherit; background: var(--neutral-0); color: var(--neutral-900); transition: border-color .15s, box-shadow .15s; box-sizing: border-box; }
.form-input:focus, .form-textarea:focus { outline: none; border-color: var(--brand-500); box-shadow: 0 0 0 3px var(--brand-100); }
.form-textarea { resize: vertical; min-height: 80px; }
.form-helper { font-size: var(--text-xs); color: var(--neutral-400); margin-top: var(--space-1); }
.reg-submit { width: 100%; padding: var(--space-3) var(--space-4); font-size: var(--text-md); font-weight: 600; margin-top: var(--space-2); }
.reg-login-hint { text-align: center; margin-top: var(--space-4); font-size: var(--text-sm); color: var(--neutral-500); }
.reg-login-hint a { color: var(--brand-600); }
.reg-error { display: none; background: var(--error-50); border: 1px solid var(--error-200); border-radius: var(--radius-md); padding: var(--space-3) var(--space-4); margin-bottom: var(--space-5); font-size: var(--text-sm); color: var(--error-700); }
.reg-error.visible { display: block; }
.success-panel { display: none; text-align: center; }
.success-panel.visible { display: block; }
.success-icon { color: var(--success-500); margin-bottom: var(--space-4); }
.success-panel h2 { font-size: var(--text-2xl); font-weight: 700; margin-bottom: var(--space-2); }
.success-panel > p { color: var(--neutral-500); margin-bottom: var(--space-6); }
.shop-code-box { background: var(--brand-50); border: 1px solid var(--brand-200); border-radius: var(--radius-lg); padding: var(--space-5) var(--space-6); margin-bottom: var(--space-6); display: flex; align-items: center; justify-content: center; gap: var(--space-3); }
.shop-code-text { font-size: var(--text-3xl); font-weight: 700; font-family: 'JetBrains Mono', monospace; color: var(--brand-700); letter-spacing: 0.05em; }
.copy-btn { display: inline-flex; align-items: center; gap: var(--space-1-5); padding: var(--space-2) var(--space-3); background: var(--brand-600); color: #fff; border: none; border-radius: var(--radius-md); font-size: var(--text-sm); font-weight: 500; cursor: pointer; transition: background .15s; }
.copy-btn:hover { background: var(--brand-700); }
.login-hint-card { background: var(--neutral-50); border: 1px solid var(--neutral-200); border-radius: var(--radius-md); padding: var(--space-4) var(--space-5); margin-bottom: var(--space-6); text-align: left; }
.login-hint-card p { font-size: var(--text-sm); font-weight: 600; color: var(--neutral-600); margin-bottom: var(--space-3); }
.login-hint-table { width: 100%; border-collapse: collapse; font-size: var(--text-sm); }
.login-hint-table td { padding: var(--space-1-5) var(--space-2); }
.login-hint-table td:first-child { color: var(--neutral-500); width: 80px; }
.login-hint-table td:last-child { font-weight: 500; color: var(--neutral-800); font-family: 'JetBrains Mono', monospace; }
.success-note { font-size: var(--text-xs); color: var(--neutral-400); margin-bottom: var(--space-6); }
.btn-loading { opacity: .7; cursor: not-allowed; }
---
<section class="reg-page">
<div class="reg-wrap">
<div class="reg-hero">
<h1>注册新门店</h1>
<p>填写以下信息,创建您的专属酒库管理账户。注册完成后系统自动分配门店编码,用于登录客户端。</p>
</div>
<!-- 注册表单 -->
<div class="reg-card" id="reg-form-card">
<div class="reg-error" id="reg-error"></div>
<!-- 门店信息 -->
<fieldset class="reg-fieldset">
<legend class="reg-legend">门店信息</legend>
<div class="form-row">
<label class="form-label" for="shop_name">店铺名称<span class="req">*</span></label>
<input class="form-input" type="text" id="shop_name" placeholder="例:强朋友名酒行" autocomplete="organization" />
<div class="form-helper">您的酒行正式名称,将显示在所有单据和商品标签上</div>
</div>
<div class="form-row">
<label class="form-label" for="address">门店地址<span class="req">*</span></label>
<input class="form-input" type="text" id="address" placeholder="例:贵州省贵阳市云岩区中华北路 88 号" autocomplete="street-address" />
<div class="form-helper">实际经营地址,打印标签和单据时展示</div>
</div>
<div class="form-row">
<label class="form-label" for="manager_name">负责人姓名<span class="req">*</span></label>
<input class="form-input" type="text" id="manager_name" placeholder="例:张三" autocomplete="name" />
<div class="form-helper">主要负责人真实姓名,也是首个管理员账号的姓名</div>
</div>
<div class="form-row">
<label class="form-label" for="phone">联系电话<span class="req">*</span></label>
<input class="form-input" type="tel" id="phone" placeholder="例:0851-12345678" autocomplete="tel" />
<div class="form-helper">对外联系电话,打印商品标签时会展示</div>
</div>
<div class="form-row">
<label class="form-label" for="description">店铺简介</label>
<textarea class="form-textarea" id="description" placeholder="简短介绍您的门店,例:专注高端白酒批发零售,主营茅台系列" maxlength="200"></textarea>
<div class="form-helper">选填,最多 200 字;客户扫商品码时可见</div>
</div>
</fieldset>
<!-- 管理员账号 -->
<fieldset class="reg-fieldset" style="margin-bottom:0">
<legend class="reg-legend">管理员账号</legend>
<div class="form-row">
<label class="form-label" for="username">用户名<span class="req">*</span></label>
<input class="form-input" type="text" id="username" placeholder="例:admin" autocomplete="username" />
<div class="form-helper">登录账号(3–30 位,字母/数字/下划线),创建后不可修改</div>
</div>
<div class="form-row">
<label class="form-label" for="password">密码<span class="req">*</span></label>
<input class="form-input" type="password" id="password" placeholder="至少 6 位" autocomplete="new-password" />
<div class="form-helper">至少 6 位,建议字母数字组合</div>
</div>
<div class="form-row">
<label class="form-label" for="password_confirm">确认密码<span class="req">*</span></label>
<input class="form-input" type="password" id="password_confirm" placeholder="再次输入密码" autocomplete="new-password" />
</div>
</fieldset>
<button class="btn btn-primary reg-submit" id="reg-btn">
<i data-lucide="store" class="icon"></i>
立即注册
</button>
<p class="reg-login-hint">已有账号?<a href="https://jiu.51yanmei.com/app/">直接登录</a></p>
</div>
<!-- 注册成功面板 -->
<div class="reg-card success-panel" id="success-panel">
<div class="success-icon">
<i data-lucide="check-circle" style="width:56px;height:56px;"></i>
</div>
<h2>注册成功!</h2>
<p>您的门店编码已生成,请妥善保存:</p>
<div class="shop-code-box">
<span class="shop-code-text" id="result-shop-code">—</span>
<button class="copy-btn" id="copy-code-btn">
<i data-lucide="copy" class="icon"></i>复制
</button>
</div>
<div class="login-hint-card">
<p>登录客户端时请输入:</p>
<table class="login-hint-table">
<tr>
<td>门店编码</td>
<td id="hint-code">—</td>
</tr>
<tr>
<td>用户名</td>
<td id="hint-username">—</td>
</tr>
<tr>
<td>密码</td>
<td>您设置的密码</td>
</tr>
</table>
</div>
<p class="success-note">如忘记门店编码,登录后可在「系统设置 → 关于」中查看。</p>
<a href="https://jiu.51yanmei.com/app/" class="btn btn-primary" style="display:inline-flex;align-items:center;gap:8px;">
<i data-lucide="log-in" class="icon"></i>前往登录
</a>
</div>
</div>
</section>
<script>
(function() {
var API_URL = 'https://jiu.51yanmei.com/api/v1/public/register';
function $(id) { return document.getElementById(id); }
function showError(msg) {
var el = $('reg-error');
el.textContent = msg;
el.classList.add('visible');
el.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
function clearError() {
var el = $('reg-error');
el.textContent = '';
el.classList.remove('visible');
}
function getVal(id) { return $(id).value.trim(); }
function validate() {
var shopName = getVal('shop_name');
var address = getVal('address');
var manager = getVal('manager_name');
var phone = getVal('phone');
var username = getVal('username');
var password = getVal('password');
var confirm = getVal('password_confirm');
if (!shopName) { showError('请填写店铺名称'); $('shop_name').focus(); return false; }
if (!address) { showError('请填写门店地址'); $('address').focus(); return false; }
if (!manager) { showError('请填写负责人姓名'); $('manager_name').focus(); return false; }
if (!phone) { showError('请填写联系电话'); $('phone').focus(); return false; }
if (!username) { showError('请填写用户名'); $('username').focus(); return false; }
if (username.length < 3 || username.length > 30) { showError('用户名长度须在 330 位之间'); $('username').focus(); return false; }
if (!/^[a-zA-Z0-9_]+$/.test(username)) { showError('用户名只能包含字母、数字和下划线'); $('username').focus(); return false; }
if (!password) { showError('请填写密码'); $('password').focus(); return false; }
if (password.length < 6) { showError('密码至少 6 位'); $('password').focus(); return false; }
if (password !== confirm) { showError('两次输入的密码不一致'); $('password_confirm').focus(); return false; }
return true;
}
$('reg-btn').addEventListener('click', function() {
clearError();
if (!validate()) return;
var btn = $('reg-btn');
btn.disabled = true;
btn.classList.add('btn-loading');
btn.textContent = '注册中…';
var payload = {
shop_name: getVal('shop_name'),
address: getVal('address'),
manager_name: getVal('manager_name'),
phone: getVal('phone'),
description: getVal('description'),
username: getVal('username'),
password: $('password').value
};
fetch(API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
.then(function(res) { return res.json().then(function(data) { return { ok: res.ok, data: data }; }); })
.then(function(r) {
if (!r.ok) {
var msg = (r.data && r.data.error) ? r.data.error : '注册失败,请稍后重试';
if (msg.indexOf('Duplicate') !== -1 || msg.indexOf('duplicate') !== -1) {
msg = '该用户名在此门店已存在,请换一个用户名';
}
showError(msg);
btn.disabled = false;
btn.classList.remove('btn-loading');
btn.innerHTML = '<i data-lucide="store" class="icon"></i>立即注册';
if (window.lucide) lucide.createIcons();
return;
}
var d = r.data.data;
$('result-shop-code').textContent = d.shop_code;
$('hint-code').textContent = d.shop_code;
$('hint-username').textContent = d.username;
$('reg-form-card').style.display = 'none';
var panel = $('success-panel');
panel.classList.add('visible');
if (window.lucide) lucide.createIcons();
panel.scrollIntoView({ behavior: 'smooth', block: 'start' });
})
.catch(function() {
showError('网络异常,请检查网络连接后重试');
btn.disabled = false;
btn.classList.remove('btn-loading');
btn.innerHTML = '<i data-lucide="store" class="icon"></i>立即注册';
if (window.lucide) lucide.createIcons();
});
});
$('copy-code-btn').addEventListener('click', function() {
var code = $('result-shop-code').textContent;
navigator.clipboard.writeText(code).then(function() {
var btn = $('copy-code-btn');
btn.innerHTML = '<i data-lucide="check" class="icon"></i>已复制';
if (window.lucide) lucide.createIcons();
setTimeout(function() {
btn.innerHTML = '<i data-lucide="copy" class="icon"></i>复制';
if (window.lucide) lucide.createIcons();
}, 2000);
});
});
document.addEventListener('DOMContentLoaded', function() {
if (window.lucide) lucide.createIcons();
});
})();
</script>