15 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 01 | 02 | execute | 1 |
|
true |
|
|
Purpose: 提供用户交互界面(per D-01: 按钮+弹窗形式,不新增独立页面/菜单;per D-03: Layer 弹窗展示) Output: toolbar 按钮 + Layer 弹窗 HTML + 结果渲染逻辑
<execution_context> @D:/code/php/amlhc/.claude/get-shit-done/workflows/execute-plan.md @D:/code/php/amlhc/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/01-omitted-number-analysis/01-RESEARCH.md @D:/code/php/amlhc/public/assets/js/backend/history.js @D:/code/php/amlhc/application/admin/view/history/index.htmlFrom public/assets/js/backend/history.js:
define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) {
var Controller = {
index: function () { ... },
add: function () { ... },
edit: function () { ... },
api: {
colorMap: {}, // 波色映射缓存 {num: colorText}
colorMapLoaded: false, // 是否已加载
loadColorMap: function (callback) { ... }, // 异步加载,完成后调用 callback
getColorByNum: function (num) { ... }, // 返回 CSS 颜色值字符串
formatter: {
numBall: function (value, row, index) { ... } // 表格内号码球渲染
},
bindevent: function () { ... }
}
};
return Controller;
});
From application/admin/view/history/index.html:
- toolbar 区域 id="toolbar",已有刷新按钮
- 使用 Bootstrap 3 样式类:btn btn-primary, form-control, text-center, text-danger 等
- Layer 对象全局可用(通过 requirejs 加载 fastadmin-layer)
FastAdmin AJAX response format:
{ "code": 1, "msg": "查询成功", "data": [...] } // success
{ "code": 0, "msg": "错误信息" } // error
code === 1 表示成功。
Layer.open() API:
Layer.open({
type: 1, // 1 = page 类型,使用 content 中的 HTML
title: '标题',
area: ['650px', '550px'], // [width, height]
content: html, // HTML 字符串
shadeClose: true // 点击遮罩关闭
});
当前 toolbar 内容(第 8-11 行):
<div id="toolbar" class="toolbar">
<a href="javascript:;" class="btn btn-primary btn-refresh" title="{:__('Refresh')}" ><i class="fa fa-refresh"></i> </a>
<!-- <a href="javascript:;" class="btn btn-success btn-add ...
</div>
在刷新按钮之后、注释的添加按钮之前,插入遗漏号码按钮:
<a href="javascript:;" class="btn btn-warning btn-missingnum" title="{:__('Missing Number Analysis')}"><i class="fa fa-search"></i> {:__('Missing Number Analysis')}</a>
完整 toolbar 变为:
<div id="toolbar" class="toolbar">
<a href="javascript:;" class="btn btn-primary btn-refresh" title="{:__('Refresh')}" ><i class="fa fa-refresh"></i> </a>
<a href="javascript:;" class="btn btn-warning btn-missingnum" title="{:__('Missing Number Analysis')}"><i class="fa fa-search"></i> {:__('Missing Number Analysis')}</a>
<!-- <a href="javascript:;" class="btn btn-success btn-add {:$auth->check('history/add')?'':'hide'}" title="{:__('Add')}" ><i class="fa fa-plus"></i> {:__('Add')}</a>-->
</div>
关键实现细节:
- 使用
btn-warning样式(黄色按钮,区别于刷新按钮的蓝色) - class 必须包含
btn-missingnum(JS 事件绑定选择器) - 图标使用
fa fa-search(搜索图标,符合查询语义) - 文本使用
{:__('Missing Number Analysis')}通过 i18n 函数获取(对应 plan 01 task 3 添加的语言键) - title 属性同样使用 i18n
<acceptance_criteria>
- application/admin/view/history/index.html 的 toolbar 中包含 class="btn-missingnum" 的 元素
- 按钮 class 包含 btn-warning
- 按钮包含 {:__('Missing Number Analysis')} 文本调用
- 按钮图标 class 为 fa fa-search
- 按钮位于 btn-refresh 之后 </acceptance_criteria> grep "btn-missingnum" application/admin/view/history/index.html history 页面 toolbar 出现黄色"遗漏号码"按钮,使用 i18n 文本和搜索图标
位置 1:在 Controller.index() 函数内,Table.api.bindevent(table) 调用之后,添加按钮事件绑定:
在 Table.api.bindevent(table); 这一行之后添加:
// 遗漏号码按钮事件
$(document).off('click', '.btn-missingnum').on('click', '.btn-missingnum', function () {
Controller.api.showMissingNumDialog();
});
使用 off().on() 防止重复绑定(FastAdmin 在 tab 切换时可能重复初始化)。
位置 2:在 Controller.api 对象中添加三个新方法(在 bindevent 方法之前):
/**
* 显示遗漏号码分析弹窗
*/
showMissingNumDialog: function () {
var html = '<div style="padding:20px;">' +
'<div class="form-group">' +
' <label>' + __('Query Periods') + ':</label>' +
' <input type="number" id="missing-periods" class="form-control" value="10" min="1" max="100" style="width:120px;display:inline-block;">' +
' <button class="btn btn-primary" id="btn-missing-query" style="margin-left:10px;"><i class="fa fa-search"></i> ' + __('Query') + '</button>' +
'</div>' +
'<div id="missing-result" style="margin-top:15px;"></div>' +
'</div>';
Layer.open({
type: 1,
title: __('Missing Number Analysis'),
area: ['650px', '550px'],
content: html,
shadeClose: true,
success: function (layero, index) {
// 绑定查询按钮事件
$('#btn-missing-query', layero).on('click', function () {
var periods = parseInt($('#missing-periods', layero).val()) || 10;
Controller.api.queryMissingNum(periods, layero);
});
}
});
},
/**
* 查询遗漏号码
*/
queryMissingNum: function (periods, layero) {
// 确保颜色映射已加载
if (!Controller.api.colorMapLoaded) {
Controller.api.loadColorMap(function () {
Controller.api._doQueryMissingNum(periods, layero);
});
} else {
Controller.api._doQueryMissingNum(periods, layero);
}
},
/**
* 执行遗漏号码查询(内部方法)
*/
_doQueryMissingNum: function (periods, layero) {
$('#missing-result', layero).html('<div class="text-center"><i class="fa fa-spinner fa-spin"></i> ' + __('Loading') + '</div>');
$.ajax({
url: 'history/missingNum',
type: 'GET',
data: { periods: periods },
dataType: 'json',
success: function (ret) {
if (ret.code == 1) {
Controller.api.renderMissingNum(ret.data, periods, layero);
} else {
$('#missing-result', layero).html('<div class="alert alert-danger">' + (ret.msg || __('Query failed')) + '</div>');
}
},
error: function () {
$('#missing-result', layero).html('<div class="alert alert-danger">' + __('Query failed') + '</div>');
}
});
},
/**
* 渲染遗漏号码结果
*/
renderMissingNum: function (data, periods, layero) {
if (!data || data.length === 0) {
$('#missing-result', layero).html('<div class="alert alert-info">' + __('No missing numbers found') + '</div>');
return;
}
var html = '<div style="display:flex;flex-wrap:wrap;gap:12px;">';
for (var i = 0; i < data.length; i++) {
var color = Controller.api.getColorByNum(data[i].num);
html += '<div style="text-align:center;">' +
'<span class="num-ball" style="display:inline-block;width:48px;height:48px;line-height:48px;text-align:center;border-radius:50%;color:#fff;background-color:' + color + ';font-weight:bold;font-size:18px;">' + data[i].num + '</span>' +
'<div style="margin-top:5px;font-size:12px;color:#666;">' + __('Missing') + ' ' + data[i].omit + ' ' + __('periods') + '</div>' +
'</div>';
}
html += '</div>';
$('#missing-result', layero).html(html);
},
关键实现细节:
- 弹窗 HTML 使用 Bootstrap 3 form-group 和 form-control 样式
- 输入框 type="number",默认值 10,min=1,max=100,宽度 120px 内联显示
- 查询按钮在输入框右侧,margin-left:10px
- 结果区域 id="missing-result",初始为空
- Layer.open 使用 type:1(page 类型),area: ['650px', '550px'],shadeClose:true(点击遮罩关闭)
- success 回调中绑定查询按钮事件,使用 layero 作为上下文选择器根(
$('#btn-missing-query', layero)) - queryMissingNum 先检查 colorMapLoaded,未加载则先调用 loadColorMap
- AJAX 使用标准 $.ajax,url 为 'history/missingNum',type 为 'GET'
- 响应判断
ret.code == 1(FastAdmin 成功标志) - 加载状态显示 spinner + 文字
- 渲染时使用 flex 布局(display:flex; flex-wrap:wrap; gap:12px)展示球网格
- 每个球 48x48px,圆角 50%,白色文字,背景色来自 getColorByNum()
- 球下方显示"遗漏 X 期"文字,12px 灰色字体
- 所有文本使用 __('key') 获取 i18n
- 空结果时显示 alert-info 提示
不要修改现有的 Controller.index、Controller.add、Controller.edit 方法签名。只在 index 函数内追加按钮事件绑定,在 api 对象内追加新方法。
<acceptance_criteria>
- public/assets/js/backend/history.js 的 Controller.index() 中包含 $(document).off('click', '.btn-missingnum').on('click', '.btn-missingnum'
- Controller.api 对象包含 showMissingNumDialog 方法
- Controller.api 对象包含 queryMissingNum 方法
- Controller.api 对象包含 _doQueryMissingNum 方法
- Controller.api 对象包含 renderMissingNum 方法
- showMissingNumDialog 调用 Layer.open({type: 1, ...})
- Layer.open 的 content 包含 input type="number" id="missing-periods"
- queryMissingNum 检查 colorMapLoaded 状态
- _doQueryMissingNum 使用 $.ajax 请求 url: 'history/missingNum'
- renderMissingNum 使用 flex-wrap 布局渲染球网格
- renderMissingNum 调用 Controller.api.getColorByNum() 获取颜色
- 结果球显示数字 + 下方"遗漏 X 期"文字
</acceptance_criteria>
grep -c "showMissingNumDialog|queryMissingNum|renderMissingNum|btn-missingnum" public/assets/js/backend/history.js | awk '$1 >= 4'
history.js 有完整的遗漏号码按钮处理、Layer 弹窗(含期数输入和查询按钮)、AJAX 请求、结果渲染(波色球+遗漏期数)逻辑
<threat_model>
Trust Boundaries
| Boundary | Description |
|---|---|
| User input (number field) → AJAX request | 用户可能在浏览器开发者工具中修改 periods 值 |
| Layer dialog HTML injection | 弹窗 HTML 由 JS 拼接,无外部输入注入风险 |
STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|---|---|---|---|---|
| T-01-05 | Tampering | 前端 periods 输入值 | mitigate | 后端已有 range 校验 1-100;前端 HTML input min/max 为 UX 辅助,不依赖前端校验 |
| T-01-06 | Information Disclosure | Layer 弹窗内容 | accept | 仅展示遗漏号码统计数据,无敏感信息 |
| </threat_model> |
<success_criteria>
- history 页面 toolbar 有"遗漏号码"按钮(btn-warning 样式)
- 点击按钮弹出 Layer 弹窗,标题为"遗漏号码分析"
- 弹窗内有期数输入框(默认值 10,范围 1-100)和查询按钮
- 点击查询后显示加载状态,然后显示结果
- 结果以 flex 网格展示,每个球显示号码、波色、遗漏期数
- 波色球颜色与表格中一致(复用 getColorByNum)
- 无遗漏号码时显示友好提示
- 点击遮罩可关闭弹窗 </success_criteria>