web 报表功能开发

背景

应同学的要求帮忙做一个他们自己店铺的退货功能,需求很简单,考虑自己也想试试用web来开发下小功能,就试了试。

需求

店铺有多个员工,原本店长(同学)把每天要退货的“订单号”“退货内容”“是否已收货”“签收员”“收货时间”放在excel中,然后不同的人收到货之后设置下“已收货”。如果每人一个excel,那么就会出现不同步,最后统计时很麻烦。

思路

使用mysql作为数据中心,然后提供一个界面给员工操作,其实很简单。

方案一

使用C/S界面,我一般都用mfc/qt 来开发小工具,所以直接就使用mfc来开发客户端,花费了1天的时间,功能基本完成,截图如下,丑是丑了点,但好歹用起来没什么问题

upload successful

如果仅仅这样子就无法满足我的初衷了,我是要用web来做UI的。

方案二

使用B/S开发,linux服务端可以选择用C++、go、python等语言开发,但我打算用openrestry+lua脚本来开发。
总体大概思路:web端发起http请求给openresty, openresty通过lua脚本集成mysql驱动获取数据通过http返回请求。

因为主要是报表的功能,所以需要选择一个js的报表库,我使用的是jsgrid,这个我是随便选的。

1
2
3
4
5
http://js-grid.com/
Lightweight Grid jQuery Plugin
jsGrid is a lightweight client-side data grid control based on jQuery.
It supports basic grid operations like inserting, filtering, editing, deleting, paging,
and sorting. jsGrid is flexible and allows to customize its appearance and components.

step 1

花费半天时间学习jsgrid,网站有demos、api文档介绍,半天时间总算知道如何通过静态数据进行一些报表的操作。

step 2

openrestry 学习。 几年前接触过,所以用起来还算可以,虽说只有简单的几个sql数据但我对lua不熟悉,调试花费了很多时间,一个下午的时间差不多也弄完了。

step 3

通过http请求数据到jsgrid中。其中功能优化、调试大概又花费了半天

step 4

线上真实数据测试。历史数据大概3000条,一次性请求按原来的逐条jsgrid insetitem 方法太慢(3000条需要1分钟),然后再仔细查看api文档、demo、issues 找到了方法一次性插入所有数据,问题解决,花费半天时间。

1
$("#jsGrid").jsGrid("option", "data", lists);

效果如下

upload successful

大体功能应该完成了,自己也熟悉了web的界面开发。

注意点

jsgrid 报表在手机app上显示有个问题,就是如果autoload时如果没数据的化,手机app是无法滑动报表表头的,
只有有数据时才能滑动,所以应该把关键的filter 列放在无数据时也能操作的地方。

代码

又重新写了一遍代码,比原来简单多了

lua

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
local cjson = require "cjson"
local mysql = require("resty.mysql")

local db_config = {
host = '127.0.0.1',
port = 3306,
database = '',
user = '',
password = '',
dbtable = ''
}

local args = ngx.req.get_uri_args()
local action = args['action']

local db,err

--自定义一个函数,time为指定的日期格式YYYY-MM-DD,
--dayChange为指定日期前后天数,用法:前一天 -1 后一天 1.
function dateChange(time,dayChange)
if string.len(time)==10 and string.match(time,"%d%d%d%d%-%d%d%-%d%d") then
local year=string.sub(time,0,4);--年份
local month=string.sub(time,6,7);--月
local day=string.sub(time,9,10);--日
local time=os.time({year=year, month=month, day=day})+dayChange*86400 --一天86400秒
return (os.date('%Y',time).."-"..os.date('%m',time).."-"..os.date('%d',time))
end
end

function dbinit()
db, err = mysql:new()
if not db then
ngx.say("failed to instantiate mysql: ", err)
return
end

db:set_timeout(5000) -- 5 sec

local ok, err, errcode, sqlstate = db:connect({
host = db_config.host,
port = db_config.port,
database = db_config.database,
user = db_config.user,
password = db_config.password})

if not ok then
ngx.log(ngx.ERR, "failed to connect: ", err, ": ", errcode, " ", sqlstate)
return
end
end

function dbclose()
-- or just close the connection right away:
local ok, err = db:close()
if not ok then
ngx.say("failed to close: ", err)
return
end
end

function getdatafromsql(sql)
local res, err, errno, sqlstate = db:query(sql)

if not res then
ngx.say(cjson.encode({code=200, message=err, data=nil}))
else
ngx.say(cjson.encode({code=200, message="", data=res}))
end
end

-- http://localhost/cailei?action=GetDataByFiter
-- json
function GetDataByFiter()

local order
local cancell
local intime
local filtersql
ngx.req.read_body()

local args, err = ngx.req.get_post_args()
if not args then
ngx.say("failed to get post args: ", err)
return
end

-- ngx 把参数都作为字符串处理
for key, val in pairs(args) do
if key == 'ordernum' then
order=val
elseif key == 'timefilter' then
intime=val
elseif key == 'cancell' then
cancell=val
end
end

if intime == '' then
ngx.say("must take time")
return
end

if intime == '0' then -- 近3天
intime = dateChange(ngx.today(),-3)
elseif intime == '1' then -- 近7天
intime = dateChange(ngx.today(),-7)
elseif intime == '2' then -- 近半月
intime = dateChange(ngx.today(),-15)
elseif intime == '3' then -- 近一月
intime = dateChange(ngx.today(),-30)
end

--intime 是必须携带的
if intime ~= '' then
filtersql = ' where intime>=\''..intime..'\''
end

if order ~= '' then
filtersql = filtersql..' and ordernum LIKE \'%'..order..'%\''
end
if cancell ~= '-1' then
filtersql = filtersql..' and cancell='..(cancell)
end

local sql = 'SELECT * FROM '..db_config.dbtable..filtersql

--ngx.say(sql)
getdatafromsql(sql)
end

-- http://localhost/cailei?action=UpdateData
-- json
function UpdateData()
ngx.req.read_body()
local order
local name
local optime
local cancell
local remark
local args, err = ngx.req.get_post_args()
if not args then
ngx.say("failed to get post args: ", err)
return
end

-- ngx 把参数都作为字符串处理
for key, val in pairs(args) do
if key == 'ordernum' then
order=val
elseif key == 'name' then
name=val
elseif key == 'cancell' then
cancell=val
elseif key == 'remark' then
remark = val
elseif key == 'timestamp' then
optime = val
end
end

-- optime = ngx.localtime()

if order == nil then
ngx.say("failed to get post args: ", err)
return
end

local sql = 'update '..db_config.dbtable..' set cancell='..cancell..' , remark=\''..remark..'\''..' ,name=\''..name..'\',timestamp=\''..optime..'\' where ordernum =\''..order..'\''
--ngx.say(sql)
getdatafromsql(sql)
end


if action == 'GetDataByFiter' then
dbinit()
GetDataByFiter()
dbclose()
elseif action == 'UpdateData' then
dbinit()
UpdateData()
dbclose()
end

js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta charset="UTF-8">
<title>退库系统</title>
<link rel="stylesheet" type="text/css" href="demos.css" />
<link href='http://fonts.googleapis.com/css?family=Open+Sans:300,600,400' rel='stylesheet' type='text/css'>

<link rel="stylesheet" type="text/css" href="../css/jsgrid.css" />
<link rel="stylesheet" type="text/css" href="../css/theme.css" />

<script src="../external/jquery/jquery-1.8.3.js"></script>
<link rel="stylesheet" href="http://code.jquery.com/ui/1.11.2/themes/cupertino/jquery-ui.css">
<script src="http://code.jquery.com/jquery-1.10.2.js"></script>
<script src="http://code.jquery.com/ui/1.11.2/jquery-ui.js"></script>
<script src="http://ajax.aspnetcdn.com/ajax/jquery.validate/1.9/jquery.validate.min.js"></script>

<script src="../src/jsgrid.core.js"></script>
<script src="../src/jsgrid.load-indicator.js"></script>
<script src="../src/jsgrid.load-strategies.js"></script>
<script src="../src/jsgrid.sort-strategies.js"></script>
<script src="../src/jsgrid.field.js"></script>
<script src="../src/fields/jsgrid.field.text.js"></script>
<script src="../src/fields/jsgrid.field.number.js"></script>
<script src="../src/fields/jsgrid.field.select.js"></script>
<script src="../src/fields/jsgrid.field.checkbox.js"></script>
<script src="../src/fields/jsgrid.field.control.js"></script>

<style>
.ui-widget *, .ui-widget input, .ui-widget select, .ui-widget button {
font-family: 'Helvetica Neue Light', 'Open Sans', Helvetica;
font-size: 14px;
font-weight: 300 !important;
}

.details-form-field input,
.details-form-field select {
width: 250px;
float: right;
}

.details-form-field {
margin: 30px 0;
}

.details-form-field:first-child {
margin-top: 10px;
}

.details-form-field:last-child {
margin-bottom: 10px;
}

.details-form-field button {
display: block;
width: 100px;
margin: 0 auto;
}

input.error, select.error {
border: 1px solid #ff9999;
background: #ffeeee;
}

label.error {
float: right;
margin-left: 100px;
font-size: .8em;
color: #ff6666;
}
</style>
</head>
<body>
<h1>退库数据</h1>

<div id="detailsDialog">
<form id="detailsForm">
<div class="details-form-field">
<label for="order">订单号:</label>
<input id="order" name="订单号" type="text" readonly="readonly" />
</div>
<div class="details-form-field">
<label for="name">名字:</label>
<select id="name" name="名字">
<option value="蔡磊">蔡磊</option>
<option value="包包">包包</option>
</select>
</div>
<div class="details-form-field">
<label for="cancell">是否已退库:</label>
<select id="cancell" name="是否已退库">
<option value="0">未退库</option>
<option value="1">已退库</option>
</select>
</div>
<div class="details-form-field">
<label for="remark">备注:</label>
<input id="remark" name="备注" type="text" />
</div>
<div class="details-form-field">
<button type="submit" id="save">修改</button>
</div>
</form>
</div>


<div id="jsGrid"></div>

<script>
let svrurl = "http://localhost/";

itemcancell = [
{ Name: "全部", Id: -1 },
{ Name: "未退库", Id: 0 },
{ Name: "已退库", Id: 1 }
];

itemintime = [
{ Name: "近三天", Id: 0 },
{ Name: "近七天", Id: 1 },
{ Name: "近半月", Id: 2 },
{ Name: "近一月", Id: 3 }
];

Date.prototype.Format = function (fmt) { // author: meizz
var o = {
"M+": this.getMonth() + 1, // 月份
"d+": this.getDate(), // 日
"h+": this.getHours(), // 小时
"m+": this.getMinutes(), // 分
"s+": this.getSeconds(), // 秒
"q+": Math.floor((this.getMonth() + 3) / 3), // 季度
"S": this.getMilliseconds() // 毫秒
};
if (/(y+)/.test(fmt))
fmt = fmt.replace(RegExp.$1, (this.getFullYear() + "").substr(4 - RegExp.$1.length));
for (var k in o)
if (new RegExp("(" + k + ")").test(fmt)) fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
return fmt;
}

$(function() {

$("#jsGrid").jsGrid({
height: "70%",
width: "100%",
autoload:true,
editing: true,
paging: true,
filtering: true,
pageButtonCount: 5,
rowClick: function(args) {
showDetailsDialog("修改", args.item);
},
controller: {
loadData: function(filter) {

var d = $.Deferred();
$.ajax({
type: "post",
url: svrurl+"cailei?action=GetDataByFiter",
data: filter,
dataType: "json",
success: function(res,status,xhr) {
d.resolve(res.data);
},
error: function(res,status){
console.log(status);
}
});

return d.promise();

},
updateItem: function(updatingClient) {

}
},

fields: [
{ name: "ordernum", title: "订单号",type: "text",width: 150 ,editing: false,visible: true },// 不指定类型,则该field不可编辑
{ name: "remark",title: "备注",sorting: false,editing: false},
{ name: "cancell",title: "是否已退库", type: "select", items: itemcancell, valueField: "Id", textField: "Name",selectedIndex: 0},
{ name: "name",title: "收货员", width: 150 ,editing: false},
{ name: "timestamp",title: "收货时间",readOnly: true},
{ name: "intime",title: "入库时间",readOnly: true,editing: false},
{ name: "timefilter",title: "最近搜索", type: "select", items: itemintime, valueField: "Id", textField: "Name",selectedIndex: 0}
// { name: "timefilter",title: "最近搜索",type:"select",items: db.time, valueField:"Id",textField: "Name",selectedIndex: 0}
// { type: "control", modeSwitchButton: false,deleteButton:false }
],
});

$("#detailsDialog").dialog({
autoOpen: false,
width: 400,
close: function() {
$("#detailsForm").validate().resetForm();
$("#detailsForm").find(".error").removeClass("error");
}
});

$("#detailsForm").validate({
rules: {
name: "required",
cancell: "required"
},
messages: {
name: "请选择",
cancell: "请选择"
},
submitHandler: function() {
formSubmitHandler();
}
});

var formSubmitHandler = $.noop;

var showDetailsDialog = function(dialogType, client) {
$("#cancell").val(client.cancell);
$("#remark").val(client.remark);
$("#order").val(client.ordernum);

formSubmitHandler = function() {
saveClient(client);
};

$("#detailsDialog").dialog("option", "title", dialogType + " 记录")
.dialog("open");
};

var saveClient = function(client) {
client.name = $("#name").val();
client.cancell = parseInt($("#cancell").val(),10);
client.remark = $("#remark").val();
client.timestamp = new Date().Format("yyyy-MM-dd hh:mm:ss");

$.ajax({
url: svrurl+"cailei?action=UpdateData",
data: client,
type: "post",
dataType: "json",
success: function(res,status,xhr)
{

},
error: function(res,status)
{

}
});
$("#jsGrid").jsGrid("updateItem", client);

$("#detailsDialog").dialog("close");
};

});

</script>
</body>
</html>