Compare commits

...

4 Commits
main ... master

Author SHA1 Message Date
qinzexin
4ab22822f8 接口调整,留言管理调整 2025-05-23 18:34:56 +08:00
qinzexin
6bc3a32e79 网站留言 2025-05-22 17:53:57 +08:00
qinzexin
03c19a0c31 新闻管理,接口
团务百科管理,接口
信息公开管理,接口
2025-05-21 18:06:43 +08:00
qinzexin
dbdfa8528a 提交代码 2025-05-20 16:33:23 +08:00
4676 changed files with 835124 additions and 0 deletions

14
.bowerrc Normal file
View File

@ -0,0 +1,14 @@
{
"directory": "public/assets/libs",
"ignoredDependencies": [
"es6-promise",
"file-saver",
"html2canvas",
"jspdf",
"jspdf-autotable",
"pdfmake"
],
"scripts":{
"postinstall": "node bower-cleanup.js"
}
}

11
.env Normal file
View File

@ -0,0 +1,11 @@
[app]
debug = true
trace = false
[database]
hostname = 127.0.0.1
database = youthleagueluoyang
username = youthleagueluoyang
password = youthleagueluoyang
hostport = 3306
prefix = fa_

11
.env.sample Normal file
View File

@ -0,0 +1,11 @@
[app]
debug = false
trace = false
[database]
hostname = 127.0.0.1
database = fastadmin
username = root
password = root
hostport = 3306
prefix = fa_

11
.gitignore vendored Normal file
View File

@ -0,0 +1,11 @@
/nbproject/
/runtime/*
.DS_Store
.idea
composer.lock
*.log
*.css.map
!.gitkeep
.svn
.vscode
.user.ini

4
.npmrc Normal file
View File

@ -0,0 +1,4 @@
# 使用自定义镜像源
registry=http://mirrors.tencent.com/npm/
#关闭SSL验证
strict-ssl=false

139
Gruntfile.js Normal file
View File

@ -0,0 +1,139 @@
module.exports = function (grunt) {
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
copy: {
main: {
files: []
}
}
});
var build = function (module, type, callback) {
var config = {
compile: {
options: type === 'js' ? {
optimizeCss: "standard",
optimize: "uglify", //可使用uglify|closure|none
preserveLicenseComments: true,
removeCombined: false,
baseUrl: "./public/assets/js/", //JS文件所在的基础目录
name: "require-" + module, //来源文件,不包含后缀
out: "./public/assets/js/require-" + module + ".min.js" //目标文件
} : {
optimizeCss: "default",
optimize: "uglify", //可使用uglify|closure|none
cssIn: "./public/assets/css/" + module + ".css", //CSS文件所在的基础目录
out: "./public/assets/css/" + module + ".min.css" //目标文件
}
}
};
var content = grunt.file.read("./public/assets/js/require-" + module + ".js"),
pattern = /^require\.config\(\{[\r\n]?[\n]?(.*?)[\r\n]?[\n]?}\);/is;
var matches = content.match(pattern);
if (matches) {
if (type === 'js') {
var data = matches[1].replaceAll(/(urlArgs|baseUrl):(.*)\n/gi, '');
const parse = require('parse-config-file'), fs = require('fs');
require('jsonminify');
data = JSON.minify("{\n" + data + "\n}");
let options = parse(data);
options.paths.tableexport = "empty:";
Object.assign(config.compile.options, options);
}
let requirejs = require("./application/admin/command/Min/r");
try {
requirejs.optimize(config.compile.options, function (buildResponse) {
// var contents = require('fs').readFileSync(config.compile.options.out, 'utf8');
callback();
}, function (err) {
console.error(err);
callback();
});
} catch (err) {
console.error(err);
callback();
}
}
};
// 加载 "copy" 插件
grunt.loadNpmTasks('grunt-contrib-copy');
grunt.registerTask('frontend:js', 'build frontend js', function () {
var done = this.async();
build('frontend', 'js', done);
});
grunt.registerTask('backend:js', 'build backend js', function () {
var done = this.async();
build('backend', 'js', done);
});
grunt.registerTask('frontend:css', 'build frontend css', function () {
var done = this.async();
build('frontend', 'css', done);
});
grunt.registerTask('backend:css', 'build frontend css', function () {
var done = this.async();
build('backend', 'css', done);
});
// 注册部署JS和CSS任务
grunt.registerTask('deploy', 'deploy', function () {
const fs = require('fs');
const path = require("path")
const nodeModulesDir = path.resolve(__dirname, "./node_modules");
const getAllFiles = function (dirPath, arrayOfFiles) {
files = fs.readdirSync(dirPath)
arrayOfFiles = arrayOfFiles || []
files.forEach(function (file) {
if (fs.statSync(dirPath + "/" + file).isDirectory()) {
arrayOfFiles = getAllFiles(dirPath + "/" + file, arrayOfFiles)
} else {
arrayOfFiles.push(path.join(__dirname, dirPath, "/", file))
}
});
return arrayOfFiles
};
const mainPackage = grunt.config.get('pkg');
let dists = mainPackage.dists || [];
let files = [];
// 兼容旧版本bower使用的目录
let specialKey = {
'fastadmin-bootstraptable': 'bootstrap-table',
'sortablejs': 'Sortable',
'tableexport.jquery.plugin': 'tableExport.jquery.plugin',
};
Object.keys(dists).forEach(key => {
let src = ["**/*LICENSE*", "**/*license*"];
src = src.concat(Array.isArray(dists[key]) ? dists[key] : [dists[key]]);
files.push({expand: true, cwd: nodeModulesDir + "/" + key, src: src, dest: 'public/assets/libs/' + (specialKey[key] || key) + "/"});
});
// 兼容bower历史路径文件
files = [...files,
{src: nodeModulesDir + "/toastr/build/toastr.min.css", dest: "public/assets/libs/toastr/toastr.min.css"},
{src: nodeModulesDir + "/bootstrap-slider/dist/css/bootstrap-slider.css", dest: "public/assets/libs/bootstrap-slider/slider.css"},
{expand: true, cwd: nodeModulesDir + "/bootstrap-slider/dist", src: ["*.js"], dest: "public/assets/libs/bootstrap-slider/"}
]
grunt.config.set('copy.main.files', files);
grunt.task.run("copy:main");
});
// 注册默认任务
grunt.registerTask('default', ['deploy', 'frontend:js', 'backend:js', 'frontend:css', 'backend:css']);
};

191
LICENSE Normal file
View File

@ -0,0 +1,191 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, "control" means (i) the power, direct or
indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising
permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
"Object" form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code,
generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included
in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that
is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works
shall not include works that remain separable from, or merely link (or bind by
name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative Works
thereof, that is intentionally submitted to Licensor for inclusion in the Work
by the copyright owner or by an individual or Legal Entity authorized to submit
on behalf of the copyright owner. For the purposes of this definition,
"submitted" means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor for
the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.
2. Grant of Copyright License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
3. Grant of Patent License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination
of their Contribution(s) with the Work to which such Contribution(s) was
submitted. If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this License
for that Work shall terminate as of the date such litigation is filed.
4. Redistribution.
You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:
You must give any other recipients of the Work or Derivative Works a copy of
this License; and
You must cause any modified files to carry prominent notices stating that You
changed the files; and
You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source form
of the Work, excluding those notices that do not pertain to any part of the
Derivative Works; and
If the Work includes a "NOTICE" text file as part of its distribution, then any
Derivative Works that You distribute must include a readable copy of the
attribution notices contained within such NOTICE file, excluding those notices
that do not pertain to any part of the Derivative Works, in at least one of the
following places: within a NOTICE text file distributed as part of the
Derivative Works; within the Source form or documentation, if provided along
with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents of
the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works that
You distribute, alongside or as an addendum to the NOTICE text from the Work,
provided that such additional attribution notices cannot be construed as
modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a whole,
provided Your use, reproduction, and distribution of the Work otherwise complies
with the conditions stated in this License.
5. Submission of Contributions.
Unless You explicitly state otherwise, any Contribution intentionally submitted
for inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of
any separate license agreement you may have executed with Licensor regarding
such Contributions.
6. Trademarks.
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
7. Disclaimer of Warranty.
Unless required by applicable law or agreed to in writing, Licensor provides the
Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
including, without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
8. Limitation of Liability.
In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work (including but not limited to
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability.
While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets "{}" replaced with your own
identifying information. (Don't include the brackets!) The text should be
enclosed in the appropriate comment syntax for the file format. We also
recommend that a file or class name and description of purpose be included on
the same "printed page" as the copyright notice for easier identification within
third-party archives.
Copyright 2017 Karson
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

36
README.en.md Normal file
View File

@ -0,0 +1,36 @@
# fastadmin带分离基础权限接口项目
#### Description
待分离后台基础权限接口的后台项目(为了方便二开)
#### Software Architecture
Software architecture description
#### Installation
1. xxxx
2. xxxx
3. xxxx
#### Instructions
1. xxxx
2. xxxx
3. xxxx
#### Contribution
1. Fork the repository
2. Create Feat_xxx branch
3. Commit your code
4. Create Pull Request
#### Gitee Feature
1. You can use Readme\_XXX.md to support different languages, such as Readme\_en.md, Readme\_zh.md
2. Gitee blog [blog.gitee.com](https://blog.gitee.com)
3. Explore open source project [https://gitee.com/explore](https://gitee.com/explore)
4. The most valuable open source project [GVP](https://gitee.com/gvp)
5. The manual of Gitee [https://gitee.com/help](https://gitee.com/help)
6. The most popular members [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)

36
README.md Normal file
View File

@ -0,0 +1,36 @@
# fastadmin带分离基础权限接口项目
#### 介绍
自带分离后台基础权限接口的后台项目(为了方便后台分离的项目二开)
但是只有基础的登录,退出,权限管理,附件管理等最基础接口,
##### 后台前端没有后台前端需自己找框架对接比如若依或者vue-element-admin等等
##### 后台前端没有后台前端需自己找框架对接比如若依或者vue-element-admin等等
##### 后台前端没有后台前端需自己找框架对接比如若依或者vue-element-admin等等
#### 安装信息
已安装完毕的fastadmin附带一些已安装的基础插件和一些自己写的工具方法
自己用,需自行在配置文件更改数据库信息,配置文件在.env 自行修改,
请自己导入数据库文件 data.sql
默认账号admin
默认密码a123456
基于fastadmin版本1.6.0.20250331
public/adminapi.html 为后台api接口可自行对接前端
菜单管理中的api权限管理包含了前端页面路由取的规则请跟前端路由一致类似 pages/upload/index
#### 参与贡献
1. Fork 本仓库
2. 新建 Feat_xxx 分支
3. 提交代码
4. 新建 Pull Request
#### 特技
1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md
2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com)
3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目
4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目
5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help)
6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)

1
addons/.gitkeep Normal file
View File

@ -0,0 +1 @@

1
addons/.htaccess Normal file
View File

@ -0,0 +1 @@
deny from all

1
addons/address/.addonrc Normal file
View File

@ -0,0 +1 @@
{"files":["public\\assets\\addons\\address\\js\\gcoord.min.js","public\\assets\\addons\\address\\js\\jquery.autocomplete.js"],"license":"regular","licenseto":"16018","licensekey":"RiQMAeO7XFh1Jymp GnbiF6jx59a8ILPxMdpIhQ==","domains":["test0rui.com"],"licensecodes":[],"validations":["d495f88a5abee2b752e64026c2ea8984"]}

View File

@ -0,0 +1,32 @@
<?php
namespace addons\address;
use think\Addons;
/**
* 地址选择
* @author [MiniLing] <[laozheyouxiang@163.com]>
*/
class Address extends Addons
{
/**
* 插件安装方法
* @return bool
*/
public function install()
{
return true;
}
/**
* 插件卸载方法
* @return bool
*/
public function uninstall()
{
return true;
}
}

34
addons/address/bootstrap.js vendored Normal file
View File

@ -0,0 +1,34 @@
require([], function () {
//绑定data-toggle=addresspicker属性点击事件
$(document).on('click', "[data-toggle='addresspicker']", function () {
var that = this;
var callback = $(that).data('callback');
var input_id = $(that).data("input-id") ? $(that).data("input-id") : "";
var lat_id = $(that).data("lat-id") ? $(that).data("lat-id") : "";
var lng_id = $(that).data("lng-id") ? $(that).data("lng-id") : "";
var zoom_id = $(that).data("zoom-id") ? $(that).data("zoom-id") : "";
var lat = lat_id ? $("#" + lat_id).val() : '';
var lng = lng_id ? $("#" + lng_id).val() : '';
var zoom = zoom_id ? $("#" + zoom_id).val() : '';
var url = "/addons/address/index/select";
url += (lat && lng) ? '?lat=' + lat + '&lng=' + lng + (input_id ? "&address=" + $("#" + input_id).val() : "") + (zoom ? "&zoom=" + zoom : "") : '';
Fast.api.open(url, '位置选择', {
callback: function (res) {
input_id && $("#" + input_id).val(res.address).trigger("change");
lat_id && $("#" + lat_id).val(res.lat).trigger("change");
lng_id && $("#" + lng_id).val(res.lng).trigger("change");
zoom_id && $("#" + zoom_id).val(res.zoom).trigger("change");
try {
//执行回调函数
if (typeof callback === 'function') {
callback.call(that, res);
}
} catch (e) {
}
}
});
});
});

132
addons/address/config.php Normal file
View File

@ -0,0 +1,132 @@
<?php
return [
[
'name' => 'maptype',
'title' => '默认地图类型',
'type' => 'radio',
'content' => [
'baidu' => '百度地图',
'amap' => '高德地图',
'tencent' => '腾讯地图',
],
'value' => 'baidu',
'rule' => 'required',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
],
[
'name' => 'zoom',
'title' => '默认缩放级别',
'type' => 'string',
'content' => [],
'value' => '11',
'rule' => 'required',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
],
[
'name' => 'lat',
'title' => '默认Lat',
'type' => 'string',
'content' => [],
'value' => '39.919990',
'rule' => 'required',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
],
[
'name' => 'lng',
'title' => '默认Lng',
'type' => 'string',
'content' => [],
'value' => '116.456270',
'rule' => 'required',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
],
[
'name' => 'baidukey',
'title' => '百度地图KEY',
'type' => 'string',
'content' => [],
'value' => '',
'rule' => '',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
],
[
'name' => 'amapkey',
'title' => '高德地图KEY',
'type' => 'string',
'content' => [],
'value' => '',
'rule' => '',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
],
[
'name' => 'amapsecurityjscode',
'title' => '高德地图安全密钥',
'type' => 'string',
'content' => [],
'value' => '',
'rule' => '',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
],
[
'name' => 'tencentkey',
'title' => '腾讯地图KEY',
'type' => 'string',
'content' => [],
'value' => '',
'rule' => '',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
],
[
'name' => 'coordtype',
'title' => '坐标系类型',
'type' => 'select',
'content' => [
'DEFAULT' => '默认(使用所选地图默认坐标系)',
'GCJ02' => 'GCJ-02',
'BD09' => 'BD-09',
],
'value' => 'DEFAULT',
'rule' => 'required',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
],
[
'name' => '__tips__',
'title' => '温馨提示',
'type' => '',
'content' => [],
'value' => '请先申请对应地图的Key配置后再使用',
'rule' => '',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => 'alert-danger-light',
],
];

View File

@ -0,0 +1,64 @@
<?php
namespace addons\address\controller;
use think\addons\Controller;
use think\Config;
use think\Hook;
class Index extends Controller
{
// 首页
public function index()
{
// 语言检测
$lang = $this->request->langset();
$lang = preg_match("/^([a-zA-Z\-_]{2,10})\$/i", $lang) ? $lang : 'zh-cn';
$site = Config::get("site");
// 配置信息
$config = [
'site' => array_intersect_key($site, array_flip(['name', 'cdnurl', 'version', 'timezone', 'languages'])),
'upload' => null,
'modulename' => 'addons',
'controllername' => 'index',
'actionname' => 'index',
'jsname' => 'addons/address',
'moduleurl' => '',
'language' => $lang
];
$config = array_merge($config, Config::get("view_replace_str"));
// 配置信息后
Hook::listen("config_init", $config);
// 加载当前控制器语言包
$this->view->assign('site', $site);
$this->view->assign('config', $config);
return $this->view->fetch();
}
// 选择地址
public function select()
{
$config = get_addon_config('address');
$zoom = (int)$this->request->get('zoom', $config['zoom']);
$lng = (float)$this->request->get('lng');
$lat = (float)$this->request->get('lat');
$address = $this->request->get('address');
$lng = $lng ?: $config['lng'];
$lat = $lat ?: $config['lat'];
$this->view->assign('zoom', $zoom);
$this->view->assign('lng', $lng);
$this->view->assign('lat', $lat);
$this->view->assign('address', $address);
$maptype = $config['maptype'];
if (!isset($config[$maptype . 'key']) || !$config[$maptype . 'key']) {
$this->error("请在配置中配置对应类型地图的密钥");
}
return $this->view->fetch('index/' . $maptype);
}
}

10
addons/address/info.ini Normal file
View File

@ -0,0 +1,10 @@
name = address
title = 地址位置选择插件
intro = 地图位置选择插件,可返回地址和经纬度
author = FastAdmin
website = https://www.fastadmin.net
version = 1.1.8
state = 1
url = /addons/address
license = regular
licenseto = 16018

View File

@ -0,0 +1,258 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<title>地址选择器</title>
<link rel="stylesheet" href="__CDN__/assets/css/frontend.min.css"/>
<link rel="stylesheet" href="__CDN__/assets/libs/font-awesome/css/font-awesome.min.css"/>
<style type="text/css">
body {
margin: 0;
padding: 0;
}
#container {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
}
.confirm {
position: absolute;
bottom: 30px;
right: 4%;
z-index: 99;
height: 50px;
width: 50px;
line-height: 50px;
font-size: 15px;
text-align: center;
background-color: white;
background: #1ABC9C;
color: white;
border: none;
cursor: pointer;
border-radius: 50%;
}
.search {
position: absolute;
width: 400px;
top: 0;
left: 50%;
padding: 5px;
margin-left: -200px;
}
.amap-marker-label {
border: 0;
background-color: transparent;
}
.info {
padding: .75rem 1.25rem;
margin-bottom: 1rem;
border-radius: .25rem;
position: fixed;
top: 2rem;
background-color: white;
width: auto;
min-width: 22rem;
border-width: 0;
left: 1.8rem;
box-shadow: 0 2px 6px 0 rgba(114, 124, 245, .5);
}
</style>
</head>
<body>
<div class="search">
<div class="input-group">
<input type="text" id="place" name="q" class="form-control" placeholder="输入地点"/>
<span class="input-group-btn">
<button type="submit" name="search" id="search-btn" class="btn btn-success">
<i class="fa fa-search"></i>
</button>
</span>
</div>
</div>
<div class="confirm">确定</div>
<div id="container"></div>
<script type="text/javascript">
window._AMapSecurityConfig = {
securityJsCode: "{$config.amapsecurityjscode|default=''}",
}
</script>
<script type="text/javascript" src="//webapi.amap.com/maps?v=1.4.11&key={$config.amapkey|default=''}&plugin=AMap.ToolBar,AMap.Autocomplete,AMap.PlaceSearch,AMap.Geocoder"></script>
<!-- UI组件库 1.0 -->
<script src="//webapi.amap.com/ui/1.0/main.js?v=1.0.11"></script>
<script src="__CDN__/assets/libs/jquery/dist/jquery.min.js"></script>
<script src="__CDN__/assets/addons/address/js/gcoord.min.js"></script>
<script type="text/javascript">
$(function () {
var as, map, geocoder, address, fromtype, totype;
address = "{$address|htmlentities}";
var lng = Number("{$lng}");
var lat = Number("{$lat}");
fromtype = "GCJ02";
totype = "{$config.coordtype|default='DEFAULT'}"
totype = totype === 'DEFAULT' ? "GCJ02" : totype;
if (lng && lat && fromtype !== totype) {
var result = gcoord.transform([lng, lat], gcoord[totype], gcoord[fromtype]);
lng = result[0] || lng;
lat = result[1] || lat;
}
var init = function () {
AMapUI.loadUI(['misc/PositionPicker', 'misc/PoiPicker'], function (PositionPicker, PoiPicker) {
//加载PositionPickerloadUI的路径参数为模块名中 'ui/' 之后的部分
map = new AMap.Map('container', {
zoom: parseInt('{$zoom}'),
center: [lng, lat]
});
geocoder = new AMap.Geocoder({
radius: 1000 //范围默认500
});
var positionPicker = new PositionPicker({
mode: 'dragMarker',//设定为拖拽地图模式,可选'dragMap'、'dragMarker',默认为'dragMap'
map: map//依赖地图对象
});
//输入提示
var autoOptions = {
input: "place"
};
var relocation = function (lnglat, addr) {
lng = lnglat.lng;
lat = lnglat.lat;
map.panTo([lng, lat]);
positionPicker.start(lnglat);
if (addr) {
// var label = '<div class="info">地址:' + addr + '<br>经度:' + lng + '<br>纬度:' + lat + '</div>';
var label = '<div class="info">地址:' + addr + '</div>';
positionPicker.marker.setLabel({
content: label //显示内容
});
} else {
geocoder.getAddress(lng + ',' + lat, function (status, result) {
if (status === 'complete' && result.regeocode) {
var address = result.regeocode.formattedAddress;
// var label = '<div class="info">地址:' + address + '<br>经度:' + lng + '<br>纬度:' + lat + '</div>';
var label = '<div class="info">地址:' + address + '</div>';
positionPicker.marker.setLabel({
content: label //显示内容
});
} else {
console.log(JSON.stringify(result));
}
});
}
};
var auto = new AMap.Autocomplete(autoOptions);
//构造地点查询类
var placeSearch = new AMap.PlaceSearch({
map: map
});
//注册监听,当选中某条记录时会触发
AMap.event.addListener(auto, "select", function (e) {
placeSearch.setCity(e.poi.adcode);
placeSearch.search(e.poi.name, function (status, result) {
$(map.getAllOverlays("marker")).each(function (i, j) {
j.on("click", function () {
relocation(j.De.position);
});
});
}); //关键字查询查询
});
AMap.event.addListener(map, 'click', function (e) {
relocation(e.lnglat);
});
//加载工具条
var tool = new AMap.ToolBar();
map.addControl(tool);
var poiPicker = new PoiPicker({
input: 'place',
placeSearchOptions: {
map: map,
pageSize: 6 //关联搜索分页
}
});
poiPicker.on('poiPicked', function (poiResult) {
poiPicker.hideSearchResults();
$('.poi .nearpoi').text(poiResult.item.name);
$('.address .info').text(poiResult.item.address);
$('#address').val(poiResult.item.address);
$("#place").val(poiResult.item.name);
relocation(poiResult.item.location);
});
positionPicker.on('success', function (positionResult) {
console.log(positionResult);
as = positionResult.position;
address = positionResult.address;
lat = as.lat;
lng = as.lng;
});
positionPicker.on('fail', function (positionResult) {
address = '';
});
positionPicker.start();
if (address) {
// 添加label
var label = '<div class="info">地址:' + address + '</div>';
positionPicker.marker.setLabel({
content: label //显示内容
});
}
//点击搜索按钮
$(document).on('click', '#search-btn', function () {
if ($("#place").val() == '')
return;
placeSearch.search($("#place").val(), function (status, result) {
$(map.getAllOverlays("marker")).each(function (i, j) {
j.on("click", function () {
relocation(j.De.position);
});
});
});
});
});
};
//点击确定后执行回调赋值
var close = function (data) {
var index = parent.Layer.getFrameIndex(window.name);
var callback = parent.$("#layui-layer" + index).data("callback");
//再执行关闭
parent.Layer.close(index);
//再调用回传函数
if (typeof callback === 'function') {
callback.call(undefined, data);
}
};
//点击搜索按钮
$(document).on('click', '.confirm', function () {
var zoom = map.getZoom();
var data = {lat: lat, lng: lng, zoom: zoom, address: address};
if (fromtype !== totype) {
var result = gcoord.transform([data.lng, data.lat], gcoord[fromtype], gcoord[totype]);
data.lng = (result[0] || data.lng).toFixed(5);
data.lat = (result[1] || data.lat).toFixed(5);
console.log(data, result, fromtype, totype);
}
close(data);
});
init();
});
</script>
</body>
</html>

View File

@ -0,0 +1,243 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<title>地址选择器</title>
<link rel="stylesheet" href="__CDN__/assets/css/frontend.min.css"/>
<link rel="stylesheet" href="__CDN__/assets/libs/font-awesome/css/font-awesome.min.css"/>
<style type="text/css">
body {
margin: 0;
padding: 0;
}
#container {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
}
.confirm {
position: absolute;
bottom: 30px;
right: 4%;
z-index: 99;
height: 50px;
width: 50px;
line-height: 50px;
font-size: 15px;
text-align: center;
background-color: white;
background: #1ABC9C;
color: white;
border: none;
cursor: pointer;
border-radius: 50%;
}
.search {
position: absolute;
width: 400px;
top: 0;
left: 50%;
padding: 5px;
margin-left: -200px;
}
label.BMapLabel {
max-width: inherit;
padding: .75rem 1.25rem;
margin-bottom: 1rem;
background-color: white;
width: auto;
min-width: 22rem;
border: none;
box-shadow: 0 2px 6px 0 rgba(114, 124, 245, .5);
}
</style>
</head>
<body>
<div class="search">
<div class="input-group">
<input type="text" id="place" name="q" class="form-control" placeholder="输入地点"/>
<div id="searchResultPanel" style="border:1px solid #C0C0C0;width:150px;height:auto; display:none;"></div>
<span class="input-group-btn">
<button type="button" name="search" id="search-btn" class="btn btn-success">
<i class="fa fa-search"></i>
</button>
</span>
</div>
</div>
<div class="confirm">确定</div>
<div id="container"></div>
<script type="text/javascript" src="//api.map.baidu.com/api?v=2.0&ak={$config.baidukey|default=''}"></script>
<script src="__CDN__/assets/libs/jquery/dist/jquery.min.js"></script>
<script src="__CDN__/assets/addons/address/js/gcoord.min.js"></script>
<script type="text/javascript">
$(function () {
var map, marker, point, fromtype, totype;
var zoom = parseInt("{$zoom}");
var address = "{$address|htmlentities}";
var lng = Number("{$lng}");
var lat = Number("{$lat}");
fromtype = "BD09";
totype = "{$config.coordtype|default='DEFAULT'}"
totype = totype === 'DEFAULT' ? "BD09" : totype;
if (lng && lat && fromtype !== totype) {
var result = gcoord.transform([lng, lat], gcoord[totype], gcoord[fromtype]);
lng = result[0] || lng;
lat = result[1] || lat;
}
var geocoder = new BMap.Geocoder();
var addPointMarker = function (point, addr) {
deletePoint();
addPoint(point);
if (addr) {
addMarker(point, addr);
} else {
geocoder.getLocation(point, function (rs) {
addMarker(point, rs.address);
});
}
};
var addPoint = function (point) {
lng = point.lng;
lat = point.lat;
marker = new BMap.Marker(point);
map.addOverlay(marker);
map.panTo(point);
};
var addMarker = function (point, addr) {
address = addr;
// var labelhtml = '<div class="info">地址:' + address + '<br>经度:' + point.lng + '<br>纬度:' + point.lat + '</div>';
var labelhtml = '<div class="info">地址:' + address + '</div>';
var label = new BMap.Label(labelhtml, {offset: new BMap.Size(16, 20)});
label.setStyle({
border: 'none',
padding: '.75rem 1.25rem'
});
marker.setLabel(label);
};
var deletePoint = function () {
var allOverlay = map.getOverlays();
for (var i = 0; i < allOverlay.length; i++) {
map.removeOverlay(allOverlay[i]);
}
};
var init = function () {
map = new BMap.Map("container"); // 创建地图实例
var point = new BMap.Point(lng, lat); // 创建点坐标
map.enableScrollWheelZoom(true); //开启鼠标滚轮缩放
map.centerAndZoom(point, zoom); // 初始化地图,设置中心点坐标和地图级别
var size = new BMap.Size(10, 20);
map.addControl(new BMap.CityListControl({
anchor: BMAP_ANCHOR_TOP_LEFT,
offset: size,
}));
if ("{$lng}" != '' && "{$lat}" != '') {
addPointMarker(point, address);
}
ac = new BMap.Autocomplete({"input": "place", "location": map}); //建立一个自动完成的对象
ac.addEventListener("onhighlight", function (e) { //鼠标放在下拉列表上的事件
var str = "";
var _value = e.fromitem.value;
var value = "";
if (e.fromitem.index > -1) {
value = _value.province + _value.city + _value.district + _value.street + _value.business;
}
str = "FromItem<br />index = " + e.fromitem.index + "<br />value = " + value;
value = "";
if (e.toitem.index > -1) {
_value = e.toitem.value;
value = _value.province + _value.city + _value.district + _value.street + _value.business;
}
str += "<br />ToItem<br />index = " + e.toitem.index + "<br />value = " + value;
$("#searchResultPanel").html(str);
});
ac.addEventListener("onconfirm", function (e) { //鼠标点击下拉列表后的事件
var _value = e.item.value;
myValue = _value.province + _value.city + _value.district + _value.street + _value.business;
$("#searchResultPanel").html("onconfirm<br />index = " + e.item.index + "<br />myValue = " + myValue);
setPlace();
});
function setPlace(text) {
map.clearOverlays(); //清除地图上所有覆盖物
function myFun() {
var results = local.getResults();
var result = local.getResults().getPoi(0);
var point = result.point; //获取第一个智能搜索的结果
map.centerAndZoom(point, 18);
// map.addOverlay(new BMap.Marker(point)); //添加标注
if (result.type != 0) {
address = results.province + results.city + result.address;
} else {
address = result.address;
}
addPointMarker(point, address);
}
var local = new BMap.LocalSearch(map, { //智能搜索
onSearchComplete: myFun
});
local.search(text || myValue);
}
map.addEventListener("click", function (e) {
//通过点击百度地图可以获取到对应的point, 由point的lng、lat属性就可以获取对应的经度纬度
addPointMarker(e.point);
});
//点击搜索按钮
$(document).on('click', '#search-btn', function () {
if ($("#place").val() == '')
return;
setPlace($("#place").val());
});
};
var close = function (data) {
var index = parent.Layer.getFrameIndex(window.name);
var callback = parent.$("#layui-layer" + index).data("callback");
//再执行关闭
parent.Layer.close(index);
//再调用回传函数
if (typeof callback === 'function') {
callback.call(undefined, data);
}
};
//点击确定后执行回调赋值
$(document).on('click', '.confirm', function () {
var zoom = map.getZoom();
var data = {lat: lat, lng: lng, zoom: zoom, address: address};
if (fromtype !== totype) {
var result = gcoord.transform([data.lng, data.lat], gcoord[fromtype], gcoord[totype]);
data.lng = (result[0] || data.lng).toFixed(5);
data.lat = (result[1] || data.lat).toFixed(5);
console.log(data, result, fromtype, totype);
}
close(data);
});
init();
});
</script>
</body>
</html>

View File

@ -0,0 +1,132 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/>
<title>地图位置(经纬度)选择插件</title>
<link rel="stylesheet" href="__CDN__/assets/css/frontend.min.css"/>
<link rel="stylesheet" href="__CDN__/assets/libs/font-awesome/css/font-awesome.min.css"/>
</head>
<body>
<div class="container">
<div class="bs-docs-section clearfix">
<div class="row">
<div class="col-lg-12">
<div class="page-header">
<h2>地图位置(经纬度)选择示例</h2>
</div>
<div class="bs-component">
<form action="" method="post" role="form">
<div class="form-group">
<label for=""></label>
<input type="text" class="form-control" name="" id="address" placeholder="地址">
</div>
<div class="form-group">
<label for=""></label>
<input type="text" class="form-control" name="" id="lng" placeholder="经度">
</div>
<div class="form-group">
<label for=""></label>
<input type="text" class="form-control" name="" id="lat" placeholder="纬度">
</div>
<div class="form-group">
<label for=""></label>
<input type="text" class="form-control" name="" id="zoom" placeholder="缩放">
</div>
<button type="button" class="btn btn-primary" data-toggle='addresspicker' data-input-id="address" data-lng-id="lng" data-lat-id="lat" data-zoom-id="zoom">点击选择</button>
</form>
</div>
<div class="page-header">
<h2 id="code">调用代码</h2>
</div>
<div class="bs-component">
<textarea class="form-control" rows="17">
<form action="" method="post" role="form">
<div class="form-group">
<label for=""></label>
<input type="text" class="form-control" name="" id="address" placeholder="地址">
</div>
<div class="form-group">
<label for=""></label>
<input type="text" class="form-control" name="" id="lng" placeholder="经度">
</div>
<div class="form-group">
<label for=""></label>
<input type="text" class="form-control" name="" id="lat" placeholder="纬度">
</div>
<div class="form-group">
<label for=""></label>
<input type="text" class="form-control" name="" id="zoom" placeholder="缩放">
</div>
<button type="button" class="btn btn-primary" data-toggle='addresspicker' data-input-id="address" data-lng-id="lng" data-lat-id="lat" data-zoom-id="zoom">点击选择</button>
</form>
</textarea>
</div>
<div class="page-header">
<h2>参数说明</h2>
</div>
<div class="bs-component" style="background:#fff;">
<table class="table table-bordered">
<thead>
<tr>
<th>参数</th>
<th>释义</th>
</tr>
</thead>
<tbody>
<tr>
<td>data-input-id</td>
<td>填充地址的文本框ID</td>
</tr>
<tr>
<td>data-lng-id</td>
<td>填充经度的文本框ID</td>
</tr>
<tr>
<td>data-lat-id</td>
<td>填充纬度的文本框ID</td>
</tr>
<tr>
<td>data-zoom-id</td>
<td>填充缩放的文本框ID</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!--@formatter:off-->
<script type="text/javascript">
var require = {
config: {$config|json_encode}
};
</script>
<!--@formatter:on-->
<script>
require.callback = function () {
define('addons/address', ['jquery', 'bootstrap', 'frontend', 'template'], function ($, undefined, Frontend, Template) {
var Controller = {
index: function () {
}
};
return Controller;
});
define('lang', function () {
return [];
});
}
</script>
<script src="__CDN__/assets/js/require.min.js" data-main="__CDN__/assets/js/require-frontend.min.js?v={$site.version}"></script>
</body>
</html>

View File

@ -0,0 +1,291 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<title>地址选择器</title>
<link rel="stylesheet" href="__CDN__/assets/css/frontend.min.css"/>
<link rel="stylesheet" href="__CDN__/assets/libs/font-awesome/css/font-awesome.min.css"/>
<style type="text/css">
body {
margin: 0;
padding: 0;
}
#container {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
}
.confirm {
position: absolute;
bottom: 30px;
right: 4%;
z-index: 99;
height: 50px;
width: 50px;
line-height: 50px;
font-size: 15px;
text-align: center;
background-color: white;
background: #1ABC9C;
color: white;
border: none;
cursor: pointer;
border-radius: 50%;
}
.search {
position: absolute;
width: 400px;
top: 0;
left: 50%;
padding: 5px;
margin-left: -200px;
}
.autocomplete-search {
text-align: left;
cursor: default;
background: #fff;
border-radius: 2px;
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
-moz-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
background-clip: padding-box;
position: absolute;
display: none;
z-index: 1036;
max-height: 254px;
overflow: hidden;
overflow-y: auto;
box-sizing: border-box;
}
.autocomplete-search .autocomplete-suggestion {
padding: 5px;
}
.autocomplete-search .autocomplete-suggestion:hover {
background: #f0f0f0;
}
</style>
</head>
<body>
<div class="search">
<div class="input-group">
<input type="text" id="place" name="q" class="form-control" placeholder="输入地点"/>
<span class="input-group-btn">
<button type="button" name="search" id="search-btn" class="btn btn-success">
<i class="fa fa-search"></i>
</button>
</span>
</div>
</div>
<div class="confirm">确定</div>
<div id="container"></div>
<script charset="utf-8" src="//map.qq.com/api/js?v=2.exp&libraries=place&key={$config.tencentkey|default=''}"></script>
<script src="__CDN__/assets/libs/jquery/dist/jquery.min.js"></script>
<script src="__CDN__/assets/addons/address/js/gcoord.min.js"></script>
<script src="__CDN__/assets/addons/address/js/jquery.autocomplete.js"></script>
<script type="text/javascript">
$(function () {
var map, marker, geocoder, infoWin, searchService, keyword, address, fromtype, totype;
address = "{$address|htmlentities}";
var lng = Number("{$lng}");
var lat = Number("{$lat}");
fromtype = "GCJ02";
totype = "{$config.coordtype|default='DEFAULT'}"
totype = totype === 'DEFAULT' ? "GCJ02" : totype;
if (lng && lat && fromtype !== totype) {
var result = gcoord.transform([lng, lat], gcoord[totype], gcoord[fromtype]);
lng = result[0] || lng;
lat = result[1] || lat;
}
var init = function () {
var center = new qq.maps.LatLng(lat, lng);
map = new qq.maps.Map(document.getElementById('container'), {
center: center,
zoom: parseInt("{$config.zoom}")
});
//实例化信息窗口
infoWin = new qq.maps.InfoWindow({
map: map
});
geocoder = {
getAddress: function (latLng) {
$.ajax({
url: "https://apis.map.qq.com/ws/geocoder/v1/?location=" + latLng.lat + "," + latLng.lng + "&key={$config.tencentkey|default=''}&output=jsonp",
dataType: "jsonp",
type: 'GET',
cache: true,
crossDomain: true,
success: function (ret) {
console.log("getAddress:", ret)
if (ret.status === 0) {
var component = ret.result.address_component;
if (ret.result.formatted_addresses && ret.result.formatted_addresses.recommend) {
var recommend = ret.result.formatted_addresses.recommend;
var standard_address = ret.result.formatted_addresses.standard_address;
var address = component.province !== component.city ? component.province + component.city : component.province;
address = address + (recommend.indexOf(component.district) === 0 ? '' : component.district) + recommend;
} else {
address = ret.result.address;
}
showMarker(ret.result.location, address);
showInfoWin(ret.result.location, address);
}
},
error: function (e) {
console.log(e, 'error')
}
});
}
};
//初始化marker
showMarker(center);
if (address) {
showInfoWin(center, address);
} else {
geocoder.getAddress(center);
}
var place = $("#place");
place.autoComplete({
minChars: 1,
cache: 0,
menuClass: 'autocomplete-search',
source: function (term, response) {
try {
xhr.abort();
} catch (e) {
}
xhr = $.ajax({
url: "https://apis.map.qq.com/ws/place/v1/suggestion?keyword=" + term + "&key={$config.tencentkey|default=''}&output=jsonp",
dataType: "jsonp",
type: 'GET',
cache: true,
success: function (ret) {
if (ret.status === 0) {
if(ret.data.length === 0){
$(".autocomplete-suggestions.autocomplete-search").html('');
}
response(ret.data);
} else {
console.log(ret);
}
}
});
},
renderItem: function (item, search) {
search = search.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
var regexp = new RegExp("(" + search.replace(/[\,|\u3000|\uff0c]/, ' ').split(' ').join('|') + ")", "gi");
return "<div class='autocomplete-suggestion' data-item='" + JSON.stringify(item) + "' data-title='" + item.title + "' data-val='" + item.title + "'>" + item.title.replace(regexp, "<b>$1</b>") + "</div>";
},
onSelect: function (e, term, sel) {
e.preventDefault();
var item = $(sel).data("item");
//调用获取位置方法
geocoder.getAddress(item.location);
var position = new qq.maps.LatLng(item.location.lat, item.location.lng);
map.setCenter(position);
}
});
//地图点击
qq.maps.event.addListener(map, 'click', function (event) {
try {
//调用获取位置方法
geocoder.getAddress(event.latLng);
} catch (e) {
console.log(e);
}
}
);
};
//显示info窗口
var showInfoWin = function (latLng, title) {
var position = new qq.maps.LatLng(latLng.lat, latLng.lng);
infoWin.open();
infoWin.setContent(title);
infoWin.setPosition(position);
};
//实例化marker和监听拖拽结束事件
var showMarker = function (latLng, title) {
console.log("showMarker", latLng, title)
var position = new qq.maps.LatLng(latLng.lat, latLng.lng);
marker && marker.setMap(null);
marker = new qq.maps.Marker({
map: map,
position: position,
draggable: true,
title: title || '拖动图标选择位置'
});
//监听拖拽结束
qq.maps.event.addListener(marker, 'dragend', function (event) {
//调用获取位置方法
geocoder.getAddress(event.latLng);
});
};
var close = function (data) {
var index = parent.Layer.getFrameIndex(window.name);
var callback = parent.$("#layui-layer" + index).data("callback");
//再执行关闭
parent.Layer.close(index);
//再调用回传函数
if (typeof callback === 'function') {
callback.call(undefined, data);
}
};
//点击确定后执行回调赋值
$(document).on('click', '.confirm', function () {
var zoom = map.getZoom();
var data = {lat: infoWin.position.lat.toFixed(5), lng: infoWin.position.lng.toFixed(5), zoom: zoom, address: infoWin.content};
if (fromtype !== totype) {
var result = gcoord.transform([data.lng, data.lat], gcoord[fromtype], gcoord[totype]);
data.lng = (result[0] || data.lng).toFixed(5);
data.lat = (result[1] || data.lat).toFixed(5);
console.log(data, result, fromtype, totype);
}
close(data);
});
//点击搜索按钮
$(document).on('click', '#search-btn', function () {
if ($("#place").val() === '')
return;
var first = $(".autocomplete-search > .autocomplete-suggestion:first");
if (!first.length) {
return;
}
var item = first.data("item");
//调用获取位置方法
geocoder.getAddress(item.location);
var position = new qq.maps.LatLng(item.location.lat, item.location.lng);
map.setCenter(position);
});
init();
});
</script>
</body>
</html>

1
addons/barcode/.addonrc Normal file
View File

@ -0,0 +1 @@
{"files":[],"license":"regular","licenseto":"16018","licensekey":"50IFXSeyuZKlRmPi aOGsHvyU7O81YgbzjaPZfw==","domains":["test0rui.com"],"licensecodes":[],"validations":["d495f88a5abee2b752e64026c2ea8984"]}

View File

@ -0,0 +1,53 @@
<?php
namespace addons\barcode;
use think\Addons;
/**
* 条码生成
*/
class Barcode extends Addons {
/**
* 插件安装方法
* @return bool
*/
public function install() {
return true;
}
/**
* 插件卸载方法
* @return bool
*/
public function uninstall() {
return true;
}
/**
* 插件启用方法
* @return bool
*/
public function enable() {
return true;
}
/**
* 插件禁用方法
* @return bool
*/
public function disable() {
return true;
}
/**
* 应用初始化
*/
public function appInit()
{
if(!class_exists("\Picqer\Barcode")){
\think\Loader::addNamespace('Picqer\Barcode', ADDON_PATH . 'barcode' . DS . 'library' . DS . 'Barcode' . DS);
}
}
}

22
addons/barcode/config.php Normal file
View File

@ -0,0 +1,22 @@
<?php
return [
[
'name' => 'rewrite',
'title' => '伪静态',
'type' => 'array',
'content' =>
[
],
'value' =>
[
'index/index' => '/barcode$',
'index/build' => '/barcode/build$',
],
'rule' => 'required',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
],
];

View File

@ -0,0 +1,43 @@
<?php
namespace addons\barcode\controller;
use think\addons\Controller;
use think\Response;
/**
* 条码生成
* @package addons\barcode\controller
*/
class Index extends Controller {
public function index() {
return $this->view->fetch();
}
// 生成条码
public function build() {
$text = $this->request->get('text', '1234567890');
$type = $this->request->get('type', 'C128');
$foreground = $this->request->get('foreground', "#000000");
$width = $this->request->get('width', 2);
$height = $this->request->get('height', 30);
$params = [
'text' => $text,
'type' => $type,
'foreground' => $foreground,
'width' => $width,
'height' => $height,
];
$barcode = \addons\barcode\library\Service::barcode($params);
// 直接显示条码
$response = Response::create()->header("Content-Type", "image/png");
header('Content-Type: image/png');
$response->content($barcode);
return $response;
}
}

10
addons/barcode/info.ini Normal file
View File

@ -0,0 +1,10 @@
name = barcode
title = 条码生成
intro = 生成条码示例插件
author = aa820t
website = https://ask.fastadmin.net/u/26861
version = 1.0.0
state = 1
license = regular
licenseto = 16018
url = /addons/barcode

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,43 @@
<?php
namespace Picqer\Barcode;
class BarcodeGeneratorHTML extends BarcodeGenerator
{
/**
* Return an HTML representation of barcode.
*
* @param string $code code to print
* @param string $type type of barcode
* @param int $widthFactor Width of a single bar element in pixels.
* @param int $totalHeight Height of a single bar element in pixels.
* @param int|string $color Foreground color for bar elements (background is transparent).
* @return string HTML code.
* @public
*/
public function getBarcode($code, $type, $widthFactor = 2, $totalHeight = 30, $color = 'black')
{
$barcodeData = $this->getBarcodeData($code, $type);
$html = '<div style="font-size:0;position:relative;width:' . ($barcodeData['maxWidth'] * $widthFactor) . 'px;height:' . ($totalHeight) . 'px;">' . "\n";
$positionHorizontal = 0;
foreach ($barcodeData['bars'] as $bar) {
$barWidth = round(($bar['width'] * $widthFactor), 3);
$barHeight = round(($bar['height'] * $totalHeight / $barcodeData['maxHeight']), 3);
if ($bar['drawBar']) {
$positionVertical = round(($bar['positionVertical'] * $totalHeight / $barcodeData['maxHeight']), 3);
// draw a vertical bar
$html .= '<div style="background-color:' . $color . ';width:' . $barWidth . 'px;height:' . $barHeight . 'px;position:absolute;left:' . $positionHorizontal . 'px;top:' . $positionVertical . 'px;">&nbsp;</div>' . "\n";
}
$positionHorizontal += $barWidth;
}
$html .= '</div>' . "\n";
return $html;
}
}

View File

@ -0,0 +1,77 @@
<?php
namespace Picqer\Barcode;
use Picqer\Barcode\Exceptions\BarcodeException;
class BarcodeGeneratorJPG extends BarcodeGenerator
{
/**
* Return a JPG image representation of barcode (requires GD or Imagick library).
*
* @param string $code code to print
* @param string $type type of barcode:
* @param int $widthFactor Width of a single bar element in pixels.
* @param int $totalHeight Height of a single bar element in pixels.
* @param array $color RGB (0-255) foreground color for bar elements (background is transparent).
* @return string image data or false in case of error.
* @public
* @throws BarcodeException
*/
public function getBarcode($code, $type, $widthFactor = 2, $totalHeight = 30, $color = array(0, 0, 0))
{
$barcodeData = $this->getBarcodeData($code, $type);
// calculate image size
$width = ($barcodeData['maxWidth'] * $widthFactor);
$height = $totalHeight;
if (function_exists('imagecreate')) {
// GD library
$imagick = false;
$jpg = imagecreate($width, $height);
$colorBackground = imagecolorallocate($jpg, 255, 255, 255);
imagecolortransparent($jpg, $colorBackground);
$colorForeground = imagecolorallocate($jpg, $color[0], $color[1], $color[2]);
} elseif (extension_loaded('imagick')) {
$imagick = true;
$colorForeground = new \imagickpixel('rgb(' . $color[0] . ',' . $color[1] . ',' . $color[2] . ')');
$jpg = new \Imagick();
$jpg->newImage($width, $height, 'white', 'jpg');
$imageMagickObject = new \imagickdraw();
$imageMagickObject->setFillColor($colorForeground);
} else {
throw new BarcodeException('Neither gd-lib or imagick are installed!');
}
// print bars
$positionHorizontal = 0;
foreach ($barcodeData['bars'] as $bar) {
$bw = round(($bar['width'] * $widthFactor), 3);
$bh = round(($bar['height'] * $totalHeight / $barcodeData['maxHeight']), 3);
if ($bar['drawBar']) {
$y = round(($bar['positionVertical'] * $totalHeight / $barcodeData['maxHeight']), 3);
// draw a vertical bar
if ($imagick && isset($imageMagickObject)) {
$imageMagickObject->rectangle($positionHorizontal, $y, ($positionHorizontal + $bw), ($y + $bh));
} else {
imagefilledrectangle($jpg, $positionHorizontal, $y, ($positionHorizontal + $bw) - 1, ($y + $bh),
$colorForeground);
}
}
$positionHorizontal += $bw;
}
ob_start();
if ($imagick && isset($imageMagickObject)) {
$jpg->drawImage($imageMagickObject);
echo $jpg;
} else {
imagejpeg($jpg);
imagedestroy($jpg);
}
$image = ob_get_clean();
return $image;
}
}

View File

@ -0,0 +1,76 @@
<?php
namespace Picqer\Barcode;
use Picqer\Barcode\Exceptions\BarcodeException;
class BarcodeGeneratorPNG extends BarcodeGenerator
{
/**
* Return a PNG image representation of barcode (requires GD or Imagick library).
*
* @param string $code code to print
* @param string $type type of barcode:
* @param int $widthFactor Width of a single bar element in pixels.
* @param int $totalHeight Height of a single bar element in pixels.
* @param array $color RGB (0-255) foreground color for bar elements (background is transparent).
* @return string image data or false in case of error.
* @public
*/
public function getBarcode($code, $type, $widthFactor = 2, $totalHeight = 30, $color = array(0, 0, 0))
{
$barcodeData = $this->getBarcodeData($code, $type);
// calculate image size
$width = ($barcodeData['maxWidth'] * $widthFactor);
$height = $totalHeight;
if (function_exists('imagecreate')) {
// GD library
$imagick = false;
$png = imagecreate($width, $height);
$colorBackground = imagecolorallocate($png, 255, 255, 255);
imagecolortransparent($png, $colorBackground);
$colorForeground = imagecolorallocate($png, $color[0], $color[1], $color[2]);
} elseif (extension_loaded('imagick')) {
$imagick = true;
$colorForeground = new \imagickpixel('rgb(' . $color[0] . ',' . $color[1] . ',' . $color[2] . ')');
$png = new \Imagick();
$png->newImage($width, $height, 'none', 'png');
$imageMagickObject = new \imagickdraw();
$imageMagickObject->setFillColor($colorForeground);
} else {
throw new BarcodeException('Neither gd-lib or imagick are installed!');
}
// print bars
$positionHorizontal = 0;
foreach ($barcodeData['bars'] as $bar) {
$bw = round(($bar['width'] * $widthFactor), 3);
$bh = round(($bar['height'] * $totalHeight / $barcodeData['maxHeight']), 3);
if ($bar['drawBar']) {
$y = round(($bar['positionVertical'] * $totalHeight / $barcodeData['maxHeight']), 3);
// draw a vertical bar
if ($imagick && isset($imageMagickObject)) {
$imageMagickObject->rectangle($positionHorizontal, $y, ($positionHorizontal + $bw), ($y + $bh));
} else {
imagefilledrectangle($png, $positionHorizontal, $y, ($positionHorizontal + $bw) - 1, ($y + $bh),
$colorForeground);
}
}
$positionHorizontal += $bw;
}
ob_start();
if ($imagick && isset($imageMagickObject)) {
$png->drawImage($imageMagickObject);
echo $png;
} else {
imagepng($png);
imagedestroy($png);
}
$image = ob_get_clean();
return $image;
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace Picqer\Barcode;
class BarcodeGeneratorSVG extends BarcodeGenerator
{
/**
* Return a SVG string representation of barcode.
*
* @param $code (string) code to print
* @param $type (const) type of barcode
* @param $widthFactor (int) Minimum width of a single bar in user units.
* @param $totalHeight (int) Height of barcode in user units.
* @param $color (string) Foreground color (in SVG format) for bar elements (background is transparent).
* @return string SVG code.
* @public
*/
public function getBarcode($code, $type, $widthFactor = 2, $totalHeight = 30, $color = 'black')
{
$barcodeData = $this->getBarcodeData($code, $type);
// replace table for special characters
$repstr = array("\0" => '', '&' => '&amp;', '<' => '&lt;', '>' => '&gt;');
$width = round(($barcodeData['maxWidth'] * $widthFactor), 3);
$svg = '<?xml version="1.0" standalone="no" ?>' . "\n";
$svg .= '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">' . "\n";
$svg .= '<svg width="' . $width . '" height="' . $totalHeight . '" viewBox="0 0 ' . $width . ' ' . $totalHeight . '" version="1.1" xmlns="http://www.w3.org/2000/svg">' . "\n";
$svg .= "\t" . '<desc>' . strtr($barcodeData['code'], $repstr) . '</desc>' . "\n";
$svg .= "\t" . '<g id="bars" fill="' . $color . '" stroke="none">' . "\n";
// print bars
$positionHorizontal = 0;
foreach ($barcodeData['bars'] as $bar) {
$barWidth = round(($bar['width'] * $widthFactor), 3);
$barHeight = round(($bar['height'] * $totalHeight / $barcodeData['maxHeight']), 3);
if ($bar['drawBar']) {
$positionVertical = round(($bar['positionVertical'] * $totalHeight / $barcodeData['maxHeight']), 3);
// draw a vertical bar
$svg .= "\t\t" . '<rect x="' . $positionHorizontal . '" y="' . $positionVertical . '" width="' . $barWidth . '" height="' . $barHeight . '" />' . "\n";
}
$positionHorizontal += $barWidth;
}
$svg .= "\t" . '</g>' . "\n";
$svg .= '</svg>' . "\n";
return $svg;
}
}

View File

@ -0,0 +1,5 @@
<?php
namespace Picqer\Barcode\Exceptions;
class BarcodeException extends \Exception {}

View File

@ -0,0 +1,5 @@
<?php
namespace Picqer\Barcode\Exceptions;
class InvalidCharacterException extends BarcodeException {}

View File

@ -0,0 +1,5 @@
<?php
namespace Picqer\Barcode\Exceptions;
class InvalidCheckDigitException extends BarcodeException {}

View File

@ -0,0 +1,5 @@
<?php
namespace Picqer\Barcode\Exceptions;
class InvalidFormatException extends BarcodeException {}

View File

@ -0,0 +1,5 @@
<?php
namespace Picqer\Barcode\Exceptions;
class InvalidLengthException extends BarcodeException {}

View File

@ -0,0 +1,5 @@
<?php
namespace Picqer\Barcode\Exceptions;
class UnknownTypeException extends BarcodeException {}

View File

@ -0,0 +1,27 @@
<?php
namespace addons\barcode\library;
class Service
{
public static function barcode($params)
{
$params = is_array($params) ? $params : [$params];
$params['text'] = isset($params['text']) ? $params['text'] : 'Hello world!';
$params['type'] = isset($params['type']) ? $params['type'] : 'C128';
$params['width'] = isset($params['width']) ? $params['width'] : 2;
$params['height'] = isset($params['height']) ? $params['height'] : 30;
$params['foreground'] = isset($params['foreground']) ? $params['foreground'] : "#000000";
// 前景色
list($r, $g, $b) = sscanf($params['foreground'], "#%02x%02x%02x");
$foregroundcolor = [$r, $g, $b];
// 创建实例
$generator = new \Picqer\Barcode\BarcodeGeneratorPNG();
$barcode = $generator->getBarcode($params['text'], $params['type'], $params['width'], $params['height'], $foregroundcolor);
return $barcode;
}
}

View File

@ -0,0 +1,99 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
<title>条码生成 - {$site.name}</title>
<link href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
<!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!--[if lt IE 9]>
<script src="http://apps.bdimg.com/libs/html5shiv/3.7/html5shiv.min.js"></script>
<script src="http://apps.bdimg.com/libs/respond.js/1.4.2/respond.js"></script>
<![endif]-->
</head>
<body>
<div class="container">
<h2>条码生成</h2>
<hr>
<div class="well">
<form action="" method="post">
<div class="row">
<div class="col-md-4">
<div class="form-group">
<label class="control-label">文本内容</label>
<input type="text" name="text" class="form-control" placeholder="" value="1234567890">
</div>
<div class="form-group">
<label class="control-label">条码类型</label>
<select name="type" class="form-control">
<option value="C128">C128</option>
<option value="C128A">C128A</option>
<option value="C128B">C128B</option>
<option value="C128C">C128C</option>
<option value="C39">C39</option>
<option value="C39+">C39+</option>
<option value="C39E">C39E</option>
<option value="C39E+">C39E+</option>
<option value="S25">S25</option>
<option value="I25">I25</option>
<option value="I25+">I25+</option>
<option value="EAN2">EAN2</option>
<option value="EAN5">EAN5</option>
<option value="EAN8">EAN8</option>
<option value="EAN13">EAN13</option>
<option value="UPCA">UPCA</option>
<option value="UPCE">UPCE</option>
<option value="MSI">MSI</option>
<option value="POSTNET">POSTNET</option>
<option value="PLANET">PLANET</option>
<option value="RMS4CC">RMS4CC</option>
<option value="KIX">KIX</option>
<option value="IMB">IMB</option>
<option value="CODABAR">CODABAR</option>
<option value="CODE11">CODE11</option>
<option value="PHARMA">PHARMA</option>
<option value="PHARMA2T">PHARMA2T</option>
</select>
</div>
<div class="form-group">
<input type="submit" class="btn btn-info" />
<input type="reset" class="btn btn-default" />
</div>
</div>
<div class="col-md-2">
<div class="form-group">
<div class="form-group">
<label class="control-label">前景色</label>
<input type="text" name="foreground" placeholder="" class="form-control" value="#000000">
</div>
<div class="form-group">
<label class="control-label">间距</label>
<input type="number" name="width" placeholder="" class="form-control" value="2">
</div>
<div class="form-group">
<label class="control-label">高度</label>
<input type="number" name="height" placeholder="" class="form-control" value="30">
</div>
</div>
</div>
</div>
</form>
</div>
<input type="text" class="form-control" id='barcodeurl' />
<img src="" alt="" id='barcodeimg' style="margin-top:15px;"/>
</div>
<script src="https://cdn.bootcss.com/jquery/2.2.4/jquery.min.js"></script>
<script type="text/javascript">
$(function () {
$("form").submit(function () {
$("#barcodeimg").prop("src", "{:addon_url('barcode/index/build',[],false)}?" + $(this).serialize());
$("#barcodeurl").val("{:addon_url('barcode/index/build',[],false,true)}?" + $(this).serialize());
return false;
});
$("form").trigger('submit');
});
</script>
</body>
</html>

View File

@ -0,0 +1 @@
{"files":["public\\assets\\addons\\bootstrapcontextmenu\\.bower.json","public\\assets\\addons\\bootstrapcontextmenu\\bootstrap-contextmenu.js","public\\assets\\addons\\bootstrapcontextmenu\\bower.json","public\\assets\\addons\\bootstrapcontextmenu\\gulpfile.js","public\\assets\\addons\\bootstrapcontextmenu\\index.html","public\\assets\\addons\\bootstrapcontextmenu\\js\\bootstrap-contextmenu.js","public\\assets\\addons\\bootstrapcontextmenu\\package.json","public\\assets\\addons\\bootstrapcontextmenu\\README.md"],"license":"regular","licenseto":"16018","licensekey":"rom5GLabySARl1Bj 3ffvlQ7FwtFqvn+DzJFWyRyN4UydK96LroByZnPwiVc=","domains":["test0rui.com"],"licensecodes":[],"validations":["d495f88a5abee2b752e64026c2ea8984"]}

View File

@ -0,0 +1,54 @@
<?php
namespace addons\bootstrapcontextmenu;
use app\common\library\Menu;
use think\Addons;
/**
* 插件
*/
class Bootstrapcontextmenu extends Addons
{
/**
* 插件安装方法
* @return bool
*/
public function install()
{
return true;
}
/**
* 插件卸载方法
* @return bool
*/
public function uninstall()
{
return true;
}
/**
* 插件启用方法
* @return bool
*/
public function enable()
{
return true;
}
/**
* 插件禁用方法
* @return bool
*/
public function disable()
{
return true;
}
}

View File

@ -0,0 +1,73 @@
require(['../addons/bootstrapcontextmenu/js/bootstrap-contextmenu'], function (undefined) {
if (Config.controllername == 'index' && Config.actionname == 'index') {
$("body").append(
'<div id="context-menu">' +
'<ul class="dropdown-menu" role="menu">' +
'<li><a tabindex="-1" data-operate="refresh"><i class="fa fa-refresh fa-fw"></i>刷新</a></li>' +
'<li><a tabindex="-1" data-operate="refreshTable"><i class="fa fa-table fa-fw"></i>刷新表格</a></li>' +
'<li><a tabindex="-1" data-operate="close"><i class="fa fa-close fa-fw"></i>关闭</a></li>' +
'<li><a tabindex="-1" data-operate="closeOther"><i class="fa fa-window-close-o fa-fw"></i>关闭其他</a></li>' +
'<li class="divider"></li>' +
'<li><a tabindex="-1" data-operate="closeAll"><i class="fa fa-power-off fa-fw"></i>关闭全部</a></li>' +
'</ul>' +
'</div>');
$(".nav-addtabs").contextmenu({
target: "#context-menu",
scopes: 'li[role=presentation]',
onItem: function (e, event) {
var $element = $(event.target);
var tab_id = e.attr('id');
var id = tab_id.substr('tab_'.length);
var con_id = 'con_' + id;
switch ($element.data('operate')) {
case 'refresh':
$("#" + con_id + " iframe").attr('src', function (i, val) {
return val;
});
break;
case 'refreshTable':
try {
if ($("#" + con_id + " iframe").contents().find(".btn-refresh").size() > 0) {
$("#" + con_id + " iframe")[0].contentWindow.$(".btn-refresh").trigger("click");
}
} catch (e) {
}
break;
case 'close':
if (e.find(".close-tab").length > 0) {
e.find(".close-tab").click();
}
break;
case 'closeOther':
e.parent().find("li[role='presentation']").each(function () {
if ($(this).attr('id') == tab_id) {
return;
}
if ($(this).find(".close-tab").length > 0) {
$(this).find(".close-tab").click();
}
});
break;
case 'closeAll':
e.parent().find("li[role='presentation']").each(function () {
if ($(this).find(".close-tab").length > 0) {
$(this).find(".close-tab").click();
}
});
break;
default:
break;
}
}
});
}
$(document).on('click', function () { // iframe内点击 隐藏菜单
try {
top.window.$(".nav-addtabs").contextmenu("closemenu");
} catch (e) {
}
});
});

View File

@ -0,0 +1,3 @@
<?php
return [];

View File

@ -0,0 +1,15 @@
<?php
namespace addons\bootstrapcontextmenu\controller;
use think\addons\Controller;
class Index extends Controller
{
public function index()
{
$this->error("当前插件暂无前台页面");
}
}

View File

@ -0,0 +1,10 @@
name = bootstrapcontextmenu
title = 菜单栏右键菜单
intro = 菜单栏添加右键菜单关闭刷新功能
author = 张尧嵩
website = https://www.fastadmin.net
version = 1.0.1
state = 1
url = /addons/bootstrapcontextmenu
license = regular
licenseto = 16018

View File

@ -0,0 +1 @@
{"files":["public\\assets\\addons\\clicaptcha\\css\\clicaptcha.css","public\\assets\\addons\\clicaptcha\\img\\1.png","public\\assets\\addons\\clicaptcha\\img\\2.png","public\\assets\\addons\\clicaptcha\\img\\3.png","public\\assets\\addons\\clicaptcha\\img\\refresh.png","public\\assets\\addons\\clicaptcha\\js\\clicaptcha.js"],"license":"regular","licenseto":"16018","licensekey":"Avjh8YDWOcFdt1ru dGkElgyzj6TJr7SL3vqe\/yje4antR9\/Kn7\/DPfmbi9g=","domains":["test0rui.com"],"licensecodes":[],"validations":["d495f88a5abee2b752e64026c2ea8984"]}

View File

@ -0,0 +1,94 @@
<?php
namespace addons\clicaptcha;
use think\Addons;
use think\Validate;
/**
* 全新点选验证码插件
*/
class Clicaptcha extends Addons
{
/**
* 插件安装方法
* @return bool
*/
public function install()
{
return true;
}
/**
* 插件卸载方法
* @return bool
*/
public function uninstall()
{
return true;
}
/**
* 插件启用方法
* @return bool
*/
public function enable()
{
return true;
}
/**
* 插件禁用方法
* @return bool
*/
public function disable()
{
return true;
}
/**
* 自定义captcha验证事件
*/
public function actionBegin()
{
$module = strtolower(request()->module());
if (in_array($module, ['index', 'admin', 'api', 'store'])) {
Validate::extend('captcha', function ($value, $id = "") {
$clicaptcha = new \addons\clicaptcha\library\Clicaptcha();
$value = $value ? $value : request()->post("captcha");
if (!$clicaptcha->check($value, true)) {
return false;
};
return true;
});
}
}
/**
* 脚本替换
*/
public function viewFilter(&$content)
{
$module = strtolower(request()->module());
if (($module == 'index' && config('fastadmin.user_register_captcha') == 'text') || ($module == 'admin' && config('fastadmin.login_captcha')) || ($module == 'store' && config('fastadmin.login_captcha'))) {
$content = preg_replace_callback('/<!--@CaptchaBegin-->([\s\S]*?)<!--@CaptchaEnd-->/i', function ($matches) {
return '<!--@CaptchaBegin--><div><input type="hidden" name="captcha" /></div><!--@CaptchaEnd-->';
}, $content);
if (preg_match('/<!--@CaptchaBegin-->([\s\S]*?)<!--@CaptchaEnd-->/i', $content)) {
$content = preg_replace('/<button(?![^>]*\s+disabled)(?=[^>]*\stype="submit")(.*?)(>)/', '<button$1 disabled$2', $content);
}
}
}
public function captchaMode()
{
return 'clicaptcha';
}
}

90
addons/clicaptcha/bootstrap.js vendored Normal file
View File

@ -0,0 +1,90 @@
require.config({
paths: {
'clicaptcha': '../addons/clicaptcha/js/clicaptcha'
},
shim: {
'clicaptcha': {
deps: [
'jquery',
'css!../addons/clicaptcha/css/clicaptcha.css'
],
exports: '$.fn.clicaptcha'
}
}
});
require(['form'], function (Form) {
window.clicaptcha = function (captcha) {
require(['clicaptcha'], function (undefined) {
captcha = captcha ? captcha : $("input[name=captcha]");
if (captcha.length > 0) {
var form = captcha.closest("form");
var parentDom = captcha.parent();
// 非文本验证码
if ($("a[data-event][data-url]", parentDom).length > 0) {
return;
}
if (captcha.parentsUntil(form, "div.form-group").length > 0) {
captcha.parentsUntil(form, "div.form-group").addClass("hidden");
} else if (parentDom.is("div.input-group")) {
parentDom.addClass("hidden");
}
captcha.attr("data-rule", "required");
// 验证失败时进行操作
captcha.on('invalid.field', function (e, result, me) {
//必须删除errors对象中的数据否则会出现Layer的Tip
delete me.errors['captcha'];
captcha.clicaptcha({
src: '/addons/clicaptcha/index/start',
success_tip: '验证成功!',
error_tip: '未点中正确区域,请重试!',
callback: function (captchainfo) {
form.trigger("submit");
return false;
}
});
});
// 监听表单错误事件
form.on("error.form", function (e, data) {
captcha.val('');
});
}
});
};
// clicaptcha($("input[name=captcha]"));
if (typeof Frontend !== 'undefined') {
Frontend.api.preparecaptcha = function (btn, type, data) {
require(['form'], function (Form) {
$("#clicaptchacontainer").remove();
$("<div />").attr("id", "clicaptchacontainer").addClass("hidden").html(Template("captchatpl", {})).appendTo("body");
var form = $("#clicaptchacontainer form");
form.data("validator-options", {
valid: function (ret) {
data.captcha = $("input[name=captcha]", form).val();
Frontend.api.sendcaptcha(btn, type, data, function (data, ret) {
console.log("ok");
});
return true;
}
})
Form.api.bindevent(form);
});
};
}
var _bindevent = Form.events.bindevent;
Form.events.bindevent = function (form) {
_bindevent.apply(this, [form]);
var captchaObj = $("input[name=captcha]", form);
if (captchaObj.length > 0) {
captchaObj.closest("form").find("button[type=submit]").removeAttr("disabled");
clicaptcha(captchaObj);
if ($(form).attr("name") === 'captcha-form') {
setTimeout(function () {
captchaObj.trigger("invalid.field", [{key: 'captcha'}, {errors: {}}]);
}, 100);
}
}
}
});

View File

@ -0,0 +1,92 @@
<?php
return [
[
'name' => 'mode',
'title' => '类型',
'type' => 'checkbox',
'content' => [
'gallery' => '图像',
'text' => '随机文字',
'custom' => '自定义文字',
],
'value' => 'gallery,text,custom',
'rule' => 'required',
'msg' => '',
'tip' => '用于前台显示点选的类型',
'ok' => '',
'extend' => '',
],
[
'name' => 'background',
'title' => '背景图',
'type' => 'images',
'content' => [],
'value' => '/assets/addons/clicaptcha/img/1.png,/assets/addons/clicaptcha/img/2.png,/assets/addons/clicaptcha/img/3.png',
'rule' => 'required',
'msg' => '',
'tip' => '验证码背景图不少于3张图片建议长宽为350*233目前仅支持gif,jpeg,png三种格式',
'ok' => '',
'extend' => '',
],
[
'name' => 'font',
'title' => '字体',
'type' => 'string',
'content' => [],
'value' => '/assets/fonts/SourceHanSansK-Regular.ttf',
'rule' => 'required',
'msg' => '',
'tip' => '验证码字体',
'ok' => '',
'extend' => '',
],
[
'name' => 'customtext',
'title' => '自定义验证文字',
'type' => 'text',
'content' => [],
'value' => '',
'rule' => '',
'msg' => '',
'tip' => '一行一个词语<br>建议不超过6个汉字',
'ok' => '',
'extend' => '',
],
[
'name' => 'textsize',
'title' => '点选文字(图像)数量',
'type' => 'number',
'content' => [],
'value' => '2',
'rule' => 'required',
'msg' => '',
'tip' => '点选的文字或图像的数量建议3-5个',
'ok' => '',
'extend' => '',
],
[
'name' => 'disturbsize',
'title' => '干扰文字(图像)数量',
'type' => 'number',
'content' => [],
'value' => '0',
'rule' => 'required',
'msg' => '',
'tip' => '干扰文字或图像的数量',
'ok' => '',
'extend' => '',
],
[
'name' => 'alpha',
'title' => '验证码透明度',
'type' => 'number',
'content' => [],
'value' => '30',
'rule' => 'required',
'msg' => '',
'tip' => '验证码透明度0-1000表示透明,100表示不透明',
'ok' => '',
'extend' => '',
],
];

View File

@ -0,0 +1,62 @@
<?php
namespace addons\clicaptcha\controller;
use addons\clicaptcha\library\Clicaptcha;
use think\Config;
/**
* 公共
*/
class Api extends \app\common\controller\Api
{
protected $noNeedLogin = '*';
public function _initialize()
{
if (isset($_SERVER['HTTP_ORIGIN'])) {
header('Access-Control-Expose-Headers: __token__');//跨域让客户端获取到
header('Access-Control-Expose-Headers: X-Clicaptcha-Text');//跨域让客户端获取到
}
//跨域检测
check_cors_request();
if (!isset($_COOKIE['PHPSESSID'])) {
Config::set('session.id', $this->request->server("HTTP_SID"));
}
parent::_initialize();
}
public function index()
{
$this->error("当前插件暂无前台页面");
}
/**
* 获取验证码
*/
public function start()
{
$clicaptcha = new Clicaptcha();
$response = $clicaptcha->create();
$contentType = $response->getHeader('Content-Type');
$text = urldecode($response->getHeader('X-Clicaptcha-Text'));
$content = $response->getContent();
$this->success($text, 'data:' . $contentType . ';base64,' . base64_encode($content));
}
/**
* 判断验证码
*/
public function check()
{
$clicaptcha = new Clicaptcha();
$result = $clicaptcha->check($this->request->post("info", $this->request->post("captcha")), false);
if ($result) {
$this->success();
} else {
$this->error();
}
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace addons\clicaptcha\controller;
use addons\clicaptcha\library\Clicaptcha;
use think\addons\Controller;
class Index extends Controller
{
public function index()
{
$this->error("当前插件暂无前台页面");
}
/**
* 初始化验证码
*/
public function start()
{
$clicaptcha = new Clicaptcha();
$do = $this->request->post('do');
if ($do == 'check') {
echo $clicaptcha->check($this->request->post("info"), false) ? 1 : 0;
return;
} else {
$config = get_addon_config('clicaptcha');
return $clicaptcha->create();
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 684 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 804 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 753 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 792 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 928 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 789 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 781 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 760 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 702 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 576 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 723 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 854 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 659 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 740 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 731 B

View File

@ -0,0 +1,10 @@
name = clicaptcha
title = 全新点选文字验证码
intro = 将前后台验证码切换为全新的点选验证码
author = FastAdmin
website = https://www.fastadmin.net
version = 1.1.6
state = 1
url = /addons/clicaptcha
license = regular
licenseto = 16018

View File

@ -0,0 +1,373 @@
<?php
namespace addons\clicaptcha\library;
use think\Config;
use think\Exception;
class Clicaptcha
{
protected $gallery = [
'square' => '正方形',
'airplane' => '飞机',
'dolphin' => '海豚',
'hamburger' => '汉堡',
'butterfly' => '蝴蝶',
'babyfeet' => '脚掌',
'whale' => '鲸鱼',
'lipstick' => '口红',
'hanger' => '晾衣架',
'hexagon' => '六边形',
'bottle' => '奶瓶',
'alarmclock' => '闹钟',
'triangle' => '三角形',
'rabbit' => '兔子',
'banana' => '香蕉',
'snowflake' => '雪花',
'cherry' => '樱桃',
'fishone' => '鱼',
'round' => '圆形',
'diamonds' => '钻石'
];
protected $custom = [];
protected $mode = [];
protected $options = [];
public function __construct($options = [])
{
if ($config = get_addon_config('clicaptcha')) {
$this->options = array_merge($this->options, $config);
}
$this->options = array_merge($this->options, $options);
$this->custom = array_map('trim', array_unique(array_filter(explode("\n", str_replace("\r", "", $config['customtext'])))));
$this->mode = explode(',', $this->options['mode']);
}
/**
* 生成图片
* @return \think\Response
* @throws Exception
*/
public function create()
{
$imagePathArr = array_map('trim', array_unique(array_filter(explode(',', $this->options['background']))));
if (!$imagePathArr) {
throw new Exception("背景图片不能为空");
}
shuffle($imagePathArr);
$imagePath = ROOT_PATH . 'public' . reset($imagePathArr);
$fontPath = ROOT_PATH . 'public' . $this->options['font'];
if (!is_file($imagePath)) {
throw new Exception("未找到背景图片");
}
if (!is_file($fontPath)) {
throw new Exception("未找到字体文件");
}
//获取验证文字和干扰文字
$text = $this->randChars($this->options['textsize'], $this->options['disturbsize']);
foreach ($text as $v) {
$tmp = [];
if (isset($this->gallery[$v])) {
//当前为图案
$tmp['isimage'] = true;
$tmp['name'] = $v;
$tmp['text'] = "<{$this->gallery[$v]}>";
$tmp['size'] = 24;
$tmp['width'] = 36;
$tmp['height'] = 36;
} else {
$tmp['isimage'] = false;
$fontSize = rand(20, 24);
//字符串文本框宽度和长度
$fontarea = imagettfbbox($fontSize, 0, $fontPath, $v);
$textWidth = $fontarea[2] - $fontarea[0];
$textHeight = $fontarea[1] - $fontarea[7];
$tmp['text'] = $v;
$tmp['size'] = $fontSize;
$tmp['width'] = $textWidth;
$tmp['height'] = $textHeight;
}
$textArr['text'][] = $tmp;
}
//图片宽高和类型
list($imageWidth, $imageHeight, $imageType) = getimagesize($imagePath);
$textArr['width'] = $imageWidth;
$textArr['height'] = $imageHeight;
$text = [];
//随机生成汉字位置
foreach ($textArr['text'] as &$v) {
list($x, $y) = $this->randPosition($textArr['text'], $v['text'], $imageWidth, $imageHeight, $v['width'], $v['height']);
$v['x'] = $x;
$v['y'] = $y;
$text[] = $v['text'];
}
unset($v);
$responseText = implode(',', array_slice($text, 0, $this->options['textsize']));
session('clicaptcha_text', $textArr);
cookie('clicaptcha_text', $responseText, ['expire' => 600, 'path' => '/', 'prefix' => '', 'httponly' => false]);
//创建图片的实例
$image = null;
$output = '';
$contentType = '';
switch ($imageType) {
case 1 :
$image = imagecreatefromgif($imagePath);
$contentType = 'image/gif';
$output = 'imagegif';
break;
case 2 :
$image = imagecreatefromjpeg($imagePath);
$contentType = 'image/jpeg';
$output = 'imagejpeg';
break;
case 3 :
$image = imagecreatefrompng($imagePath);
$contentType = 'image/png';
$output = 'imagepng';
break;
}
if (!$image) {
throw new Exception("读取背景图片错误");
}
//渲染图片和文字
foreach ($textArr['text'] as $v) {
if ($v['isimage']) {
//绘画图片
$stamp = imagecreatefrompng(ADDON_PATH . 'clicaptcha' . DS . 'data' . DS . 'gallery' . DS . $v['name'] . '.png');
$this->imagecopymerge_alpha($image, $stamp, $v['x'], $v['y'], 0, 0, 36, 36, $this->options['alpha']);
} else {
//字体颜色
$color = imagecolorallocatealpha($image, 230, 230, 230, 127 - intval($this->options['alpha'] * (127 / 100)));
//绘画文字
imagettftext($image, $v['size'], 0, $v['x'], $v['y'] + $v['height'], $color, $fontPath, $v['text']);
}
}
//输出图片
ob_start();
call_user_func($output, $image);
imagedestroy($image);
$content = ob_get_clean();
return response($content, 200, ['Content-Length' => strlen($content), 'Content-Type' => $contentType, 'X-Clicaptcha-Text' => urlencode($responseText)])->contentType($contentType);
}
/**
* 拷贝透明图片
*/
protected function imagecopymerge_alpha($dst_im, $src_im, $dst_x, $dst_y, $src_x, $src_y, $src_w, $src_h, $pct)
{
$cut = imagecreatetruecolor($src_w, $src_h);
imagecopy($cut, $dst_im, 0, 0, $dst_x, $dst_y, $src_w, $src_h);
imagecopy($cut, $src_im, 0, 0, $src_x, $src_y, $src_w, $src_h);
imagecopymerge($dst_im, $cut, $dst_x, $dst_y, 0, 0, $src_w, $src_h, $pct);
}
/**
* 验证
* @param string $info
* @param bool $unset
* @return bool
*/
public function check($info, $unset = true)
{
$flag = false;
$textArr = session('clicaptcha_text');
if ($textArr) {
list($xy, $w, $h) = explode(';', $info);
$xyArr = explode('-', $xy);
$xpro = $w / $textArr['width'];//宽度比例
$ypro = $h / $textArr['height'];//高度比例
$textsize = $this->options['textsize'];
$success = 0;
$xyArr = array_slice($xyArr, 0, $textsize);
foreach ($xyArr as $k => $v) {
$xy = explode(',', $v);
$x = $xy[0];
$y = $xy[1];
if ($x / $xpro < $textArr['text'][$k]['x'] || $x / $xpro > $textArr['text'][$k]['x'] + $textArr['text'][$k]['width']) {
$flag = false;
break;
}
if ($y / $ypro < $textArr['text'][$k]['y'] || $y / $ypro > $textArr['text'][$k]['y'] + $textArr['text'][$k]['height']) {
$flag = false;
break;
}
$success++;
}
if ($success == $textsize) {
$flag = true;
}
if ($unset) {
session('clicaptcha_text', null);
}
}
return $flag;
}
/**
* 随机生成字符
* @param int $textsize
* @param int $disturbsize
* @return array
*/
private function randChars($textsize = 4, $disturbsize = 2)
{
/**
* 字符串截取,支持中文和其他编码
* @static
* @access public
* @param string $str 需要转换的字符串
* @param int $start 开始位置
* @param int $length 截取长度
* @param string $charset 编码格式
* @param bool $suffix 截断显示字符
* @return string
*/
function msubstr($str, $start = 0, $length = 1, $charset = 'utf-8', $suffix = true)
{
if (function_exists('mb_substr')) {
$slice = mb_substr($str, $start, $length, $charset);
} else if (function_exists('iconv_substr')) {
$slice = iconv_substr($str, $start, $length, $charset);
} else {
$re['utf-8'] = '/[\x01-\x7f]|[\xc2-\xdf][\x80-\xbf]|[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xff][\x80-\xbf]{3}/';
$re['gb2312'] = '/[\x01-\x7f]|[\xb0-\xf7][\xa0-\xfe]/';
$re['gbk'] = '/[\x01-\x7f]|[\x81-\xfe][\x40-\xfe]/';
$re['big5'] = '/[\x01-\x7f]|[\x81-\xfe]([\x40-\x7e]|\xa1-\xfe])/';
preg_match_all($re[$charset], $str, $match);
$slice = join('', array_slice($match[0], $start, $length));
}
return $suffix ? $slice . '...' : $slice;
}
$return = [];
$textsize = $textsize + $disturbsize;
//文字
if (in_array('text', $this->mode)) {
$text = [];
$chars = '们以我到他会作时要动国产的是工就年阶义发成部民可出能方进在了不和有大这主中人上为来分生对于学下级地个用同行面说种过命度革而多子后自社加小机也经力线本电高量长党得实家定深法表着水理化争现所起政三好十战无农使前等反体合斗路图把结第里正新开论之物从当两些还天资事队批点育重其思与间内去因件日利相由压员气业代全组数果期导平各基或月毛然如应形想制心样干都向变关问比展那它最及外没看治提五解系林者米群头意只明四道马认次文通但条较克又公孔领军流入接席位情运器并飞原油放立题质指建区验活众很教决特此常石强极土少已根共直团统式转别造切九你取西持总料连任志观调七么山程百报更见必真保热委手改管处己将修支识病象几先老光专什六型具示复安带每东增则完风回南广劳轮科北打积车计给节做务被整联步类集号列温装即毫知轴研单色坚据速防史拉世设达尔场织历花受求传口断况采精金界品判参层止边清至万确究书术状厂须离再目海交权且儿青才证低越际八试规斯近注办布门铁需走议县兵固除般引齿千胜细影济白格效置推空配刀叶率述今选养德话查差半敌始片施响收华觉备名红续均标记难存测士身紧液派准斤角降维板许破述技消底床田势端感往神便贺村构照容非搞亚磨族火段算适讲按值美态黄易彪服早班麦削信排台声该击素张密害侯草何树肥继右属市严径螺检左页抗苏显苦英快称坏移约巴材省黑武培著河帝仅针怎植京助升王眼她抓含苗副杂普谈围食射源例致酸旧却充足短划剂宣环落首尺波承粉践府鱼随考刻靠够满夫失包住促枝局菌杆周护岩师举曲春元超负砂封换太模贫减阳扬江析亩木言球朝医校古呢稻宋听唯输滑站另卫字鼓刚写刘微略范供阿块某功套友限项余倒卷创律雨让骨远帮初皮播优占圈伟季训控激找叫云互跟裂粮粒母练塞钢顶策双留误础吸阻故寸盾晚丝女散焊功株亲院冷彻弹错散商视艺灭版烈零室轻血倍缺厘泵察绝富城冲喷壤简否柱李望盘磁雄似困巩益洲脱投送奴侧润盖挥距触星松送获兴独官混纪依未突架宽冬章湿偏纹吃执阀矿寨责熟稳夺硬价努翻奇甲预职评读背协损棉侵灰虽矛厚罗泥辟告卵箱掌氧恩爱停曾溶营终纲孟钱待尽俄缩沙退陈讨奋械载胞幼哪剥迫旋征槽倒握担仍呀鲜吧卡粗介钻逐弱脚怕盐末阴丰雾冠丙街莱贝辐肠付吉渗瑞惊顿挤秒悬姆烂森糖圣凹陶词迟蚕亿矩康遵牧遭幅园腔订香肉弟屋敏恢忘编印蜂急拿扩伤飞露核缘游振操央伍域甚迅辉异序免纸夜乡久隶缸夹念兰映沟乙吗儒汽磷艰晶插埃燃欢铁补咱芽永瓦倾阵碳演威附牙芽永瓦斜灌欧献顺猪洋腐请透司危括脉宜笑若尾束壮暴企菜穗楚汉愈绿拖牛份染既秋遍锻玉夏疗尖殖井费州访吹荣铜沿替滚客召旱悟刺脑措贯藏敢令隙炉壳硫煤迎铸粘探临薄旬善福纵择礼愿伏残雷延烟句纯渐耕跑泽慢栽鲁赤繁境潮横掉锥希池败船假亮谓托伙哲怀割摆贡呈劲财仪沉炼麻罪祖息车穿货销齐鼠抽画饲龙库守筑房歌寒喜哥洗蚀废纳腹乎录镜妇恶脂庄擦险赞钟摇典柄辩竹谷卖乱虚桥奥伯赶垂途额壁网截野遗静谋弄挂课镇妄盛耐援扎虑键归符庆聚绕摩忙舞遇索顾胶羊湖钉仁音迹碎伸灯避泛亡答勇频皇柳哈揭甘诺概宪浓岛袭谁洪谢炮浇斑讯懂灵蛋闭孩释乳巨徒私银伊景坦累匀霉杜乐勒隔弯绩招绍胡呼痛峰零柴簧午跳居尚丁秦稍追梁折耗碱殊岗挖氏刃剧堆赫荷胸衡勤膜篇登驻案刊秧缓凸役剪川雪链渔啦脸户洛孢勃盟买杨宗焦赛旗滤硅炭股坐蒸凝竟陷黎救冒暗洞犯筒您宋弧爆谬涂味津臂障褐陆啊健尊豆拔莫抵桑坡缝警挑污冰柬嘴啥饭塑寄赵喊垫丹渡耳刨虎笔稀昆浪萨茶滴浅拥穴覆伦娘吨浸袖珠雌妈紫戏塔锤震岁貌洁剖牢锋疑霸闪埔猛诉刷狠忽灾闹乔唐漏闻沈熔氯荒男凡抢像浆旁玻亦忠唱蒙予纷捕锁尤乘乌智淡允叛畜俘摸锈扫毕璃宝芯爷鉴秘净蒋钙肩腾枯抛轨堂拌爸循诱祝励肯酒绳穷塘燥泡袋朗喂铝软渠颗惯贸粪综墙趋彼届墨碍启逆卸航衣孙龄岭休借';
for ($i = 0; $i < $textsize; $i++) {
$text[] = msubstr($chars, floor(mt_rand(0, mb_strlen($chars, 'utf-8') - 1)), 1, 'utf-8', false);
}
$return = array_merge($return, $text);
}
//图像
if (in_array('gallery', $this->mode)) {
$gallery = array_keys($this->gallery);
shuffle($gallery);
$gallery = array_slice($gallery, 0, $textsize);
$return = array_merge($return, $gallery);
}
//自定义
if (in_array('custom', $this->mode)) {
$return = array_merge($return, array_slice($this->custom, 0, $textsize));
}
shuffle($return);
$return = array_slice($return, 0, $textsize);
return $return;
}
/**
* 随机生成位置布局
* @return array
*/
private function randPosition($textArr, $text, $imgW, $imgH, $fontW, $fontH)
{
$return = array();
$x = rand(0, $imgW - $fontW);
$y = rand(0, $imgH - $fontH);
//碰撞验证
if (!$this->checkPosition($textArr, $text, $x, $y, $fontW, $fontH)) {
$return = $this->randPosition($textArr, $text, $imgW, $imgH, $fontW, $fontH);
} else {
$return = array($x, $y);
}
return $return;
}
/**
* 验证位置是否可用
* @return bool
*/
private function checkPosition($textArr, $text, $x, $y, $w, $h)
{
$flag = true;
foreach ($textArr as $v) {
if ($v['text'] == $text) {
continue;
}
if (isset($v['x']) && isset($v['y'])) {
//分别判断X和Y是否都有交集如果都有交集则判断为覆盖
$flagX = true;
if ($v['x'] > $x) {
if ($x + $w > $v['x']) {
$flagX = false;
}
} else if ($x > $v['x']) {
if ($v['x'] + $v['width'] > $x) {
$flagX = false;
}
} else {
$flagX = false;
}
$flagY = true;
if ($v['y'] > $y) {
if ($y + $h > $v['y']) {
$flagY = false;
}
} else if ($y > $v['y']) {
if ($v['y'] + $v['height'] > $y) {
$flagY = false;
}
} else {
$flagY = false;
}
if (!$flagX && !$flagY) {
$flag = false;
}
}
}
return $flag;
}
/**
* 获取图片某个定点上的主要色
* @return array
*/
private function getImageColor($img, $x, $y)
{
list($imageWidth, $imageHeight, $imageType) = getimagesize($img);
switch ($imageType) {
case 1://GIF
$im = imagecreatefromgif($img);
break;
case 2://JPG
$im = imagecreatefromjpeg($img);
break;
case 3://PNG
$im = imagecreatefrompng($img);
break;
}
$rgb = imagecolorat($im, $x, $y);
$r = ($rgb >> 16) & 0xFF;
$g = ($rgb >> 8) & 0xFF;
$b = $rgb & 0xFF;
return array($r, $g, $b);
}
}

1
addons/command/.addonrc Normal file
View File

@ -0,0 +1 @@
{"files":["application\\admin\\controller\\Command.php","application\\admin\\lang\\zh-cn\\command.php","application\\admin\\model\\Command.php","application\\admin\\validate\\Command.php","application\\admin\\view\\command\\add.html","application\\admin\\view\\command\\detail.html","application\\admin\\view\\command\\index.html","public\\assets\\js\\backend\\command.js"],"license":"regular","licenseto":"16018","licensekey":"ep0i1Tfu7H4WK3aJ AX6ezWiklD4QQKaPFOEDvQ==","domains":["test0rui.com"],"licensecodes":[],"validations":["d495f88a5abee2b752e64026c2ea8984"],"menus":["command","command\/index","command\/add","command\/detail","command\/command","command\/execute","command\/del","command\/multi"]}

View File

@ -0,0 +1,70 @@
<?php
namespace addons\command;
use app\common\library\Menu;
use think\Addons;
/**
* 在线命令插件
*/
class Command extends Addons
{
/**
* 插件安装方法
* @return bool
*/
public function install()
{
$menu = [
[
'name' => 'command',
'title' => '在线命令管理',
'icon' => 'fa fa-terminal',
'sublist' => [
['name' => 'command/index', 'title' => '查看'],
['name' => 'command/add', 'title' => '添加'],
['name' => 'command/detail', 'title' => '详情'],
['name' => 'command/command', 'title' => '生成并执行命令'],
['name' => 'command/execute', 'title' => '再次执行命令'],
['name' => 'command/del', 'title' => '删除'],
['name' => 'command/multi', 'title' => '批量更新'],
]
]
];
Menu::create($menu);
return true;
}
/**
* 插件卸载方法
* @return bool
*/
public function uninstall()
{
Menu::delete('command');
return true;
}
/**
* 插件启用方法
* @return bool
*/
public function enable()
{
Menu::enable('command');
return true;
}
/**
* 插件禁用方法
* @return bool
*/
public function disable()
{
Menu::disable('command');
return true;
}
}

View File

@ -0,0 +1,4 @@
<?php
return [
];

View File

@ -0,0 +1,15 @@
<?php
namespace addons\command\controller;
use think\addons\Controller;
class Index extends Controller
{
public function index()
{
$this->error("当前插件暂无前台页面");
}
}

10
addons/command/info.ini Normal file
View File

@ -0,0 +1,10 @@
name = command
title = 在线命令
intro = 可在线执行一键生成CRUD、一键生成菜单等相关命令
author = FastAdmin
website = https://www.fastadmin.net
version = 1.1.2
state = 1
url = /addons/command
license = regular
licenseto = 16018

View File

@ -0,0 +1,12 @@
CREATE TABLE IF NOT EXISTS `__PREFIX__command` (
`id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'ID',
`type` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '类型',
`params` varchar(1500) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '参数',
`command` varchar(1500) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '命令',
`content` text COMMENT '返回结果',
`executetime` bigint(16) UNSIGNED DEFAULT NULL COMMENT '执行时间',
`createtime` bigint(16) UNSIGNED DEFAULT NULL COMMENT '创建时间',
`updatetime` bigint(16) UNSIGNED DEFAULT NULL COMMENT '更新时间',
`status` enum('successed','failured') CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT 'failured' COMMENT '状态',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '在线命令表';

View File

@ -0,0 +1,28 @@
<?php
namespace addons\command\library;
/**
* Class Output
*/
class Output extends \think\console\Output
{
protected $message = [];
public function __construct($driver = 'console')
{
parent::__construct($driver);
}
protected function block($style, $message)
{
$this->message[] = $message;
}
public function getMessage()
{
return $this->message;
}
}

1
addons/cropper/.addonrc Normal file
View File

@ -0,0 +1 @@
{"files":["public\\assets\\addons\\cropper\\css\\cropper.css","public\\assets\\addons\\cropper\\css\\main.css","public\\assets\\addons\\cropper\\js\\cropper-license.txt","public\\assets\\addons\\cropper\\js\\cropper.js"],"license":"regular","licenseto":"16018","licensekey":"onVqfLZAUaBzuNyD +4nohe6yz57e2w0tyor\/zw==","domains":["test0rui.com"],"licensecodes":[],"validations":["d495f88a5abee2b752e64026c2ea8984"]}

View File

@ -0,0 +1,40 @@
<?php
namespace addons\cropper;
use think\Addons;
/**
* 图片裁剪插件
*/
class Cropper extends Addons
{
/**
* 插件安装方法
* @return bool
*/
public function install()
{
return true;
}
/**
* 插件卸载方法
* @return bool
*/
public function uninstall()
{
return true;
}
/**
* @param $params
*/
public function configInit(&$params)
{
$config = $this->getConfig();
$params['cropper'] = ['dialogWidth' => $config['dialogWidth'] ?? 880, 'dialogHeight' => $config['dialogHeight'] ?? 600];
}
}

86
addons/cropper/bootstrap.js vendored Normal file
View File

@ -0,0 +1,86 @@
require(['form', 'upload'], function (Form, Upload) {
var _bindevent = Form.events.bindevent;
Form.events.bindevent = function (form) {
_bindevent.apply(this, [form]);
if ($("#croppertpl").length == 0) {
var allowAttr = [
'aspectRatio', 'autoCropArea', 'cropBoxMovable', 'cropBoxResizable', 'minCropBoxWidth', 'minCropBoxHeight', 'minContainerWidth', 'minContainerHeight',
'minCanvasHeight', 'minCanvasWidth', 'croppedWidth', 'croppedHeight', 'croppedMinWidth', 'croppedMinHeight', 'croppedMaxWidth', 'croppedMaxHeight', 'fillColor',
'containerMinHeight', 'containerMaxHeight', 'customWidthHeight', 'customAspectRatio'
];
String.prototype.toLineCase = function () {
return this.replace(/[A-Z]/g, function (match) {
return "-" + match.toLowerCase();
});
};
var btnAttr = [];
$.each(allowAttr, function (i, j) {
btnAttr.push('data-' + j.toLineCase() + '="<%=data.' + j + '%>"');
});
var btn = '<button class="btn btn-success btn-cropper btn-xs" data-input-id="<%=data.inputId%>" ' + btnAttr.join(" ") + ' style="position:absolute;top:10px;right:15px;">裁剪</button>';
var insertBtn = function () {
return arguments[0].replace(arguments[2], btn + arguments[2]);
};
$("<script type='text/html' id='croppertpl'>" + Upload.config.previewtpl.replace(/<li(.*?)>(.*?)<\/li>/, insertBtn) + "</script>").appendTo("body");
}
$(".plupload[data-preview-id],.faupload[data-preview-id]").each(function () {
var preview_id = $(this).data("preview-id");
var previewObj = $("#" + preview_id);
var tpl = previewObj.length > 0 ? previewObj.data("template") : '';
if (!tpl) {
if (!$(this).hasClass("cropper")) {
$(this).addClass("cropper");
}
previewObj.data("template", "croppertpl");
}
});
//图片裁剪
$(document).off('click', '.btn-cropper').on('click', '.btn-cropper', function () {
var image = $(this).closest("li").find('.thumbnail').data('url');
var input = $("#" + $(this).data("input-id"));
var url = image;
var data = $(this).data();
var params = [];
$.each(allowAttr, function (i, j) {
if (typeof data[j] !== 'undefined' && data[j] !== '') {
params.push(j + '=' + data[j]);
}
});
try {
var parentWin = (parent ? parent : window);
parentWin.Fast.api.open('/addons/cropper/index/cropper?url=' + image + (params.length > 0 ? '&' + params.join('&') : ''), '裁剪', {
callback: function (data) {
if (typeof data !== 'undefined') {
var arr = data.dataURI.split(','), mime = arr[0].match(/:(.*?);/)[1],
bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
var urlArr = url.split('.');
var suffix = 'png';
url = urlArr.join('');
var filename = url.substr(url.lastIndexOf('/') + 1);
var exp = new RegExp("\\." + suffix + "$", "i");
filename = exp.test(filename) ? filename : filename + "." + suffix;
var file = new File([u8arr], filename, {type: mime});
Upload.api.send(file, function (data) {
input.val(input.val().replace(image, data.url)).trigger("change");
}, function (data) {
});
}
},
area: [Math.min(parentWin.$(parentWin.window).width(), Config.cropper.dialogWidth) + "px", Math.min(parentWin.$(parentWin.window).height(), Config.cropper.dialogHeight) + "px"],
});
} catch (e) {
console.error(e);
}
return false;
});
}
});

296
addons/cropper/config.php Normal file
View File

@ -0,0 +1,296 @@
<?php
return [
[
'name' => 'dialogWidth',
'title' => '弹窗宽度',
'type' => 'number',
'content' => [
],
'value' => '800',
'rule' => '',
'msg' => '',
'tip' => '单位为px可视窗口高度小于该值时自适应',
'ok' => '',
'extend' => '',
],
[
'name' => 'dialogHeight',
'title' => '弹窗高度',
'type' => 'number',
'content' => [
],
'value' => '600',
'rule' => '',
'msg' => '',
'tip' => '单位为px可视窗口宽度小于该值时自适应',
'ok' => '',
'extend' => '',
],
[
'name' => 'containerMinHeight',
'title' => '容器最小高度',
'type' => 'number',
'content' => [
],
'value' => '200',
'rule' => '',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
],
[
'name' => 'containerMaxHeight',
'title' => '容器最大高度',
'type' => 'number',
'content' => [
],
'value' => '800',
'rule' => '',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
],
[
'name' => 'customWidthHeight',
'title' => '自定义宽度和高度',
'type' => 'radio',
'content' => [
1 => '开',
0 => '关',
],
'value' => '1',
'rule' => '',
'msg' => '',
'tip' => '是否开启手动设置宽度和高度,需关闭固定剪裁比例',
'ok' => '',
'extend' => '',
],
[
'name' => 'customAspectRatio',
'title' => '自定义剪裁比例',
'type' => 'radio',
'content' => [
1 => '开',
0 => '关',
],
'value' => '1',
'rule' => '',
'msg' => '',
'tip' => '是否开启自定义剪裁比例选项,需关闭固定剪裁比例',
'ok' => '',
'extend' => '',
],
[
'name' => 'aspectRatio',
'title' => '固定剪裁比例',
'type' => 'number',
'content' => [],
'value' => '0',
'rule' => '',
'msg' => '',
'tip' => '宽:高=比例值16:9=1.777777 4:3=1.333333',
'ok' => '',
'extend' => '',
],
[
'name' => 'cropBoxMovable',
'title' => '是否可移动图像',
'type' => 'radio',
'content' => [
1 => '是',
0 => '否',
],
'value' => '1',
'rule' => '',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
],
[
'name' => 'cropBoxResizable',
'title' => '是否允许调整裁剪框的大小',
'type' => 'radio',
'content' => [
1 => '是',
0 => '否',
],
'value' => '1',
'rule' => '',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
],
[
'name' => 'minCropBoxWidth',
'title' => '剪切框宽度最小值',
'type' => 'number',
'content' => [],
'value' => '0',
'rule' => '',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
],
[
'name' => 'minCropBoxHeight',
'title' => '剪切框高度最小值',
'type' => 'number',
'content' => [],
'value' => '0',
'rule' => '',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
],
[
'name' => 'minContainerWidth',
'title' => '容器宽度最小值',
'type' => 'number',
'content' => [],
'value' => '200',
'rule' => '',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
],
[
'name' => 'minContainerHeight',
'title' => '容器高度最小值',
'type' => 'number',
'content' => [],
'value' => '100',
'rule' => '',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
],
[
'name' => 'minCanvasWidth',
'title' => '画布宽度最小值',
'type' => 'number',
'content' => [],
'value' => '0',
'rule' => '',
'msg' => '',
'tip' => '单位为px',
'ok' => '',
'extend' => '',
],
[
'name' => 'minCanvasHeight',
'title' => '画布高度最小值',
'type' => 'number',
'content' => [],
'value' => '0',
'rule' => '',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
],
[
'name' => 'autoCropArea',
'title' => '自动剪裁区域的大小比例',
'type' => 'number',
'content' => [],
'value' => '0.8',
'rule' => '',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
],
[
'name' => 'croppedWidth',
'title' => '默认宽度',
'type' => 'number',
'content' => [],
'value' => '0',
'rule' => '',
'msg' => '',
'tip' => '单位为px',
'ok' => '',
'extend' => '',
],
[
'name' => 'croppedHeight',
'title' => '默认高度',
'type' => 'number',
'content' => [],
'value' => '0',
'rule' => '',
'msg' => '',
'tip' => '单位为px',
'ok' => '',
'extend' => '',
],
[
'name' => 'croppedMinWidth',
'title' => '默认输出最小宽度',
'type' => 'number',
'content' => [],
'value' => '0',
'rule' => '',
'msg' => '',
'tip' => '单位为px',
'ok' => '',
'extend' => '',
],
[
'name' => 'croppedMinHeight',
'title' => '默认输出最小高度',
'type' => 'number',
'content' => [],
'value' => '0',
'rule' => '',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
],
[
'name' => 'croppedMaxWidth',
'title' => '默认输出最大宽度',
'type' => 'number',
'content' => [],
'value' => '',
'rule' => '',
'msg' => '',
'tip' => '默认为无限制单位为px',
'ok' => '',
'extend' => '',
],
[
'name' => 'croppedMaxHeight',
'title' => '默认输出最大高度',
'type' => 'number',
'content' => [],
'value' => '',
'rule' => '',
'msg' => '',
'tip' => '默认为无限制单位为px',
'ok' => '',
'extend' => '',
],
[
'name' => 'fillColor',
'title' => '填充颜色',
'type' => 'string',
'content' => [],
'value' => '',
'rule' => '',
'msg' => '',
'tip' => '默认为透明',
'ok' => '',
'extend' => '',
],
];

View File

@ -0,0 +1,95 @@
<?php
namespace addons\cropper\controller;
use think\addons\Controller;
use think\Config;
use think\Hook;
/**
* 图片裁剪
*
*/
class Index extends Controller
{
protected $model = null;
public function _initialize()
{
// 语言检测
$lang = $this->request->langset();
$lang = preg_match("/^([a-zA-Z0-9\-_]{2,10})\$/i", $lang) ? $lang : 'zh-cn';
$site = Config::get("site");
$upload = \app\common\model\Config::upload();
// 上传信息配置后
Hook::listen("upload_config_init", $upload);
// 配置信息
$config = [
'site' => array_intersect_key($site, array_flip(['name', 'cdnurl', 'version', 'timezone', 'languages'])),
'upload' => $upload,
'modulename' => 'addons',
'controllername' => 'index',
'actionname' => $this->request->action(),
'jsname' => 'cropper',
'moduleurl' => rtrim(url("/index", '', false), '/'),
'language' => $lang
];
$config = array_merge($config, Config::get("view_replace_str"));
Config::set('upload', array_merge(Config::get('upload'), $upload));
// 配置信息后
Hook::listen("config_init", $config);
$this->view->assign('jsconfig', $config);
$this->view->assign('site', $site);
parent::_initialize();
}
public function index()
{
return $this->view->fetch();
}
/**
* 图片剪裁
*/
public function cropper()
{
$config = get_addon_config('cropper');
$get = $this->request->get();
$allowAttr = [
'aspectRatio',
'autoCropArea',
'cropBoxMovable',
'cropBoxResizable',
'minCropBoxWidth',
'minCropBoxHeight',
'minContainerWidth',
'minContainerHeight',
'minCanvasHeight',
'minCanvasWidth',
'croppedWidth',
'croppedHeight',
'croppedMinWidth',
'croppedMinHeight',
'croppedMaxWidth',
'croppedMaxHeight',
'containerMinHeight',
'containerMaxHeight',
'customWidthHeight',
'customAspectRatio'
];
$attr = array_intersect_key($get, array_flip($allowAttr));
foreach ($attr as $index => &$item) {
$item = floatval($item);
}
$config = array_merge($config, $attr, ['url' => $get['url'] ?? '', 'fillColor' => $get['fillColor'] ?? '']);
$config['fillColor'] = $config['fillColor'] && $config['fillColor'] !== 'transparent' ? '#' . ltrim($config['fillColor'], '#') : 'transparent';
$this->view->assign("cropper", $config);
return $this->view->fetch();
}
}

10
addons/cropper/info.ini Normal file
View File

@ -0,0 +1,10 @@
name = cropper
title = 图片裁剪插件
intro = 基于Cropper.js实现的图片裁剪
author = FastAdmin
website = https://www.fastadmin.net
version = 1.1.2
state = 1
url = /addons/cropper
license = regular
licenseto = 16018

View File

@ -0,0 +1,630 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>图片剪裁</title>
<link href="__CDN__/assets/css/frontend{$Think.config.app_debug?'':'.min'}.css?v={$Think.config.site.version}" rel="stylesheet">
<!--[if lt IE 9]>
<script src="__CDN__/assets/js/html5shiv.js"></script>
<script src="__CDN__/assets/js/respond.min.js"></script>
<![endif]-->
<link rel="stylesheet" href="__ADDON__/css/cropper.css">
<link rel="stylesheet" href="__ADDON__/css/main.css">
<!--@formatter:off-->
<style>
.img-container {
min-height: {$cropper.containerMinHeight|default=200}px;
max-height: {$cropper.containerMaxHeight|default=400}px;
}
</style>
<style data-render="darktheme">
body.darktheme {
background-color: #262626;
}
</style>
<!--@formatter:on-->
</head>
<body style="padding:15px;">
<script>if (parent.document.body.classList.contains("darktheme")) {document.body.classList.add("darktheme");}</script>
<!--@formatter:off-->
<script type="text/javascript">
var require = {
config: {$jsconfig|json_encode}
};
</script>
<!--@formatter:on-->
<!-- Content -->
<div class="">
<div class="row">
<div class="col-md-9 col-sm-9 col-xs-9">
<!-- <h3>Demo:</h3> -->
<div class="img-container">
<img id="image" src="{$cropper.url|cdnurl|htmlentities}"/>
</div>
</div>
<div class="col-md-3 col-sm-3 col-xs-3" style="padding-left:0;">
<!-- <h3>Preview:</h3> -->
<div class="docs-preview clearfix">
<div class="img-preview preview-lg"></div>
<div class="img-preview preview-md"></div>
<div class="img-preview preview-sm"></div>
<div class="img-preview preview-xs"></div>
</div>
<!-- <h3>Data:</h3> -->
<div class="docs-data">
<div class="input-group">
<span class="input-group-addon">
<label class="input-group-text" for="dataX">X</label>
</span>
<input type="text" class="form-control" id="dataX" placeholder="x" readonly>
<span class="input-group-addon">
<span class="input-group-text">px</span>
</span>
</div>
<div class="input-group">
<span class="input-group-addon">
<label class="input-group-text" for="dataY">Y</label>
</span>
<input type="text" class="form-control" id="dataY" placeholder="y" readonly>
<span class="input-group-addon">
<span class="input-group-text">px</span>
</span>
</div>
{if $cropper.customWidthHeight && !$cropper.aspectRatio}
<div class="input-group">
<span class="input-group-addon">
<label class="input-group-text" for="dataWidth">宽度</label>
</span>
<input type="text" class="form-control" id="dataWidth" placeholder="width">
<span class="input-group-addon">
<span class="input-group-text">px</span>
</span>
</div>
<div class="input-group">
<span class="input-group-addon">
<label class="input-group-text" for="dataHeight">高度</label>
</span>
<input type="text" class="form-control" id="dataHeight" placeholder="height">
<span class="input-group-addon">
<span class="input-group-text">px</span>
</span>
</div>
{/if}
<div class="input-group">
<span class="input-group-addon">
<label class="input-group-text" for="dataRotate">旋转</label>
</span>
<input type="text" class="form-control" id="dataRotate" placeholder="rotate" readonly>
<span class="input-group-addon">
<span class="input-group-text">deg</span>
</span>
</div>
<div class="input-group hidden">
<span class="input-group-addon">
<label class="input-group-text" for="dataScaleX">水平方向翻转</label>
</span>
<input type="text" class="form-control" id="dataScaleX" placeholder="scaleX">
</div>
<div class="input-group hidden">
<span class="input-group-addon">
<label class="input-group-text" for="dataScaleY">垂直方向翻转</label>
</span>
<input type="text" class="form-control" id="dataScaleY" placeholder="scaleY">
</div>
</div>
<div class="docs-toggles">
{if $cropper.customAspectRatio && !$cropper.aspectRatio}
<div class="btn-group d-flex flex-nowrap" data-toggle="buttons" style="margin-top:0px;">
<label class="btn btn-primary active">
<input type="radio" class="sr-only" id="aspectRatio0" name="aspectRatio" value="1.7777777777777777">
<span class="docs-tooltip" data-toggle="tooltip" data-animation="false" title="比例: 16 / 9">
16:9
</span>
</label>
<label class="btn btn-primary">
<input type="radio" class="sr-only" id="aspectRatio1" name="aspectRatio" value="1.3333333333333333">
<span class="docs-tooltip" data-toggle="tooltip" data-animation="false" title="比例: 4 / 3">
4:3
</span>
</label>
<label class="btn btn-primary">
<input type="radio" class="sr-only" id="aspectRatio2" name="aspectRatio" value="1">
<span class="docs-tooltip" data-toggle="tooltip" data-animation="false" title="比例: 1 / 1">
1:1
</span>
</label>
<label class="btn btn-primary">
<input type="radio" class="sr-only" id="aspectRatio3" name="aspectRatio" value="0.6666666666666666">
<span class="docs-tooltip" data-toggle="tooltip" data-animation="false" title="比例: 2 / 3">
2:3
</span>
</label>
<label class="btn btn-primary">
<input type="radio" class="sr-only" id="aspectRatio4" name="aspectRatio" value="NaN">
<span class="docs-tooltip" data-toggle="tooltip" data-animation="false" title="比例: NaN">
Free
</span>
</label>
</div>
{/if}
<div class="btn-group d-flex flex-nowrap" data-toggle="buttons">
<label class="btn btn-primary active">
<input type="radio" class="sr-only" id="viewMode0" name="viewMode" value="0" checked="">
<span class="docs-tooltip" data-toggle="tooltip" title="" data-original-title="显示模示 0">
VM0
</span>
</label>
<label class="btn btn-primary">
<input type="radio" class="sr-only" id="viewMode1" name="viewMode" value="1">
<span class="docs-tooltip" data-toggle="tooltip" title="" data-original-title="显示模示 1">
VM1
</span>
</label>
<label class="btn btn-primary">
<input type="radio" class="sr-only" id="viewMode2" name="viewMode" value="2">
<span class="docs-tooltip" data-toggle="tooltip" title="" data-original-title="显示模示 2">
VM2
</span>
</label>
<label class="btn btn-primary">
<input type="radio" class="sr-only" id="viewMode3" name="viewMode" value="3">
<span class="docs-tooltip" data-toggle="tooltip" title="" data-original-title="显示模示 3">
VM3
</span>
</label>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-9 col-sm-9 col-xs-9 docs-buttons">
<!-- <h3>Toolbar:</h3> -->
<div class="btn-group">
<button type="button" class="btn btn-primary" data-method="setDragMode" data-option="move" title="移动">
<span class="docs-tooltip" data-toggle="tooltip" data-animation="false" title="移动">
<span class="fa fa-arrows"></span>
</span>
</button>
<button type="button" class="btn btn-primary" data-method="setDragMode" data-option="crop" title="剪裁">
<span class="docs-tooltip" data-toggle="tooltip" data-animation="false" title="剪裁">
<span class="fa fa-crop"></span>
</span>
</button>
</div>
<div class="btn-group">
<button type="button" class="btn btn-primary" data-method="zoom" data-option="0.1" title="缩小">
<span class="docs-tooltip" data-toggle="tooltip" data-animation="false" title="缩小">
<span class="fa fa-search-plus"></span>
</span>
</button>
<button type="button" class="btn btn-primary" data-method="zoom" data-option="-0.1" title="放大">
<span class="docs-tooltip" data-toggle="tooltip" data-animation="false" title="放大">
<span class="fa fa-search-minus"></span>
</span>
</button>
</div>
<div class="btn-group">
<button type="button" class="btn btn-primary" data-method="move" data-option="-10" data-second-option="0" title="左移">
<span class="docs-tooltip" data-toggle="tooltip" data-animation="false" title="左移">
<span class="fa fa-arrow-left"></span>
</span>
</button>
<button type="button" class="btn btn-primary" data-method="move" data-option="10" data-second-option="0" title="右移">
<span class="docs-tooltip" data-toggle="tooltip" data-animation="false" title="右移">
<span class="fa fa-arrow-right"></span>
</span>
</button>
<button type="button" class="btn btn-primary" data-method="move" data-option="0" data-second-option="-10" title="上移">
<span class="docs-tooltip" data-toggle="tooltip" data-animation="false" title="上移">
<span class="fa fa-arrow-up"></span>
</span>
</button>
<button type="button" class="btn btn-primary" data-method="move" data-option="0" data-second-option="10" title="下移">
<span class="docs-tooltip" data-toggle="tooltip" data-animation="false" title="下移">
<span class="fa fa-arrow-down"></span>
</span>
</button>
</div>
<div class="btn-group">
<button type="button" class="btn btn-primary" data-method="rotate" data-option="-90" title="向左翻转">
<span class="docs-tooltip" data-toggle="tooltip" data-animation="false" title="向左翻转">
<span class="fa fa-rotate-left"></span>
</span>
</button>
<button type="button" class="btn btn-primary" data-method="rotate" data-option="90" title="向右翻转">
<span class="docs-tooltip" data-toggle="tooltip" data-animation="false" title="向右翻转">
<span class="fa fa-rotate-right"></span>
</span>
</button>
</div>
<div class="btn-group">
<button type="button" class="btn btn-primary" data-method="scaleX" data-option="-1" title="水平翻转">
<span class="docs-tooltip" data-toggle="tooltip" data-animation="false" title="水平翻转">
<span class="fa fa-arrows-h"></span>
</span>
</button>
<button type="button" class="btn btn-primary" data-method="scaleY" data-option="-1" title="垂直翻转">
<span class="docs-tooltip" data-toggle="tooltip" data-animation="false" title="垂直翻转">
<span class="fa fa-arrows-v"></span>
</span>
</button>
</div>
<div class="btn-group hidden">
<button type="button" class="btn btn-primary" data-method="disable" title="禁用">
<span class="docs-tooltip" data-toggle="tooltip" data-animation="false" title="禁用">
<span class="fa fa-lock"></span>
</span>
</button>
<button type="button" class="btn btn-primary" data-method="enable" title="启用">
<span class="docs-tooltip" data-toggle="tooltip" data-animation="false" title="启用">
<span class="fa fa-unlock"></span>
</span>
</button>
</div>
<div class="btn-group">
<button type="button" class="btn btn-primary" data-method="reset" title="重置">
<span class="docs-tooltip" data-toggle="tooltip" data-animation="false" title="重置">
<span class="fa fa-refresh"></span>
</span>
</button>
<label class="btn btn-primary btn-upload" for="inputImage" title="上传图片">
<input type="file" class="sr-only" id="inputImage" name="file" accept=".jpg,.jpeg,.png,.gif,.bmp,.tiff">
<span class="docs-tooltip" data-toggle="tooltip" data-animation="false" title="重新上传图片">
<span class="fa fa-upload"></span>
</span>
</label>
</div>
<div class="btn-group btn-group-crop">
<button type="button" class="btn btn-primary" data-method="getCroppedCanvas">
<span class="docs-tooltip" data-toggle="tooltip" data-animation="false" title="预览&下载">
预览&下载
</span>
</button>
</div>
<!-- Show the cropped image in modal -->
<div class="modal fade docs-cropped" id="getCroppedCanvasModal" aria-hidden="true" aria-labelledby="getCroppedCanvasTitle" role="dialog" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="getCroppedCanvasTitle">已剪裁</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body"></div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">关闭</button>
<a class="btn btn-primary" id="download" href="javascript:void(0);" download="cropped.jpg">下载图片</a>
</div>
</div>
</div>
</div><!-- /.modal -->
</div><!-- /.docs-buttons -->
<div class="col-md-3 col-sm-3 col-xs-3" style="padding-left:0;">
<div style="margin:0;">
<div class="d-flex">
<button type="button" class="btn btn-success btn-submit btn-embossed mr-1">确定</button>
<button type="button" class="btn btn-default btn-cancel btn-embossed ml-1">取消</button>
</div>
</div>
</div><!-- /.docs-toggles -->
</div>
</div>
<script>
require.callback = function () {
define('cropper', ['jquery', 'bootstrap', 'frontend', 'template', '../addons/cropper/js/cropper'], function ($, undefined, Frontend, Template, undefined, Cropper) {
var Controller = {
cropper: function () {
$("[data-toggle='tooltip']").data("container", "body");
var URL = window.URL || window.webkitURL;
var $image = $('#image');
window.$image = $image;
var console = window.console || {
log: function () {
}
};
var $download = $('#download');
var $dataX = $('#dataX');
var $dataY = $('#dataY');
var $dataHeight = $('#dataHeight');
var $dataWidth = $('#dataWidth');
var $dataRotate = $('#dataRotate');
var $dataScaleX = $('#dataScaleX');
var $dataScaleY = $('#dataScaleY');
var options = {
aspectRatio: parseFloat("{$cropper.aspectRatio|default='NaN'}"),
preview: '.img-preview',
autoCropArea: parseFloat("{$cropper.autoCropArea|default='.8'}"),
cropBoxMovable: !!parseInt("{$cropper.cropBoxMovable|default='1'}"),
cropBoxResizable: !!parseInt("{$cropper.cropBoxResizable|default='1'}"),
minCropBoxWidth: parseInt("{$cropper.minCropBoxWidth|default='0'}"),
minCropBoxHeight: parseInt("{$cropper.minCropBoxHeight|default='0'}"),
minContainerWidth: parseInt("{$cropper.minContainerWidth|default='0'}"),
minContainerHeight: parseInt("{$cropper.minContainerHeight|default='0'}"),
minCanvasWidth: parseInt("{$cropper.minCanvasWidth|default='0'}"),
minCanvasHeight: parseInt("{$cropper.minCanvasHeight|default='0'}"),
crop: function (e) {
$dataX.val(Math.round(e.detail.x));
$dataY.val(Math.round(e.detail.y));
$dataHeight.val(Math.round(e.detail.height));
$dataWidth.val(Math.round(e.detail.width));
$dataRotate.val(e.detail.rotate);
$dataScaleX.val(e.detail.scaleX);
$dataScaleY.val(e.detail.scaleY);
}
};
var croppedOptions = {
minWidth: parseInt("{$cropper.croppedMinWidth|default='0'}"),
minHeight: parseInt("{$cropper.croppedMinHeight|default='0'}"),
maxWidth: parseInt("{$cropper.croppedMaxWidth|default='2048'}"),
maxHeight: parseInt("{$cropper.croppedMaxHeight|default='2048'}"),
fillColor: "{:$cropper.fillColor ? '#' . $cropper.fillColor : 'transparent'}",
};
if (parseInt("{$cropper.croppedMinWidth|default='0'}") > 0) {
croppedOptions.width = parseInt("{$cropper.croppedWidth}");
}
if (parseInt("{$cropper.croppedMinHeight|default='0'}") > 0) {
croppedOptions.height = parseInt("{$cropper.croppedHeight}");
}
var originalImageURL = $image.attr('src');
var uploadedImageName = 'cropped.jpg';
var uploadedImageType = 'image/jpeg';
var uploadedImageURL;
// 实例化
$image.cropper(options);
//确认事件
$(document).on("click", ".btn-submit", function () {
var data = $image.cropper('getData');
var dataURI = $image.cropper('getCroppedCanvas', croppedOptions).toDataURL('image/png');
data.dataURI = dataURI;
Fast.api.close(data);
});
//取消事件
$(document).on("click", ".btn-cancel", function () {
Fast.api.close();
});
// Buttons
if (!$.isFunction(document.createElement('canvas').getContext)) {
$('button[data-method="getCroppedCanvas"]').prop('disabled', true);
}
if (typeof document.createElement('cropper').style.transition === 'undefined') {
$('button[data-method="rotate"]').prop('disabled', true);
$('button[data-method="scale"]').prop('disabled', true);
}
// $dataWidth,$dataHeight点击事件
$('#dataWidth,#dataHeight').change(function () {
const cropBoxData = $image.cropper('getCropBoxData');
const imageData = $image.cropper('getImageData');
const newHeight = imageData.height / imageData.naturalHeight * parseFloat($dataHeight.val());
const newWidth = imageData.width / imageData.naturalWidth * parseFloat($dataWidth.val());
const newCropBoxData = {
left: cropBoxData.left,
top: cropBoxData.top,
height: newHeight,
width: newWidth,
};
$image.cropper('setCropBoxData', newCropBoxData);
});
// Download
if (typeof $download[0].download === 'undefined') {
$download.addClass('disabled');
}
// Options
$('.docs-toggles').on('change', 'input', function () {
var $this = $(this);
var name = $this.attr('name');
var type = $this.prop('type');
var cropBoxData;
var canvasData;
if (!$image.data('cropper')) {
return;
}
if (type === 'checkbox') {
options[name] = $this.prop('checked');
cropBoxData = $image.cropper('getCropBoxData');
canvasData = $image.cropper('getCanvasData');
options.ready = function () {
$image.cropper('setCropBoxData', cropBoxData);
$image.cropper('setCanvasData', canvasData);
};
} else if (type === 'radio') {
options[name] = $this.val();
}
$image.cropper('destroy').cropper(options);
});
// Methods
$('.docs-buttons').on('click', '[data-method]', function () {
var $this = $(this);
var data = $this.data();
var cropper = $image.data('cropper');
var cropped;
var $target;
var result;
if ($this.prop('disabled') || $this.hasClass('disabled')) {
return;
}
if (cropper && data.method) {
data = $.extend({}, data); // Clone a new one
if (typeof data.target !== 'undefined') {
$target = $(data.target);
if (typeof data.option === 'undefined') {
try {
data.option = JSON.parse($target.val());
} catch (e) {
console.log(e.message);
}
}
}
cropped = cropper.cropped;
switch (data.method) {
case 'rotate':
if (cropped && options.viewMode > 0) {
$image.cropper('clear');
}
break;
case 'getCroppedCanvas':
if (uploadedImageType === 'image/jpeg') {
if (!data.option) {
data.option = {};
}
$.extend(data.option, croppedOptions);
data.option.fillColor = '#fff';
}
break;
}
result = $image.cropper(data.method, data.option);
switch (data.method) {
case 'rotate':
if (cropped && options.viewMode > 0) {
$image.cropper('crop');
}
break;
case 'scaleX':
case 'scaleY':
$(this).data('option', -data.option);
break;
case 'getCroppedCanvas':
if (result) {
// Bootstrap's Modal
$('#getCroppedCanvasModal').modal().find('.modal-body').html(result);
if (!$download.hasClass('disabled')) {
download.download = uploadedImageName;
$download.attr('href', result.toDataURL(uploadedImageType));
}
}
break;
case 'destroy':
if (uploadedImageURL) {
URL.revokeObjectURL(uploadedImageURL);
uploadedImageURL = '';
$image.attr('src', originalImageURL);
}
break;
}
if ($.isPlainObject(result) && $target) {
try {
$target.val(JSON.stringify(result));
} catch (e) {
console.log(e.message);
}
}
}
});
// 键盘支持
$(document.body).on('keydown', function (e) {
if (e.target !== this || !$image.data('cropper') || this.scrollTop > 300) {
return;
}
switch (e.which) {
case 37:
e.preventDefault();
$image.cropper('move', -1, 0);
break;
case 38:
e.preventDefault();
$image.cropper('move', 0, -1);
break;
case 39:
e.preventDefault();
$image.cropper('move', 1, 0);
break;
case 40:
e.preventDefault();
$image.cropper('move', 0, 1);
break;
}
});
// 上传图片
var $inputImage = $('#inputImage');
if (URL) {
$inputImage.change(function () {
var files = this.files;
var file;
if (!$image.data('cropper')) {
return;
}
if (files && files.length) {
file = files[0];
if (/^image\/\w+$/.test(file.type)) {
uploadedImageName = file.name;
uploadedImageType = file.type;
if (uploadedImageURL) {
URL.revokeObjectURL(uploadedImageURL);
}
uploadedImageURL = URL.createObjectURL(file);
$image.cropper('destroy').attr('src', uploadedImageURL).cropper(options);
$inputImage.val('');
} else {
window.alert('请选择一张图片');
}
}
});
} else {
$inputImage.prop('disabled', true).parent().addClass('disabled');
}
}
};
return Controller;
});
};
</script>
<script src="__CDN__/assets/js/require{$Think.config.app_debug?'':'.min'}.js" data-main="__CDN__/assets/js/require-frontend{$Think.config.app_debug?'':'.min'}.js?v={$site.version}"></script>
</body>
</html>

View File

@ -0,0 +1,169 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>图片剪裁示例</title>
<link href="__CDN__/assets/css/frontend{$Think.config.app_debug?'':'.min'}.css?v={$Think.config.site.version}" rel="stylesheet">
<!-- HTML5 shim, for IE6-8 support of HTML5 elements. All other JS at the end of file. -->
<!--[if lt IE 9]>
<script src="__CDN__/assets/js/html5shiv.js"></script>
<script src="__CDN__/assets/js/respond.min.js"></script>
<![endif]-->
<link rel="stylesheet" href="__ADDON__/css/cropper.css">
<link rel="stylesheet" href="__ADDON__/css/main.css">
</head>
<body>
<!-- Content -->
<div class="container">
<div class="clearfix">
<div class="row">
<div class="col-lg-12">
<div class="page-header">
<h2 id="options">剪裁参数</h2>
</div>
<div class="alert alert-warning-light">
<textarea class="form-control" style="height:220px;">
<div class="input-group">
<input id="c-image" class="form-control" size="50" name="row[image]" type="text" value="">
<div class="input-group-addon no-border no-padding">
<span><button type="button" id="faupload-image" class="btn btn-danger faupload" data-aspect-ratio="0.75" data-auto-crop-area="0.5" data-cropped-width="300" data-cropped-height="300" data-input-id="c-image" data-mimetype="image/gif,image/jpeg,image/png,image/jpg,image/bmp,image/webp" data-multiple="false" data-preview-id="p-image"><i class="fa fa-upload"></i> 上传</button></span>
</div>
<span class="msg-box n-right" for="c-image"></span>
</div>
<ul class="row list-inline faupload-preview" id="p-image"></ul>
</textarea>
</div>
<table class="table table-condensed table-hover">
<thead>
<tr>
<th>参数</th>
<th>示例</th>
<th>说明</th>
<th>默认</th>
</tr>
</thead>
<tbody>
<tr class="text-danger">
<td>aspectRatio</td>
<td>data-aspect-ratio="0.8"</td>
<td>比例</td>
<td>0.8</td>
</tr>
<tr>
<td>autoCropArea</td>
<td>data-auto-crop-area="0.8"</td>
<td>默认自动剪裁的区域大小</td>
<td>0.8</td>
</tr>
<tr>
<td>cropBoxMovable</td>
<td>data-crop-box-movable="1"</td>
<td>剪裁框是否可移动</td>
<td>1</td>
</tr>
<tr>
<td>cropBoxResizable</td>
<td>data-crop-box-resizable="1"</td>
<td>剪裁框是否可变大小</td>
<td>1</td>
</tr>
<tr>
<td>minCropBoxWidth</td>
<td>data-min-crop-box-width="0"</td>
<td>最小剪裁框宽度</td>
<td>0</td>
</tr>
<tr>
<td>minCropBoxHeight</td>
<td>data-min-crop-box-height="0"</td>
<td>最小剪裁框高度</td>
<td>0</td>
</tr>
<tr>
<td>minContainerWidth</td>
<td>data-min-container-width="0"</td>
<td>最小窗口宽度</td>
<td>0</td>
</tr>
<tr>
<td>minContainerHeight</td>
<td>data-min-container-height="0"</td>
<td>最小窗口高度</td>
<td>0</td>
</tr>
<tr>
<td>minCanvasHeight</td>
<td>data-min-canvas-height="0"</td>
<td>最小画布宽度</td>
<td>0</td>
</tr>
<tr>
<td>minCanvasWidth</td>
<td>data-min-canvas-width="0"</td>
<td>最小画布高度</td>
<td>0</td>
</tr>
<tr class="text-danger">
<td>croppedWidth</td>
<td>data-cropped-width="300"</td>
<td>剪裁输出宽度</td>
<td>实际宽度</td>
</tr>
<tr class="text-danger">
<td>croppedHeight</td>
<td>data-cropped-height="300"</td>
<td>剪裁输出宽度</td>
<td>实际高度</td>
</tr>
<tr>
<td>croppedMinWidth</td>
<td>data-cropped-min-width="400"</td>
<td>最小画布高度</td>
<td>0</td>
</tr>
<tr>
<td>croppedMinHeight</td>
<td>data-cropped-min-height="400"</td>
<td>最小画布高度</td>
<td>0</td>
</tr>
<tr>
<td>croppedMaxWidth</td>
<td>data-cropped-max-width="500"</td>
<td>最大画布高度</td>
<td>0</td>
</tr>
<tr>
<td>croppedMaxHeight</td>
<td>data-cropped-max-height="300"</td>
<td>最大画布高度</td>
<td>0</td>
</tr>
<tr>
<td>fillColor</td>
<td>data-fill-color="ffffff"</td>
<td>背景填充色,默认为透明</td>
<td>transparent</td>
</tr>
</tbody>
</table>
<div class="page-header">
<h2 id="thanks">特别感谢</h2>
</div>
<div class="alert alert-danger-light">
Cropper.js<a href="https://github.com/fengyuanchen/cropper" target="_blank">https://github.com/fengyuanchen/cropper</a><br>
QQ小伙伴CARPE DIEM
</div>
</div>
</div>
</div>
</div>
</body>
</html>

1
addons/csmtable/.addonrc Normal file
View File

@ -0,0 +1 @@
{"files":["application\\admin\\controller\\csmtable\\Cligenerateexcel.php","application\\admin\\controller\\csmtable\\Csmgenerate.php","application\\admin\\controller\\csmtable\\Csmxlstable.php","application\\admin\\controller\\csmtable\\Datasource.php","application\\admin\\controller\\csmtable\\Test.php","application\\admin\\controller\\csmtable\\Xlstask.php","application\\admin\\lang\\zh-cn\\csmtable\\test.php","application\\admin\\lang\\zh-cn\\csmtable\\xlstask.php","application\\admin\\model\\csmtable\\Test.php","application\\admin\\model\\csmtable\\Xlstask.php","application\\admin\\validate\\csmtable\\Xlstask.php","application\\admin\\view\\csmtable\\test\\add.html","application\\admin\\view\\csmtable\\test\\edit.html","application\\admin\\view\\csmtable\\test\\index.html","application\\admin\\view\\csmtable\\test\\recyclebin.html","application\\admin\\view\\csmtable\\xcore\\layout\\readme.txt","application\\admin\\view\\csmtable\\xcore\\page_iframe.html","application\\admin\\view\\csmtable\\xlstask\\add.html","application\\admin\\view\\csmtable\\xlstask\\edit.html","application\\admin\\view\\csmtable\\xlstask\\index.html","application\\api\\lang\\zh-cn\\csmtable\\xc_clogin_api.php","public\\assets\\js\\backend\\csmtable\\test.js","public\\assets\\js\\backend\\csmtable\\xlstask.js","public\\assets\\addons\\csmtable\\css\\bootstrap-table-fixed-columns.css","public\\assets\\addons\\csmtable\\css\\bootstrap-table-reorder-rows.css","public\\assets\\addons\\csmtable\\css\\bootstrap-table-sticky-header.css","public\\assets\\addons\\csmtable\\css\\bootstrap-table-tree-column.css","public\\assets\\addons\\csmtable\\css\\csmtable.css","public\\assets\\addons\\csmtable\\css\\jquery.treegrid.min.css","public\\assets\\addons\\csmtable\\css\\select2-bootstrap.css","public\\assets\\addons\\csmtable\\css\\select2.css","public\\assets\\addons\\csmtable\\img\\collapse.png","public\\assets\\addons\\csmtable\\img\\expand.png","public\\assets\\addons\\csmtable\\js\\bootstrap-table-fixed-columns.js","public\\assets\\addons\\csmtable\\js\\bootstrap-table-reorder-rows.js","public\\assets\\addons\\csmtable\\js\\bootstrap-table-sticky-header.js","public\\assets\\addons\\csmtable\\js\\bootstrap-table-tree-column.js","public\\assets\\addons\\csmtable\\js\\bootstrap-table-treegrid.js","public\\assets\\addons\\csmtable\\js\\csmtable.js","public\\assets\\addons\\csmtable\\js\\jquery.tablednd.min.js","public\\assets\\addons\\csmtable\\js\\jquery.treegrid.min.js","public\\assets\\addons\\csmtable\\js\\select2.js","public\\assets\\addons\\csmtable\\library\\xcore\\css\\clogin.css","public\\assets\\addons\\csmtable\\library\\xcore\\css\\csminputstyle.css","public\\assets\\addons\\csmtable\\library\\xcore\\css\\xcore.css","public\\assets\\addons\\csmtable\\library\\xcore\\js\\clogin.js","public\\assets\\addons\\csmtable\\library\\xcore\\js\\csminputstyle.js","public\\assets\\addons\\csmtable\\library\\xcore\\js\\jquery.simple-color.js","public\\assets\\addons\\csmtable\\library\\xcore\\js\\xcore.js"],"license":"regular","licenseto":"16018","licensekey":"IPsy6A1viCU2Ke4H E8kSeu+9NWq4bhU3Ai2jYQ==","domains":["test0rui.com"],"licensecodes":[],"validations":["d495f88a5abee2b752e64026c2ea8984"],"menus":["csmtable","csmtable\/test","csmtable\/test\/index","csmtable\/test\/recyclebin","csmtable\/test\/add","csmtable\/test\/edit","csmtable\/test\/del","csmtable\/test\/destroy","csmtable\/test\/restore","csmtable\/test\/multi","csmtable\/test\/import","csmtable\/datasource\/admin","csmtable\/cligenerateexcel\/index","csmtable\/xlstask","csmtable\/xlstask\/index","csmtable\/xlstask\/add","csmtable\/xlstask\/edit","csmtable\/xlstask\/del","csmtable\/xlstask\/multi","csmtable\/csmgeneratesub\/index","csmtable\/csmgenerate\/generate"]}

View File

@ -0,0 +1,241 @@
<?php
namespace addons\csmtable;
use addons\csmtable\library\xcore\xcore\utils\XcAdminSessionUtils;
use addons\csmtable\library\xcore\xcore\utils\XcRequestUtils;
use think\Addons;
use app\common\library\Menu;
use addons\csmtable\library\xcore\xcore\utils\XcConfigUtils;
use addons\csmtable\library\xcore\xcore\utils\XcResponseUtils;
/**
* 插件
*/
class Csmtable extends Addons
{
/**
* 插件安装方法
* @return bool
*/
public function install()
{
$menu = [
[
'name' => 'csmtable',
'title' => 'Table功能增强',
'sublist' => [
[
'name' => 'csmtable/test',
'title' => '使用示例',
'icon' => 'fa fa-meetup',
'sublist' => [
[
'name' => 'csmtable/test/index',
'title' => '查询'
],
[
'name' => 'csmtable/test/recyclebin',
'title' => '回收站'
],
[
'name' => 'csmtable/test/add',
'title' => '添加'
],
[
'name' => 'csmtable/test/edit',
'title' => '修改'
],
[
'name' => 'csmtable/test/del',
'title' => '删除'
],
[
'name' => 'csmtable/test/destroy',
'title' => '真实删除'
],
[
'name' => 'csmtable/test/restore',
'title' => '还原'
],
[
'name' => 'csmtable/test/multi',
'title' => '批量更新'
],
[
'name' => 'csmtable/test/import',
'title' => '导入'
],
[
'name' => 'csmtable/datasource/admin',
'title' => '人员信息读取'
],
[
'name' => 'csmtable/cligenerateexcel/index',
'title' => '下载Excel'
]
]
],
[
'name' => 'csmtable/xlstask',
'title' => '下载任务',
'icon' => 'fa fa-meetup',
'sublist' => [
[
'name' => 'csmtable/xlstask/index',
'title' => '查询'
],
[
'name' => 'csmtable/xlstask/add',
'title' => '添加'
],
[
'name' => 'csmtable/xlstask/edit',
'title' => '修改'
],
[
'name' => 'csmtable/xlstask/del',
'title' => '删除'
],
[
'name' => 'csmtable/xlstask/multi',
'title' => '批量'
],
[
'name' => 'csmtable/csmgeneratesub/index',
'title' => '重新执行'
],
[
'name' => 'csmtable/csmgenerate/generate',
'title' => '生成文件'
],
]
]
]
]
];
Menu::create($menu);
return true;
}
/**
* 插件卸载方法
* @return bool
*/
public function uninstall()
{
Menu::delete(XcConfigUtils::getAddonCode());
return true;
}
/**
* 插件启用方法
* @return bool
*/
public function enable()
{
Menu::enable(XcConfigUtils::getAddonCode());
return true;
}
/**
* 插件禁用方法
* @return bool
*/
public function disable()
{
Menu::disable(XcConfigUtils::getAddonCode());
return true;
}
/**
* JS 获取配置的方式 Config.csmtable.cloginwxmp
*/
public function configInit(&$params)
{
$config = $this->getConfig();
$addons = XcConfigUtils::xpconfig('addons_code');
$params[$addons] = [
];
}
public function actionBegin($call)
{
$request = XcRequestUtils::getRequest();
if (true) {
// 判断是否安装了插件:表格无刷新行内编辑
$path = $request->path();
if ($path == 'csmtable/test') {
$editable = get_addon_info('editable');
if (! $editable || ! $editable['state']) {
XcResponseUtils::error("为更好的演示本功能,请安装【表格无刷新行内编辑】插件", null, null, 60);
}
}
}
if (true) {
// 异步下载用
$method = $request->request('csmtable_method');
$filesource = $request->request('csmtable_filesource');
if ($method == 'download_excel') {
set_time_limit(0);
$dao = new \app\admin\model\csmtable\Xlstask();
$admin_id = XcAdminSessionUtils::getUserId();
// 限制下载
if (true) {
$row = $dao->where("admin_id", "=", $admin_id)
->where("progress", "<", "100")
->where("createtime", ">", time() - 1800)
->where("iserror", "<>", "Y")
->find();
if ($row) {
XcResponseUtils::error("当前有下载任务,请任务结束后再尝试下载。");
}
}
// 生成任务记录
$dao->where("admin_id", "=", $admin_id)
->where("filesource", '=', $filesource)
->where("status", "=", "normal")
->update([
"status" => "hidden"
]);
// 触发异步生成Excel任务
$classname = get_class($call[0]);
$getparams = [
'search' => $request->request('search'),
'filter' => $request->request('filter'),
'op' => $request->request('op'),
'sort' => $request->request('sort'),
'order' => $request->request('order'),
'offset' => $request->request('offset'),
'limit' => $request->request('limit'),
'csmtable_classname' => str_replace('\\', '/', $classname),
'csmtable_methodname' => 'index',
'csmtable_columns' => $request->request('csmtable_columns')
];
$param = [
'admin_id' => $admin_id,
'filesource' => $filesource,
'param' => json_encode($getparams),
'createtime' => time()
];
$row = $dao->create($param);
$url = XcRequestUtils::urlBase("/addons/csmtable/csmgenerate/index?id={$row->id}&clogintoken=".XcAdminSessionUtils::getToken());
$this->callremote2($url);
}
}
}
private function callremote2($url)
{
\fast\Http::sendRequest($url);
}
}

43
addons/csmtable/bootstrap.js vendored Normal file
View File

@ -0,0 +1,43 @@
require.config({
paths: {
'csmtable_xcore': '../addons/csmtable/library/xcore/js/xcore',
'csmtable_csminputstyle': '../addons/csmtable/library/xcore/js/csminputstyle',
'jquery.simple-color': '../addons/csmtable/library/xcore/js/jquery.simple-color',
'csmtable': '../addons/csmtable/js/csmtable',
'fixedcolumns': '../addons/csmtable/js/bootstrap-table-fixed-columns',
'tablereorderrows': '../addons/csmtable/js/bootstrap-table-reorder-rows',
'tablestickyheader': '../addons/csmtable/js/bootstrap-table-sticky-header',
'tabletreegrid': '../addons/csmtable/js/bootstrap-table-treegrid',
'jquerytablednd': '../addons/csmtable/js/jquery.tablednd.min',
'jquerytreegrid': '../addons/csmtable/js/jquery.treegrid.min',
'xeditable2': '../addons/csmtable/js/select2',
},
shim: {
'csmtable_xcore': {
deps: ["css!../addons/csmtable/library/xcore/css/xcore.css"]
},
'csmtable_csminputstyle': {
deps: ["css!../addons/csmtable/library/xcore/css/csminputstyle.css"]
},
'csmtable': {
deps: ["css!../addons/csmtable/css/csmtable.css", 'bootstrap-table']
},
'fixedcolumns': {
deps: ["css!../addons/csmtable/css/bootstrap-table-fixed-columns.css", 'bootstrap-table']
},
'tablereorderrows':{
deps: ["css!../addons/csmtable/css/bootstrap-table-reorder-rows.css",'jquerytablednd', 'bootstrap-table']
},
'tablestickyheader':{
deps: ["css!../addons/csmtable/css/bootstrap-table-sticky-header.css", 'bootstrap-table']
},
'tabletreegrid':{
deps: ["css!../addons/csmtable/css/jquery.treegrid.min.css",'jquerytreegrid', 'bootstrap-table']
},
'xeditable2':{
deps: ["css!../addons/csmtable/css/select2.css","css!../addons/csmtable/css/select2-bootstrap.css",'bootstrap-table','editable']
},
}
});

View File

@ -0,0 +1,41 @@
<?php
return [
[
'group' => '基本信息',
'name' => 'xcbaseurl',
'title' => '网站域名',
'type' => 'string',
'content' => [],
'value' => 'http://www.example.com/',
'rule' => '',
'msg' => '',
'tip' => '',
'ok' => '',
'extend' => '',
],
[
'name' => 'excelmaxrecoredcount',
'title' => 'Excel的最大记录数(异步下载用)',
'type' => 'string',
'content' => [],
'value' => '10000',
'rule' => '',
'msg' => '',
'tip' => '最大值是30000,数值越大服务器设置的内存越大,否则会溢出',
'ok' => '',
'extend' => '',
],
[
'name' => 'controlpagesize',
'title' => '单次读取数据库记录数',
'type' => 'string',
'content' => [],
'value' => '1000',
'rule' => '',
'msg' => '',
'tip' => '记录数越大,速度越快,运算占内存越高',
'ok' => '',
'extend' => '',
],
];

View File

@ -0,0 +1,166 @@
<?php
namespace addons\csmtable\controller;
use addons\csmtable\library\xcore\xcore\utils\XcConfigUtils;
use addons\csmtable\library\xcore\xcore\utils\XcDaoUtils;
use addons\csmtable\library\xcore\xcore\utils\XcRequestUtils;
use addons\csmtable\library\xcore\xcore\base\XcAAdminApi;
use addons\csmtable\library\xapp\csmtable\utils\CsmTableUtils;
use addons\csmtable\library\xapp\csmtable\utils\CsmgenerateTask;
class Csmgenerate extends XcAAdminApi
{
protected $noNeedRight = ["*"];
protected $xlstaskdao = null;
// config 配置
protected $excelmaxrecoredcount = null;
protected $controlpagesize = null;
protected $uploadtmppath = RUNTIME_PATH . 'temp' . DS;
protected function xinit()
{
$this->xlstaskdao = new \app\admin\model\csmtable\Xlstask();
$this->excelmaxrecoredcount = (int)XcConfigUtils::config("excelmaxrecoredcount");
$this->controlpagesize = XcConfigUtils::config("controlpagesize");
}
public function index()
{
$id = XcRequestUtils::get("id", true);
$ret = $this->_index($id);
if ($ret === true) {
echo "操作成功!";
} else {
echo $ret;
}
}
private function _index($id)
{
static::p('----generateExcelByClassname begin:');
set_time_limit(0);
$params = $this->getXlstaskParams($id);
$this->setProgress($id, 10);
// 按记录数切分多个任务
$recordtotal = $this->getDataRecordTotal($params);
$excelCount = (int) ($recordtotal / $this->excelmaxrecoredcount); // 总记录数/单个xls记录数=200/100=2
if ($recordtotal == 0) {
// 没有数据,则报错
$errmsg = "没有数据,无法下载";
$this->setErrorXlstask($id, $errmsg);
static::p('----generateExcelByClassname end:');
return $errmsg;
}
$result = '';
$excelFiles = [];
for ($i = 0; $i <= $excelCount; $i++) {
static::p($i);
$fileno = ($i + 1);
$offset = $i * $this->excelmaxrecoredcount;
$task = new CsmgenerateTask();
$filename = $task->index($id, $fileno, $offset);
$result = $filename . ';';
$progress = (int) 80 * ($i + 1) / ($excelCount + 2) + 10;
$this->setProgress($id, $progress);
if ($filename != null) {
$excelFiles[] = $filename;
static::p($filename);
} else {
$errmsg = '下载报错,可能是PHP内存设置过小或数据查询报错造成';
$this->setErrorXlstask($id, $errmsg);
return $errmsg;
}
}
$this->setProgress($id, 90);
$zipfilename = static::saveExcelToZip($excelFiles);
$this->setProgress($id, 100, $zipfilename, $result);
static::p('----generateExcelByClassname end:' . $result);
return true;
}
public function saveExcelToZip(&$excelFiles)
{
$zipfn = 'csmtable_' . time() . '.zip';
$zipfilename = $this->uploadtmppath . $zipfn;
$zip = new \ZipArchive();
$zip->open($zipfilename, \ZipArchive::CREATE | \ZipArchive::CREATE);
static::p('saveExcelToZip');
static::p($excelFiles);
foreach ($excelFiles as $item) {
$zip->addFile($this->uploadtmppath . $item, $item);
}
$zip->close();
foreach ($excelFiles as $item) {
unlink($this->uploadtmppath . $item);
}
return $zipfn;
}
private function setProgress(&$csmtable_xlstask_id, $progress, $filename = '', $errormsg = '')
{
$this->xlstaskdao->where("id", "=", $csmtable_xlstask_id)->update([
'progress' => $progress,
'filename' => $filename,
'updatetime' => time(),
'errormsg' => $errormsg //v2.1.8 增加日志和查询日志调试功能,便于排查问题
]);
static::p('progress:' . $progress);
}
private function setErrorXlstask($csmtable_xlstask_id, $errormsg)
{
XcDaoUtils::getRowById($this->xlstaskdao, $csmtable_xlstask_id)->save([
'iserror' => 'Y',
'errormsg' => substr($errormsg, 0, 1000),
'updatetime' => time()
]);
static::p('progress:' . $errormsg);
}
// 获取记录数
public function getDataRecordTotal(&$params)
{
$controlData = CsmTableUtils::callRemoteControl($params, 0, 1);
return $controlData->getData()['total'];
}
public function getXlstaskParams($id)
{
$xlstaskrow = XcDaoUtils::getRowById($this->xlstaskdao, $id);
if ($xlstaskrow == null) {
return;
}
return json_decode($xlstaskrow->param, true);
}
private static function p($str)
{
trace($str);
//echo($str."<BR>");
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace addons\csmtable\controller;
use think\addons\Controller;
class Index extends Controller
{
public function index()
{
$this->error("当前插件暂无前台页面");
}
}

10
addons/csmtable/info.ini Normal file
View File

@ -0,0 +1,10 @@
name = csmtable
title = FastAdmin表格优化增强
intro = FastAdmin表格优化增强
author = chenshiming
website = https://www.fastadmin.net
version = 3.0.1
state = 1
license = regular
licenseto = 16018
url = /addons/csmtable

View File

@ -0,0 +1,37 @@
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
SET AUTOCOMMIT = 0;
START TRANSACTION;
SET time_zone = "+00:00";
--
-- 表的结构 `__PREFIX__csmtable_xlstask`
--
CREATE TABLE IF NOT EXISTS `__PREFIX__csmtable_xlstask` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'id',
`admin_id` int(10) unsigned DEFAULT 0 COMMENT '管理员',
`filesource` varchar(200) DEFAULT NULL COMMENT '文件来源标记',
`filename` varchar(500) DEFAULT NULL COMMENT '文件名',
`param` text DEFAULT NULL COMMENT '参数',
`progress` int(10) unsigned DEFAULT 0 COMMENT '下载进度',
`status` enum('normal','hidden') DEFAULT 'normal' COMMENT '状态',
`createtime` bigint(16) COMMENT '创建时间',
`updatetime` bigint(16) COMMENT '更新时间',
`iserror` enum('Y','N') DEFAULT 'N' COMMENT '是否处理错误',
`errormsg` text DEFAULT NULL COMMENT '错误信息',
`b1` varchar(100) DEFAULT NULL COMMENT '备用字段1',
`b2` varchar(100) DEFAULT NULL COMMENT '备用字段2',
`b3` varchar(100) DEFAULT NULL COMMENT '备用字段3',
`b4` varchar(100) DEFAULT NULL COMMENT '备用字段4',
`b5` varchar(100) DEFAULT NULL COMMENT '备用字段5',
`b6` varchar(100) DEFAULT NULL COMMENT '备用字段6',
`b7` varchar(100) DEFAULT NULL COMMENT '备用字段7',
`b8` varchar(100) DEFAULT NULL COMMENT '备用字段8',
`b9` varchar(100) DEFAULT NULL COMMENT '备用字段9',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=200 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='Excel下载任务表'
;
COMMIT;

View File

@ -0,0 +1,13 @@
<?php
namespace addons\csmtable\library\xapp\csmtable;
use addons\csmtable\library\xcore\xcore\base\XcAAddons;
class XpAddons extends XcAAddons
{
public function xpMergeUser($src_user_id, $target_user_id)
{
return;
}
}

View File

@ -0,0 +1,81 @@
<?php
namespace addons\csmtable\library\xapp\csmtable\model;
use think\Request;
class CsmRequest extends Request
{
private $datas = [];
private $methoddatas = [];
protected static $instance;
public function _initialize()
{
parent::_initialize();
}
public static function instance($options = [])
{
if (is_null(self::$instance)) {
self::$instance = new CsmRequest($options);
}
self::$instance->clear();
return self::$instance;
}
public function clear()
{
$this->datas = [];
$this->methoddatas = [];
}
protected function __construct($options = [])
{
parent::__construct($options);
}
public function setMethodReturn($methodname, $value)
{
$this->methoddatas[$methodname] = $value;
}
public function isAjax($ajax = false)
{
$methodname = 'isAjax';
if (isset($this->methoddatas[$methodname])) {
return $this->methoddatas[$methodname];
}
return parent::isAjax($ajax);
}
public function set($name, $value)
{
$this->datas[$name] = $value;
}
public function get($name = '', $default = null, $filter = '')
{
if(is_array($name)){
foreach($name as $kk=>$vv){
static::p($kk.'='.$vv);
return $vv;
}
}
if (isset($this->datas[$name])) {
static::p($name.'='.$this->datas[$name]);
return $this->datas[$name];
}
return parent::get($name, $default, $filter);
}
public static function p($str)
{
trace($str);
//echo($str."<BR>");
}
}

View File

@ -0,0 +1,122 @@
<?php
namespace addons\csmtable\library\xapp\csmtable\utils;
use addons\csmtable\library\xcore\xcore\utils\XcAdminSessionUtils;
use think\App;
use think\Loader;
use think\Request;
use addons\csmtable\library\xapp\csmtable\model\CsmRequest;
class CsmTableUtils
{
/**
*
* @usaged
* var_dump(CsmTableUtils::getInstanceAndMethod("fa/test"));
* var_dump(CsmTableUtils::getInstanceAndMethod("fa/test/test"));
* array (size=2)
* 0 => string '\app\admin\controller\fa\Test' (length=29)
* 1 => string 'index' (length=5)
*
* var_dump(CsmTableUtils::getInstanceAndMethod("category"));
* var_dump(CsmTableUtils::getInstanceAndMethod("category/test"));
* array (size=2)
* 0 => string '\app\admin\controller\Category' (length=30)
* 1 => string 'index' (length=5)
*/
public static function getInstanceAndMethod(&$url)
{
if(stripos($url,"?")!==false){
$url = substr($url,0,stripos($url,"?"));
}
$im = static::_isClassExists($url);
if ($im == null) {
$im = static::_isClassExists($url . "/index");
}
if ($im == null) {
$im = static::_isClassExists($url . "index");
}
return $im;
}
private static function _isClassExists($url)
{
$sr = null;
$clsname = "\\app\\admin\\controller";
$patharr = explode("/", $url);
foreach ($patharr as $k => $v) {
if ($k == (count($patharr) - 2)) {
$clsname .= "\\" . ucfirst($v);
break;
} else {
$clsname .= "\\" . $v;
}
}
$b = class_exists($clsname);
if ($b === true) {
$methodname = $patharr[count($patharr) - 1];
if (count($patharr) == 1) {
$methodname = 'index';
}
$sr = [
$clsname,
$methodname
];
}
return $sr;
}
public static function getParamValue(&$params, $key, $defaultvalue = null)
{
$sr = null;
if (isset($params[$key])) {
$sr = $params[$key];
}
$sr = ($sr == null) ? $defaultvalue : $sr;
return $sr;
}
public static function callRemoteControl(&$params, $offset, $limit)
{
XcAdminSessionUtils::directApplicationAdminLogin();
$classname = str_replace('/', '\\', static::getParamValue($params, 'csmtable_classname'));
$methodname = static::getParamValue($params, 'csmtable_methodname');
$controller = null;
if(true){
$request = CsmRequest::instance();
$onlyClassname = Loader::parseName(substr($classname,strrpos($classname,"\\")+1),0);
$tt = substr($classname,0,strrpos($classname,"\\"));
$tt = Loader::parseName(substr($tt,strrpos($tt,"\\")+1),1);
$controller = "{$tt}.{$onlyClassname}";
}
$request->controller($controller);
$request->action($methodname);
$instance = new $classname($request);
$request->set('search', static::getParamValue($params, 'search'));
$request->set('filter', static::getParamValue($params, 'filter'));
$request->set('op', static::getParamValue($params, 'op'));
$request->set('sort', static::getParamValue($params, 'sort'));
$request->set('order', static::getParamValue($params, 'order'));
$request->set("offset", $offset);
$request->set("offset/d", $offset);//v2.1.8 修复在1.2.0.20201008_beta版本下导出数据记录数错误的问题
$request->set('limit', $limit);
$request->set('limit/d', $limit);//v2.1.8 修复在1.2.0.20201008_beta版本下导出数据记录数错误的问题
$request->setMethodReturn("isAjax", true);
//2.2.3 适配1.3.3版本,修复导出数据错乱的问题
//修复了control分页使用paginate方法造成导出分页信息无法获取原因是Paginator#getCurrentPage获取page是直接通过Request而不是control的Request方法
$page = $limit ? intval($offset / $limit) + 1 : 1;
Request::instance()->get([config('paginate.var_page') => $page]);
return App::invokeMethod([
$instance,
$methodname
], null);
}
}

Some files were not shown because too many files have changed in this diff Show More