parent
ad2eb895e5
commit
48138b1f7f
Before Width: | Height: | Size: 110 KiB |
After Width: | Height: | Size: 2.8 KiB |
File diff suppressed because one or more lines are too long
@ -0,0 +1,40 @@
|
||||
//= require rails-ujs
|
||||
//= require activestorage
|
||||
//= require turbolinks
|
||||
//= require jquery3
|
||||
//= require popper
|
||||
//= require bootstrap-sprockets
|
||||
//= require jquery.validate.min
|
||||
//= require additional-methods.min
|
||||
//= require jquery-validate-message-zh
|
||||
//= require bootstrap-notify
|
||||
//= require jquery.cookie.min
|
||||
//= require select2
|
||||
//= require select2-i18n.zh-CN
|
||||
//= require jquery.cxselect
|
||||
|
||||
//= require_tree ./admins
|
||||
|
||||
// ******** select2 global config ********
|
||||
$.fn.select2.defaults.set('theme', 'bootstrap4');
|
||||
$.fn.select2.defaults.set('language', 'zh-CN');
|
||||
|
||||
$(document).on('turbolinks:load', function(){
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
$('[data-toggle="popover"]').popover();
|
||||
|
||||
// flash alert提示框自动关闭
|
||||
if($('.admin-alert-container .alert').length > 0){
|
||||
setTimeout(function(){
|
||||
$('.admin-alert-container .alert').alert('close');
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on("turbolinks:before-cache", function () {
|
||||
$('[data-toggle="tooltip"]').tooltip('hide');
|
||||
$('[data-toggle="popover"]').popover('hide');
|
||||
});
|
||||
|
||||
$(function () {
|
||||
});
|
@ -0,0 +1,156 @@
|
||||
$(document).on('turbolinks:load', function() {
|
||||
if ($('body.admins-users-edit-page, body.admins-users-update-page').length > 0) {
|
||||
var initDepartmentSelect = true;
|
||||
|
||||
// ************** 学校选择 *************
|
||||
var matcherFunc = function(params, data){
|
||||
if ($.trim(params.term) === '') {
|
||||
return data;
|
||||
}
|
||||
if (typeof data.text === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (data.name && data.name.indexOf(params.term) > -1) {
|
||||
var modifiedData = $.extend({}, data, true);
|
||||
return modifiedData;
|
||||
}
|
||||
|
||||
// Return `null` if the term should not be displayed
|
||||
return null;
|
||||
}
|
||||
|
||||
var defineSchoolSelect = function (schools) {
|
||||
$('.school-select').select2({
|
||||
theme: 'bootstrap4',
|
||||
placeholder: '查询学校/单位',
|
||||
minimumInputLength: 1,
|
||||
data: schools,
|
||||
templateResult: function (item) {
|
||||
if(!item.id || item.id === '') return item.text;
|
||||
return item.name;
|
||||
},
|
||||
templateSelection: function(item){
|
||||
if (item.id) {
|
||||
$('#user_school_id').val(item.id);
|
||||
getDepartmentsData(item.id, defineDepartmentSelect2);
|
||||
}
|
||||
return item.name || item.text;
|
||||
},
|
||||
matcher: matcherFunc
|
||||
});
|
||||
};
|
||||
|
||||
var defineDepartmentSelect2 = function(departments){
|
||||
departments.unshift({ id: '-1', name: '未选择' }); // 可不选
|
||||
|
||||
if (!initDepartmentSelect) { $('.department-select').empty(); } // 为了能够回填部门
|
||||
initDepartmentSelect = false;
|
||||
|
||||
$('.department-select').select2({
|
||||
theme: 'bootstrap4',
|
||||
placeholder: '查询学院/部门',
|
||||
minimumInputLength: 0,
|
||||
data: departments,
|
||||
templateResult: function (item) {
|
||||
if(!item.id || item.id === '') return item.text;
|
||||
return item.name;
|
||||
},
|
||||
templateSelection: function(item){
|
||||
if (item.id) {
|
||||
$('#user_department_id').val(item.id);
|
||||
}
|
||||
return item.name || item.text;
|
||||
},
|
||||
matcher: matcherFunc
|
||||
});
|
||||
};
|
||||
|
||||
var getDepartmentsData = function(school_id, callback){
|
||||
$.ajax({
|
||||
url: '/api/schools/' + school_id + '/departments/for_option.json',
|
||||
dataType: 'json',
|
||||
type: 'GET',
|
||||
success: function(data) {
|
||||
callback(data.departments);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化学校选择器
|
||||
$.ajax({
|
||||
url: '/api/schools/for_option.json',
|
||||
dataType: 'json',
|
||||
type: 'GET',
|
||||
success: function(data) {
|
||||
defineSchoolSelect(data.schools);
|
||||
}
|
||||
});
|
||||
|
||||
// **************** 地区选择 ****************
|
||||
$('.province-city-select').cxSelect({
|
||||
url: '/javascripts/educoder/province-data.json',
|
||||
selects: ['province-select', 'city-select']
|
||||
});
|
||||
|
||||
// *********** 职业选择 ************
|
||||
var identityData = [
|
||||
{
|
||||
"v": "teacher",
|
||||
"n": "教师",
|
||||
"s": [{"n": "教授", "v": "教授"},{"n": "副教授", "v": "副教授"},{"n": "讲师", "v": "讲师"},{"n": "助教", "v": "助教"}]
|
||||
},
|
||||
{
|
||||
"v": "student",
|
||||
"n": "学生",
|
||||
"s": []
|
||||
},
|
||||
{
|
||||
"v": "professional",
|
||||
"n": "专业人士",
|
||||
"s": [{"n": "企业管理者", "v": "企业管理者"},{"n": "部门管理者", "v": "部门管理者"},{"n": "高级工程师", "v": "高级工程师"},{"n": "工程师", "v": "工程师"},{"n": "助理工程师", "v": "助理工程师"}]
|
||||
}
|
||||
];
|
||||
$('.user-identity-select').cxSelect({
|
||||
data: identityData,
|
||||
jsonValue: 'v',
|
||||
selects: ['identity-select', 'technical-title-select']
|
||||
});
|
||||
$('.identity-select').on('change', function(){
|
||||
if($(this).val() === 'student'){
|
||||
$('.technical-title-select-wrapper').hide();
|
||||
$('.form-group.user_student_id').show();
|
||||
} else {
|
||||
$('.technical-title-select-wrapper').show();
|
||||
$('.form-group.user_student_id').hide();
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
var $form = $('form.edit_user')
|
||||
$form.validate({
|
||||
errorElement: 'span',
|
||||
errorClass: 'danger text-danger',
|
||||
rules: {
|
||||
"user[password]": {
|
||||
required: false,
|
||||
minlength: 5
|
||||
},
|
||||
"user[password_confirmation]": {
|
||||
required: false,
|
||||
minlength: 5,
|
||||
equalTo: "#user_password"
|
||||
},
|
||||
},
|
||||
messages: {
|
||||
"user[password_confirmation]": {
|
||||
equalTo: "两次密码输入不一致"
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
$form.submit(function(e){
|
||||
if(!$form.valid()){ e.preventDefault(); }
|
||||
})
|
||||
}
|
||||
});
|
@ -0,0 +1,121 @@
|
||||
$(document).on('turbolinks:load', function(){
|
||||
if ($('body.admins-users-index-page').length > 0) {
|
||||
|
||||
var showSuccessNotify = function() {
|
||||
$.notify({
|
||||
message: '操作成功'
|
||||
},{
|
||||
type: 'success'
|
||||
});
|
||||
}
|
||||
|
||||
// lock user
|
||||
$('.users-list-container').on('click', '.lock-action', function(){
|
||||
var $lockAction = $(this);
|
||||
var $unlockAction = $lockAction.siblings('.unlock-action');
|
||||
|
||||
var userId = $lockAction.data('id');
|
||||
|
||||
$.ajax({
|
||||
url: '/admins/users/' + userId + '/lock',
|
||||
method: 'POST',
|
||||
dataType: 'json',
|
||||
success: function() {
|
||||
showSuccessNotify();
|
||||
$lockAction.hide();
|
||||
$unlockAction.show();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// unlock user
|
||||
$('.users-list-container').on('click', '.unlock-action', function(){
|
||||
var $unlockAction = $(this);
|
||||
var $lockAction = $unlockAction.siblings('.lock-action');
|
||||
|
||||
var userId = $unlockAction.data('id');
|
||||
|
||||
$.ajax({
|
||||
url: '/admins/users/' + userId + '/unlock',
|
||||
method: 'POST',
|
||||
dataType: 'json',
|
||||
success: function() {
|
||||
showSuccessNotify();
|
||||
$lockAction.show();
|
||||
$unlockAction.hide();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// active user
|
||||
$('.users-list-container').on('click', '.active-action', function(){
|
||||
var $activeAction = $(this);
|
||||
var $unlockAction = $activeAction.siblings('.unlock-action');
|
||||
var $lockAction = $activeAction.siblings('.lock-action');
|
||||
|
||||
var userId = $activeAction.data('id');
|
||||
|
||||
$.ajax({
|
||||
url: '/admins/users/' + userId + '/unlock',
|
||||
method: 'POST',
|
||||
dataType: 'json',
|
||||
success: function() {
|
||||
showSuccessNotify();
|
||||
$activeAction.hide();
|
||||
$lockAction.show();
|
||||
$unlockAction.hide();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ***************** reward grade modal *****************
|
||||
var $rewardGradeModal = $('.admin-users-reward-grade-modal');
|
||||
var $form = $rewardGradeModal.find('form.admin-users-reward-grade-form');
|
||||
|
||||
$form.validate({
|
||||
errorElement: 'span',
|
||||
errorClass: 'danger text-danger',
|
||||
rules: {
|
||||
grade: {
|
||||
required: true,
|
||||
digits: true
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
// modal ready fire
|
||||
$rewardGradeModal.on('show.bs.modal', function (event) {
|
||||
var $link = $(event.relatedTarget);
|
||||
|
||||
var userId = $link.data('id');
|
||||
$rewardGradeModal.find('.modal-body input[name="user_id"]').val(userId);
|
||||
});
|
||||
// modal visited fire
|
||||
$rewardGradeModal.on('shown.bs.modal', function(){
|
||||
$rewardGradeModal.find('.modal-body input[name="grade"]').focus();
|
||||
});
|
||||
|
||||
$('.admin-users-reward-grade-modal .submit-btn').on('click', function(){
|
||||
$form.find('.error').html('');
|
||||
|
||||
if ($form.valid()) {
|
||||
var userId = $form.find('input[name="user_id"]').val();
|
||||
|
||||
$.ajax({
|
||||
method: 'POST',
|
||||
dataType: 'json',
|
||||
url: "/admins/users/" + userId + "/reward_grade",
|
||||
data: $form.serialize(),
|
||||
success: function(data) {
|
||||
showSuccessNotify();
|
||||
$('.users-list-container .user-item-' + userId + ' td.grade-content').html(data.grade);
|
||||
$rewardGradeModal.modal('hide');
|
||||
},
|
||||
error: function(res) {
|
||||
$rewardGradeModal.find('.error').html(res.responseJSON.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
@ -0,0 +1,350 @@
|
||||
/*
|
||||
* Project: Bootstrap Notify = v3.1.3
|
||||
* Description: Turns standard Bootstrap alerts into "Growl-like" notifications.
|
||||
* Author: Mouse0270 aka Robert McIntosh
|
||||
* License: MIT License
|
||||
* Website: https://github.com/mouse0270/bootstrap-growl
|
||||
*/
|
||||
(function (factory) {
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
// AMD. Register as an anonymous module.
|
||||
define(['jquery'], factory);
|
||||
} else if (typeof exports === 'object') {
|
||||
// Node/CommonJS
|
||||
factory(require('jquery'));
|
||||
} else {
|
||||
// Browser globals
|
||||
factory(jQuery);
|
||||
}
|
||||
}(function ($) {
|
||||
// Create the defaults once
|
||||
var defaults = {
|
||||
element: 'body',
|
||||
position: null,
|
||||
type: "info",
|
||||
allow_dismiss: true,
|
||||
newest_on_top: false,
|
||||
showProgressbar: false,
|
||||
placement: {
|
||||
from: "top",
|
||||
align: "right"
|
||||
},
|
||||
offset: 20,
|
||||
spacing: 10,
|
||||
z_index: 1031,
|
||||
delay: 1000,
|
||||
timer: 1000,
|
||||
url_target: '_blank',
|
||||
mouse_over: null,
|
||||
animate: {
|
||||
enter: 'animated fadeInDown',
|
||||
exit: 'animated fadeOutUp'
|
||||
},
|
||||
onShow: null,
|
||||
onShown: null,
|
||||
onClose: null,
|
||||
onClosed: null,
|
||||
icon_type: 'class',
|
||||
template: '<div data-notify="container" class="col-xs-4 col-sm-2 alert alert-{0}" role="alert"><button type="button" aria-hidden="true" class="close" data-notify="dismiss">×</button><span data-notify="icon"></span> <span data-notify="title">{1}</span> <span data-notify="message">{2}</span><div class="progress" data-notify="progressbar"><div class="progress-bar progress-bar-{0}" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%;"></div></div><a href="{3}" target="{4}" data-notify="url"></a></div>'
|
||||
};
|
||||
|
||||
String.format = function() {
|
||||
var str = arguments[0];
|
||||
for (var i = 1; i < arguments.length; i++) {
|
||||
str = str.replace(RegExp("\\{" + (i - 1) + "\\}", "gm"), arguments[i]);
|
||||
}
|
||||
return str;
|
||||
};
|
||||
|
||||
function Notify ( element, content, options ) {
|
||||
// Setup Content of Notify
|
||||
var content = {
|
||||
content: {
|
||||
message: typeof content == 'object' ? content.message : content,
|
||||
title: content.title ? content.title : '',
|
||||
icon: content.icon ? content.icon : '',
|
||||
url: content.url ? content.url : '#',
|
||||
target: content.target ? content.target : '-'
|
||||
}
|
||||
};
|
||||
|
||||
options = $.extend(true, {}, content, options);
|
||||
this.settings = $.extend(true, {}, defaults, options);
|
||||
this._defaults = defaults;
|
||||
if (this.settings.content.target == "-") {
|
||||
this.settings.content.target = this.settings.url_target;
|
||||
}
|
||||
this.animations = {
|
||||
start: 'webkitAnimationStart oanimationstart MSAnimationStart animationstart',
|
||||
end: 'webkitAnimationEnd oanimationend MSAnimationEnd animationend'
|
||||
}
|
||||
|
||||
if (typeof this.settings.offset == 'number') {
|
||||
this.settings.offset = {
|
||||
x: this.settings.offset,
|
||||
y: this.settings.offset
|
||||
};
|
||||
}
|
||||
|
||||
this.init();
|
||||
};
|
||||
|
||||
$.extend(Notify.prototype, {
|
||||
init: function () {
|
||||
var self = this;
|
||||
|
||||
this.buildNotify();
|
||||
if (this.settings.content.icon) {
|
||||
this.setIcon();
|
||||
}
|
||||
if (this.settings.content.url != "#") {
|
||||
this.styleURL();
|
||||
}
|
||||
this.placement();
|
||||
this.bind();
|
||||
|
||||
this.notify = {
|
||||
$ele: this.$ele,
|
||||
update: function(command, update) {
|
||||
var commands = {};
|
||||
if (typeof command == "string") {
|
||||
commands[command] = update;
|
||||
}else{
|
||||
commands = command;
|
||||
}
|
||||
for (var command in commands) {
|
||||
switch (command) {
|
||||
case "type":
|
||||
this.$ele.removeClass('alert-' + self.settings.type);
|
||||
this.$ele.find('[data-notify="progressbar"] > .progress-bar').removeClass('progress-bar-' + self.settings.type);
|
||||
self.settings.type = commands[command];
|
||||
this.$ele.addClass('alert-' + commands[command]).find('[data-notify="progressbar"] > .progress-bar').addClass('progress-bar-' + commands[command]);
|
||||
break;
|
||||
case "icon":
|
||||
var $icon = this.$ele.find('[data-notify="icon"]');
|
||||
if (self.settings.icon_type.toLowerCase() == 'class') {
|
||||
$icon.removeClass(self.settings.content.icon).addClass(commands[command]);
|
||||
}else{
|
||||
if (!$icon.is('img')) {
|
||||
$icon.find('img');
|
||||
}
|
||||
$icon.attr('src', commands[command]);
|
||||
}
|
||||
break;
|
||||
case "progress":
|
||||
var newDelay = self.settings.delay - (self.settings.delay * (commands[command] / 100));
|
||||
this.$ele.data('notify-delay', newDelay);
|
||||
this.$ele.find('[data-notify="progressbar"] > div').attr('aria-valuenow', commands[command]).css('width', commands[command] + '%');
|
||||
break;
|
||||
case "url":
|
||||
this.$ele.find('[data-notify="url"]').attr('href', commands[command]);
|
||||
break;
|
||||
case "target":
|
||||
this.$ele.find('[data-notify="url"]').attr('target', commands[command]);
|
||||
break;
|
||||
default:
|
||||
this.$ele.find('[data-notify="' + command +'"]').html(commands[command]);
|
||||
};
|
||||
}
|
||||
var posX = this.$ele.outerHeight() + parseInt(self.settings.spacing) + parseInt(self.settings.offset.y);
|
||||
self.reposition(posX);
|
||||
},
|
||||
close: function() {
|
||||
self.close();
|
||||
}
|
||||
};
|
||||
},
|
||||
buildNotify: function () {
|
||||
var content = this.settings.content;
|
||||
this.$ele = $(String.format(this.settings.template, this.settings.type, content.title, content.message, content.url, content.target));
|
||||
this.$ele.attr('data-notify-position', this.settings.placement.from + '-' + this.settings.placement.align);
|
||||
if (!this.settings.allow_dismiss) {
|
||||
this.$ele.find('[data-notify="dismiss"]').css('display', 'none');
|
||||
}
|
||||
if ((this.settings.delay <= 0 && !this.settings.showProgressbar) || !this.settings.showProgressbar) {
|
||||
this.$ele.find('[data-notify="progressbar"]').remove();
|
||||
}
|
||||
},
|
||||
setIcon: function() {
|
||||
if (this.settings.icon_type.toLowerCase() == 'class') {
|
||||
this.$ele.find('[data-notify="icon"]').addClass(this.settings.content.icon);
|
||||
}else{
|
||||
if (this.$ele.find('[data-notify="icon"]').is('img')) {
|
||||
this.$ele.find('[data-notify="icon"]').attr('src', this.settings.content.icon);
|
||||
}else{
|
||||
this.$ele.find('[data-notify="icon"]').append('<img src="'+this.settings.content.icon+'" alt="Notify Icon" />');
|
||||
}
|
||||
}
|
||||
},
|
||||
styleURL: function() {
|
||||
this.$ele.find('[data-notify="url"]').css({
|
||||
backgroundImage: 'url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)',
|
||||
height: '100%',
|
||||
left: '0px',
|
||||
position: 'absolute',
|
||||
top: '0px',
|
||||
width: '100%',
|
||||
zIndex: this.settings.z_index + 1
|
||||
});
|
||||
this.$ele.find('[data-notify="dismiss"]').css({
|
||||
position: 'absolute',
|
||||
right: '10px',
|
||||
top: '5px',
|
||||
zIndex: this.settings.z_index + 2
|
||||
});
|
||||
},
|
||||
placement: function() {
|
||||
var self = this,
|
||||
offsetAmt = this.settings.offset.y,
|
||||
css = {
|
||||
display: 'inline-block',
|
||||
margin: '0px auto',
|
||||
position: this.settings.position ? this.settings.position : (this.settings.element === 'body' ? 'fixed' : 'absolute'),
|
||||
transition: 'all .5s ease-in-out',
|
||||
zIndex: this.settings.z_index
|
||||
},
|
||||
hasAnimation = false,
|
||||
settings = this.settings;
|
||||
|
||||
$('[data-notify-position="' + this.settings.placement.from + '-' + this.settings.placement.align + '"]:not([data-closing="true"])').each(function() {
|
||||
return offsetAmt = Math.max(offsetAmt, parseInt($(this).css(settings.placement.from)) + parseInt($(this).outerHeight()) + parseInt(settings.spacing));
|
||||
});
|
||||
if (this.settings.newest_on_top == true) {
|
||||
offsetAmt = this.settings.offset.y;
|
||||
}
|
||||
css[this.settings.placement.from] = offsetAmt+'px';
|
||||
|
||||
switch (this.settings.placement.align) {
|
||||
case "left":
|
||||
case "right":
|
||||
css[this.settings.placement.align] = this.settings.offset.x+'px';
|
||||
break;
|
||||
case "center":
|
||||
css.left = 0;
|
||||
css.right = 0;
|
||||
break;
|
||||
}
|
||||
this.$ele.css(css).addClass(this.settings.animate.enter);
|
||||
$.each(Array('webkit', 'moz', 'o', 'ms', ''), function(index, prefix) {
|
||||
self.$ele[0].style[prefix+'AnimationIterationCount'] = 1;
|
||||
});
|
||||
|
||||
$(this.settings.element).append(this.$ele);
|
||||
|
||||
if (this.settings.newest_on_top == true) {
|
||||
offsetAmt = (parseInt(offsetAmt)+parseInt(this.settings.spacing)) + this.$ele.outerHeight();
|
||||
this.reposition(offsetAmt);
|
||||
}
|
||||
|
||||
if ($.isFunction(self.settings.onShow)) {
|
||||
self.settings.onShow.call(this.$ele);
|
||||
}
|
||||
|
||||
this.$ele.one(this.animations.start, function(event) {
|
||||
hasAnimation = true;
|
||||
}).one(this.animations.end, function(event) {
|
||||
if ($.isFunction(self.settings.onShown)) {
|
||||
self.settings.onShown.call(this);
|
||||
}
|
||||
});
|
||||
|
||||
setTimeout(function() {
|
||||
if (!hasAnimation) {
|
||||
if ($.isFunction(self.settings.onShown)) {
|
||||
self.settings.onShown.call(this);
|
||||
}
|
||||
}
|
||||
}, 600);
|
||||
},
|
||||
bind: function() {
|
||||
var self = this;
|
||||
|
||||
this.$ele.find('[data-notify="dismiss"]').on('click', function() {
|
||||
self.close();
|
||||
})
|
||||
|
||||
this.$ele.mouseover(function(e) {
|
||||
$(this).data('data-hover', "true");
|
||||
}).mouseout(function(e) {
|
||||
$(this).data('data-hover', "false");
|
||||
});
|
||||
this.$ele.data('data-hover', "false");
|
||||
|
||||
if (this.settings.delay > 0) {
|
||||
self.$ele.data('notify-delay', self.settings.delay);
|
||||
var timer = setInterval(function() {
|
||||
var delay = parseInt(self.$ele.data('notify-delay')) - self.settings.timer;
|
||||
if ((self.$ele.data('data-hover') === 'false' && self.settings.mouse_over == "pause") || self.settings.mouse_over != "pause") {
|
||||
var percent = ((self.settings.delay - delay) / self.settings.delay) * 100;
|
||||
self.$ele.data('notify-delay', delay);
|
||||
self.$ele.find('[data-notify="progressbar"] > div').attr('aria-valuenow', percent).css('width', percent + '%');
|
||||
}
|
||||
if (delay <= -(self.settings.timer)) {
|
||||
clearInterval(timer);
|
||||
self.close();
|
||||
}
|
||||
}, self.settings.timer);
|
||||
}
|
||||
},
|
||||
close: function() {
|
||||
var self = this,
|
||||
$successors = null,
|
||||
posX = parseInt(this.$ele.css(this.settings.placement.from)),
|
||||
hasAnimation = false;
|
||||
|
||||
this.$ele.data('closing', 'true').addClass(this.settings.animate.exit);
|
||||
self.reposition(posX);
|
||||
|
||||
if ($.isFunction(self.settings.onClose)) {
|
||||
self.settings.onClose.call(this.$ele);
|
||||
}
|
||||
|
||||
this.$ele.one(this.animations.start, function(event) {
|
||||
hasAnimation = true;
|
||||
}).one(this.animations.end, function(event) {
|
||||
$(this).remove();
|
||||
if ($.isFunction(self.settings.onClosed)) {
|
||||
self.settings.onClosed.call(this);
|
||||
}
|
||||
});
|
||||
|
||||
setTimeout(function() {
|
||||
if (!hasAnimation) {
|
||||
self.$ele.remove();
|
||||
if (self.settings.onClosed) {
|
||||
self.settings.onClosed(self.$ele);
|
||||
}
|
||||
}
|
||||
}, 600);
|
||||
},
|
||||
reposition: function(posX) {
|
||||
var self = this,
|
||||
notifies = '[data-notify-position="' + this.settings.placement.from + '-' + this.settings.placement.align + '"]:not([data-closing="true"])',
|
||||
$elements = this.$ele.nextAll(notifies);
|
||||
if (this.settings.newest_on_top == true) {
|
||||
$elements = this.$ele.prevAll(notifies);
|
||||
}
|
||||
$elements.each(function() {
|
||||
$(this).css(self.settings.placement.from, posX);
|
||||
posX = (parseInt(posX)+parseInt(self.settings.spacing)) + $(this).outerHeight();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$.notify = function ( content, options ) {
|
||||
var plugin = new Notify( this, content, options );
|
||||
return plugin.notify;
|
||||
};
|
||||
$.notifyDefaults = function( options ) {
|
||||
defaults = $.extend(true, {}, defaults, options);
|
||||
return defaults;
|
||||
};
|
||||
$.notifyClose = function( command ) {
|
||||
if (typeof command === "undefined" || command == "all") {
|
||||
$('[data-notify]').find('[data-notify="dismiss"]').trigger('click');
|
||||
}else{
|
||||
$('[data-notify-position="'+command+'"]').find('[data-notify="dismiss"]').trigger('click');
|
||||
}
|
||||
};
|
||||
|
||||
}));
|
File diff suppressed because one or more lines are too long
@ -0,0 +1,33 @@
|
||||
(function( factory ) {
|
||||
if ( typeof define === "function" && define.amd ) {
|
||||
define( ["jquery", "../jquery.validate"], factory );
|
||||
} else {
|
||||
factory( jQuery );
|
||||
}
|
||||
}(function( $ ) {
|
||||
|
||||
/*
|
||||
* Translated default messages for the jQuery validation plugin.
|
||||
* Locale: ZH (Chinese, 中文 (Zhōngwén), 汉语, 漢語)
|
||||
*/
|
||||
$.extend($.validator.messages, {
|
||||
required: "这是必填字段",
|
||||
remote: "请修正此字段",
|
||||
email: "请输入有效的电子邮件地址",
|
||||
url: "请输入有效的网址",
|
||||
date: "请输入有效的日期",
|
||||
dateISO: "请输入有效的日期 (YYYY-MM-DD)",
|
||||
number: "请输入有效的数字",
|
||||
digits: "只能输入数字",
|
||||
creditcard: "请输入有效的信用卡号码",
|
||||
equalTo: "你的输入不相同",
|
||||
extension: "请输入有效的后缀",
|
||||
maxlength: $.validator.format("最多可以输入 {0} 个字符"),
|
||||
minlength: $.validator.format("最少要输入 {0} 个字符"),
|
||||
rangelength: $.validator.format("请输入长度在 {0} 到 {1} 之间的字符串"),
|
||||
range: $.validator.format("请输入范围在 {0} 到 {1} 之间的数值"),
|
||||
max: $.validator.format("请输入不大于 {0} 的数值"),
|
||||
min: $.validator.format("请输入不小于 {0} 的数值")
|
||||
});
|
||||
|
||||
}));
|
@ -0,0 +1,2 @@
|
||||
/*! jquery.cookie v1.4.1 | MIT */
|
||||
!function(a){"function"==typeof define&&define.amd?define(["jquery"],a):"object"==typeof exports?a(require("jquery")):a(jQuery)}(function(a){function b(a){return h.raw?a:encodeURIComponent(a)}function c(a){return h.raw?a:decodeURIComponent(a)}function d(a){return b(h.json?JSON.stringify(a):String(a))}function e(a){0===a.indexOf('"')&&(a=a.slice(1,-1).replace(/\\"/g,'"').replace(/\\\\/g,"\\"));try{return a=decodeURIComponent(a.replace(g," ")),h.json?JSON.parse(a):a}catch(b){}}function f(b,c){var d=h.raw?b:e(b);return a.isFunction(c)?c(d):d}var g=/\+/g,h=a.cookie=function(e,g,i){if(void 0!==g&&!a.isFunction(g)){if(i=a.extend({},h.defaults,i),"number"==typeof i.expires){var j=i.expires,k=i.expires=new Date;k.setTime(+k+864e5*j)}return document.cookie=[b(e),"=",d(g),i.expires?"; expires="+i.expires.toUTCString():"",i.path?"; path="+i.path:"",i.domain?"; domain="+i.domain:"",i.secure?"; secure":""].join("")}for(var l=e?void 0:{},m=document.cookie?document.cookie.split("; "):[],n=0,o=m.length;o>n;n++){var p=m[n].split("="),q=c(p.shift()),r=p.join("=");if(e&&e===q){l=f(r,g);break}e||void 0===(r=f(r))||(l[q]=r)}return l};h.defaults={},a.removeCookie=function(b,c){return void 0===a.cookie(b)?!1:(a.cookie(b,"",a.extend({},c,{expires:-1})),!a.cookie(b))}});
|
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@ -0,0 +1,3 @@
|
||||
/*! Select2 4.0.8 | https://github.com/select2/select2/blob/master/LICENSE.md */
|
||||
|
||||
!function(){if(jQuery&&jQuery.fn&&jQuery.fn.select2&&jQuery.fn.select2.amd)var n=jQuery.fn.select2.amd;n.define("select2/i18n/zh-CN",[],function(){return{errorLoading:function(){return"无法载入结果。"},inputTooLong:function(n){return"请删除"+(n.input.length-n.maximum)+"个字符"},inputTooShort:function(n){return"请再输入至少"+(n.minimum-n.input.length)+"个字符"},loadingMore:function(){return"载入更多结果…"},maximumSelected:function(n){return"最多只能选择"+n.maximum+"个项目"},noResults:function(){return"未找到结果"},searching:function(){return"搜索中…"},removeAllItems:function(){return"删除所有项目"}}}),n.define,n.require}();
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@ -0,0 +1,44 @@
|
||||
@import "bootstrap";
|
||||
@import "font-awesome-sprockets";
|
||||
@import "font-awesome";
|
||||
@import "select2.min";
|
||||
@import "select2-bootstrap4.min";
|
||||
|
||||
@import "admins/*";
|
||||
|
||||
body {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
max-width: 100vw;
|
||||
max-height: 100vh;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
font-size: 14px;
|
||||
background: #efefef;
|
||||
}
|
||||
|
||||
a {
|
||||
&:hover {
|
||||
text-decoration: unset;
|
||||
}
|
||||
}
|
||||
|
||||
input.danger {
|
||||
border-color: #dc3545!important;
|
||||
}
|
||||
|
||||
label.error {
|
||||
color: #dc3545!important;
|
||||
}
|
||||
|
||||
.simple_form {
|
||||
.form-group {
|
||||
.collection_radio_buttons {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.form-check-inline {
|
||||
height: calc(1.5em + 0.75rem + 2px)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
.admin-body-container {
|
||||
padding: 20px;
|
||||
flex: 1;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: scroll;
|
||||
|
||||
& > .content {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
|
||||
.box {
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
/* 面包屑 */
|
||||
.breadcrumb {
|
||||
padding-left: 5px;
|
||||
font-size: 20px;
|
||||
background: unset;
|
||||
}
|
||||
|
||||
/* 内容表格 */
|
||||
table {
|
||||
table-layout: fixed;
|
||||
|
||||
td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
tr {
|
||||
&.no-data {
|
||||
&:hover {
|
||||
color: darkgrey;
|
||||
background: unset;
|
||||
}
|
||||
|
||||
& > td {
|
||||
height: 300px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-container {
|
||||
.action {
|
||||
padding: 0 3px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 分页 */
|
||||
.paginate-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.paginate-total {
|
||||
margin-bottom: 10px;
|
||||
color: darkgrey;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 搜索表单 */
|
||||
.search-form-container {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.search-form {
|
||||
select, input {
|
||||
margin-right: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,215 @@
|
||||
#sidebar {
|
||||
min-width: 200px;
|
||||
max-width: 200px;
|
||||
background: #272822;
|
||||
color: #fff;
|
||||
transition: all 0.5s;
|
||||
overflow-y: scroll;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display:none
|
||||
}
|
||||
|
||||
&.active {
|
||||
min-width: 60px;
|
||||
max-width: 60px;
|
||||
text-align: center;
|
||||
|
||||
.sidebar-header {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&-logo {
|
||||
padding-left: 5px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
ul li a {
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
font-size: 0.85em;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
span { display: none }
|
||||
|
||||
i {
|
||||
margin-right: 0;
|
||||
display: block;
|
||||
font-size: 1.8em;
|
||||
margin-bottom: 5px;
|
||||
width: 30px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-toggle::after {
|
||||
top: auto;
|
||||
bottom: 10px;
|
||||
right: 50%;
|
||||
-webkit-transform: translateX(50%);
|
||||
-ms-transform: translateX(50%);
|
||||
transform: translateX(50%);
|
||||
}
|
||||
|
||||
ul ul a {
|
||||
padding: 10px !important;
|
||||
|
||||
span { display: none }
|
||||
|
||||
i {
|
||||
margin-left: 0px;
|
||||
display: block;
|
||||
font-size: 0.8em;
|
||||
width: 30px;
|
||||
height: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 20px;
|
||||
background: #272822;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#sidebarCollapse {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
text-align: right;
|
||||
|
||||
&.active {
|
||||
width: 40px;
|
||||
height: 30px;
|
||||
background: #3f3f3f;
|
||||
border: 1px solid grey;
|
||||
border-radius: 3px;
|
||||
|
||||
i.fold { display: none; }
|
||||
i.unfold { display: block; }
|
||||
}
|
||||
|
||||
i.fold {
|
||||
display: block;
|
||||
}
|
||||
i.unfold { display: none; }
|
||||
}
|
||||
|
||||
a, a:hover, a:focus {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
& > ul > li > a > i {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
ul {
|
||||
&.components {
|
||||
padding: 20px 0;
|
||||
border-bottom: 1px solid #3f3f3f;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #fff;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
li > a {
|
||||
padding: 10px;
|
||||
font-size: 1em;
|
||||
display: block;
|
||||
text-align: left;
|
||||
|
||||
i {
|
||||
margin-right: 10px;
|
||||
font-size: 1em;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
li a {
|
||||
&:hover, &.active {
|
||||
color: #fff;
|
||||
background: #276891;
|
||||
}
|
||||
}
|
||||
|
||||
li.active > a, a[aria-expanded="true"] {
|
||||
color: #fff;
|
||||
//background: #276891;
|
||||
}
|
||||
|
||||
ul a {
|
||||
font-size: 0.9em !important;
|
||||
padding-left: 30px !important;
|
||||
background: #3f3f3f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
#sidebar {
|
||||
&.active {
|
||||
padding: 10px 5px;
|
||||
min-width: 40px;
|
||||
max-width: 40px;
|
||||
text-align: center;
|
||||
margin-left: 0;
|
||||
transform: none;
|
||||
|
||||
.sidebar-header {
|
||||
padding: 0px;
|
||||
|
||||
.sidebar-header-logo {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#sidebarCollapse {
|
||||
width: 30px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
ul li a {
|
||||
padding: 10px;
|
||||
font-size: 0.85em;
|
||||
|
||||
i {
|
||||
margin-right: 0;
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
& > ul > li > a > i {
|
||||
font-size: 1.8em;
|
||||
}
|
||||
|
||||
ul ul a {
|
||||
padding: 10px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-toggle::after {
|
||||
top: auto;
|
||||
bottom: 10px;
|
||||
right: 50%;
|
||||
-webkit-transform: translateX(50%);
|
||||
-ms-transform: translateX(50%);
|
||||
transform: translateX(50%);
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
.admins-users-index-page {
|
||||
.user-list-form {
|
||||
}
|
||||
|
||||
.users-list-container {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
.admins-users-edit-page, .admins-users-update-page {
|
||||
.user-edit-container {
|
||||
.user-info {
|
||||
&-content {
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
&-name {
|
||||
flex: 2;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
&-auth {
|
||||
flex: 1;
|
||||
|
||||
i.fa {
|
||||
margin-right: 10px;
|
||||
font-size: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
@import "bootstrap";
|
@ -0,0 +1 @@
|
||||
.select2-container--bootstrap4 .select2-selection--single{height:calc(1.5em + .75rem + 2px)!important}.select2-container--bootstrap4 .select2-selection--single .select2-selection__placeholder{color:#757575;line-height:calc(1.5em + .75rem)}.select2-container--bootstrap4 .select2-selection--single .select2-selection__arrow{position:absolute;top:50%;right:3px;width:20px}.select2-container--bootstrap4 .select2-selection--single .select2-selection__arrow b{top:60%;border-color:#343a40 transparent transparent;border-style:solid;border-width:5px 4px 0;width:0;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute}.select2-container--bootstrap4 .select2-selection--single .select2-selection__rendered{line-height:calc(1.5em + .75rem)}.select2-search--dropdown .select2-search__field{border:1px solid #ced4da;border-radius:.25rem}.select2-results__message{color:#6c757d}.select2-container--bootstrap4 .select2-selection--multiple{min-height:calc(1.5em + .75rem + 2px)!important}.select2-container--bootstrap4 .select2-selection--multiple .select2-selection__rendered{-webkit-box-sizing:border-box;box-sizing:border-box;list-style:none;margin:0;padding:0 5px;width:100%}.select2-container--bootstrap4 .select2-selection--multiple .select2-selection__choice{color:#343a40;border:1px solid #bdc6d0;border-radius:.2rem;padding:0 5px 0 0;cursor:pointer;float:left;margin-top:.3em;margin-right:5px}.select2-container--bootstrap4 .select2-selection--multiple .select2-selection__choice__remove{color:#bdc6d0;font-weight:700;margin-left:3px;margin-right:1px;padding-right:3px;padding-left:3px;float:left}.select2-container--bootstrap4 .select2-selection--multiple .select2-selection__choice__remove:hover{color:#343a40}.select2-container{display:block}.select2-container :focus{outline:0}.input-group .select2-container--bootstrap4{-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1}.input-group-prepend~.select2-container--bootstrap4 .select2-selection{border-top-left-radius:0;border-bottom-left-radius:0}.select2-container--bootstrap4 .select2-selection{border:1px solid #ced4da;border-radius:.25rem;width:100%}.select2-container--bootstrap4.select2-container--focus .select2-selection{border-color:#17a2b8;-webkit-box-shadow:0 0 0 .2rem rgba(0,123,255,.25);box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.select2-container--bootstrap4.select2-container--focus.select2-container--open .select2-selection{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--bootstrap4.select2-container--disabled.select2-container--focus .select2-selection,.select2-container--bootstrap4.select2-container--disabled .select2-selection{background-color:#e9ecef;cursor:not-allowed;border-color:#ced4da;-webkit-box-shadow:none;box-shadow:none}.select2-container--bootstrap4.select2-container--disabled.select2-container--focus .select2-search__field,.select2-container--bootstrap4.select2-container--disabled .select2-search__field{background-color:transparent}form.was-validated select:invalid~.select2-container--bootstrap4 .select2-selection,select.is-invalid~.select2-container--bootstrap4 .select2-selection{border-color:#dc3545}form.was-validated select:valid~.select2-container--bootstrap4 .select2-selection,select.is-valid~.select2-container--bootstrap4 .select2-selection{border-color:#28a745}.select2-container--bootstrap4 .select2-dropdown{border-color:#ced4da;border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-container--bootstrap4 .select2-dropdown.select2-dropdown--above{border-top:1px solid #ced4da;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.select2-container--bootstrap4 .select2-dropdown .select2-results__option[aria-selected=true]{background-color:#e9ecef}.select2-container--bootstrap4 .select2-results__option--highlighted,.select2-container--bootstrap4 .select2-results__option--highlighted.select2-results__option[aria-selected=true]{background-color:#007bff;color:#f8f9fa}.select2-container--bootstrap4 .select2-results__option[role=group]{padding:0}.select2-container--bootstrap4 .select2-results>.select2-results__options{max-height:15em;overflow-y:auto}.select2-container--bootstrap4 .select2-results__group{padding:6px;display:list-item;color:#6c757d}.select2-container--bootstrap4 .select2-selection__clear{width:1.2em;height:1.2em;line-height:1.15em;padding-left:.3em;margin-top:.5em;border-radius:100%;background-color:#6c757d;color:#f8f9fa;float:right;margin-right:.3em}.select2-container--bootstrap4 .select2-selection__clear:hover{background-color:#343a40}
|
File diff suppressed because one or more lines are too long
@ -1,3 +0,0 @@
|
||||
// Place all the styles related to the users/banks controller here.
|
||||
// They will automatically be included in application.css.
|
||||
// You can use Sass (SCSS) here: http://sass-lang.com/
|
@ -0,0 +1,24 @@
|
||||
class Admins::BaseController < ApplicationController
|
||||
include Admins::PaginateHelper
|
||||
include Admins::RenderHelper
|
||||
include Admins::ErrorRescueHandler
|
||||
|
||||
layout 'admin'
|
||||
|
||||
before_action :require_login, :require_admin!
|
||||
|
||||
private
|
||||
|
||||
def require_login
|
||||
return if User.current.logged?
|
||||
|
||||
redirect_to "/login?back_url=#{CGI::escape(request.fullpath)}"
|
||||
end
|
||||
|
||||
def require_admin!
|
||||
return if current_user.blank? || !current_user.logged?
|
||||
return if current_user.admin_or_business?
|
||||
|
||||
render_forbidden
|
||||
end
|
||||
end
|
@ -0,0 +1,4 @@
|
||||
class Admins::DashboardsController < Admins::BaseController
|
||||
def index
|
||||
end
|
||||
end
|
@ -0,0 +1,62 @@
|
||||
class Admins::UsersController < Admins::BaseController
|
||||
def index
|
||||
params[:sort_by] = params[:sort_by].presence || 'created_on'
|
||||
params[:sort_direction] = params[:sort_direction].presence || 'desc'
|
||||
|
||||
users = Admins::UserQuery.call(params)
|
||||
@users = paginate users.includes(user_extension: :school)
|
||||
end
|
||||
|
||||
def edit
|
||||
@user = User.find(params[:id])
|
||||
end
|
||||
|
||||
def update
|
||||
@user = User.find(params[:id])
|
||||
|
||||
Admins::UpdateUserService.call(@user, update_params)
|
||||
flash[:success] = '保存成功'
|
||||
redirect_to edit_admins_user_path(@user)
|
||||
rescue ActiveRecord::RecordInvalid
|
||||
flash.now[:danger] = '保存失败'
|
||||
render 'edit'
|
||||
rescue Admins::UpdateUserService::Error => ex
|
||||
flash.now[:danger] = ex.message
|
||||
render 'edit'
|
||||
end
|
||||
|
||||
def destroy
|
||||
User.find(params[:id]).destroy!
|
||||
|
||||
render_delete_success
|
||||
end
|
||||
|
||||
def lock
|
||||
User.find(params[:user_id]).lock!
|
||||
|
||||
render_ok
|
||||
end
|
||||
|
||||
def unlock
|
||||
User.find(params[:user_id]).activate!
|
||||
|
||||
render_ok
|
||||
end
|
||||
|
||||
def reward_grade
|
||||
user = User.find(params[:user_id])
|
||||
return render_unprocessable_entity('金币数量必须大于0') if params[:grade].to_i <= 0
|
||||
|
||||
RewardGradeService.call(user, container_id: user.id, container_type: 'Feedback', score: params[:grade].to_i, not_unique: true)
|
||||
|
||||
render_ok(grade: user.grade)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def update_params
|
||||
params.require(:user).permit(%i[lastname nickname gender identity technical_title student_id
|
||||
mail phone location location_city school_id department_id admin business is_test
|
||||
password professional_certification authentication])
|
||||
end
|
||||
end
|
@ -0,0 +1,22 @@
|
||||
module Admins::ErrorRescueHandler
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
rescue_from Exception, Educoder::TipException do |e|
|
||||
Util.logger_error e
|
||||
internal_server_error
|
||||
end
|
||||
|
||||
rescue_from ActionView::MissingTemplate, ActiveRecord::RecordNotFound, with: :render_not_found
|
||||
rescue_from ActionController::ParameterMissing do
|
||||
render_unprocessable_entity('参数缺失')
|
||||
end
|
||||
# form validation error
|
||||
rescue_from ActiveModel::ValidationError do |ex|
|
||||
render_unprocessable_entity(ex.model.errors.full_messages.join(','))
|
||||
end
|
||||
rescue_from ActiveRecord::RecordInvalid do |ex|
|
||||
render_unprocessable_entity(ex.record.errors.full_messages.join(','))
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,15 @@
|
||||
module Admins::PaginateHelper
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def page
|
||||
params[:page].to_i <= 0 ? 1 : params[:page].to_i
|
||||
end
|
||||
|
||||
def per_page
|
||||
params[:per_page].to_i <= 0 || params[:per_page].to_i > 100 ? 20 : params[:per_page].to_i
|
||||
end
|
||||
|
||||
def paginate(relations)
|
||||
relations.page(page).per(per_page)
|
||||
end
|
||||
end
|
@ -0,0 +1,47 @@
|
||||
module Admins::RenderHelper
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def render_forbidden
|
||||
respond_to do |format|
|
||||
format.html { redirect_to '/403' }
|
||||
format.json { super }
|
||||
end
|
||||
end
|
||||
|
||||
def render_not_found
|
||||
respond_to do |format|
|
||||
format.html { render 'admins/shared/404' }
|
||||
format.js { render_js_error('资源未找到') }
|
||||
format.json { render status: 404, json: { message: '资源未找到' } }
|
||||
end
|
||||
end
|
||||
|
||||
def render_unprocessable_entity(message)
|
||||
respond_to do |format|
|
||||
format.html { render 'admins/shared/422' }
|
||||
format.js { render_js_error(message) }
|
||||
format.json { render status: 422, json: { message: message } }
|
||||
end
|
||||
end
|
||||
alias_method :render_error, :render_unprocessable_entity
|
||||
|
||||
def internal_server_error
|
||||
respond_to do |format|
|
||||
format.html { render 'admins/shared/500' }
|
||||
format.js { render_js_error(message) }
|
||||
format.json { render status: 500, json: { message: '系统错误' } }
|
||||
end
|
||||
end
|
||||
|
||||
def render_js_template(template, **opts)
|
||||
render({ template: template, formats: :js }.merge(opts))
|
||||
end
|
||||
|
||||
def render_delete_success
|
||||
render_js_template 'admins/shared/delete'
|
||||
end
|
||||
|
||||
def render_js_error(message)
|
||||
render_js_template 'admins/shared/error', locals: { message: message }
|
||||
end
|
||||
end
|
@ -0,0 +1,11 @@
|
||||
class SidebarUtil
|
||||
class << self
|
||||
def controller_name(name)
|
||||
sidebar_controller_map[name]
|
||||
end
|
||||
|
||||
def sidebar_controller_map
|
||||
@_sidebar_controller_map ||= YAML.load_file(Rails.root.join('config/admins', 'sidebar.yml'))
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,40 @@
|
||||
class Admins::UserQuery < ApplicationQuery
|
||||
include CustomSortable
|
||||
|
||||
attr_reader :params
|
||||
|
||||
sort_columns :created_on, :last_login_on, :experience, :grade, default_by: :created_on, default_direction: :desc
|
||||
|
||||
def initialize(params)
|
||||
@params = params
|
||||
end
|
||||
|
||||
def call
|
||||
users = User.where(type: 'User')
|
||||
|
||||
# 状态
|
||||
status = params[:status]
|
||||
users = users.where(status: status) if status.present?
|
||||
|
||||
# 职业
|
||||
users = users.joins(:user_extension).where(user_extensions: { identity: params[:identity] }) if params[:identity].present?
|
||||
|
||||
# 授权类型
|
||||
if params[:auto_trial].present?
|
||||
users = users.joins(user_extension: :school).where(schools: { auto_users_trial: params[:auto_trial].to_i == 1 })
|
||||
end
|
||||
|
||||
# 关键字检索
|
||||
keyword = params[:keyword].to_s.strip.presence
|
||||
if keyword
|
||||
sql = 'CONCAT(lastname, firstname) LIKE :keyword OR login LIKE :keyword OR mail LIKE :keyword OR phone LIKE :keyword'
|
||||
users = users.where(sql, keyword: keyword)
|
||||
end
|
||||
|
||||
# 学校名称
|
||||
school_name = params[:school_name].to_s.strip.presence
|
||||
users = users.joins(user_extension: :school).where('schools.name LIKE ?', "%#{school_name}%") if school_name
|
||||
|
||||
custom_sort(users, params[:sort_by], params[:sort_direction])
|
||||
end
|
||||
end
|
@ -0,0 +1,52 @@
|
||||
class Admins::UpdateUserService < ApplicationService
|
||||
Error = Class.new(StandardError)
|
||||
|
||||
attr_reader :user, :params
|
||||
|
||||
def initialize(user, params)
|
||||
@user = user
|
||||
@params = params
|
||||
end
|
||||
|
||||
def call
|
||||
user.assign_attributes(user_attributes)
|
||||
user.firstname = ''
|
||||
user.password = password if params[:password].present?
|
||||
|
||||
if params[:identity].to_s == 'student'
|
||||
params[:technical_title] = nil
|
||||
else
|
||||
params[:student_id] = nil
|
||||
end
|
||||
user.user_extension.assign_attributes(user_extension_attributes)
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
user.save!
|
||||
user.user_extension.save!
|
||||
|
||||
update_gitlab_password if params[:password].present?
|
||||
end
|
||||
|
||||
user
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def user_attributes
|
||||
params.slice(*%i[lastname nickname mail phone admin business is_test
|
||||
professional_certification authentication])
|
||||
end
|
||||
|
||||
def user_extension_attributes
|
||||
params.slice(*%i[gender identity technical_title student_id location location_city school_id department_id])
|
||||
end
|
||||
|
||||
def update_gitlab_password
|
||||
return if user.gid.blank?
|
||||
# 同步修改gitlab密码
|
||||
Gitlab.client.edit_user(user.gid, password: params[:password])
|
||||
rescue Exception => ex
|
||||
Util.logger_error(ex)
|
||||
raise Error, '保存失败'
|
||||
end
|
||||
end
|
@ -0,0 +1,188 @@
|
||||
<% define_admin_breadcrumbs do %>
|
||||
<% add_admin_breadcrumb('概览', admins_path) %>
|
||||
<% end %>
|
||||
|
||||
<div class="header bg-gradient-primary pb-8 pt-md-8">
|
||||
<div class="container-fluid">
|
||||
<div class="header-body">
|
||||
<!-- Card stats -->
|
||||
<div class="row">
|
||||
<div class="col-xl-3 col-lg-6">
|
||||
<div class="card card-stats mb-4 mb-xl-0">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h5 class="card-title text-uppercase text-muted mb-0">Traffic</h5>
|
||||
<span class="h2 font-weight-bold mb-0">350,897</span>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="icon icon-shape bg-danger text-white rounded-circle shadow">
|
||||
<i class="fas fa-pie-chart"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-3 mb-0 text-muted text-sm">
|
||||
<span class="text-success mr-2"><i class="fa fa-arrow-up"></i> 3.48%</span>
|
||||
<span class="text-nowrap">Since last month</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-3 col-lg-6">
|
||||
<div class="card card-stats mb-4 mb-xl-0">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h5 class="card-title text-uppercase text-muted mb-0">New users</h5>
|
||||
<span class="h2 font-weight-bold mb-0">2,356</span>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="icon icon-shape bg-warning text-white rounded-circle shadow">
|
||||
<i class="fas fa-pie-chart"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-3 mb-0 text-muted text-sm">
|
||||
<span class="text-danger mr-2"><i class="fas fa-arrow-down"></i> 3.48%</span>
|
||||
<span class="text-nowrap">Since last week</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-3 col-lg-6">
|
||||
<div class="card card-stats mb-4 mb-xl-0">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h5 class="card-title text-uppercase text-muted mb-0">Sales</h5>
|
||||
<span class="h2 font-weight-bold mb-0">924</span>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="icon icon-shape bg-yellow text-white rounded-circle shadow">
|
||||
<i class="fas fa-user"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-3 mb-0 text-muted text-sm">
|
||||
<span class="text-warning mr-2"><i class="fas fa-arrow-down"></i> 1.10%</span>
|
||||
<span class="text-nowrap">Since yesterday</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-3 col-lg-6">
|
||||
<div class="card card-stats mb-4 mb-xl-0">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h5 class="card-title text-uppercase text-muted mb-0">Performance</h5>
|
||||
<span class="h2 font-weight-bold mb-0">49,65%</span>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<div class="icon icon-shape bg-info text-white rounded-circle shadow">
|
||||
<i class="fas fa-pie-chart"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-3 mb-0 text-muted text-sm">
|
||||
<span class="text-success mr-2"><i class="fas fa-arrow-up"></i> 12%</span>
|
||||
<span class="text-nowrap">Since last month</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-fluid mt--7">
|
||||
<div class="row mt-5">
|
||||
<div class="col-xl-8 mb-5 mb-xl-0">
|
||||
<div class="card shadow">
|
||||
<div class="card-header border-0">
|
||||
<div class="row align-items-center">
|
||||
<div class="col">
|
||||
<h3 class="mb-0">Page visits</h3>
|
||||
</div>
|
||||
<div class="col text-right">
|
||||
<a href="#!" class="btn btn-sm btn-primary">Test</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<!-- Projects table -->
|
||||
<table class="table align-items-center table-flush">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th scope="col">Test</th>
|
||||
<th scope="col">Test</th>
|
||||
<th scope="col">Test</th>
|
||||
<th scope="col">Test</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% 5.times do %>
|
||||
<tr>
|
||||
<th scope="row">/test/</th>
|
||||
<td>4,569</td>
|
||||
<td>340</td>
|
||||
<td>
|
||||
<i class="fas fa-arrow-up text-success mr-3"></i> 46,53%
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-4">
|
||||
<div class="card shadow">
|
||||
<div class="card-header border-0">
|
||||
<div class="row align-items-center">
|
||||
<div class="col">
|
||||
<h3 class="mb-0">Test</h3>
|
||||
</div>
|
||||
<div class="col text-right">
|
||||
<a href="#!" class="btn btn-sm btn-primary">Test</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<!-- Projects table -->
|
||||
<table class="table align-items-center table-flush">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th scope="col">Test</th>
|
||||
<th scope="col">Test</th>
|
||||
<th scope="col"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% 5.times do %>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
Test
|
||||
</th>
|
||||
<td>
|
||||
1,480
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="mr-2">60%</span>
|
||||
<div>
|
||||
<div class="progress">
|
||||
<div class="progress-bar bg-gradient-danger" role="progressbar" aria-valuenow="60" aria-valuemin="0" aria-valuemax="100" style="width: 60%;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,11 @@
|
||||
<%# Link to the "First" page
|
||||
- available local variables
|
||||
url: url to the first page
|
||||
current_page: a page object for the currently displayed page
|
||||
total_pages: total number of pages
|
||||
per_page: number of items to fetch per page
|
||||
remote: data-remote
|
||||
-%>
|
||||
<li class="page-item first-page">
|
||||
<%= link_to_unless current_page.first?, t('views.pagination.first').html_safe, url, remote: remote, class: 'page-link' %>
|
||||
</li>
|
@ -0,0 +1,13 @@
|
||||
<%# Non-link tag that stands for skipped pages...
|
||||
- available local variables
|
||||
current_page: a page object for the currently displayed page
|
||||
total_pages: total number of pages
|
||||
per_page: number of items to fetch per page
|
||||
remote: data-remote
|
||||
-%>
|
||||
<li class="page-item">
|
||||
<%= link_to 'javascript:void(0)', { class: 'page-link' } do %>
|
||||
<%= t('views.pagination.truncate').html_safe %>
|
||||
<span class="sr-only">(current)</span>
|
||||
<% end %>
|
||||
</li>
|
@ -0,0 +1,11 @@
|
||||
<%# Link to the "Last" page
|
||||
- available local variables
|
||||
url: url to the last page
|
||||
current_page: a page object for the currently displayed page
|
||||
total_pages: total number of pages
|
||||
per_page: number of items to fetch per page
|
||||
remote: data-remote
|
||||
-%>
|
||||
<li class="page-item last-page">
|
||||
<%= link_to_unless(current_page.last?, t('views.pagination.last').html_safe, url, remote: remote, class: 'page-link') %>
|
||||
<li class="page-item last-page">
|
@ -0,0 +1,11 @@
|
||||
<%# Link to the "Next" page
|
||||
- available local variables
|
||||
url: url to the next page
|
||||
current_page: a page object for the currently displayed page
|
||||
total_pages: total number of pages
|
||||
per_page: number of items to fetch per page
|
||||
remote: data-remote
|
||||
-%>
|
||||
<li class="page-item next">
|
||||
<%= link_to_unless current_page.last?, t('views.pagination.next').html_safe, url, rel: 'next', remote: remote, class: 'page-link' %>
|
||||
</li>
|
@ -0,0 +1,19 @@
|
||||
<%# Link showing page number
|
||||
- available local variables
|
||||
page: a page object for "this" page
|
||||
url: url to this page
|
||||
current_page: a page object for the currently displayed page
|
||||
total_pages: total number of pages
|
||||
per_page: number of items to fetch per page
|
||||
remote: data-remote
|
||||
-%>
|
||||
<li class="page-item page<%= ' current active' if page.current? %>">
|
||||
<% if page.current? %>
|
||||
<%= link_to url, {remote: remote, rel: page.rel, class: 'page-link'} do %>
|
||||
<%= page %>
|
||||
<span class="sr-only">(current)</span>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= link_to page, url, {remote: remote, rel: page.rel, class: 'page-link'} %>
|
||||
<% end %>
|
||||
</li>
|
@ -0,0 +1,27 @@
|
||||
<%# The container tag
|
||||
- available local variables
|
||||
current_page: a page object for the currently displayed page
|
||||
total_pages: total number of pages
|
||||
per_page: number of items to fetch per page
|
||||
remote: data-remote
|
||||
paginator: the paginator that renders the pagination tags inside
|
||||
-%>
|
||||
<%= paginator.render do -%>
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination justify-content-center">
|
||||
<%= first_page_tag unless current_page.first? %>
|
||||
<%= prev_page_tag unless current_page.first? %>
|
||||
<% each_page do |page| -%>
|
||||
<% if page.display_tag? -%>
|
||||
<%= page_tag page %>
|
||||
<% elsif !page.was_truncated? -%>
|
||||
<%= gap_tag %>
|
||||
<% end -%>
|
||||
<% end -%>
|
||||
<% unless current_page.out_of_range? %>
|
||||
<%= next_page_tag unless current_page.last? %>
|
||||
<%= last_page_tag unless current_page.last? %>
|
||||
<% end %>
|
||||
</ul>
|
||||
</nav>
|
||||
<% end -%>
|
@ -0,0 +1,11 @@
|
||||
<%# Link to the "Previous" page
|
||||
- available local variables
|
||||
url: url to the previous page
|
||||
current_page: a page object for the currently displayed page
|
||||
total_pages: total number of pages
|
||||
per_page: number of items to fetch per page
|
||||
remote: data-remote
|
||||
-%>
|
||||
<li class="page-item prev">
|
||||
<%= link_to_unless current_page.first?, t('views.pagination.previous').html_safe, url, rel: 'prev', remote: remote, class: 'page-link' %>
|
||||
</li>
|
@ -0,0 +1 @@
|
||||
<h3>403</h3>
|
@ -0,0 +1,6 @@
|
||||
<div class="d-flex flex-column align-items-center justify-content-center not-found">
|
||||
<div class="not-found-img">
|
||||
<span>404</span>
|
||||
</div>
|
||||
<div class="not-found-text">资源未找到</div>
|
||||
</div>
|
@ -0,0 +1,6 @@
|
||||
<div class="d-flex flex-column align-items-center justify-content-center not-found">
|
||||
<div class="not-found-img">
|
||||
<span>422</span>
|
||||
</div>
|
||||
<div class="not-found-text"><%= @message %></div>
|
||||
</div>
|
@ -0,0 +1,6 @@
|
||||
<div class="d-flex flex-column align-items-center justify-content-center not-found">
|
||||
<div class="not-found-img">
|
||||
<span>500</span>
|
||||
</div>
|
||||
<div class="not-found-text">系统错误</div>
|
||||
</div>
|
@ -0,0 +1,6 @@
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<%= message %>
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
@ -0,0 +1,13 @@
|
||||
<% if @_admin_breadcrumbs.present? %>
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<% @_admin_breadcrumbs&.each_with_index do |item, index| %>
|
||||
<% if item.url.present? && index != @_admin_breadcrumbs.size - 1 %>
|
||||
<li class="breadcrumb-item"><%= link_to item.text, item.url %></li>
|
||||
<% else %>
|
||||
<li class="breadcrumb-item active" aria-current="page"><%= item.text %></li>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</ol>
|
||||
</nav>
|
||||
<% end %>
|
@ -0,0 +1,20 @@
|
||||
<% flash.each do |k, v| %>
|
||||
<% next unless %w(success danger warning info).include?(k.to_s) %>
|
||||
|
||||
<div class="alert alert-<%= k %> alert-dismissible fade show" role="alert">
|
||||
<%= v %>
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<% end %>
|
||||
<% flash.now.as_json.each do |k, v| %>
|
||||
<% next unless %w(success danger warning info).include?(k.to_s) %>
|
||||
|
||||
<div class="alert alert-<%= k %> alert-dismissible fade show" role="alert">
|
||||
<%= v %>
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<% end %>
|
@ -0,0 +1 @@
|
||||
<tr class="no-data"><td colspan="100">暂无数据</td></tr>
|
@ -0,0 +1,6 @@
|
||||
<div class="paginate-container">
|
||||
<% if objects.size.nonzero? %>
|
||||
<div class="paginate-total"><%= page_entries_info objects %></div>
|
||||
<% end %>
|
||||
<%= paginate objects, views_prefix: 'admins', remote: true %>
|
||||
</div>
|
@ -0,0 +1,63 @@
|
||||
<% sidebar_collapse = request.cookies['admin_sidebar_collapse'].to_s == 'true' %>
|
||||
<nav id="sidebar" class="<%= sidebar_collapse ? 'active' : '' %>">
|
||||
<div class="sidebar-header">
|
||||
<a href="/" class="sidebar-header-logo" data-toggle="tooltip" title="返回主站">
|
||||
<%= image_tag('logo.png') %>
|
||||
</a>
|
||||
<div id="sidebarCollapse" class="navbar-btn <%= sidebar_collapse ? 'active' : '' %>">
|
||||
<i class="fa fa-chevron-left fold" data-toggle="tooltip" data-placement="right" data-boundary="window" title="收起"></i>
|
||||
<i class="fa fa-bars unfold" data-toggle="tooltip" data-placement="right" data-boundary="window" title="展开"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar Links -->
|
||||
<ul class="list-unstyled components">
|
||||
<li><%= sidebar_item(admins_path, '概览', icon: 'dashboard', controller: 'admins-dashboards') %></li>
|
||||
<li>
|
||||
<%= sidebar_item_group('#school-submenu', '学校统计', icon: 'area-chart') do %>
|
||||
<li><%= sidebar_item('#', '统计总表', icon: 'bar-chart', controller: '') %></li>
|
||||
<li><%= sidebar_item('#', '数据变化报表', icon: 'line-chart', controller: '') %></li>
|
||||
<% end %>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<%= sidebar_item_group('#course-submenu', '课堂+', icon: 'mortar-board') do %>
|
||||
<li><%= sidebar_item('#', '课程列表', icon: 'calendar', controller: '') %></li>
|
||||
<li><%= sidebar_item('#', '课堂列表', icon: 'book', controller: '') %></li>
|
||||
<li><%= sidebar_item('#', '实训作业', icon: 'file-code-o', controller: '') %></li>
|
||||
<li><%= sidebar_item('#', '项目列表', icon: 'sitemap', controller: '') %></li>
|
||||
<% end %>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<%= sidebar_item_group('#user-submenu', '用户', icon: 'user') do %>
|
||||
<li><%= sidebar_item(admins_users_path, '用户列表', icon: 'user', controller: 'admins-users') %></li>
|
||||
<li><%= sidebar_item('#', '试用授权列表', icon: 'id-card-o', controller: '') %></li>
|
||||
<li><%= sidebar_item('#', '自动授权列表', icon: 'id-card', controller: '') %></li>
|
||||
<% end %>
|
||||
</li>
|
||||
|
||||
<li><%= sidebar_item('/', '返回主站', icon: 'sign-out', controller: 'root') %></li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<script>
|
||||
$(function(){
|
||||
$('#sidebarCollapse').on('click', function () {
|
||||
$(this).toggleClass('active');
|
||||
$('#sidebar').toggleClass('active');
|
||||
$.cookie('admin_sidebar_collapse', $(this).hasClass('active'), { path: '/admins' });
|
||||
});
|
||||
});
|
||||
|
||||
$(document).on('turbolinks:load', function(){
|
||||
var sidebarController = '<%= admin_sidebar_controller %>';
|
||||
if (sidebarController.length > 0) {
|
||||
$('#sidebar a.active').removeClass('active');
|
||||
$('#sidebar ul.collapse.show').removeClass('show');
|
||||
var activeLi = $('#sidebar a[data-controller="' + sidebarController + '"]');
|
||||
activeLi.addClass('active');
|
||||
activeLi.parent().parent('ul.collapse').addClass('show');
|
||||
}
|
||||
});
|
||||
</script>
|
@ -0,0 +1,20 @@
|
||||
var deleteRow = $('<%= params[:element] %>');
|
||||
var refreshUrl = '<%= params[:refresh_url] %>';
|
||||
|
||||
var refreshFunc = function(url) {
|
||||
$.ajax({
|
||||
url: url.length > 0 ? url : window.location.href,
|
||||
method: 'GET',
|
||||
dataType: "script"
|
||||
})
|
||||
}
|
||||
|
||||
if(deleteRow.length > 0){
|
||||
var needRefresh = deleteRow.siblings().length == 0;
|
||||
|
||||
deleteRow.remove();
|
||||
|
||||
if(needRefresh){ refreshFunc(refreshUrl); }
|
||||
} else {
|
||||
refreshFunc(refreshUrl);
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
$('.admin-alert-container').html('<%= j( render partial: 'admins/shared/alert', locals: { message: message } ) %>');
|
||||
|
||||
setTimeout(function() {
|
||||
if ($('.admin-alert-container button.close').length > 0) {
|
||||
$('.admin-alert-container button.close').trigger('click');
|
||||
}
|
||||
}, 2000)
|
@ -0,0 +1,137 @@
|
||||
<%
|
||||
define_admin_breadcrumbs do
|
||||
add_admin_breadcrumb('用户管理', admins_users_path)
|
||||
add_admin_breadcrumb('用户详情')
|
||||
end
|
||||
%>
|
||||
|
||||
<div class="box user-edit-container">
|
||||
<div class="user-info mb-4 row">
|
||||
<%= link_to "/users/#{@user.login}", class: 'user-info-avatar col-md-1', target: '_blank', data: { toggle: 'tooltip', title: '个人中心' } do %>
|
||||
<img src="/images/<%= url_to_avatar(@user) %>" class="rounded-circle" width="80" height="80" />
|
||||
<% end %>
|
||||
<div class="d-flex flex-column justify-content-between col-md-3 user-info-content">
|
||||
<div class="user-info-name flex"><%= @user.real_name %> | <%= @user.id %> | <%= @user.login %></div>
|
||||
|
||||
<div class="d-flex flex-row user-info-auth">
|
||||
<% if @user.authentication? %>
|
||||
<i class="fa fa-user text-success" data-toggle="tooltip" data-placement="bottom" title="已实名认证"></i>
|
||||
<% elsif @user.process_real_name_apply.present? %>
|
||||
<i class="fa fa-user text-danger" data-toggle="tooltip" data-placement="bottom" title="实名认证中"></i>
|
||||
<% else %>
|
||||
<i class="fa fa-user text-muted" data-toggle="tooltip" data-placement="bottom" title="未实名认证"></i>
|
||||
<% end %>
|
||||
|
||||
<% if @user.professional_certification %>
|
||||
<i class="fa fa-list-alt text-success" data-toggle="tooltip" data-placement="bottom" title="已职业认证"></i>
|
||||
<% elsif @user.process_professional_apply.present? %>
|
||||
<i class="fa fa-list-alt text-danger" data-toggle="tooltip" data-placement="bottom" title="职业认证中"></i>
|
||||
<% else %>
|
||||
<i class="fa fa-list-alt text-muted" data-toggle="tooltip" data-placement="bottom" title="未职业认证"></i>
|
||||
<% end %>
|
||||
|
||||
<% if @user.phone.present? %>
|
||||
<i class="fa fa-mobile text-success" data-toggle="tooltip" data-placement="bottom" title="已绑定手机"></i>
|
||||
<% else %>
|
||||
<i class="fa fa-mobile text-muted" data-toggle="tooltip" data-placement="bottom" title="未绑定手机"></i>
|
||||
<% end %>
|
||||
|
||||
<% if @user.mail.present? %>
|
||||
<i class="fa fa-envelope text-success" data-toggle="tooltip" data-placement="bottom" title="已绑定邮箱"></i>
|
||||
<% else %>
|
||||
<i class="fa fa-envelope text-muted" data-toggle="tooltip" data-placement="bottom" title="未绑定邮箱"></i>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="user-info-last-login">最近登录:<%= @user.last_login_on&.strftime('%Y-%m-%d %H:%M') %></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= simple_form_for(@user, url: admins_user_path(@user)) do |f| %>
|
||||
|
||||
<div><h6>基本信息</h6></div>
|
||||
<div class="form-group px-2">
|
||||
<div class="form-row">
|
||||
<%= f.input :lastname, label: '姓名', wrapper_html: { class: 'col-md-3' }, input_html: { class: 'col-md-11', value: @user.only_real_name } %>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<%= f.input :nickname, label: '昵称', wrapper_html: { class: 'col-md-3' }, input_html: { class: 'col-md-11' } %>
|
||||
<%= f.input :gender, as: :radio_buttons, label: '性别', collection: [%w(男 0), %w(女 1)], wrapper_html: { class: 'col-md-3' } %>
|
||||
</div>
|
||||
|
||||
<div class="form-row user-identity-select">
|
||||
<div class="form-group select optional col-md-1">
|
||||
<%= f.label :identity, label: '职业' %>
|
||||
<%= select_tag('user[identity]', [], class: 'form-control identity-select optional', 'data-value': @user.user_extension&.identity, 'data-first-title': '请选择') %>
|
||||
</div>
|
||||
<div class="form-group technical-title-select-wrapper optional col-md-1" style="<%= @user.user_extension.student? ? 'display:none;' : '' %>">
|
||||
<%= f.label :technical_title, label: '职称' %>
|
||||
<%= select_tag('user[technical_title]', [], class: 'form-control technical-title-select optional', 'data-value': @user.technical_title) %>
|
||||
</div>
|
||||
|
||||
<%= f.input :student_id, as: :tel, label: '学号', wrapper_html: { class: 'col-md-2', style: @user.user_extension.student? ? '' : 'display:none;' }, input_html: { class: 'student-id-input' } %>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<%= f.input :mail, as: :email, label: '邮箱地址', wrapper_html: { class: 'col-md-3' }, input_html: { class: 'col-sm-11' } %>
|
||||
<%= f.input :phone, as: :tel, label: '手机号', wrapper_html: { class: 'col-md-3' }, input_html: { class: 'col-sm-11' } %>
|
||||
</div>
|
||||
|
||||
<div class="form-row province-city-select">
|
||||
<div class="form-group select optional col-md-2">
|
||||
<%= f.label :location, label: '省份' %>
|
||||
<%= select_tag('user[location]', [], class: 'form-control province-select optional', 'data-value': @user.location, 'data-first-title': '请选择') %>
|
||||
</div>
|
||||
<div class="form-group select optional col-md-2">
|
||||
<%= f.label :location_city, label: '城市' %>
|
||||
<%= select_tag('user[location_city]', [], class: 'form-control city-select optional', 'data-value': @user.location_city) %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<%= f.input :school_id, as: :hidden %>
|
||||
<%= f.input :department_id, as: :hidden %>
|
||||
<div class="form-group select optional col-md-2">
|
||||
<%= f.label :school_name, label: '所属学校/单位' %>
|
||||
<%= f.select :school_name, [@user.school_id], {}, class: 'form-control school-select optional' %>
|
||||
</div>
|
||||
<div class="form-group select optional col-md-2">
|
||||
<%= f.label :department_name, label: '所属学院/部门' %>
|
||||
<%= f.select :department_name, [@user.department_id], {}, class: 'form-control department-select optional' %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4"><h6>管理</h6></div>
|
||||
<div class="form-group px-2">
|
||||
<% if current_user.admin? %>
|
||||
<div class="form-group check_boxes optional">
|
||||
<%= f.label :role, label: '角色' %>
|
||||
<div class="d-flex">
|
||||
<%= f.input :admin, as: :boolean, label: '管理员', checked_value: 1, unchecked_value: 0 %>
|
||||
<%= f.input :business, as: :boolean, label: '运营人员', wrapper_html: { class: 'ml-3' }, checked_value: 1, unchecked_value: 0 %>
|
||||
<%= f.input :is_test, as: :boolean, label: '测试账号', wrapper_html: { class: 'ml-3' }, checked_value: 1, unchecked_value: 0 %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="form-group check_boxes optional">
|
||||
<%= f.label :role, label: '认证信息' %>
|
||||
<div class="d-flex">
|
||||
<%= f.input :professional_certification, as: :boolean, label: '职业认证', checked_value: 1, unchecked_value: 0 %>
|
||||
<%= f.input :authentication, as: :boolean, label: '实名认证', wrapper_html: { class: 'ml-3' }, checked_value: 1, unchecked_value: 0 %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<%= f.input :password, as: :password, label: '修改密码', wrapper_html: { class: 'col-md-3' }, input_html: { class: 'col-sm-11' } %>
|
||||
<%= f.input :password_confirmation, as: :password, label: '确认密码', wrapper_html: { class: 'col-md-3' }, input_html: { class: 'col-sm-11' } %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row mt-4">
|
||||
<%= f.button :submit, value: '保存', class: 'btn-primary mr-3 px-4' %>
|
||||
<%= link_to '取消', 'javascript:history.go(-1)', class: 'btn btn-secondary px-4' %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
@ -0,0 +1,35 @@
|
||||
<% define_admin_breadcrumbs do %>
|
||||
<% add_admin_breadcrumb('用户管理', admins_users_path) %>
|
||||
<% end %>
|
||||
|
||||
<div class="box search-form-container user-list-form">
|
||||
<%= form_tag(admins_users_path, method: :get, class: 'form-inline search-form', remote: true) do %>
|
||||
<div class="form-group mr-2">
|
||||
<label for="status">状态:</label>
|
||||
<% status_options = [['全部', ''], ['正常', User::STATUS_ACTIVE], ['未激活', User::STATUS_REGISTERED], ['已锁定', User::STATUS_LOCKED]] %>
|
||||
<%= select_tag(:status, options_for_select(status_options), class: 'form-control') %>
|
||||
</div>
|
||||
|
||||
<div class="form-group mr-2">
|
||||
<label for="identity">职业:</label>
|
||||
<% identity_options = [['全部', '']] + UserExtension.identities.map { |k, v| [I18n.t("user.identity.#{k}"), v] } %>
|
||||
<%= select_tag(:identity, options_for_select(identity_options), class: 'form-control') %>
|
||||
</div>
|
||||
|
||||
<div class="form-group mr-2">
|
||||
<label for="identity">授权类型:</label>
|
||||
<% auto_trial_options = [['全部', ''], ['自动授权', 1], ['手动授权', 0]] %>
|
||||
<%= select_tag(:auto_trial, options_for_select(auto_trial_options), class: 'form-control') %>
|
||||
</div>
|
||||
|
||||
<%= text_field_tag(:keyword, params[:keyword], class: 'form-control col-sm-2 ml-3', placeholder: 'ID/姓名/邮箱/手机号检索') %>
|
||||
<%= text_field_tag(:school_name, params[:school_name], class: 'form-control col-sm-2', placeholder: '学校/单位检索') %>
|
||||
<%= submit_tag('搜索', class: 'btn btn-primary ml-3') %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="box users-list-container">
|
||||
<%= render partial: 'admins/users/shared/user_list', locals: { users: @users } %>
|
||||
</div>
|
||||
|
||||
<%= render partial: 'admins/users/shared/reward_grade_modal' %>
|
@ -0,0 +1 @@
|
||||
$('.users-list-container').html("<%= j( render partial: 'admins/users/shared/user_list', locals: { users: @users } ) %>");
|
@ -0,0 +1,26 @@
|
||||
<div class="modal fade admin-users-reward-grade-modal" tabindex="-1" role="dialog" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="exampleModalLongTitle">奖励金币</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="admin-users-reward-grade-form">
|
||||
<%= hidden_field_tag(:user_id, nil) %>
|
||||
<div class="form-group">
|
||||
<label for="grade" class="col-form-label">金币数量:</label>
|
||||
<%= number_field_tag(:grade, nil, class: 'form-control') %>
|
||||
</div>
|
||||
<div class="error text-danger"></div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
|
||||
<button type="button" class="btn btn-primary submit-btn">确认</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,60 @@
|
||||
<table class="table table-hover users-list-table">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th width="10%">ID</th>
|
||||
<th width="10%">真实姓名</th>
|
||||
<th width="16%">邮件地址</th>
|
||||
<th width="10%">手机号码</th>
|
||||
<th width="14%">单位</th>
|
||||
<th width="12%"><%= sort_tag('创建于', name: 'created_on', path: admins_users_path) %></th>
|
||||
<th width="12%"><%= sort_tag('最后登录', name: 'last_login_on', path: admins_users_path) %></th>
|
||||
<th width="10%"><%= sort_tag('经验值', name: 'experience', path: admins_users_path) %></th>
|
||||
<th width="8%"><%= sort_tag('金币', name: 'grade', path: admins_users_path) %></th>
|
||||
<th width="14%">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% if users.present? %>
|
||||
<% users.each do |user| %>
|
||||
<tr class="user-item-<%= user.id %>">
|
||||
<td>
|
||||
<%= link_to "/users/#{user.login}", target: '_blank' do %>
|
||||
<%= overflow_hidden_span user.login, width: 100 %>
|
||||
<% end %>
|
||||
</td>
|
||||
<td>
|
||||
<%= link_to edit_admins_user_path(user) do %>
|
||||
<%= overflow_hidden_span user.real_name, width: 100 %>
|
||||
<% end %>
|
||||
</td>
|
||||
<td><%= overflow_hidden_span display_text(user.mail), width: 150 %></td>
|
||||
<td><%= overflow_hidden_span display_text(user.phone), width: 100 %></td>
|
||||
<td><%= overflow_hidden_span display_text(user.school_name), width: 150 %></td>
|
||||
<td><%= display_text(user.created_on&.strftime('%Y-%m-%d %H:%M')) %></td>
|
||||
<td><%= display_text(user.last_login_on&.strftime('%Y-%m-%d %H:%M')) %></td>
|
||||
<td><%= user.experience.to_i %></td>
|
||||
<td class="grade-content"><%= user.grade.to_i %></td>
|
||||
<td class="action-container">
|
||||
<%= javascript_void_link('奖励', class: 'action reward-grade-action', data: { toggle: 'modal', target: '.admin-users-reward-grade-modal', id: user.id }) %>
|
||||
|
||||
<%= javascript_void_link '解锁', class: 'action unlock-action', data: { id: user.id, confirm: '确认解锁吗?' }, style: user.locked? ? '' : 'display: none;' %>
|
||||
|
||||
<% if user.registered? %>
|
||||
<%= javascript_void_link '激活', class: 'action active-action', data: { id: user.id, confirm: '确认激活吗?' } %>
|
||||
<% end %>
|
||||
|
||||
<% if user.id != current_user.id %>
|
||||
<%= javascript_void_link '加锁', class: 'action lock-action', data: { id: user.id, confirm: '确认加锁吗?' }, style: user.locked? || user.registered? ? 'display: none;' : '' %>
|
||||
<% end %>
|
||||
|
||||
<%= delete_link '删除', admins_user_path(user, element: ".user-item-#{user.id}"), class: 'delete-user-action' %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= render 'admins/shared/no_data_for_table' %>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<%= render partial: 'admins/shared/paginate', locals: { objects: users } %>
|
@ -0,0 +1,6 @@
|
||||
<% define_admin_breadcrumbs do %>
|
||||
<% add_admin_breadcrumb('用户管理', admins_users_path) %>
|
||||
<% add_admin_breadcrumb('用户详情') %>
|
||||
<% end %>
|
||||
|
||||
<h3>Users Show</h3>
|
@ -0,0 +1 @@
|
||||
admins-users: admins-users
|
@ -0,0 +1,22 @@
|
||||
defaults: &defaults
|
||||
access_key_id: 'test'
|
||||
access_key_secret: 'test'
|
||||
base_url: 'http://vod.cn-shanghai.aliyuncs.com'
|
||||
cate_id: '-1'
|
||||
callback_url: 'http://47.96.87.25:48080/api/callbacks/aliyun_vod.json'
|
||||
signature_key: 'test12345678'
|
||||
|
||||
development:
|
||||
<<: *defaults
|
||||
access_key_id: 'LTAI4kRL1DxQPdM2'
|
||||
access_key_secret: 'yiz68rxE6imziBTITggWcOeSqjUeUu'
|
||||
cate_id: '1000068305'
|
||||
base_url: 'http://vod.cn-shanghai.aliyuncs.com'
|
||||
callback_url: 'http://47.96.87.25:48080/api/callbacks/aliyun_vod.json'
|
||||
signature_key: 'sdgdfDGH14DHD5g465123'
|
||||
|
||||
test:
|
||||
<<: *defaults
|
||||
|
||||
production:
|
||||
<<: *defaults
|
@ -0,0 +1,179 @@
|
||||
# frozen_string_literal: true
|
||||
#
|
||||
# Uncomment this and change the path if necessary to include your own
|
||||
# components.
|
||||
# See https://github.com/plataformatec/simple_form#custom-components to know
|
||||
# more about custom components.
|
||||
# Dir[Rails.root.join('lib/components/**/*.rb')].each { |f| require f }
|
||||
#
|
||||
# Use this setup block to configure all options available in SimpleForm.
|
||||
SimpleForm.setup do |config|
|
||||
# Wrappers are used by the form builder to generate a
|
||||
# complete input. You can remove any component from the
|
||||
# wrapper, change the order or even add your own to the
|
||||
# stack. The options given below are used to wrap the
|
||||
# whole input.
|
||||
config.wrappers :default, class: :input,
|
||||
hint_class: :field_with_hint, error_class: :field_with_errors, valid_class: :field_without_errors do |b|
|
||||
## Extensions enabled by default
|
||||
# Any of these extensions can be disabled for a
|
||||
# given input by passing: `f.input EXTENSION_NAME => false`.
|
||||
# You can make any of these extensions optional by
|
||||
# renaming `b.use` to `b.optional`.
|
||||
|
||||
# Determines whether to use HTML5 (:email, :url, ...)
|
||||
# and required attributes
|
||||
b.use :html5
|
||||
|
||||
# Calculates placeholders automatically from I18n
|
||||
# You can also pass a string as f.input placeholder: "Placeholder"
|
||||
b.use :placeholder
|
||||
|
||||
## Optional extensions
|
||||
# They are disabled unless you pass `f.input EXTENSION_NAME => true`
|
||||
# to the input. If so, they will retrieve the values from the model
|
||||
# if any exists. If you want to enable any of those
|
||||
# extensions by default, you can change `b.optional` to `b.use`.
|
||||
|
||||
# Calculates maxlength from length validations for string inputs
|
||||
# and/or database column lengths
|
||||
b.optional :maxlength
|
||||
|
||||
# Calculate minlength from length validations for string inputs
|
||||
b.optional :minlength
|
||||
|
||||
# Calculates pattern from format validations for string inputs
|
||||
b.optional :pattern
|
||||
|
||||
# Calculates min and max from length validations for numeric inputs
|
||||
b.optional :min_max
|
||||
|
||||
# Calculates readonly automatically from readonly attributes
|
||||
b.optional :readonly
|
||||
|
||||
## Inputs
|
||||
# b.use :input, class: 'input', error_class: 'is-invalid', valid_class: 'is-valid'
|
||||
b.use :label_input
|
||||
b.use :hint, wrap_with: { tag: :span, class: :hint }
|
||||
b.use :error, wrap_with: { tag: :span, class: :error }
|
||||
|
||||
## full_messages_for
|
||||
# If you want to display the full error message for the attribute, you can
|
||||
# use the component :full_error, like:
|
||||
#
|
||||
# b.use :full_error, wrap_with: { tag: :span, class: :error }
|
||||
end
|
||||
|
||||
# The default wrapper to be used by the FormBuilder.
|
||||
config.default_wrapper = :default
|
||||
|
||||
# Define the way to render check boxes / radio buttons with labels.
|
||||
# Defaults to :nested for bootstrap config.
|
||||
# inline: input + label
|
||||
# nested: label > input
|
||||
config.boolean_style = :nested
|
||||
|
||||
# Default class for buttons
|
||||
config.button_class = 'btn'
|
||||
|
||||
# Method used to tidy up errors. Specify any Rails Array method.
|
||||
# :first lists the first message for each field.
|
||||
# Use :to_sentence to list all errors for each field.
|
||||
# config.error_method = :first
|
||||
|
||||
# Default tag used for error notification helper.
|
||||
config.error_notification_tag = :div
|
||||
|
||||
# CSS class to add for error notification helper.
|
||||
config.error_notification_class = 'error_notification'
|
||||
|
||||
# Series of attempts to detect a default label method for collection.
|
||||
# config.collection_label_methods = [ :to_label, :name, :title, :to_s ]
|
||||
|
||||
# Series of attempts to detect a default value method for collection.
|
||||
# config.collection_value_methods = [ :id, :to_s ]
|
||||
|
||||
# You can wrap a collection of radio/check boxes in a pre-defined tag, defaulting to none.
|
||||
# config.collection_wrapper_tag = nil
|
||||
|
||||
# You can define the class to use on all collection wrappers. Defaulting to none.
|
||||
# config.collection_wrapper_class = nil
|
||||
|
||||
# You can wrap each item in a collection of radio/check boxes with a tag,
|
||||
# defaulting to :span.
|
||||
# config.item_wrapper_tag = :span
|
||||
|
||||
# You can define a class to use in all item wrappers. Defaulting to none.
|
||||
# config.item_wrapper_class = nil
|
||||
|
||||
# How the label text should be generated altogether with the required text.
|
||||
# config.label_text = lambda { |label, required, explicit_label| "#{required} #{label}" }
|
||||
|
||||
# You can define the class to use on all labels. Default is nil.
|
||||
# config.label_class = nil
|
||||
|
||||
# You can define the default class to be used on forms. Can be overriden
|
||||
# with `html: { :class }`. Defaulting to none.
|
||||
# config.default_form_class = nil
|
||||
|
||||
# You can define which elements should obtain additional classes
|
||||
# config.generate_additional_classes_for = [:wrapper, :label, :input]
|
||||
|
||||
# Whether attributes are required by default (or not). Default is true.
|
||||
# config.required_by_default = true
|
||||
|
||||
# Tell browsers whether to use the native HTML5 validations (novalidate form option).
|
||||
# These validations are enabled in SimpleForm's internal config but disabled by default
|
||||
# in this configuration, which is recommended due to some quirks from different browsers.
|
||||
# To stop SimpleForm from generating the novalidate option, enabling the HTML5 validations,
|
||||
# change this configuration to true.
|
||||
config.browser_validations = false
|
||||
|
||||
# Collection of methods to detect if a file type was given.
|
||||
# config.file_methods = [ :mounted_as, :file?, :public_filename, :attached? ]
|
||||
|
||||
# Custom mappings for input types. This should be a hash containing a regexp
|
||||
# to match as key, and the input type that will be used when the field name
|
||||
# matches the regexp as value.
|
||||
# config.input_mappings = { /count/ => :integer }
|
||||
|
||||
# Custom wrappers for input types. This should be a hash containing an input
|
||||
# type as key and the wrapper that will be used for all inputs with specified type.
|
||||
# config.wrapper_mappings = { string: :prepend }
|
||||
|
||||
# Namespaces where SimpleForm should look for custom input classes that
|
||||
# override default inputs.
|
||||
# config.custom_inputs_namespaces << "CustomInputs"
|
||||
|
||||
# Default priority for time_zone inputs.
|
||||
# config.time_zone_priority = nil
|
||||
|
||||
# Default priority for country inputs.
|
||||
# config.country_priority = nil
|
||||
|
||||
# When false, do not use translations for labels.
|
||||
# config.translate_labels = true
|
||||
|
||||
# Automatically discover new inputs in Rails' autoload path.
|
||||
# config.inputs_discovery = true
|
||||
|
||||
# Cache SimpleForm inputs discovery
|
||||
# config.cache_discovery = !Rails.env.development?
|
||||
|
||||
# Default class for inputs
|
||||
# config.input_class = nil
|
||||
|
||||
# Define the default class of the input wrapper of the boolean input.
|
||||
config.boolean_label_class = 'checkbox'
|
||||
|
||||
# Defines if the default input wrapper class should be included in radio
|
||||
# collection wrappers.
|
||||
# config.include_default_input_wrapper_class = true
|
||||
|
||||
# Defines which i18n scope will be used in Simple Form.
|
||||
# config.i18n_scope = 'simple_form'
|
||||
|
||||
# Defines validation classes to the input_field. By default it's nil.
|
||||
# config.input_field_valid_class = 'is-valid'
|
||||
# config.input_field_error_class = 'is-invalid'
|
||||
end
|
@ -0,0 +1,439 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Please do not make direct changes to this file!
|
||||
# This generator is maintained by the community around simple_form-bootstrap:
|
||||
# https://github.com/rafaelfranca/simple_form-bootstrap
|
||||
# All future development, tests, and organization should happen there.
|
||||
# Background history: https://github.com/plataformatec/simple_form/issues/1561
|
||||
|
||||
# Uncomment this and change the path if necessary to include your own
|
||||
# components.
|
||||
# See https://github.com/plataformatec/simple_form#custom-components
|
||||
# to know more about custom components.
|
||||
# Dir[Rails.root.join('lib/components/**/*.rb')].each { |f| require f }
|
||||
|
||||
# Use this setup block to configure all options available in SimpleForm.
|
||||
SimpleForm.setup do |config|
|
||||
# Default class for buttons
|
||||
config.button_class = 'btn'
|
||||
|
||||
# Define the default class of the input wrapper of the boolean input.
|
||||
config.boolean_label_class = 'form-check-label'
|
||||
|
||||
# How the label text should be generated altogether with the required text.
|
||||
config.label_text = lambda { |label, required, explicit_label| "#{label} #{required}" }
|
||||
|
||||
# Define the way to render check boxes / radio buttons with labels.
|
||||
config.boolean_style = :inline
|
||||
|
||||
# You can wrap each item in a collection of radio/check boxes with a tag
|
||||
config.item_wrapper_tag = :div
|
||||
|
||||
# Defines if the default input wrapper class should be included in radio
|
||||
# collection wrappers.
|
||||
config.include_default_input_wrapper_class = false
|
||||
|
||||
# CSS class to add for error notification helper.
|
||||
config.error_notification_class = 'alert alert-danger'
|
||||
|
||||
# Method used to tidy up errors. Specify any Rails Array method.
|
||||
# :first lists the first message for each field.
|
||||
# :to_sentence to list all errors for each field.
|
||||
config.error_method = :to_sentence
|
||||
|
||||
# add validation classes to `input_field`
|
||||
config.input_field_error_class = 'is-invalid'
|
||||
config.input_field_valid_class = 'is-valid'
|
||||
|
||||
|
||||
# vertical forms
|
||||
#
|
||||
# vertical default_wrapper
|
||||
config.wrappers :vertical_form, tag: 'div', class: 'form-group', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b|
|
||||
b.use :html5
|
||||
b.use :placeholder
|
||||
b.optional :maxlength
|
||||
b.optional :minlength
|
||||
b.optional :pattern
|
||||
b.optional :min_max
|
||||
b.optional :readonly
|
||||
b.use :label, class: 'form-control-label'
|
||||
b.use :input, class: 'form-control', error_class: 'is-invalid'
|
||||
b.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback' }
|
||||
b.use :hint, wrap_with: { tag: 'small', class: 'form-text text-muted' }
|
||||
end
|
||||
|
||||
# vertical input for boolean
|
||||
config.wrappers :vertical_boolean, tag: 'fieldset', class: 'form-group', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b|
|
||||
b.use :html5
|
||||
b.optional :readonly
|
||||
b.wrapper :form_check_wrapper, tag: 'div', class: 'form-check' do |bb|
|
||||
bb.use :input, class: 'form-check-input', error_class: 'is-invalid'
|
||||
bb.use :label, class: 'form-check-label'
|
||||
bb.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback' }
|
||||
bb.use :hint, wrap_with: { tag: 'small', class: 'form-text text-muted' }
|
||||
end
|
||||
end
|
||||
|
||||
# vertical input for radio buttons and check boxes
|
||||
config.wrappers :vertical_collection, item_wrapper_class: 'form-check', tag: 'fieldset', class: 'form-group', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b|
|
||||
b.use :html5
|
||||
b.optional :readonly
|
||||
b.wrapper :legend_tag, tag: 'legend', class: 'col-form-label pt-0' do |ba|
|
||||
ba.use :label_text
|
||||
end
|
||||
b.use :input, class: 'form-check-input', error_class: 'is-invalid'
|
||||
b.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback d-block' }
|
||||
b.use :hint, wrap_with: { tag: 'small', class: 'form-text text-muted' }
|
||||
end
|
||||
|
||||
# vertical input for inline radio buttons and check boxes
|
||||
config.wrappers :vertical_collection_inline, item_wrapper_class: 'form-check form-check-inline', tag: 'fieldset', class: 'form-group', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b|
|
||||
b.use :html5
|
||||
b.optional :readonly
|
||||
b.wrapper :legend_tag, tag: 'legend', class: 'col-form-label pt-0' do |ba|
|
||||
ba.use :label_text
|
||||
end
|
||||
b.use :input, class: 'form-check-input', error_class: 'is-invalid'
|
||||
b.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback d-block' }
|
||||
b.use :hint, wrap_with: { tag: 'small', class: 'form-text text-muted' }
|
||||
end
|
||||
|
||||
# vertical file input
|
||||
config.wrappers :vertical_file, tag: 'div', class: 'form-group', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b|
|
||||
b.use :html5
|
||||
b.use :placeholder
|
||||
b.optional :maxlength
|
||||
b.optional :minlength
|
||||
b.optional :readonly
|
||||
b.use :label
|
||||
b.use :input, class: 'form-control-file', error_class: 'is-invalid'
|
||||
b.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback d-block' }
|
||||
b.use :hint, wrap_with: { tag: 'small', class: 'form-text text-muted' }
|
||||
end
|
||||
|
||||
# vertical multi select
|
||||
config.wrappers :vertical_multi_select, tag: 'div', class: 'form-group', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b|
|
||||
b.use :html5
|
||||
b.optional :readonly
|
||||
b.use :label, class: 'form-control-label'
|
||||
b.wrapper tag: 'div', class: 'd-flex flex-row justify-content-between align-items-center' do |ba|
|
||||
ba.use :input, class: 'form-control mx-1', error_class: 'is-invalid'
|
||||
end
|
||||
b.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback d-block' }
|
||||
b.use :hint, wrap_with: { tag: 'small', class: 'form-text text-muted' }
|
||||
end
|
||||
|
||||
# vertical range input
|
||||
config.wrappers :vertical_range, tag: 'div', class: 'form-group', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b|
|
||||
b.use :html5
|
||||
b.use :placeholder
|
||||
b.optional :readonly
|
||||
b.optional :step
|
||||
b.use :label
|
||||
b.use :input, class: 'form-control-range', error_class: 'is-invalid'
|
||||
b.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback d-block' }
|
||||
b.use :hint, wrap_with: { tag: 'small', class: 'form-text text-muted' }
|
||||
end
|
||||
|
||||
|
||||
# horizontal forms
|
||||
#
|
||||
# horizontal default_wrapper
|
||||
config.wrappers :horizontal_form, tag: 'div', class: 'form-group row', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b|
|
||||
b.use :html5
|
||||
b.use :placeholder
|
||||
b.optional :maxlength
|
||||
b.optional :minlength
|
||||
b.optional :pattern
|
||||
b.optional :min_max
|
||||
b.optional :readonly
|
||||
b.use :label, class: 'col-sm-3 col-form-label'
|
||||
b.wrapper :grid_wrapper, tag: 'div', class: 'col-sm-9' do |ba|
|
||||
ba.use :input, class: 'form-control', error_class: 'is-invalid'
|
||||
ba.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback' }
|
||||
ba.use :hint, wrap_with: { tag: 'small', class: 'form-text text-muted' }
|
||||
end
|
||||
end
|
||||
|
||||
# horizontal input for boolean
|
||||
config.wrappers :horizontal_boolean, tag: 'div', class: 'form-group row', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b|
|
||||
b.use :html5
|
||||
b.optional :readonly
|
||||
b.wrapper tag: 'label', class: 'col-sm-3' do |ba|
|
||||
ba.use :label_text
|
||||
end
|
||||
b.wrapper :grid_wrapper, tag: 'div', class: 'col-sm-9' do |wr|
|
||||
wr.wrapper :form_check_wrapper, tag: 'div', class: 'form-check' do |bb|
|
||||
bb.use :input, class: 'form-check-input', error_class: 'is-invalid'
|
||||
bb.use :label, class: 'form-check-label'
|
||||
bb.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback d-block' }
|
||||
bb.use :hint, wrap_with: { tag: 'small', class: 'form-text text-muted' }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# horizontal input for radio buttons and check boxes
|
||||
config.wrappers :horizontal_collection, item_wrapper_class: 'form-check', tag: 'div', class: 'form-group row', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b|
|
||||
b.use :html5
|
||||
b.optional :readonly
|
||||
b.use :label, class: 'col-sm-3 form-control-label'
|
||||
b.wrapper :grid_wrapper, tag: 'div', class: 'col-sm-9' do |ba|
|
||||
ba.use :input, class: 'form-check-input', error_class: 'is-invalid'
|
||||
ba.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback d-block' }
|
||||
ba.use :hint, wrap_with: { tag: 'small', class: 'form-text text-muted' }
|
||||
end
|
||||
end
|
||||
|
||||
# horizontal input for inline radio buttons and check boxes
|
||||
config.wrappers :horizontal_collection_inline, item_wrapper_class: 'form-check form-check-inline', tag: 'div', class: 'form-group row', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b|
|
||||
b.use :html5
|
||||
b.optional :readonly
|
||||
b.use :label, class: 'col-sm-3 form-control-label'
|
||||
b.wrapper :grid_wrapper, tag: 'div', class: 'col-sm-9' do |ba|
|
||||
ba.use :input, class: 'form-check-input', error_class: 'is-invalid'
|
||||
ba.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback d-block' }
|
||||
ba.use :hint, wrap_with: { tag: 'small', class: 'form-text text-muted' }
|
||||
end
|
||||
end
|
||||
|
||||
# horizontal file input
|
||||
config.wrappers :horizontal_file, tag: 'div', class: 'form-group row', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b|
|
||||
b.use :html5
|
||||
b.use :placeholder
|
||||
b.optional :maxlength
|
||||
b.optional :minlength
|
||||
b.optional :readonly
|
||||
b.use :label, class: 'col-sm-3 form-control-label'
|
||||
b.wrapper :grid_wrapper, tag: 'div', class: 'col-sm-9' do |ba|
|
||||
ba.use :input, error_class: 'is-invalid'
|
||||
ba.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback d-block' }
|
||||
ba.use :hint, wrap_with: { tag: 'small', class: 'form-text text-muted' }
|
||||
end
|
||||
end
|
||||
|
||||
# horizontal multi select
|
||||
config.wrappers :horizontal_multi_select, tag: 'div', class: 'form-group row', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b|
|
||||
b.use :html5
|
||||
b.optional :readonly
|
||||
b.use :label, class: 'col-sm-3 control-label'
|
||||
b.wrapper :grid_wrapper, tag: 'div', class: 'col-sm-9' do |ba|
|
||||
ba.wrapper tag: 'div', class: 'd-flex flex-row justify-content-between align-items-center' do |bb|
|
||||
bb.use :input, class: 'form-control mx-1', error_class: 'is-invalid'
|
||||
end
|
||||
ba.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback d-block' }
|
||||
ba.use :hint, wrap_with: { tag: 'small', class: 'form-text text-muted' }
|
||||
end
|
||||
end
|
||||
|
||||
# horizontal range input
|
||||
config.wrappers :horizontal_range, tag: 'div', class: 'form-group row', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b|
|
||||
b.use :html5
|
||||
b.use :placeholder
|
||||
b.optional :readonly
|
||||
b.optional :step
|
||||
b.use :label, class: 'col-sm-3 form-control-label'
|
||||
b.wrapper :grid_wrapper, tag: 'div', class: 'col-sm-9' do |ba|
|
||||
ba.use :input, class: 'form-control-range', error_class: 'is-invalid'
|
||||
ba.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback d-block' }
|
||||
ba.use :hint, wrap_with: { tag: 'small', class: 'form-text text-muted' }
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# inline forms
|
||||
#
|
||||
# inline default_wrapper
|
||||
config.wrappers :inline_form, tag: 'span', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b|
|
||||
b.use :html5
|
||||
b.use :placeholder
|
||||
b.optional :maxlength
|
||||
b.optional :minlength
|
||||
b.optional :pattern
|
||||
b.optional :min_max
|
||||
b.optional :readonly
|
||||
b.use :label, class: 'sr-only'
|
||||
|
||||
b.use :input, class: 'form-control', error_class: 'is-invalid'
|
||||
b.use :error, wrap_with: { tag: 'div', class: 'invalid-feedback' }
|
||||
b.optional :hint, wrap_with: { tag: 'small', class: 'form-text text-muted' }
|
||||
end
|
||||
|
||||
# inline input for boolean
|
||||
config.wrappers :inline_boolean, tag: 'span', class: 'form-check flex-wrap justify-content-start mr-sm-2', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b|
|
||||
b.use :html5
|
||||
b.optional :readonly
|
||||
b.use :input, class: 'form-check-input', error_class: 'is-invalid'
|
||||
b.use :label, class: 'form-check-label'
|
||||
b.use :error, wrap_with: { tag: 'div', class: 'invalid-feedback' }
|
||||
b.optional :hint, wrap_with: { tag: 'small', class: 'form-text text-muted' }
|
||||
end
|
||||
|
||||
|
||||
# bootstrap custom forms
|
||||
#
|
||||
# custom input for boolean
|
||||
config.wrappers :custom_boolean, tag: 'fieldset', class: 'form-group', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b|
|
||||
b.use :html5
|
||||
b.optional :readonly
|
||||
b.wrapper :form_check_wrapper, tag: 'div', class: 'custom-control custom-checkbox' do |bb|
|
||||
bb.use :input, class: 'custom-control-input', error_class: 'is-invalid'
|
||||
bb.use :label, class: 'custom-control-label'
|
||||
bb.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback' }
|
||||
bb.use :hint, wrap_with: { tag: 'small', class: 'form-text text-muted' }
|
||||
end
|
||||
end
|
||||
|
||||
config.wrappers :custom_boolean_switch, tag: 'fieldset', class: 'form-group', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b|
|
||||
b.use :html5
|
||||
b.optional :readonly
|
||||
b.wrapper :form_check_wrapper, tag: 'div', class: 'custom-control custom-checkbox-switch' do |bb|
|
||||
bb.use :input, class: 'custom-control-input', error_class: 'is-invalid'
|
||||
bb.use :label, class: 'custom-control-label'
|
||||
bb.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback' }
|
||||
bb.use :hint, wrap_with: { tag: 'small', class: 'form-text text-muted' }
|
||||
end
|
||||
end
|
||||
|
||||
# custom input for radio buttons and check boxes
|
||||
config.wrappers :custom_collection, item_wrapper_class: 'custom-control', tag: 'fieldset', class: 'form-group', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b|
|
||||
b.use :html5
|
||||
b.optional :readonly
|
||||
b.wrapper :legend_tag, tag: 'legend', class: 'col-form-label pt-0' do |ba|
|
||||
ba.use :label_text
|
||||
end
|
||||
b.use :input, class: 'custom-control-input', error_class: 'is-invalid'
|
||||
b.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback d-block' }
|
||||
b.use :hint, wrap_with: { tag: 'small', class: 'form-text text-muted' }
|
||||
end
|
||||
|
||||
# custom input for inline radio buttons and check boxes
|
||||
config.wrappers :custom_collection_inline, item_wrapper_class: 'custom-control custom-control-inline', tag: 'fieldset', class: 'form-group', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b|
|
||||
b.use :html5
|
||||
b.optional :readonly
|
||||
b.wrapper :legend_tag, tag: 'legend', class: 'col-form-label pt-0' do |ba|
|
||||
ba.use :label_text
|
||||
end
|
||||
b.use :input, class: 'custom-control-input', error_class: 'is-invalid'
|
||||
b.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback d-block' }
|
||||
b.use :hint, wrap_with: { tag: 'small', class: 'form-text text-muted' }
|
||||
end
|
||||
|
||||
# custom file input
|
||||
config.wrappers :custom_file, tag: 'div', class: 'form-group', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b|
|
||||
b.use :html5
|
||||
b.use :placeholder
|
||||
b.optional :maxlength
|
||||
b.optional :minlength
|
||||
b.optional :readonly
|
||||
b.use :label, class: 'form-control-label'
|
||||
b.wrapper :custom_file_wrapper, tag: 'div', class: 'custom-file' do |ba|
|
||||
ba.use :input, class: 'custom-file-input', error_class: 'is-invalid'
|
||||
ba.use :label, class: 'custom-file-label'
|
||||
ba.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback' }
|
||||
end
|
||||
b.use :hint, wrap_with: { tag: 'small', class: 'form-text text-muted' }
|
||||
end
|
||||
|
||||
# custom multi select
|
||||
config.wrappers :custom_multi_select, tag: 'div', class: 'form-group', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b|
|
||||
b.use :html5
|
||||
b.optional :readonly
|
||||
b.use :label, class: 'form-control-label'
|
||||
b.wrapper tag: 'div', class: 'd-flex flex-row justify-content-between align-items-center' do |ba|
|
||||
ba.use :input, class: 'custom-select mx-1', error_class: 'is-invalid'
|
||||
end
|
||||
b.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback d-block' }
|
||||
b.use :hint, wrap_with: { tag: 'small', class: 'form-text text-muted' }
|
||||
end
|
||||
|
||||
# custom range input
|
||||
config.wrappers :custom_range, tag: 'div', class: 'form-group', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b|
|
||||
b.use :html5
|
||||
b.use :placeholder
|
||||
b.optional :readonly
|
||||
b.optional :step
|
||||
b.use :label, class: 'form-control-label'
|
||||
b.use :input, class: 'custom-range', error_class: 'is-invalid'
|
||||
b.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback d-block' }
|
||||
b.use :hint, wrap_with: { tag: 'small', class: 'form-text text-muted' }
|
||||
end
|
||||
|
||||
|
||||
# Input Group - custom component
|
||||
# see example app and config at https://github.com/rafaelfranca/simple_form-bootstrap
|
||||
# config.wrappers :input_group, tag: 'div', class: 'form-group', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b|
|
||||
# b.use :html5
|
||||
# b.use :placeholder
|
||||
# b.optional :maxlength
|
||||
# b.optional :minlength
|
||||
# b.optional :pattern
|
||||
# b.optional :min_max
|
||||
# b.optional :readonly
|
||||
# b.use :label, class: 'form-control-label'
|
||||
# b.wrapper :input_group_tag, tag: 'div', class: 'input-group' do |ba|
|
||||
# ba.optional :prepend
|
||||
# ba.use :input, class: 'form-control', error_class: 'is-invalid'
|
||||
# ba.optional :append
|
||||
# end
|
||||
# b.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback d-block' }
|
||||
# b.use :hint, wrap_with: { tag: 'small', class: 'form-text text-muted' }
|
||||
# end
|
||||
|
||||
|
||||
# Floating Labels form
|
||||
#
|
||||
# floating labels default_wrapper
|
||||
config.wrappers :floating_labels_form, tag: 'div', class: 'form-label-group', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b|
|
||||
b.use :html5
|
||||
b.use :placeholder
|
||||
b.optional :maxlength
|
||||
b.optional :minlength
|
||||
b.optional :pattern
|
||||
b.optional :min_max
|
||||
b.optional :readonly
|
||||
b.use :input, class: 'form-control', error_class: 'is-invalid'
|
||||
b.use :label, class: 'form-control-label'
|
||||
b.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback' }
|
||||
b.use :hint, wrap_with: { tag: 'small', class: 'form-text text-muted' }
|
||||
end
|
||||
|
||||
# custom multi select
|
||||
config.wrappers :floating_labels_select, tag: 'div', class: 'form-label-group', error_class: 'form-group-invalid', valid_class: 'form-group-valid' do |b|
|
||||
b.use :html5
|
||||
b.optional :readonly
|
||||
b.use :input, class: 'custom-select custom-select-lg', error_class: 'is-invalid'
|
||||
b.use :label, class: 'form-control-label'
|
||||
b.use :full_error, wrap_with: { tag: 'div', class: 'invalid-feedback' }
|
||||
b.use :hint, wrap_with: { tag: 'small', class: 'form-text text-muted' }
|
||||
end
|
||||
|
||||
|
||||
# The default wrapper to be used by the FormBuilder.
|
||||
config.default_wrapper = :vertical_form
|
||||
|
||||
# Custom wrappers for input types. This should be a hash containing an input
|
||||
# type as key and the wrapper that will be used for all inputs with specified type.
|
||||
config.wrapper_mappings = {
|
||||
boolean: :vertical_boolean,
|
||||
check_boxes: :vertical_collection_inline,
|
||||
date: :vertical_multi_select,
|
||||
datetime: :vertical_multi_select,
|
||||
file: :vertical_file,
|
||||
radio_buttons: :vertical_collection_inline,
|
||||
range: :vertical_range,
|
||||
time: :vertical_multi_select
|
||||
}
|
||||
|
||||
# enable custom form wrappers
|
||||
# config.wrapper_mappings = {
|
||||
# boolean: :custom_boolean,
|
||||
# check_boxes: :custom_collection,
|
||||
# date: :custom_multi_select,
|
||||
# datetime: :custom_multi_select,
|
||||
# file: :custom_file,
|
||||
# radio_buttons: :custom_collection,
|
||||
# range: :custom_range,
|
||||
# time: :custom_multi_select
|
||||
# }
|
||||
end
|
@ -0,0 +1,17 @@
|
||||
zh-CN:
|
||||
views:
|
||||
pagination:
|
||||
first: "« 首页"
|
||||
last: "尾页 »"
|
||||
previous: "‹ 上一页"
|
||||
next: "下一页 ›"
|
||||
truncate: "…"
|
||||
helpers:
|
||||
page_entries_info:
|
||||
one_page:
|
||||
display_entries:
|
||||
zero: "暂无数据"
|
||||
one: "共<b>1</b>条数据"
|
||||
other: "共<b>%{count}</b>条数据"
|
||||
more_pages:
|
||||
display_entries: "当前<b>%{first} - %{last}</b>,共<b>%{total}</b>条数据"
|
@ -0,0 +1,31 @@
|
||||
en:
|
||||
simple_form:
|
||||
"yes": 'Yes'
|
||||
"no": 'No'
|
||||
required:
|
||||
text: 'required'
|
||||
mark: '*'
|
||||
# You can uncomment the line below if you need to overwrite the whole required html.
|
||||
# When using html, text and mark won't be used.
|
||||
# html: '<abbr title="required">*</abbr>'
|
||||
error_notification:
|
||||
default_message: "Please review the problems below:"
|
||||
# Examples
|
||||
# labels:
|
||||
# defaults:
|
||||
# password: 'Password'
|
||||
# user:
|
||||
# new:
|
||||
# email: 'E-mail to sign in.'
|
||||
# edit:
|
||||
# email: 'E-mail.'
|
||||
# hints:
|
||||
# defaults:
|
||||
# username: 'User name to sign in.'
|
||||
# password: 'No special characters, please.'
|
||||
# include_blanks:
|
||||
# defaults:
|
||||
# age: 'Rather not say'
|
||||
# prompts:
|
||||
# defaults:
|
||||
# age: 'Select your age'
|
@ -0,0 +1,15 @@
|
||||
<%# frozen_string_literal: true %>
|
||||
<%%= simple_form_for(@<%= singular_table_name %>) do |f| %>
|
||||
<%%= f.error_notification %>
|
||||
<%%= f.error_notification message: f.object.errors[:base].to_sentence if f.object.errors[:base].present? %>
|
||||
|
||||
<div class="form-inputs">
|
||||
<%- attributes.each do |attribute| -%>
|
||||
<%%= f.<%= attribute.reference? ? :association : :input %> :<%= attribute.name %> %>
|
||||
<%- end -%>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<%%= f.button :submit %>
|
||||
</div>
|
||||
<%% end %>
|
@ -0,0 +1,254 @@
|
||||
[
|
||||
{
|
||||
"n": "北京",
|
||||
"s": [
|
||||
{ "n": "东城" },
|
||||
{ "n": "西城" },
|
||||
{ "n": "朝阳" },
|
||||
{ "n": "丰台" },
|
||||
{ "n": "石景山" },
|
||||
{ "n": "海淀" },
|
||||
{ "n": "门头沟" },
|
||||
{ "n": "房山" },
|
||||
{ "n": "通州" },
|
||||
{ "n": "顺义" },
|
||||
{ "n": "昌平" },
|
||||
{ "n": "大兴" },
|
||||
{ "n": "平谷" },
|
||||
{ "n": "怀柔" },
|
||||
{ "n": "密云" },
|
||||
{ "n": "延庆" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"n": "上海",
|
||||
"s": [
|
||||
{ "n": "崇明" }, { "n": "黄浦" }, { "n": "卢湾" }, { "n": "徐汇" }, { "n": "长宁" }, { "n": "静安" }, { "n": "普陀" }, { "n": "闸北" }, { "n": "虹口" }, { "n": "杨浦" }, { "n": "闵行" },
|
||||
{ "n": "宝山" }, { "n": "嘉定" }, { "n": "浦东" }, { "n": "金山" }, { "n": "松江" }, { "n": "青浦" }, { "n": "南汇" }, { "n": "奉贤" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"n": "广东",
|
||||
"s": [
|
||||
{ "n": "广州" }, { "n": "深圳" }, { "n": "珠海" }, { "n": "东莞" }, { "n": "中山" }, { "n": "佛山" }, { "n": "惠州" }, { "n": "河源" }, { "n": "潮州" }, { "n": "江门" }, { "n": "揭阳" }, { "n": "茂名" },
|
||||
{ "n": "梅州" }, { "n": "清远" }, { "n": "汕头" }, { "n": "汕尾" }, { "n": "韶关" }, { "n": "顺德" }, { "n": "阳江" }, { "n": "云浮" }, { "n": "湛江" }, { "n": "肇庆" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"n": "江苏",
|
||||
"s": [
|
||||
{ "n": "南京" }, { "n": "常熟" }, { "n": "常州" }, { "n": "海门" }, { "n": "淮安" }, { "n": "江都" }, { "n": "江阴" }, { "n": "昆山" }, { "n": "连云港" }, { "n": "南通" },
|
||||
{ "n": "启东" }, { "n": "沭阳" }, { "n": "宿迁" }, { "n": "苏州" }, { "n": "太仓" }, { "n": "泰州" }, { "n": "同里" }, { "n": "无锡" }, { "n": "徐州" }, { "n": "盐城" },
|
||||
{ "n": "扬州" }, { "n": "宜兴" }, { "n": "仪征" }, { "n": "张家港" }, { "n": "镇江" }, { "n": "周庄" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"n": "浙江",
|
||||
"s": [
|
||||
{ "n": "杭州" }, { "n": "安吉" }, { "n": "慈溪" }, { "n": "定海" }, { "n": "奉化" }, { "n": "海盐" }, { "n": "黄岩" }, { "n": "湖州" }, { "n": "嘉兴" }, { "n": "金华" }, { "n": "临安" },
|
||||
{ "n": "临海" }, { "n": "丽水" }, { "n": "宁波" }, { "n": "瓯海" }, { "n": "平湖" }, { "n": "千岛湖" }, { "n": "衢州" }, { "n": "江山" }, { "n": "瑞安" }, { "n": "绍兴" }, { "n": "嵊州" },
|
||||
{ "n": "台州" }, { "n": "温岭" }, { "n": "温州" }, { "n": "余姚" }, { "n": "舟山" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"n": "重庆",
|
||||
"s": [
|
||||
{ "n": "万州" }, { "n": "涪陵" }, { "n": "渝中" }, { "n": "大渡口" }, { "n": "江北" }, { "n": "沙坪坝" }, { "n": "九龙坡" }, { "n": "南岸" }, { "n": "北碚" }, { "n": "万盛" },
|
||||
{ "n": "双挢" }, { "n": "渝北" }, { "n": "巴南" }, { "n": "黔江" }, { "n": "长寿" }, { "n": "綦江" }, { "n": "潼南" }, { "n": "铜梁" }, { "n": "大足" }, { "n": "荣昌" }, { "n": "壁山" },
|
||||
{ "n": "梁平" }, { "n": "城口" }, { "n": "丰都" }, { "n": "垫江" }, { "n": "武隆" }, { "n": "忠县" }, { "n": "开县" }, { "n": "云阳" }, { "n": "奉节" }, { "n": "巫山" }, { "n": "巫溪" },
|
||||
{ "n": "石柱" }, { "n": "秀山" }, { "n": "酉阳" }, { "n": "彭水" }, { "n": "江津" }, { "n": "合川" }, { "n": "永川" }, { "n": "南川" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"n": "安徽",
|
||||
"s": [
|
||||
{ "n": "合肥" }, { "n": "安庆" }, { "n": "蚌埠" }, { "n": "亳州" }, { "n": "巢湖" }, { "n": "滁州" }, { "n": "阜阳" }, { "n": "贵池" }, { "n": "淮北" }, { "n": "淮化" }, { "n": "淮南" },
|
||||
{ "n": "黄山" }, { "n": "九华山" }, { "n": "六安" }, { "n": "马鞍山" }, { "n": "宿州" }, { "n": "铜陵" }, { "n": "屯溪" }, { "n": "芜湖" }, { "n": "宣城" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"n": "福建",
|
||||
"s": [
|
||||
{ "n": "福州" }, { "n": "厦门" }, { "n": "泉州" }, { "n": "漳州" }, { "n": "龙岩" }, { "n": "南平" }, { "n": "宁德" }, { "n": "莆田" }, { "n": "三明" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"n": "甘肃",
|
||||
"s": [
|
||||
{ "n": "兰州" }, { "n": "白银" }, { "n": "定西" }, { "n": "敦煌" }, { "n": "甘南" }, { "n": "金昌" }, { "n": "酒泉" }, { "n": "临夏" }, { "n": "平凉" }, { "n": "天水" },
|
||||
{ "n": "武都" }, { "n": "武威" }, { "n": "西峰" }, { "n": "张掖" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"n": "广西",
|
||||
"s": [
|
||||
{ "n": "南宁" }, { "n": "百色" }, { "n": "北海" }, { "n": "桂林" }, { "n": "防城港" }, { "n": "贵港" }, { "n": "河池" }, { "n": "贺州" }, { "n": "柳州" }, { "n": "钦州" }, { "n": "梧州" }, { "n": "玉林" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"n": "贵州",
|
||||
"s": [
|
||||
{ "n": "贵阳" }, { "n": "安顺" }, { "n": "毕节" }, { "n": "都匀" }, { "n": "凯里" }, { "n": "六盘水" }, { "n": "铜仁" }, { "n": "兴义" }, { "n": "玉屏" }, { "n": "遵义" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"n": "海南",
|
||||
"s": [
|
||||
{ "n": "海口" }, { "n": "儋县" }, { "n": "陵水" }, { "n": "琼海" }, { "n": "三亚" }, { "n": "通什" }, { "n": "万宁" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"n": "河北",
|
||||
"s": [
|
||||
{ "n": "石家庄" }, { "n": "保定" }, { "n": "北戴河" }, { "n": "沧州" }, { "n": "承德" }, { "n": "丰润" }, { "n": "邯郸" }, { "n": "衡水" }, { "n": "廊坊" }, { "n": "南戴河" }, { "n": "秦皇岛" },
|
||||
{ "n": "唐山" }, { "n": "新城" }, { "n": "邢台" }, { "n": "张家口" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"n": "黑龙江",
|
||||
"s": [
|
||||
{ "n": "哈尔滨" }, { "n": "北安" }, { "n": "大庆" }, { "n": "大兴安岭" }, { "n": "鹤岗" }, { "n": "黑河" }, { "n": "佳木斯" }, { "n": "鸡西" }, { "n": "牡丹江" }, { "n": "齐齐哈尔" },
|
||||
{ "n": "七台河" }, { "n": "双鸭山" }, { "n": "绥化" }, { "n": "伊春" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"n": "河南",
|
||||
"s": [
|
||||
{ "n": "郑州" }, { "n": "安阳" }, { "n": "鹤壁" }, { "n": "潢川" }, { "n": "焦作" }, { "n": "济源" }, { "n": "开封" }, { "n": "漯河" }, { "n": "洛阳" }, { "n": "南阳" }, { "n": "平顶山" },
|
||||
{ "n": "濮阳" }, { "n": "三门峡" }, { "n": "商丘" }, { "n": "新乡" }, { "n": "信阳" }, { "n": "许昌" }, { "n": "周口" }, { "n": "驻马店" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"n": "湖北",
|
||||
"s": [
|
||||
{ "n": "武汉" }, { "n": "恩施" }, { "n": "鄂州" }, { "n": "黄冈" }, { "n": "黄石" }, { "n": "荆门" }, { "n": "荆州" }, { "n": "潜江" }, { "n": "十堰" }, { "n": "随州" }, { "n": "武穴" },
|
||||
{ "n": "仙桃" }, { "n": "咸宁" }, { "n": "襄阳" }, { "n": "襄樊" }, { "n": "孝感" }, { "n": "宜昌" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"n": "湖南",
|
||||
"s": [
|
||||
{ "n": "长沙" }, { "n": "常德" }, { "n": "郴州" }, { "n": "衡阳" }, { "n": "怀化" }, { "n": "吉首" }, { "n": "娄底" }, { "n": "邵阳" }, { "n": "湘潭" }, { "n": "益阳" }, { "n": "岳阳" },
|
||||
{ "n": "永州" }, { "n": "张家界" }, { "n": "株洲" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"n": "江西",
|
||||
"s": [
|
||||
{ "n": "南昌" }, { "n": "抚州" }, { "n": "赣州" }, { "n": "吉安" }, { "n": "景德镇" }, { "n": "井冈山" }, { "n": "九江" }, { "n": "庐山" }, { "n": "萍乡" },
|
||||
{ "n": "上饶" }, { "n": "新余" }, { "n": "宜春" }, { "n": "鹰潭" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"n": "吉林",
|
||||
"s": [
|
||||
{ "n": "长春" }, { "n": "吉林" }, { "n": "白城" }, { "n": "白山" }, { "n": "珲春" }, { "n": "辽源" }, { "n": "梅河" }, { "n": "四平" }, { "n": "松原" }, { "n": "通化" }, { "n": "延吉" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"n": "辽宁",
|
||||
"s": [
|
||||
{ "n": "沈阳" }, { "n": "鞍山" }, { "n": "本溪" }, { "n": "朝阳" }, { "n": "大连" }, { "n": "丹东" }, { "n": "抚顺" }, { "n": "阜新" }, { "n": "葫芦岛" }, { "n": "锦州" },
|
||||
{ "n": "辽阳" }, { "n": "盘锦" }, { "n": "铁岭" }, { "n": "营口" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"n": "内蒙古",
|
||||
"s": [
|
||||
{ "n": "呼和浩特" }, { "n": "阿拉善盟" }, { "n": "包头" }, { "n": "赤峰" }, { "n": "东胜" }, { "n": "海拉尔" }, { "n": "集宁" }, { "n": "临河" }, { "n": "通辽" }, { "n": "乌海" },
|
||||
{ "n": "乌兰浩特" }, { "n": "锡林浩特" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"n": "宁夏",
|
||||
"s": [
|
||||
{ "n": "银川" }, { "n": "固源" }, { "n": "石嘴山" }, { "n": "吴忠" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"n": "青海",
|
||||
"s": [
|
||||
{ "n": "西宁" }, { "n": "德令哈" }, { "n": "格尔木" }, { "n": "共和" }, { "n": "海东" }, { "n": "海晏" }, { "n": "玛沁" }, { "n": "同仁" }, { "n": "玉树" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"n": "山东",
|
||||
"s": [
|
||||
{ "n": "济南" }, { "n": "滨州" }, { "n": "兖州" }, { "n": "德州" }, { "n": "东营" }, { "n": "菏泽" }, { "n": "济宁" }, { "n": "莱芜" }, { "n": "聊城" }, { "n": "临沂" },
|
||||
{ "n": "蓬莱" }, { "n": "青岛" }, { "n": "曲阜" }, { "n": "日照" }, { "n": "泰安" }, { "n": "潍坊" }, { "n": "威海" }, { "n": "烟台" }, { "n": "枣庄" }, { "n": "淄博" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"n": "山西",
|
||||
"s": [
|
||||
{ "n": "太原" }, { "n": "长治" }, { "n": "大同" }, { "n": "候马" }, { "n": "晋城" }, { "n": "离石" }, { "n": "临汾" }, { "n": "宁武" }, { "n": "朔州" }, { "n": "忻州" },
|
||||
{ "n": "阳泉" }, { "n": "榆次" }, { "n": "运城" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"n": "陕西",
|
||||
"s": [
|
||||
{ "n": "西安" }, { "n": "安康" }, { "n": "宝鸡" }, { "n": "汉中" }, { "n": "渭南" }, { "n": "商州" }, { "n": "绥德" }, { "n": "铜川" }, { "n": "咸阳" }, { "n": "延安" }, { "n": "榆林" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"n": "四川",
|
||||
"s": [
|
||||
{ "n": "成都" }, { "n": "巴中" }, { "n": "达川" }, { "n": "德阳" }, { "n": "都江堰" }, { "n": "峨眉山" }, { "n": "涪陵" }, { "n": "广安" }, { "n": "广元" }, { "n": "九寨沟" },
|
||||
{ "n": "康定" }, { "n": "乐山" }, { "n": "泸州" }, { "n": "马尔康" }, { "n": "绵阳" }, { "n": "眉山" }, { "n": "南充" }, { "n": "内江" }, { "n": "攀枝花" }, { "n": "遂宁" },
|
||||
{ "n": "汶川" }, { "n": "西昌" }, { "n": "雅安" }, { "n": "宜宾" }, { "n": "自贡" }, { "n": "资阳" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"n": "天津",
|
||||
"s": [
|
||||
{ "n": "天津" }, { "n": "和平" }, { "n": "东丽" }, { "n": "河东" }, { "n": "西青" }, { "n": "河西" }, { "n": "津南" }, { "n": "南开" }, { "n": "北辰" }, { "n": "河北" }, { "n": "武清" }, { "n": "红挢" },
|
||||
{ "n": "塘沽" }, { "n": "汉沽" }, { "n": "大港" }, { "n": "宁河" }, { "n": "静海" }, { "n": "宝坻" }, { "n": "蓟县" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"n": "新疆",
|
||||
"s": [
|
||||
{ "n": "乌鲁木齐" }, { "n": "阿克苏" }, { "n": "阿勒泰" }, { "n": "阿图什" }, { "n": "博乐" }, { "n": "昌吉" }, { "n": "东山" }, { "n": "哈密" }, { "n": "和田" }, { "n": "喀什" },
|
||||
{ "n": "克拉玛依" }, { "n": "库车" }, { "n": "库尔勒" }, { "n": "奎屯" }, { "n": "石河子" }, { "n": "塔城" }, { "n": "吐鲁番" }, { "n": "伊宁" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"n": "西藏",
|
||||
"s": [
|
||||
{ "n": "拉萨" }, { "n": "阿里" }, { "n": "昌都" }, { "n": "林芝" }, { "n": "那曲" }, { "n": "日喀则" }, { "n": "山南" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"n": "云南",
|
||||
"s": [
|
||||
{ "n": "昆明" }, { "n": "大理" }, { "n": "保山" }, { "n": "楚雄" }, { "n": "大理" }, { "n": "东川" }, { "n": "个旧" }, { "n": "景洪" }, { "n": "开远" }, { "n": "临沧" }, { "n": "丽江" },
|
||||
{ "n": "六库" }, { "n": "潞西" }, { "n": "曲靖" }, { "n": "思茅" }, { "n": "文山" }, { "n": "西双版纳" }, { "n": "玉溪" }, { "n": "中甸" }, { "n": "昭通" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"n": "香港特别行政区",
|
||||
"s": [
|
||||
{ "n": "香港" }, { "n": "九龙" }, { "n": "新界" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"n": "澳门特别行政区",
|
||||
"s": [
|
||||
{ "n": { "n": "澳门" } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"n": "台湾",
|
||||
"s": [
|
||||
{ "n": "台北" }, { "n": "基隆" }, { "n": "台南" }, { "n": "台中" }, { "n": "高雄" }, { "n": "屏东" }, { "n": "南投" }, { "n": "云林" }, { "n": "新竹" }, { "n": "彰化" }, { "n": "苗栗" },
|
||||
{ "n": "嘉义" }, { "n": "花莲" }, { "n": "桃园" }, { "n": "宜兰" }, { "n": "台东" }, { "n": "金门" }, { "n": "马祖" }, { "n": "澎湖" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"n": "海外",
|
||||
"s": [
|
||||
{ "n": "美国" }, { "n": "日本" }, { "n": "英国" }, { "n": "法国" }, { "n": "德国" }, { "n": "其他" }
|
||||
]
|
||||
}
|
||||
]
|
Loading…
Reference in new issue