feat: 初始提交(仅核心代码,已排除大文件)
This commit is contained in:
@@ -0,0 +1,316 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>理性消费决策锁</title>
|
||||
<link rel="stylesheet" href="/common/common.css" />
|
||||
<link rel="stylesheet" href="/tools/rational_lock/rational_lock.css" />
|
||||
</head>
|
||||
<body>
|
||||
<main class="page">
|
||||
<header class="hero">
|
||||
<div class="hero-main">
|
||||
<div>
|
||||
<h1>理性消费决策锁</h1>
|
||||
<p>耐用品消费需完成全量勾选与冷静期后,才可解锁购买许可</p>
|
||||
</div>
|
||||
<div class="hero-auth">
|
||||
<div id="auth-login-box" class="auth-login-inline">
|
||||
<input id="login-username" autocomplete="off" readonly />
|
||||
<input id="login-password" type="password" autocomplete="off" readonly />
|
||||
<button id="btn-login" class="btn btn-primary" type="button">登录</button>
|
||||
</div>
|
||||
<div id="auth-logged-box" style="display:none">
|
||||
<div class="auth-row">
|
||||
<div id="auth-current-user" class="auth-current-user"></div>
|
||||
<button id="btn-logout" class="btn" type="button">退出登录</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="auth-status" class="status"></div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div id="app-content" style="display:none">
|
||||
<section class="card" id="warning-box" style="display:none"></section>
|
||||
|
||||
<section class="card">
|
||||
<h2>想买申请</h2>
|
||||
<form id="application-form">
|
||||
<div class="grid two">
|
||||
<label>商品名称<input id="item-name" required /></label>
|
||||
<label>商品分类
|
||||
<select id="item-category" required>
|
||||
<option value="">请选择</option>
|
||||
<optgroup label="低风险类">
|
||||
<option value="园艺绿植低风险类">园艺绿植低风险类</option>
|
||||
<option value="家居软装小物件">家居软装小物件</option>
|
||||
<option value="日用消耗非接触类">日用消耗非接触类</option>
|
||||
<option value="临时/消耗/低值易耗品">临时 / 消耗 / 低值易耗品</option>
|
||||
<option value="其他低风险">其他低风险</option>
|
||||
</optgroup>
|
||||
<optgroup label="高风险类">
|
||||
<option value="食品&入口类">食品 & 入口类</option>
|
||||
<option value="健康&身体接触类">健康 & 身体接触类</option>
|
||||
<option value="安全&电器类">安全 & 电器类</option>
|
||||
<option value="贵重&保真刚需品">贵重 & 保真刚需品</option>
|
||||
<option value="其他高风险">其他高风险</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
<div id="item-category-help" class="field-tip field-tip-rich">
|
||||
<div class="field-tip-title">分类示例说明</div>
|
||||
<div id="item-category-help-body">请选择商品分类后查看该分类对应的常见示例。</div>
|
||||
</div>
|
||||
</label>
|
||||
<label>预估价格<input id="item-price" type="number" min="0" step="0.01" required /></label>
|
||||
<label>购买原因简述<input id="item-reason" required /></label>
|
||||
</div>
|
||||
<div id="shopping-risk-group-tip" class="status"></div>
|
||||
<h3>网购风险核验</h3>
|
||||
<div class="grid two">
|
||||
<label>购物方式
|
||||
<select id="shopping-method" required>
|
||||
<option value="">请选择</option>
|
||||
<option value="offline">线下店</option>
|
||||
<option value="online">网购</option>
|
||||
</select>
|
||||
</label>
|
||||
<label id="shopping-guarantee-wrap" style="display:none">购物保障
|
||||
<select id="shopping-guarantee">
|
||||
<option value="">请选择</option>
|
||||
<option value="seven-day">有七天无理由退换</option>
|
||||
<option value="no-return">没有无理由退换</option>
|
||||
<option value="xianyu-used">闲鱼二手购买</option>
|
||||
</select>
|
||||
</label>
|
||||
<label id="shopping-platform-wrap" style="display:none">购买平台
|
||||
<select id="shopping-platform">
|
||||
<option value="">请选择</option>
|
||||
<option value="pdd">拼多多</option>
|
||||
<option value="taobao">淘宝</option>
|
||||
<option value="xianyu">闲鱼</option>
|
||||
<option value="tmall">天猫</option>
|
||||
<option value="jd">京东</option>
|
||||
<option value="other">其他平台</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div id="shopping-online-extra-wrap" class="grid two" style="display:none">
|
||||
<label>店铺类型
|
||||
<select id="shopping-seller-type">
|
||||
<option value="">请选择</option>
|
||||
<option value="brand-flagship">品牌官旗</option>
|
||||
<option value="platform-premium">平台优选 / 自营</option>
|
||||
<option value="enterprise-shop">企业店</option>
|
||||
<option value="normal-shop">普通店</option>
|
||||
<option value="personal-seller">个人卖家</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>品相
|
||||
<select id="shopping-condition">
|
||||
<option value="">请选择</option>
|
||||
<option value="new-sealed">全新未拆</option>
|
||||
<option value="new-opened">全新已拆</option>
|
||||
<option value="used-good">二手良品</option>
|
||||
<option value="used-flawed">二手瑕疵</option>
|
||||
<option value="unknown">品相不确定</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div id="shopping-risk-tip" class="warn" style="display:none"></div>
|
||||
<div id="shopping-progress-wrap" style="display:none">
|
||||
<div id="shopping-progress-text" class="status"></div>
|
||||
<div id="shopping-progress-lights" class="risk-lights">
|
||||
<span class="risk-light" id="shopping-light-1"></span>
|
||||
<span class="risk-light" id="shopping-light-2"></span>
|
||||
<span class="risk-light" id="shopping-light-3"></span>
|
||||
<span class="risk-light" id="shopping-light-4"></span>
|
||||
<span class="risk-light" id="shopping-light-5"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="shopping-questions-wrap" class="grid one" style="display:none">
|
||||
<label>沟通问题1
|
||||
<input id="shopping-q1" placeholder="例如:商品是否全新?是否有拆修或暗病?" />
|
||||
</label>
|
||||
<label>沟通问题2
|
||||
<input id="shopping-q2" placeholder="例如:发货时间、物流方式、到货时效?" />
|
||||
</label>
|
||||
<label>沟通问题3
|
||||
<input id="shopping-q3" placeholder="例如:不满意时是否支持退换?具体规则?" />
|
||||
</label>
|
||||
<label>沟通问题4
|
||||
<input id="shopping-q4" placeholder="例如:售后联系人和处理时效?" />
|
||||
</label>
|
||||
<label>沟通问题5
|
||||
<input id="shopping-q5" placeholder="例如:描述不符时如何处理?" />
|
||||
</label>
|
||||
</div>
|
||||
<div id="shopping-submit-tip" class="status"></div>
|
||||
|
||||
<h3>冷静期设置</h3>
|
||||
<div class="grid one">
|
||||
<label>冷静期天数
|
||||
<select id="cooling-days" required>
|
||||
<option value="3" selected>3天</option>
|
||||
<option value="4">4天</option>
|
||||
<option value="5">5天</option>
|
||||
<option value="6">6天</option>
|
||||
<option value="7">7天</option>
|
||||
<option value="8">8天</option>
|
||||
<option value="9">9天</option>
|
||||
<option value="10">10天</option>
|
||||
<option value="11">11天</option>
|
||||
<option value="12">12天</option>
|
||||
<option value="13">13天</option>
|
||||
<option value="14">14天</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<h3>冷静期强制承诺</h3>
|
||||
<div class="check-grid" id="commit-cooling">
|
||||
<label><input type="checkbox" value="1" />我确认不是即时冲动,愿意等待<span id="cooling-days-text">3</span>天冷静期</label>
|
||||
<label><input type="checkbox" value="1" />我不是因打折、直播、促销被迫下单</label>
|
||||
<label><input type="checkbox" value="1" />我不是跟风、攀比、炫耀而购买</label>
|
||||
<label><input type="checkbox" value="1" />本次购买不属于吃喝类,需严格审核</label>
|
||||
</div>
|
||||
|
||||
<h3>现有物品替代检查</h3>
|
||||
<div class="check-grid" id="commit-replacement">
|
||||
<label><input type="checkbox" value="1" data-replacement="no-similar" />家中无完全可替代的同类物品</label>
|
||||
<label><input type="checkbox" value="1" data-replacement="damaged" />现有同类物品已损坏、过时或无法使用</label>
|
||||
<label><input type="checkbox" value="1" data-replacement="unrepairable" />现有物品无法维修或改造继续使用</label>
|
||||
<label><input type="checkbox" value="1" data-replacement="unique" />该物品功能独特且无可替代</label>
|
||||
</div>
|
||||
|
||||
<h3>真实使用场景验证</h3>
|
||||
<div class="grid one">
|
||||
<label>场景1:我将在<input id="scene-where" required placeholder="输入使用场景" /></label>
|
||||
<label>场景2:我预计每周/每月使用<input id="scene-frequency" required placeholder="输入次数" /></label>
|
||||
<label>场景3:该物品能解决我<input id="scene-problem" required placeholder="输入实际问题" /></label>
|
||||
</div>
|
||||
|
||||
<h3>必要性评分(1-5分)</h3>
|
||||
<div class="grid two">
|
||||
<label>实用程度
|
||||
<select id="score-practical" required>
|
||||
<option value="">请选择</option><option>1</option><option>2</option><option>3</option><option>4</option><option>5</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>必需程度
|
||||
<select id="score-necessary" required>
|
||||
<option value="">请选择</option><option>1</option><option>2</option><option>3</option><option>4</option><option>5</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>预期使用率
|
||||
<select id="score-usage" required>
|
||||
<option value="">请选择</option><option>1</option><option>2</option><option>3</option><option>4</option><option>5</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>性价比合理性
|
||||
<select id="score-cost" required>
|
||||
<option value="">请选择</option><option>1</option><option>2</option><option>3</option><option>4</option><option>5</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>后悔概率(越低越好)
|
||||
<select id="score-regret" required>
|
||||
<option value="">请选择</option><option>1</option><option>2</option><option>3</option><option>4</option><option>5</option>
|
||||
</select>
|
||||
</label>
|
||||
<div class="score-box">总分:<span id="score-total">0</span> / 25(低于14不建议购买)</div>
|
||||
</div>
|
||||
|
||||
<h3>最终防冲动验证(必须全选“否”)</h3>
|
||||
<div class="radio-grid">
|
||||
<div>
|
||||
<div>是否只是觉得好看/新鲜?</div>
|
||||
<label><input type="radio" name="impulse-1" value="是" />是</label>
|
||||
<label><input type="radio" name="impulse-1" value="否" />否</label>
|
||||
</div>
|
||||
<div>
|
||||
<div>是否买后大概率闲置吃灰?</div>
|
||||
<label><input type="radio" name="impulse-2" value="是" />是</label>
|
||||
<label><input type="radio" name="impulse-2" value="否" />否</label>
|
||||
</div>
|
||||
<div>
|
||||
<div>是否为了发朋友圈/满足虚荣心?</div>
|
||||
<label><input type="radio" name="impulse-3" value="是" />是</label>
|
||||
<label><input type="radio" name="impulse-3" value="否" />否</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>反悔可能</h3>
|
||||
<div class="grid one">
|
||||
<label>任何物品都有反悔的可能,你写下哪些反悔的可能?
|
||||
<textarea id="item-regret-possibility" required placeholder="例如:使用频率可能低于预期、同类已有替代、后续维护成本偏高"></textarea>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<h3>最终承诺</h3>
|
||||
<div class="check-grid" id="commit-final">
|
||||
<label><input type="checkbox" value="1" />我确认所有信息真实有效</label>
|
||||
<label><input type="checkbox" value="1" />我确认是真需要,而非单纯想要</label>
|
||||
<label><input type="checkbox" value="1" />我愿意为本次消费承担责任,不浪费</label>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button id="btn-save-draft" class="btn" type="button">保存草稿</button>
|
||||
<button id="btn-load-draft" class="btn" type="button">继续填写草稿</button>
|
||||
<button id="btn-clear-draft" class="btn" type="button">清空草稿</button>
|
||||
<button id="btn-open-settings" class="btn" type="button">设置</button>
|
||||
</div>
|
||||
<button id="btn-submit" class="btn btn-primary btn-soft-disabled" type="submit">提交并开始冷静期</button>
|
||||
<div id="form-status" class="status"></div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>冷静期与结果</h2>
|
||||
<div id="decision-panel" class="decision"></div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>历史记录</h2>
|
||||
<div class="history-toolbar">
|
||||
<label>搜索
|
||||
<input id="history-search" placeholder="商品名称/分类/原因" />
|
||||
</label>
|
||||
<label>开始日期
|
||||
<input id="history-start-date" type="date" />
|
||||
</label>
|
||||
<label>结束日期
|
||||
<input id="history-end-date" type="date" />
|
||||
</label>
|
||||
<label class="history-check">
|
||||
<input id="history-show-archived" type="checkbox" />
|
||||
显示归档
|
||||
</label>
|
||||
<button id="btn-history-reset" class="btn" type="button">重置筛选</button>
|
||||
</div>
|
||||
<div id="stats" class="stats"></div>
|
||||
<div id="history-list" class="list"></div>
|
||||
<div class="history-pager">
|
||||
<button id="history-prev" class="btn" type="button">上一页</button>
|
||||
<span id="history-page-info" class="meta"></span>
|
||||
<button id="history-next" class="btn" type="button">下一页</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
<div id="app-modal" class="modal-mask" style="display:none">
|
||||
<div class="modal-panel">
|
||||
<div id="modal-title" class="modal-title"></div>
|
||||
<div id="modal-message" class="modal-message"></div>
|
||||
<div id="modal-actions" class="modal-actions"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="busy-mask" class="busy-mask" style="display:none">
|
||||
<div class="busy-panel">
|
||||
<div class="busy-spinner"></div>
|
||||
<div id="busy-text" class="busy-text">正在帮你处理哦,请稍等~</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/tools/rational_lock/rational_lock.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,611 @@
|
||||
body {
|
||||
margin: 0;
|
||||
background: #f3f6fb;
|
||||
color: #1f2937;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
}
|
||||
|
||||
.page {
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
padding: 12px;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.hero {
|
||||
background: #111827;
|
||||
color: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.hero-main {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.hero p {
|
||||
margin: 0;
|
||||
opacity: .92;
|
||||
}
|
||||
|
||||
.hero-auth {
|
||||
min-width: 320px;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
justify-items: end;
|
||||
}
|
||||
|
||||
.auth-login-inline {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.auth-login-inline input {
|
||||
width: 110px;
|
||||
height: 34px;
|
||||
border-color: #334155;
|
||||
background: #0f172a;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.auth-login-inline input::placeholder {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.hero-auth .btn-primary {
|
||||
width: auto;
|
||||
margin-top: 0;
|
||||
height: 34px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.auth-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.hero-auth .auth-row .btn {
|
||||
height: 34px;
|
||||
padding: 0 12px;
|
||||
background: #1e293b;
|
||||
}
|
||||
|
||||
.auth-current-user {
|
||||
font-size: 13px;
|
||||
color: #e2e8f0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.hero-auth .status {
|
||||
margin-top: 0;
|
||||
min-height: 16px;
|
||||
font-size: 12px;
|
||||
color: #cbd5e1;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 14px;
|
||||
box-shadow: 0 2px 12px rgba(17, 24, 39, 0.06);
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0 0 12px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 14px 0 8px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
input, select, textarea {
|
||||
height: 38px;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 8px;
|
||||
padding: 0 10px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 88px;
|
||||
height: auto;
|
||||
padding: 10px;
|
||||
resize: vertical;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.grid.two {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.grid.one {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.check-grid, .radio-grid {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.check-grid label {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.check-grid input[type="checkbox"] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.radio-grid > div {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto auto;
|
||||
align-items: center;
|
||||
column-gap: 12px;
|
||||
}
|
||||
|
||||
.radio-grid > div > label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.radio-grid input[type="radio"] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.score-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: 700;
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.btn {
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
height: 38px;
|
||||
padding: 0 14px;
|
||||
background: #334155;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: .45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-soft-disabled {
|
||||
opacity: .5;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
width: 100%;
|
||||
background: #2563eb;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
height: auto;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
color: #1d4ed8;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.status {
|
||||
min-height: 20px;
|
||||
margin-top: 8px;
|
||||
color: #334155;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.field-tip {
|
||||
margin-top: 6px;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.field-tip-rich {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.field-tip-title {
|
||||
color: #334155;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.field-tip-current {
|
||||
color: #334155;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.field-tip-list {
|
||||
margin: 0;
|
||||
padding-left: 18px;
|
||||
}
|
||||
|
||||
.field-tip-list li {
|
||||
margin: 0 0 4px;
|
||||
}
|
||||
|
||||
.field-tip-empty {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.decision {
|
||||
border: 1px dashed #cbd5e1;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
.decision.pass {
|
||||
border-color: #16a34a;
|
||||
background: #f0fdf4;
|
||||
}
|
||||
|
||||
.decision.reject {
|
||||
border-color: #dc2626;
|
||||
background: #fef2f2;
|
||||
}
|
||||
|
||||
.decision.wait {
|
||||
border-color: #2563eb;
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.stats .item {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stats .value {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.row {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.row-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.row .meta {
|
||||
font-size: 13px;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.row .meta .tag-low {
|
||||
display: inline-block;
|
||||
padding: 1px 6px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid #ef4444;
|
||||
color: #b91c1c;
|
||||
background: #fef2f2;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.row .meta .tag-archived {
|
||||
display: inline-block;
|
||||
padding: 1px 6px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid #94a3b8;
|
||||
color: #475569;
|
||||
background: #f8fafc;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.row .meta.cooling-tip {
|
||||
color: #1d4ed8;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.row .actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.history-toolbar {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr 1fr auto auto;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.history-check {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.history-check input[type="checkbox"] {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.history-pager {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.warn {
|
||||
background: #fee2e2;
|
||||
border: 1px solid #ef4444;
|
||||
color: #991b1b;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.risk-lights {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin: 6px 0 10px;
|
||||
}
|
||||
|
||||
.risk-light {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
background: #d1d5db;
|
||||
border: 1px solid #9ca3af;
|
||||
box-shadow: inset 0 1px 1px rgba(255,255,255,.45);
|
||||
}
|
||||
|
||||
.risk-light.on {
|
||||
background: #22c55e;
|
||||
border-color: #16a34a;
|
||||
box-shadow: 0 0 6px rgba(34,197,94,.55), inset 0 1px 1px rgba(255,255,255,.45);
|
||||
}
|
||||
|
||||
.modal-mask {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(15, 23, 42, 0.45);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-panel {
|
||||
width: min(92vw, 460px);
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 40px rgba(15, 23, 42, 0.25);
|
||||
padding: 14px;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.modal-panel.wide {
|
||||
width: min(92vw, 760px);
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.modal-message {
|
||||
font-size: 14px;
|
||||
color: #334155;
|
||||
line-height: 1.55;
|
||||
white-space: pre-wrap;
|
||||
max-height: 62vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.modal-message.is-rich {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.detail-section-title {
|
||||
margin: 10px 0 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: grid;
|
||||
grid-template-columns: 130px 1fr;
|
||||
gap: 8px;
|
||||
align-items: start;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
color: #0f172a;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
color: #1d4ed8;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.modal-actions .btn-primary {
|
||||
width: auto;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.busy-mask {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(15, 23, 42, 0.38);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1100;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.busy-panel {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
min-width: min(92vw, 280px);
|
||||
padding: 16px;
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
gap: 12px;
|
||||
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.24);
|
||||
}
|
||||
|
||||
.busy-spinner {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
border: 3px solid #dbeafe;
|
||||
border-top-color: #2563eb;
|
||||
animation: spin .9s linear infinite;
|
||||
}
|
||||
|
||||
.busy-text {
|
||||
color: #334155;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.hero-main {
|
||||
flex-direction: column;
|
||||
}
|
||||
.hero-auth {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
justify-items: stretch;
|
||||
}
|
||||
.auth-login-inline {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr auto;
|
||||
}
|
||||
.auth-row {
|
||||
justify-content: space-between;
|
||||
}
|
||||
.hero-auth .status {
|
||||
text-align: left;
|
||||
}
|
||||
.grid.two {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.stats {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
.history-toolbar {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.row .actions .btn {
|
||||
flex: 1 1 calc(50% - 8px);
|
||||
min-width: 120px;
|
||||
}
|
||||
.history-pager {
|
||||
justify-content: space-between;
|
||||
}
|
||||
.detail-row {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 4px;
|
||||
}
|
||||
.hero h1 {
|
||||
font-size: 19px;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,303 @@
|
||||
(() => {
|
||||
const API_BASE = '/api/rational_lock'
|
||||
const DEFAULT_USERNAME = 'admin'
|
||||
const $ = id => document.getElementById(id)
|
||||
const now = () => Date.now()
|
||||
const uid = () => `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
|
||||
const emptyDB = () => ({ items: [], applications: [] })
|
||||
|
||||
const state = {
|
||||
db: emptyDB(),
|
||||
auth: { currentUser: '' },
|
||||
ui: { editingId: '' }
|
||||
}
|
||||
|
||||
const normalizeDB = value => {
|
||||
const base = value && typeof value === 'object' ? value : {}
|
||||
return {
|
||||
items: Array.isArray(base.items) ? base.items : [],
|
||||
applications: Array.isArray(base.applications) ? base.applications : []
|
||||
}
|
||||
}
|
||||
|
||||
const setAuthStatus = text => {
|
||||
$('auth-status').textContent = text || ''
|
||||
}
|
||||
|
||||
const setFormStatus = text => {
|
||||
$('form-status').textContent = text || ''
|
||||
}
|
||||
|
||||
const openModal = ({ title, message, okText = '知道了', cancelText = '', tone = 'normal' }) => {
|
||||
return new Promise(resolve => {
|
||||
const mask = $('app-modal')
|
||||
const titleBox = $('modal-title')
|
||||
const msgBox = $('modal-message')
|
||||
const actions = $('modal-actions')
|
||||
actions.innerHTML = ''
|
||||
titleBox.textContent = title || ''
|
||||
msgBox.textContent = message || ''
|
||||
const close = val => {
|
||||
mask.style.display = 'none'
|
||||
resolve(val)
|
||||
}
|
||||
if (cancelText) {
|
||||
const cancelBtn = document.createElement('button')
|
||||
cancelBtn.className = 'btn'
|
||||
cancelBtn.type = 'button'
|
||||
cancelBtn.textContent = cancelText
|
||||
cancelBtn.addEventListener('click', () => close(false))
|
||||
actions.appendChild(cancelBtn)
|
||||
}
|
||||
const okBtn = document.createElement('button')
|
||||
okBtn.className = tone === 'primary' ? 'btn btn-primary' : 'btn'
|
||||
okBtn.type = 'button'
|
||||
okBtn.textContent = okText
|
||||
okBtn.addEventListener('click', () => close(true))
|
||||
actions.appendChild(okBtn)
|
||||
mask.style.display = ''
|
||||
})
|
||||
}
|
||||
|
||||
const modalAlert = (title, message) => openModal({ title, message, okText: '我知道了' })
|
||||
const modalConfirm = (title, message, okText = '继续', cancelText = '取消') => openModal({ title, message, okText, cancelText, tone: 'primary' })
|
||||
|
||||
const apiRequest = async (path, options = {}) => {
|
||||
const res = await fetch(`${API_BASE}${path}`, {
|
||||
method: options.method || 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(options.headers || {})
|
||||
},
|
||||
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
||||
credentials: 'include'
|
||||
})
|
||||
let payload = null
|
||||
try {
|
||||
payload = await res.json()
|
||||
} catch {
|
||||
payload = null
|
||||
}
|
||||
if (!res.ok) {
|
||||
const msg = payload && payload.error ? String(payload.error) : `http_${res.status}`
|
||||
throw new Error(msg)
|
||||
}
|
||||
return payload || {}
|
||||
}
|
||||
|
||||
const renderAuth = () => {
|
||||
const user = state.auth.currentUser
|
||||
$('auth-login-box').style.display = user ? 'none' : ''
|
||||
$('auth-logged-box').style.display = user ? '' : 'none'
|
||||
$('app-content').style.display = user ? '' : 'none'
|
||||
$('auth-current-user').textContent = user ? `当前账号:${user}` : ''
|
||||
}
|
||||
|
||||
const loadDB = async () => {
|
||||
if (!state.auth.currentUser) {
|
||||
state.db = emptyDB()
|
||||
return
|
||||
}
|
||||
const payload = await apiRequest('/data')
|
||||
state.db = normalizeDB(payload.db)
|
||||
}
|
||||
|
||||
const saveDB = async () => {
|
||||
if (!state.auth.currentUser) return
|
||||
await apiRequest('/data', { method: 'PUT', body: { db: state.db } })
|
||||
}
|
||||
|
||||
const upsertItem = (name, category, note) => {
|
||||
const normalizedName = String(name || '').trim()
|
||||
if (!normalizedName) return null
|
||||
const normalizedCategory = String(category || '其他').trim() || '其他'
|
||||
const idx = state.db.items.findIndex(it => String(it.name || '').trim().toLowerCase() === normalizedName.toLowerCase() && String(it.category || '').trim() === normalizedCategory)
|
||||
if (idx >= 0) {
|
||||
const next = {
|
||||
...state.db.items[idx],
|
||||
name: normalizedName,
|
||||
category: normalizedCategory,
|
||||
note: String(note || '').trim(),
|
||||
updatedAt: now()
|
||||
}
|
||||
state.db.items[idx] = next
|
||||
return next
|
||||
}
|
||||
const row = {
|
||||
id: uid(),
|
||||
name: normalizedName,
|
||||
category: normalizedCategory,
|
||||
note: String(note || '').trim(),
|
||||
createdAt: now()
|
||||
}
|
||||
state.db.items.unshift(row)
|
||||
return row
|
||||
}
|
||||
|
||||
const renderLibrary = () => {
|
||||
$('btn-lib-add').textContent = state.ui.editingId ? '保存修改' : '加入物品库'
|
||||
$('btn-lib-cancel-edit').style.display = state.ui.editingId ? '' : 'none'
|
||||
const box = $('lib-list')
|
||||
box.innerHTML = ''
|
||||
if (!state.db.items.length) {
|
||||
box.textContent = '暂无物品库记录'
|
||||
return
|
||||
}
|
||||
state.db.items.forEach(it => {
|
||||
const row = document.createElement('div')
|
||||
row.className = 'row'
|
||||
row.innerHTML = `
|
||||
<div><strong>${it.name}</strong> · ${it.category}</div>
|
||||
<div class="meta">${it.note || '无备注'}</div>
|
||||
<div class="actions">
|
||||
<button class="btn" data-lib-edit="${it.id}" type="button">修改</button>
|
||||
<button class="btn" data-lib-del="${it.id}" type="button">删除</button>
|
||||
</div>
|
||||
`
|
||||
box.appendChild(row)
|
||||
})
|
||||
}
|
||||
|
||||
const renderAll = () => {
|
||||
renderAuth()
|
||||
renderLibrary()
|
||||
}
|
||||
|
||||
const bindLibrary = () => {
|
||||
$('btn-lib-add').addEventListener('click', async () => {
|
||||
if (!state.auth.currentUser) {
|
||||
await modalAlert('请先登录', '请先输入账号和密码登录后再操作。')
|
||||
return
|
||||
}
|
||||
const name = $('lib-name').value.trim()
|
||||
const category = $('lib-category').value
|
||||
const note = $('lib-note').value.trim()
|
||||
if (!name) {
|
||||
await modalAlert('无法保存', '请先填写物品名称。')
|
||||
return
|
||||
}
|
||||
if (state.ui.editingId) {
|
||||
const idx = state.db.items.findIndex(it => it.id === state.ui.editingId)
|
||||
if (idx >= 0) {
|
||||
state.db.items[idx] = { ...state.db.items[idx], name, category, note, updatedAt: now() }
|
||||
}
|
||||
state.ui.editingId = ''
|
||||
await modalAlert('✅ 保存成功!', `你改的【${name}】信息已经存好啦,不会丢的~`)
|
||||
} else {
|
||||
upsertItem(name, category, note)
|
||||
await modalAlert('🎉 新增成功!', `你添加的【${name}】已经入库啦,后续会用于同类提醒。`)
|
||||
}
|
||||
await saveDB()
|
||||
$('lib-name').value = ''
|
||||
$('lib-note').value = ''
|
||||
renderLibrary()
|
||||
setFormStatus('物品库已更新')
|
||||
})
|
||||
|
||||
$('btn-lib-cancel-edit').addEventListener('click', () => {
|
||||
state.ui.editingId = ''
|
||||
$('lib-name').value = ''
|
||||
$('lib-note').value = ''
|
||||
renderLibrary()
|
||||
})
|
||||
|
||||
$('lib-list').addEventListener('click', async e => {
|
||||
if (!state.auth.currentUser) {
|
||||
await modalAlert('请先登录', '请先输入账号和密码登录后再操作。')
|
||||
return
|
||||
}
|
||||
const target = e.target
|
||||
const editId = target && target.getAttribute('data-lib-edit')
|
||||
if (editId) {
|
||||
const it = state.db.items.find(x => x.id === editId)
|
||||
if (!it) return
|
||||
state.ui.editingId = editId
|
||||
$('lib-name').value = it.name || ''
|
||||
$('lib-category').value = it.category || '其他'
|
||||
$('lib-note').value = it.note || ''
|
||||
renderLibrary()
|
||||
setFormStatus(`已加载物品库【${it.name}】到编辑区,修改后点击“保存修改”`)
|
||||
return
|
||||
}
|
||||
const id = target && target.getAttribute('data-lib-del')
|
||||
if (!id) return
|
||||
const it = state.db.items.find(x => x.id === id)
|
||||
const ok = await modalConfirm('确认删除', `确认删除物品库中的【${it ? it.name : '该条'}】吗?这不会改动历史申请记录。`, '确定删除', '再想想')
|
||||
if (!ok) return
|
||||
state.db.items = state.db.items.filter(x => x.id !== id)
|
||||
if (state.ui.editingId === id) state.ui.editingId = ''
|
||||
await saveDB()
|
||||
renderLibrary()
|
||||
setFormStatus('物品库已更新')
|
||||
await modalAlert('✅ 删除成功!', `已删除【${it ? it.name : '该物品'}】。历史申请快照不会被改写。`)
|
||||
})
|
||||
}
|
||||
|
||||
const bindAuth = () => {
|
||||
const tryLogin = async () => {
|
||||
const username = String($('login-username').value || '').trim()
|
||||
const password = String($('login-password').value || '')
|
||||
if (!username || !password) {
|
||||
setAuthStatus('请输入账号和密码')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await apiRequest('/auth/login', { method: 'POST', body: { username, password } })
|
||||
} catch {
|
||||
setAuthStatus('账号或密码不正确,请重试')
|
||||
await modalAlert('登录失败', '账号或密码不正确,请重新输入。')
|
||||
return
|
||||
}
|
||||
state.auth.currentUser = username
|
||||
$('login-password').value = ''
|
||||
await loadDB()
|
||||
renderAll()
|
||||
setAuthStatus(`登录成功,欢迎你:${username}`)
|
||||
setFormStatus('可以开始维护物品库')
|
||||
}
|
||||
|
||||
$('btn-login').addEventListener('click', tryLogin)
|
||||
$('login-password').addEventListener('keydown', async e => {
|
||||
if (e.key !== 'Enter') return
|
||||
e.preventDefault()
|
||||
await tryLogin()
|
||||
})
|
||||
$('btn-logout').addEventListener('click', async () => {
|
||||
try { await apiRequest('/auth/logout', { method: 'POST' }) } catch {}
|
||||
state.auth.currentUser = ''
|
||||
state.db = emptyDB()
|
||||
state.ui.editingId = ''
|
||||
$('lib-name').value = ''
|
||||
$('lib-note').value = ''
|
||||
setFormStatus('')
|
||||
setAuthStatus('已退出登录')
|
||||
renderAll()
|
||||
})
|
||||
}
|
||||
|
||||
const bindNav = () => {
|
||||
$('btn-back-main').addEventListener('click', () => {
|
||||
window.location.href = '/tools/rational_lock/index.html'
|
||||
})
|
||||
}
|
||||
|
||||
const init = async () => {
|
||||
bindAuth()
|
||||
bindLibrary()
|
||||
bindNav()
|
||||
try {
|
||||
const payload = await apiRequest('/auth/me')
|
||||
state.auth.currentUser = String(payload.username || DEFAULT_USERNAME)
|
||||
await loadDB()
|
||||
setAuthStatus(`已登录:${state.auth.currentUser}`)
|
||||
setFormStatus('可以开始维护物品库')
|
||||
} catch {
|
||||
state.auth.currentUser = ''
|
||||
state.db = emptyDB()
|
||||
setAuthStatus('请先登录')
|
||||
}
|
||||
renderAll()
|
||||
}
|
||||
|
||||
init()
|
||||
})()
|
||||
Reference in New Issue
Block a user