Files
jiu/web/register.njk
T
2026-06-08 22:33:16 +08:00

328 lines
16 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: 1.5px 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-input.error, .form-textarea.error { border-color: var(--error-500); box-shadow: 0 0 0 3px rgba(239,68,68,.12); }
.form-textarea { resize: vertical; min-height: 80px; }
.form-helper { font-size: var(--text-xs); color: var(--neutral-400); margin-top: var(--space-1); }
.field-error { font-size: var(--text-xs); color: var(--error-600); margin-top: 4px; display: none; }
.field-error.visible { display: block; }
.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="field-error" id="err-shop_name"></div>
<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="field-error" id="err-address"></div>
<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="field-error" id="err-manager_name"></div>
<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="field-error" id="err-phone"></div>
<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="field-error" id="err-username"></div>
<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="field-error" id="err-password"></div>
<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 class="field-error" id="err-password_confirm"></div>
</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="/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="/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 = "{{ site.appBaseUrl }}/api/v1/public/register";
function $(id) { return document.getElementById(id); }
function getVal(id) { return $(id).value.trim(); }
function setFieldError(id, msg) {
var input = $(id);
var errEl = $('err-' + id);
if (msg) {
input.classList.add('error');
if (errEl) { errEl.textContent = msg; errEl.classList.add('visible'); }
} else {
input.classList.remove('error');
if (errEl) { errEl.textContent = ''; errEl.classList.remove('visible'); }
}
}
function clearAllErrors() {
['shop_name','address','manager_name','phone','username','password','password_confirm'].forEach(function(id) {
setFieldError(id, '');
});
}
// 用户修改字段后清除该字段的错误提示
['shop_name','address','manager_name','phone','username','password','password_confirm'].forEach(function(id) {
var el = $(id);
if (el) el.addEventListener('input', function() { setFieldError(id, ''); });
});
function validate() {
clearAllErrors();
var ok = true;
var firstErr = null;
function require(id, msg) {
if (!getVal(id)) { setFieldError(id, msg); if (!firstErr) firstErr = id; ok = false; }
}
require('shop_name', '请填写店铺名称');
require('address', '请填写门店地址');
require('manager_name', '请填写负责人姓名');
require('phone', '请填写联系电话');
var username = getVal('username');
if (!username) {
setFieldError('username', '请填写用户名'); if (!firstErr) firstErr = 'username'; ok = false;
} else if (username.length < 3 || username.length > 30) {
setFieldError('username', '用户名长度须在 330 位之间'); if (!firstErr) firstErr = 'username'; ok = false;
} else if (!/^[a-zA-Z0-9_]+$/.test(username)) {
setFieldError('username', '用户名只能包含字母、数字和下划线'); if (!firstErr) firstErr = 'username'; ok = false;
}
var password = $('password').value;
if (!password) {
setFieldError('password', '请填写密码'); if (!firstErr) firstErr = 'password'; ok = false;
} else if (password.length < 6) {
setFieldError('password', '密码至少 6 位'); if (!firstErr) firstErr = 'password'; ok = false;
}
var confirm = $('password_confirm').value;
if (!confirm) {
setFieldError('password_confirm', '请再次输入密码'); if (!firstErr) firstErr = 'password_confirm'; ok = false;
} else if (password && password !== confirm) {
setFieldError('password_confirm', '两次输入的密码不一致'); if (!firstErr) firstErr = 'password_confirm'; ok = false;
}
if (firstErr) { $(firstErr).focus(); $(firstErr).scrollIntoView({ behavior: 'smooth', block: 'center' }); }
return ok;
}
function showTopError(msg) {
var el = $('reg-error');
el.textContent = msg;
el.classList.add('visible');
el.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
$('reg-btn').addEventListener('click', function() {
$('reg-error').classList.remove('visible');
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 = '该用户名在此门店已存在,请换一个用户名';
}
showTopError(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() {
showTopError('网络异常,请检查网络连接后重试');
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>