Laravel后台管理扩展包——Voyager简介(一)

在平常的开发中,无论是工作上还是自己想搞点有趣的东西的时候,经常遇到需要开发一个后台数据维护系统用来给前台应用提供数据,这时候我们可能会想到内容管理系统。可是仅仅从维护数据的角度需求来看,大部分内容管理系统的功能都过于复杂,而且二次开发起来费时费力,其实我们只不过是要一个增删改查而已,要是不用写代码就更好了,下面介绍的Voyager或许可以正好满足这种需求。

Voyager简介

github

https://github.com/the-control-group/voyager

Voyager是一个依赖于Laravel框架的开箱即用的后台管理Package,包含了数据库BREAD(CRUD),媒体管理,菜单构建,权限管理等系列功能,对我来说最吸引我使用的特性是它不像一个CMS系统那么冗杂,用来做一个后台管理十分快捷方便,当然此类专用于Admin快速搭建的开源框架可以说一直就很常见,但是在写这篇文章时,Voyager 1.0正式版还未发布,在github上已经收获了3600多star,说明其却有过人之处,使用几天下来体验也的确不错。

Voyager安装

Voyager因为是依赖于Laravel的,所以前提是装好Laravel以及Composer,这个不在多说了,我们从创建一个新的project说起吧。

创建laravel project

1
2
cd your_project_location
laravel new voyager-demmo

用Composer引入voyager

1
2
cd voyager-demmo
composer require tcg/voyager

这里说一下,因为在写这篇blog时候,voyager 1.0 正式版还未发布,voyager 1.0带来了一些新的特性,其中对我最重要的就是支持多语言,不用在源代码中翻译了,不然实在是一件很头疼的事情,所以我实际composer使用的下面这个命令。

1
composer require tcg/voyager:dev-v1.0-pre-release

先使用了pre release版,bug还是有一些的,但本身代码不复杂,可以自己尝试修复,或者在github issue中找找答案,待1.0发布后就直接使用正式版就可以了,bug会少一点。

配置数据库

composer完成后,先配置laravel的数据库连接,也就是laravel的.env文件,修改数据库部分,使应用可以正常连接mysql。

添加provider

配置文件添加voyager的provider,打开config/app.php,在providers数组中添加TCG\Voyager\VoyagerServiceProvider::class,

如下:

1
2
3
4
5
6
7
8
9
10
11
'providers' => [
// Laravel Framework Service Providers...
//...
// Package Service Providers
TCG\Voyager\VoyagerServiceProvider::class,
// ...
// Application Service Providers
// ...
],

install voyager

最后使用Laravel的artisan工具安装voyager,主要就是一些数据库的导入和资源文件的编译等,命令如下(mysql 5.7.7以下的版本先看下方的低版本注意):

1
php artisan voyager:install --with-dummy

--with-dummy会添加一些测试数据用来尝试各项功能,如果不需要则去掉该选项,执行完毕后,至此安装完成,启动http服务后,如果服务在本地的话直接访问你的localhost/admin就行了,或者干脆使用php自带的http服务器,在项目目录下使用php artisan serve就行了,注意下端口号,如果有端口号别忘了改下.env文件的APP_URL为正确的访问地址,不然资源文件会无法访问。
默认创建的管理员账户为:

1
2
email: admin@admin.com
password: password

下面说一下几个需要我们优化的部分和一些主要的功能及自定义开发的一些详情。

低版本mysql注意

mysql 5.7.7之前的版本会提示一些错误,无法安装,下面是laravel官方给出的说明。

https://laravel-news.com/laravel-5-4-key-too-long-error

Laravel 5.4: Specified key was too long error
Laravel 5.4 made a change to the default database character set, and it’s now utf8mb4 which includes support for storing emojis. This only affects new applications and as long as you are running MySQL v5.7.7 and higher you do not need to do anything.

For those running MariaDB or older versions of MySQL you may hit this error when trying to run migrations:

[Illuminate\Database\QueryException]
SQLSTATE[42000]: Syntax error or access violation: 1071 Specified key was too long; max key length is 767 bytes (SQL: alter table users add unique users_email_unique(email))

[PDOException]
SQLSTATE[42000]: Syntax error or access violation: 1071 Specified key was too long; max key length is 767 bytes
As outlined in the Migrations guide to fix this all you have to do is edit your AppServiceProvider.php file and inside the boot method set a default string length:

1
2
3
4
5
use Illuminate\Support\Facades\Schema;
public function boot()
{
Schema::defaultStringLength(191);
}

After that everything should work as normal.

简单来说就是是5.4版本的 Laravel 将 mysql 默认字符编码换成了utf8mb4用来支持emoji之类的字符,老版本的mysql会提示字符长度错误,所以需要添加一个默认方法来修正这个问题,高版本数据库则什么都不用做。

找到app/Providers/AppServiceProvider.php
useIlluminate\Support\Facades\Schema
boot()中添加一个Schema::defaultStringLength(191);方法。

系统翻译

系统如果自己使用还好,要是给别人用,中文UI肯定是必须的,v1.0提供了多语言功能,可惜预置的本地化文件中并没有包含中文语言文件,所以这个我们需要自己翻译下,量不大也没什么难度,大家都可以轻松搞定,我会把我翻译的语言文件粘在文章最后,时间匆忙,简单粗略的翻译下,肯定有词不达意和错误的地方,但可以参考下,下面主要说一下如何使用语言文件。

创建语言文件

首先在resources/lang/下面建立一个名为zh_cn的文件夹,然后将vendor/tcg/voyager/publishable/lang/en中的voyager.php文件拷贝到刚才建立的zh-cn文件夹,然后翻译这个zh-cn/voyager.php文件就行了,该文件中为一个数组,描述着翻译的对应关系,我们把其中的英文键值全部翻译为中文就行了。

配置系统locate

打开config/app.php,修改locale的值为zh_cn,fallback_locale的值为en,这样支持多语言的模块会首先去lang/zh_cn找文件,找不到会去lang/en下找。

至此基本系统就可以使用了,啰啰嗦嗦码了这么多字,后面的内容分到下一篇文章吧,而且也正在使用这个系统进行开发,索性吧后续遇到的问题都归纳到这系列文章里面吧。

我的翻译(zh-cn/voyager.php

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
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
<?php
return [
'date' => [
'last_week' => '上周',
'last_year' => '去年',
'this_week' => '本周',
'this_year' => '本年',
],
'generic' => [
'action' => '操作',
'actions' => '操作',
'add' => '新建',
'add_folder' => '新建目录',
'add_new' => '添加',
'all_done' => '全部完成',
'are_you_sure' => '请确认',
'are_you_sure_delete' => '确认删除',
'auto_increment' => '自动增长',
'browse' => '浏览',
'builder' => '构建',
'cancel' => '取消',
'choose_type' => '选择类型',
'click_here' => '点击',
'close' => '关闭',
'created_at' => '创建时间',
'custom' => '自定义',
'dashboard' => '控制台',
'database' => '数据库',
'default' => '默认',
'delete' => '删除',
'delete_confirm' => '删除',
'delete_question' => '确认删除',
'delete_this_confirm' => '删除',
'deselect_all' => '全不选',
'download' => '下载',
'edit' => '编辑',
'email' => '邮箱',
'error_deleting' => '抱歉,在删除过程中出现问题',
'exception' => '异常',
'featured' => '特色',
'field_does_not_exist' => '字段不存在',
'how_to_use' => '如何使用',
'index' => '索引',
'internal_error' => '网络错误',
'items' => '条目',
'keep_sidebar_open' => '保持右侧菜单打开',
'key' => 'Key',
'last_modified' => '最后更新',
'length' => '长度',
'login' => '登陆',
'media' => '媒体',
'menu_builder' => '菜单构建',
'move' => '移动',
'name' => '名称',
'new' => '新建',
'no' => '否',
'no_thanks' => '不 谢谢',
'not_null' => '不为空',
'options' => '选项',
'password' => '密码',
'permissions' => '权限',
'profile' => '资料',
'public_url' => '公共 URL',
'read' => '读取',
'rename' => '重命名',
'required' => '必填',
'return_to_list' => '返回列表',
'route' => '路由',
'save' => '保存',
'search' => '搜索',
'select_all' => '全选',
'settings' => '设置',
'showing_entries' => 'Showing :from to :to of :all entrie|Showing :from to :to of :all entries',
'submit' => '提交',
'successfully_added_new' => '添加完成',
'successfully_deleted' => '删除完成',
'successfully_updated' => '更新完成',
'timestamp' => '时间戳',
'title' => '标题',
'type' => '类型',
'unsigned' => '无符号',
'unstick_sidebar' => '分开菜单',
'update' => '保存',
'update_failed' => '保存失败',
'upload' => '上传',
'url' => 'URL',
'view' => '预览',
'viewing' => '浏览',
'yes' => '是',
'yes_please' => '好的',
],
'login' => [
'loginin' => '登陆',
'signin_below' => '在此登陆:',
'welcome' => '欢迎',
],
'profile' => [
'avatar' => '头像',
'edit' => '编辑资料',
'edit_user' => '编辑用户',
'password' => '密码',
'password_hint' => '留空保持原密码',
'role' => '角色',
'user_role' => '用户角色',
],
'settings' => [
'usage_help' => '通过调用,您可以在任何地方获得每个设置的值',
'save' => '保存设置',
'new' => '创建新设置项',
'help_name' => '设置名称 例: Admin Title',
'help_key' => '设置键值 例: admin_title',
'help_option' => '(可选, 只适用于某些类型,如下拉框或单选按钮)',
'add_new' => '添加新设置',
'delete_question' => '确定删除 :setting ?',
'delete_confirm' => '确定删除',
'successfully_created' => '创建完成',
'successfully_saved' => '保存完成',
'successfully_deleted' => '删除完成',
'already_at_top' => '这已经在最顶部了',
'already_at_bottom' => '这已经在最底部了',
'moved_order_up' => '向上移动 :name',
'moved_order_down' => '向下移动 :name',
'successfully_removed' => '移除 :name 值完成',
],
'media' => [
'add_new_folder' => '添加文件夹',
'audio_support' => '您的浏览器不支持 audio 元素.',
'create_new_folder' => '创建文件夹',
'delete_folder_question' => '删除文件夹将同时删除其中的文件和文件夹',
'destination_folder' => '目标文件夹',
'drag_drop_info' => '拖拽文件到这里或点击上传按钮',
'error_already_exists' => '文件已存在.',
'error_creating_dir' => '创建文件夹失败, '.
'请确认权限',
'error_deleting_file' => '删除文件失败 '.
'请确认权限',
'error_deleting_folder' => '删除文件夹失败 '.
'请确认权限',
'error_may_exist' => '文件名已存在'.
'请先删除.',
'error_moving' => '文件移动失败 '.
'请确认权限.',
'error_uploading' => '上传失败: 未知错误!',
'folder_exists_already' => '文件夹已存在 '.
'您可以将原来的文件夹删除',
'image_does_not_exist' => '图片不存在',
'image_removed' => '图片移动完成',
'library' => '媒体库',
'loading' => '加载中...',
'move_file_folder' => '移动 文件/文件夹',
'new_file_folder' => '新建 文件/文件夹',
'new_folder_name' => '新文件夹名称',
'no_files_here' => '无文件.',
'no_files_in_folder' => '该文件夹无文件.',
'nothing_selected' => '未选择文件或文件夹',
'rename_file_folder' => '重命名',
'success_uploaded_file' => '文件上传完成!',
'success_uploading' => '图片上传完成!',
'uploading_wrong_type' => '上传失败: 格式不支持或文件体积过大!',
'video_support' => '您的浏览器不支持 video 标签.',
],
'menu_builder' => [
'color' => 'RGB 或 hex (可选)',
'color_ph' => '颜色 (ex. #ffffff or rgb(255, 255, 255)',
'create_new_item' => '创建新的菜单项',
'delete_item_confirm' => '确认删除该菜单项',
'delete_item_question' => '确认删除?',
'drag_drop_info' => '拖拽以重新排序',
'dynamic_route' => '动态路由',
'edit_item' => '编辑菜单项',
'icon_class' => '系统提供的图标 (',
'icon_class2' => '参看此页</a> )',
'icon_class_ph' => '图标类名 (可选)',
'item_route' => '菜单项的路由',
'item_title' => '菜单项标题',
'link_type' => '超链接类型',
'new_menu_item' => '新的菜单项',
'open_in' => '打开方式',
'open_new' => '新窗口打开',
'open_same' => '同窗口打开',
'route_parameter' => '路由参数 (如果存在)',
'static_url' => '静态路由',
'successfully_created' => '菜单项创建完成.',
'successfully_deleted' => '菜单项删除完成.',
'successfully_updated' => '菜单项更新完成.',
'updated_order' => '菜单项顺序更新完成.',
'url' => 'URL',
'usage_hint' => '你可以在任意位置通过调用输出菜单',
],
'post' => [
'category' => '文章分类',
'content' => '文章内容',
'details' => '文章详细',
'excerpt' => '简介 <small>简单描述</small>',
'image' => '文章图片',
'meta_description' => '标签描述',
'meta_keywords' => '标签关键字',
'new' => '新建文章',
'seo_content' => 'SEO内容',
'seo_title' => 'Seo辩题',
'slug' => 'URL别名',
'status' => '文章状态',
'status_draft' => '草稿',
'status_pending' => '带发布',
'status_published' => '已发布',
'title' => '文章标题',
'title_sub' => '副标题',
'update' => '更新',
],
'database' => [
'add_bread' => '为该表添加BREAD',
'add_new_column' => '添加字段',
'add_softdeletes' => '添加软删除',
'add_timestamps' => '添加时间戳',
'already_exists' => '已经存在',
'already_exists_table' => '数据表 :table 已经存在',
'bread_crud_actions' => 'BREAD/CRUD 动作',
'bread_info' => 'BREAD信息',
'column' => '列',
'composite_warning' => '警告: 该列是复合索引的一部分',
'controller_name' => '控制器名称',
'controller_name_hint' => '例. PageController, 如果为空则使用 BREAD 控制器',
'create_bread_for_table' => '为 :table 创建 BREAD',
'create_migration' => '创建 migration?',
'create_model_table' => '创建 model?',
'create_new_table' => '新建表',
'create_your_new_table' => '新建表',
'default' => '默认',
'delete_bread' => '删除 BREAD',
'delete_bread_before_table' => '删除表前请先删除该表的 BREAD 信息.',
'delete_table_bread_conf' => '删除 BREAD',
'delete_table_bread_quest' => '确认删除表 :table 的BREAD?',
'delete_table_confirm' => '删除该表',
'delete_table_question' => '确认删除 :table 表?',
'description' => '描述',
'display_name' => '显示名称',
'display_name_plural' => '显示名称 (多条)',
'display_name_singular' => '显示名称 (单条)',
'edit_bread' => '编辑 BREAD',
'edit_bread_for_table' => '编辑表:table BREAD',
'edit_rows' => '编辑 :table 表数据',
'edit_table' => '编辑 :table 表结构',
'edit_table_not_exist' => '所编辑的表不存在',
'error_creating_bread' => '创建BREAD时出现错误',
'error_removing_bread' => '删除BREAD时出现错误',
'error_updating_bread' => '更新BREAD时出现错误',
'extra' => '拓展',
'field' => '字段',
'field_safe_failed' => '保存字段 :field 失败,操作撤销',
'generate_permissions' => '生成权限管理',
'icon_class' => '数据表图标',
'icon_hint' => '图标 (可选) 使用',
'icon_hint2' => '系统图标',
'index' => '索引',
'input_type' => '输入类型',
'key' => 'Key',
'model_class' => '模型类名',
'model_name' => '模型名称',
'model_name_ph' => '例. \App\User, 如果为空则尝试使用表名',
'name_warning' => '请在添加索引之前给列命名',
'no_composites_warning' => '此表存在目前不受支持到复合索引。请注他们目前不受支持。在尝试添加/删除索引时要小心',
'null' => 'Null',
'optional_details' => '选项详细',
'primary' => '主键',
'server_pagination' => '服务端分页',
'success_create_table' => '表 :table 创建完成',
'success_created_bread' => 'BREAD创建完成',
'success_delete_table' => '表 :table 删除完成',
'success_remove_bread' => '删除:datatype的BREAD完成',
'success_update_bread' => '更新:datatype的BREAD完成',
'success_update_table' => '表 :table 更新完成',
'table_actions' => '表操作',
'table_columns' => '表行数据',
'table_has_index' => '该表已经存在主索引.',
'table_name' => '表名',
'table_no_columns' => '该表没有列名',
'type' => '数据类型',
'type_not_supported' => '不支持该类型',
'unique' => '唯一',
'unknown_type' => '未知的类型',
'update_table' => '更新表',
'url_slug' => 'URL别名 (必须唯一)',
'url_slug_ph' => 'URL别名 (例. posts)',
'visibility' => '可见',
],
'dimmer' => [
'page' => '页面',
'page_link_text' => '浏览全部页面',
'page_text' => '共 :count :string 数据. 点击下面按钮浏览全部.',
'post' => '文章',
'post_link_text' => '浏览全部页面',
'post_text' => '共 :count :string 数据. 点击下面按钮浏览全部.',
'user' => '用户',
'user_link_text' => '浏览全部用户',
'user_text' => '共 :count :string 数据. 点击下面按钮浏览全部.',
],
'form' => [
'field_password_keep' => '填空保持不变',
'field_select_dd_relationship' => '确保在类:class中建立了正确的方法:method method of ',
'type_checkbox' => '复选框(Check Box)',
'type_codeeditor' => '代码编辑器(Code Editor)',
'type_file' => '文件(File)',
'type_image' => '图片(Image)',
'type_radiobutton' => '单选框(Radio Button)',
'type_richtextbox' => '富文本(Rich Textbox)',
'type_selectdropdown' => '下拉选择(Select Dropdown)',
'type_textarea' => '文本域(Text Area)',
'type_textbox' => '文本框(Text Box)',
],
// DataTable translations from: https://github.com/DataTables/Plugins/tree/master/i18n
'datatable' => [
'sEmptyTable' => '表中数据为空',
'sInfo' => '显示第 _START_ 至 _END_ 项结果,共 _TOTAL_ 项',
'sInfoEmpty' => '显示第 0 至 0 项结果,共 0 项',
'sInfoFiltered' => '(由 _MAX_ 项结果过滤)',
'sInfoPostFix' => '',
'sInfoThousands' => ',',
'sLengthMenu' => '显示 _MENU_ 项结果',
'sLoadingRecords' => '载入中...',
'sProcessing' => '处理中...',
'sSearch' => '搜索:',
'sZeroRecords' => '没有匹配结果',
'oPaginate' => [
'sFirst' => '首页',
'sLast' => '上页',
'sNext' => '下页',
'sPrevious' => '末页',
],
'oAria' => [
'sSortAscending' => ': 以升序排列此列',
'sSortDescending' => ': 以降序排列此列',
],
],
'theme' => [
'footer_copyright' => 'Made with <i class="voyager-heart"></i> by',
'footer_copyright2' => 'Made with rum and even more rum',
],
'json' => [
'invalid' => '无效的JSON',
'invalid_message' => '引入了一些无效的JSON',
'valid' => '有效的JSON',
'validation_errors' => '验证错误',
],
'analytics' => [
'by_pageview' => 'By pageview',
'by_sessions' => 'By sessions',
'by_users' => 'By users',
'no_client_id' => 'To view analytics you\'ll need to get a google analytics client id and '.
'add it to your settings for the key <code>google_analytics_client_id'.
'</code>. Get your key in your Google developer console:',
'set_view' => 'Select a View',
'this_vs_last_week' => 'This Week vs Last Week',
'this_vs_last_year' => 'This Year vs Last Year',
'top_browsers' => 'Top Browsers',
'top_countries' => 'Top Countries',
'various_visualizations' => 'Various visualizations',
],
'error' => [
'symlink_created_text' => 'We just created the missing symlink for you.',
'symlink_created_title' => 'Missing storage symlink created',
'symlink_failed_text' => 'We failed to generate the missing symlink for your application. '.
'It seems like your hosting provider does not support it.',
'symlink_failed_title' => 'Could not create missing storage symlink',
'symlink_missing_button' => 'Fix it',
'symlink_missing_text' => 'We could not find a storage symlink. This could cause problems with '.
'loading media files from the browser.',
'symlink_missing_title' => 'Missing storage symlink',
],
];

P.S.

另外本博客的访问量我想大概无限接近于0,主要是用来打发时间,做做记录,要是能帮助到别人当然也是非常的好,如果真的你在看的话,也算是个缘分吧😂,感谢浏览。