链接管理模块
This commit is contained in:
@@ -0,0 +1,15 @@
|
|||||||
|
-- 分流链接:平台分配域名(支持多个,一行一个)
|
||||||
|
SET NAMES utf8mb4;
|
||||||
|
|
||||||
|
INSERT INTO `fa_config` (`name`, `group`, `title`, `tip`, `type`, `visible`, `value`, `content`, `rule`, `extend`, `setting`)
|
||||||
|
SELECT 'split_platform_domain', 'split', '平台分配域名', '一行填写一个根域名,如 example.com;不含 http:// 与路径,保存后在分流链接弹窗中可选用', 'text', '', '', '', '', ' rows="8" placeholder="example.com another.com"', NULL
|
||||||
|
FROM DUAL
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM `fa_config` WHERE `name` = 'split_platform_domain' LIMIT 1);
|
||||||
|
|
||||||
|
UPDATE `fa_config`
|
||||||
|
SET `group` = 'split',
|
||||||
|
`type` = 'text',
|
||||||
|
`title` = '平台分配域名',
|
||||||
|
`tip` = '一行填写一个根域名,如 example.com;不含 http:// 与路径,保存后在分流链接弹窗中可选用',
|
||||||
|
`extend` = ' rows="8" placeholder="example.com another.com"'
|
||||||
|
WHERE `name` = 'split_platform_domain';
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
-- 分流管理 - 链接管理表
|
||||||
|
SET NAMES utf8mb4;
|
||||||
|
SET FOREIGN_KEY_CHECKS = 0;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS `fa_split_link`;
|
||||||
|
CREATE TABLE `fa_split_link` (
|
||||||
|
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
||||||
|
`admin_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '管理员ID',
|
||||||
|
`countries` varchar(500) NOT NULL DEFAULT '' COMMENT '投放国家(ISO2逗号分隔)',
|
||||||
|
`link_code` char(9) NOT NULL DEFAULT '' COMMENT '分流链接(9位小写字母)',
|
||||||
|
`description` varchar(255) NOT NULL DEFAULT '' COMMENT '链接描述',
|
||||||
|
`auto_reply` text COMMENT '自动回复语句(一行一条)',
|
||||||
|
`ip_protect` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT 'IP防护:0=关闭,1=开启',
|
||||||
|
`random_shuffle` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '随机打乱:0=关闭,1=开启',
|
||||||
|
`status` enum('normal','hidden') NOT NULL DEFAULT 'normal' COMMENT '状态:normal=正常,hidden=停用',
|
||||||
|
`createtime` bigint(16) DEFAULT NULL COMMENT '创建时间',
|
||||||
|
`updatetime` bigint(16) DEFAULT NULL COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `link_code` (`link_code`),
|
||||||
|
KEY `admin_id` (`admin_id`),
|
||||||
|
KEY `status` (`status`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='分流链接表';
|
||||||
|
|
||||||
|
SET FOREIGN_KEY_CHECKS = 1;
|
||||||
|
|
||||||
|
INSERT INTO `fa_auth_rule` (`type`, `pid`, `name`, `title`, `icon`, `condition`, `remark`, `ismenu`, `createtime`, `updatetime`, `weigh`, `status`)
|
||||||
|
SELECT 'file', 0, 'split', '分流管理', 'fa fa-random', '', '', 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), 0, 'normal'
|
||||||
|
FROM DUAL
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM `fa_auth_rule` WHERE `name` = 'split' LIMIT 1);
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
-- 分流链接:自动回复语句字段(一行一条,关联 link_code)
|
||||||
|
SET NAMES utf8mb4;
|
||||||
|
|
||||||
|
ALTER TABLE `fa_split_link`
|
||||||
|
ADD COLUMN `auto_reply` text COMMENT '自动回复语句(一行一条)' AFTER `description`;
|
||||||
|
|
||||||
|
INSERT INTO `fa_auth_rule` (`type`, `pid`, `name`, `title`, `icon`, `condition`, `remark`, `ismenu`, `createtime`, `updatetime`, `weigh`, `status`)
|
||||||
|
SELECT 'file', r.id, 'split.link/autoreply', '自动回复', 'fa fa-circle-o', '', '', 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), 0, 'normal'
|
||||||
|
FROM `fa_auth_rule` r
|
||||||
|
WHERE r.name = 'split.link' AND r.ismenu = 0
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM `fa_auth_rule` WHERE `name` = 'split.link/autoreply' LIMIT 1)
|
||||||
|
LIMIT 1;
|
||||||
@@ -0,0 +1,367 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\admin\controller\split;
|
||||||
|
|
||||||
|
use app\admin\model\Domain as DomainModel;
|
||||||
|
use app\common\controller\Backend;
|
||||||
|
use app\common\library\CountryIso;
|
||||||
|
use app\common\service\SplitAutoReplyService;
|
||||||
|
use app\common\service\SplitLinkCodeService;
|
||||||
|
use app\common\service\SplitPlatformDomainService;
|
||||||
|
use think\Db;
|
||||||
|
use think\Exception;
|
||||||
|
use think\exception\DbException;
|
||||||
|
use think\exception\PDOException;
|
||||||
|
use think\exception\ValidateException;
|
||||||
|
use think\response\Json;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 链接管理
|
||||||
|
*
|
||||||
|
* @icon fa fa-link
|
||||||
|
* @remark 分流链接管理,自动生成9位字母链接码
|
||||||
|
*/
|
||||||
|
class Link extends Backend
|
||||||
|
{
|
||||||
|
/** @var \app\admin\model\split\Link */
|
||||||
|
protected $model = null;
|
||||||
|
|
||||||
|
protected $searchFields = 'link_code,description';
|
||||||
|
|
||||||
|
protected $dataLimit = 'personal';
|
||||||
|
|
||||||
|
protected $modelValidate = true;
|
||||||
|
|
||||||
|
protected $modelSceneValidate = true;
|
||||||
|
|
||||||
|
/** @var string[] 无需鉴权的操作 */
|
||||||
|
protected $noNeedRight = ['script', 'copyinfo'];
|
||||||
|
|
||||||
|
/** @var string patches 视图目录(未完整部署时使用) */
|
||||||
|
private const PATCH_VIEW_DIR = 'patches/application/admin/view/split/link/';
|
||||||
|
|
||||||
|
public function _initialize()
|
||||||
|
{
|
||||||
|
parent::_initialize();
|
||||||
|
|
||||||
|
$lang = $this->request->langset();
|
||||||
|
$lang = preg_match('/^([a-zA-Z\-_]{2,10})$/i', $lang) ? $lang : 'zh-cn';
|
||||||
|
$langFile = ROOT_PATH . 'patches/application/admin/lang/' . $lang . '/split/link.php';
|
||||||
|
if (is_file($langFile)) {
|
||||||
|
\think\Lang::load($langFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->model = new \app\admin\model\split\Link();
|
||||||
|
$countryList = CountryIso::getOptions();
|
||||||
|
$this->view->assign('countryList', $countryList);
|
||||||
|
$this->view->assign('ipProtectList', $this->model->getIpProtectList());
|
||||||
|
$this->view->assign('randomShuffleList', $this->model->getRandomShuffleList());
|
||||||
|
$this->view->assign('statusList', $this->model->getStatusList());
|
||||||
|
$this->assignconfig('countryList', $countryList);
|
||||||
|
$this->assignconfig('ipProtectList', $this->model->getIpProtectList());
|
||||||
|
$this->assignconfig('randomShuffleList', $this->model->getRandomShuffleList());
|
||||||
|
$this->assignconfig('statusList', $this->model->getStatusList());
|
||||||
|
|
||||||
|
$this->setupPatchFrontend();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 未部署 JS 到 public 时,或 patches 版本较新时,jsname 指向 script 接口
|
||||||
|
*/
|
||||||
|
private function setupPatchFrontend(): void
|
||||||
|
{
|
||||||
|
$patchJs = ROOT_PATH . 'patches/public/assets/js/backend/split/link.js';
|
||||||
|
$publicJs = ROOT_PATH . 'public/assets/js/backend/split/link.js';
|
||||||
|
$usePatchJs = is_file($patchJs) && (
|
||||||
|
!is_file($publicJs) || filemtime($patchJs) >= filemtime($publicJs)
|
||||||
|
);
|
||||||
|
if (!$usePatchJs) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$cfg = is_array($this->view->config ?? null) ? $this->view->config : [];
|
||||||
|
$version = (string) \think\Config::get('site.version');
|
||||||
|
$scriptUrl = (string) url('split.link/script', ['v' => $version], false, true);
|
||||||
|
if (strpos($scriptUrl, '?') === false) {
|
||||||
|
$scriptUrl .= '?v=' . $version;
|
||||||
|
}
|
||||||
|
if (strpos($scriptUrl, '://') === false) {
|
||||||
|
$scriptUrl = $this->request->domain() . $scriptUrl;
|
||||||
|
}
|
||||||
|
$cfg['jsname'] = $scriptUrl;
|
||||||
|
$this->view->assign('config', $cfg);
|
||||||
|
$this->view->config = $cfg;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染模板(优先 patches 视图,回退 application)
|
||||||
|
*/
|
||||||
|
private function fetchPatch(string $template): string
|
||||||
|
{
|
||||||
|
$patchFile = ROOT_PATH . self::PATCH_VIEW_DIR . $template . '.html';
|
||||||
|
$appFile = APP_PATH . 'admin/view/split/link/' . $template . '.html';
|
||||||
|
if (is_file($patchFile)) {
|
||||||
|
$file = $patchFile;
|
||||||
|
} elseif (is_file($appFile)) {
|
||||||
|
$file = $appFile;
|
||||||
|
} else {
|
||||||
|
$this->error('模板文件不存在');
|
||||||
|
}
|
||||||
|
return (string) $this->view->fetch($file);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列表
|
||||||
|
*
|
||||||
|
* @return string|Json
|
||||||
|
* @throws DbException
|
||||||
|
* @throws \think\Exception
|
||||||
|
*/
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$this->request->filter(['strip_tags', 'trim']);
|
||||||
|
if (false === $this->request->isAjax()) {
|
||||||
|
return $this->fetchPatch('index');
|
||||||
|
}
|
||||||
|
if ($this->request->request('keyField')) {
|
||||||
|
return $this->selectpage();
|
||||||
|
}
|
||||||
|
[$where, $sort, $order, $offset, $limit] = $this->buildparams();
|
||||||
|
$list = $this->model
|
||||||
|
->where($where)
|
||||||
|
->order($sort, $order)
|
||||||
|
->paginate($limit);
|
||||||
|
$result = ['total' => $list->total(), 'rows' => $list->items()];
|
||||||
|
return json($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加(打开表单时预生成分流链接,可编辑后保存)
|
||||||
|
*/
|
||||||
|
public function add()
|
||||||
|
{
|
||||||
|
if (false === $this->request->isPost()) {
|
||||||
|
try {
|
||||||
|
$defaultLinkCode = (new SplitLinkCodeService())->generateUnique();
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->error($e->getMessage());
|
||||||
|
}
|
||||||
|
$this->view->assign('defaultLinkCode', $defaultLinkCode);
|
||||||
|
return $this->fetchPatch('add');
|
||||||
|
}
|
||||||
|
|
||||||
|
$params = $this->request->post('row/a', []);
|
||||||
|
if ($params === []) {
|
||||||
|
$this->error(__('Parameter %s can not be empty', ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
$params = $this->preExcludeFields($params);
|
||||||
|
|
||||||
|
if (isset($params['link_code'])) {
|
||||||
|
$params['link_code'] = SplitLinkCodeService::normalize((string) $params['link_code']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->dataLimit && $this->dataLimitFieldAutoFill) {
|
||||||
|
$params[$this->dataLimitField] = $this->auth->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = false;
|
||||||
|
Db::startTrans();
|
||||||
|
try {
|
||||||
|
if ($this->modelValidate) {
|
||||||
|
$name = str_replace('\\model\\', '\\validate\\', get_class($this->model));
|
||||||
|
$validate = $this->modelSceneValidate ? $name . '.add' : $name;
|
||||||
|
$this->model->validateFailException()->validate($validate, $params);
|
||||||
|
}
|
||||||
|
$result = $this->model->allowField(true)->save($params);
|
||||||
|
Db::commit();
|
||||||
|
} catch (ValidateException $e) {
|
||||||
|
Db::rollback();
|
||||||
|
$this->error($e->getMessage());
|
||||||
|
} catch (PDOException|Exception $e) {
|
||||||
|
Db::rollback();
|
||||||
|
$this->error($e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($result === false) {
|
||||||
|
$this->error(__('No rows were inserted'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->success();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 复制分流链接弹窗所需数据(平台域名 + 我的域名列表)
|
||||||
|
*
|
||||||
|
* @return Json
|
||||||
|
*/
|
||||||
|
public function copyinfo(): Json
|
||||||
|
{
|
||||||
|
$platformDomains = $this->getPlatformDomains();
|
||||||
|
|
||||||
|
$domainQuery = (new DomainModel())->order('id', 'desc');
|
||||||
|
if ($this->dataLimit) {
|
||||||
|
$adminIds = $this->getDataLimitAdminIds();
|
||||||
|
if (is_array($adminIds)) {
|
||||||
|
$domainQuery->where($this->dataLimitField, 'in', $adminIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$myDomains = [];
|
||||||
|
foreach ($domainQuery->select() as $domainRow) {
|
||||||
|
$myDomains[] = [
|
||||||
|
'id' => (int) $domainRow['id'],
|
||||||
|
'domain' => (string) $domainRow['domain'],
|
||||||
|
'ns_status' => (string) $domainRow['ns_status'],
|
||||||
|
'ns_status_text' => (string) $domainRow['ns_status_text'],
|
||||||
|
'dns_status' => (string) $domainRow['dns_status'],
|
||||||
|
'dns_status_text' => (string) $domainRow['dns_status_text'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->success('', null, [
|
||||||
|
'platform_domains' => $platformDomains,
|
||||||
|
'my_domains' => $myDomains,
|
||||||
|
'domain_index_url' => (string) url('domain/index'),
|
||||||
|
'domain_add_url' => (string) url('domain/add'),
|
||||||
|
'config_index_url' => (string) url('general/config/index'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取平台分配域名列表(优先 site 配置,回退数据库)
|
||||||
|
*
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
private function getPlatformDomains(): array
|
||||||
|
{
|
||||||
|
$raw = trim((string) \think\Config::get('site.split_platform_domain'));
|
||||||
|
if ($raw === '') {
|
||||||
|
$raw = trim((string) Db::name('config')->where('name', 'split_platform_domain')->value('value'));
|
||||||
|
}
|
||||||
|
return SplitPlatformDomainService::parseList($raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动回复:读取 / 保存与当前链接关联的回复语句
|
||||||
|
*
|
||||||
|
* @param string|null $ids 链接 ID
|
||||||
|
* @return Json
|
||||||
|
* @throws DbException
|
||||||
|
*/
|
||||||
|
public function autoreply($ids = null): Json
|
||||||
|
{
|
||||||
|
$row = $this->model->get($ids);
|
||||||
|
if (!$row) {
|
||||||
|
$this->error(__('No Results were found'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$adminIds = $this->getDataLimitAdminIds();
|
||||||
|
if (is_array($adminIds) && !in_array((int) $row[$this->dataLimitField], $adminIds, true)) {
|
||||||
|
$this->error(__('You have no permission'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->request->isPost()) {
|
||||||
|
$autoReply = (string) $this->request->post('auto_reply', '');
|
||||||
|
$lines = SplitAutoReplyService::parseLines($autoReply);
|
||||||
|
try {
|
||||||
|
$row->save([
|
||||||
|
'auto_reply' => SplitAutoReplyService::formatStorage($lines),
|
||||||
|
]);
|
||||||
|
} catch (PDOException|Exception $e) {
|
||||||
|
$this->error($e->getMessage());
|
||||||
|
}
|
||||||
|
$this->success(__('Auto reply saved'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->success('', null, [
|
||||||
|
'id' => (int) $row['id'],
|
||||||
|
'link_code' => (string) $row['link_code'],
|
||||||
|
'auto_reply' => SplitAutoReplyService::formatDisplay((string) $row->getAttr('auto_reply')),
|
||||||
|
'line_count' => count($row->getAutoReplyLines()),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑(分流链接码不可修改)
|
||||||
|
*
|
||||||
|
* @param string|null $ids
|
||||||
|
* @return string
|
||||||
|
* @throws DbException
|
||||||
|
*/
|
||||||
|
public function edit($ids = null)
|
||||||
|
{
|
||||||
|
$row = $this->model->get($ids);
|
||||||
|
if (!$row) {
|
||||||
|
$this->error(__('No Results were found'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$adminIds = $this->getDataLimitAdminIds();
|
||||||
|
if (is_array($adminIds) && !in_array($row[$this->dataLimitField], $adminIds)) {
|
||||||
|
$this->error(__('You have no permission'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (false === $this->request->isPost()) {
|
||||||
|
$this->view->assign('row', $row);
|
||||||
|
$this->view->assign('selectedCountries', $row->getSelectedCountries());
|
||||||
|
return $this->fetchPatch('edit');
|
||||||
|
}
|
||||||
|
|
||||||
|
$params = $this->request->post('row/a', []);
|
||||||
|
if ($params === []) {
|
||||||
|
$this->error(__('Parameter %s can not be empty', ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
$params = $this->preExcludeFields($params);
|
||||||
|
unset($params['link_code']);
|
||||||
|
|
||||||
|
$result = false;
|
||||||
|
Db::startTrans();
|
||||||
|
try {
|
||||||
|
if ($this->modelValidate) {
|
||||||
|
$name = str_replace('\\model\\', '\\validate\\', get_class($this->model));
|
||||||
|
$validate = $this->modelSceneValidate ? $name . '.edit' : $name;
|
||||||
|
$row->validateFailException()->validate($validate, $params);
|
||||||
|
}
|
||||||
|
$result = $row->allowField(true)->save($params);
|
||||||
|
Db::commit();
|
||||||
|
} catch (ValidateException $e) {
|
||||||
|
Db::rollback();
|
||||||
|
$this->error($e->getMessage());
|
||||||
|
} catch (PDOException|Exception $e) {
|
||||||
|
Db::rollback();
|
||||||
|
$this->error($e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($result === false) {
|
||||||
|
$this->error(__('No rows were updated'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->success();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 输出后台列表/表单 JS(patches 未部署到 public 时由 RequireJS 加载)
|
||||||
|
*/
|
||||||
|
public function script(): void
|
||||||
|
{
|
||||||
|
$jsFile = ROOT_PATH . 'patches/public/assets/js/backend/split/link.js';
|
||||||
|
if (!is_file($jsFile)) {
|
||||||
|
$jsFile = ROOT_PATH . 'public/assets/js/backend/split/link.js';
|
||||||
|
}
|
||||||
|
if (!is_file($jsFile)) {
|
||||||
|
$this->error('脚本文件不存在');
|
||||||
|
}
|
||||||
|
$content = file_get_contents($jsFile);
|
||||||
|
if ($content === false) {
|
||||||
|
$this->error('读取脚本失败');
|
||||||
|
}
|
||||||
|
$response = response($content, 200, [
|
||||||
|
'Content-Type' => 'application/javascript; charset=utf-8',
|
||||||
|
'Cache-Control' => 'public, max-age=3600',
|
||||||
|
]);
|
||||||
|
throw new \think\exception\HttpResponseException($response);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
'Countries' => '投放国家',
|
||||||
|
'Countries help' => '可多选,存储为 ISO 两位国家代码',
|
||||||
|
'Link_code' => '分流链接',
|
||||||
|
'Link code auto tip' => '保存后系统自动生成 9 位小写字母链接码',
|
||||||
|
'Description' => '链接描述',
|
||||||
|
'Ip_protect' => 'IP防护',
|
||||||
|
'Random_shuffle' => '随机打乱',
|
||||||
|
'Status' => '状态',
|
||||||
|
'Createtime' => '创建时间',
|
||||||
|
'Ip protect off' => '关闭',
|
||||||
|
'Ip protect on' => '开启',
|
||||||
|
'Random shuffle off' => '关闭',
|
||||||
|
'Random shuffle on' => '开启',
|
||||||
|
'Status normal' => '正常',
|
||||||
|
'Status hidden' => '停用',
|
||||||
|
'Copy split link' => '复制分流链接',
|
||||||
|
'Manage my domains' => '管理我的域名',
|
||||||
|
'Select main domain' => '选择主域名',
|
||||||
|
'Platform assigned domain' => '平台分配域名',
|
||||||
|
'My domains' => '我的域名',
|
||||||
|
'Split platform domain empty' => '尚未配置平台分配域名,请前往',
|
||||||
|
'Go system config' => '系统设置',
|
||||||
|
'Split my domain empty' => '暂无域名,请先在域名管理中添加',
|
||||||
|
'Split domain prefix tip' => '想使用自己的域名作为分流前缀,可以先去「我的域名」完成接入。',
|
||||||
|
'Go add domain' => '去添加',
|
||||||
|
'Generated result' => '生成结果',
|
||||||
|
'Copy' => '复制',
|
||||||
|
'Copy success' => '复制成功',
|
||||||
|
'Copy failed' => '复制失败',
|
||||||
|
'Open test' => '打开测试',
|
||||||
|
'Close' => '关闭',
|
||||||
|
'Split url empty' => '请先选择有效主域名',
|
||||||
|
'System config' => '系统配置',
|
||||||
|
'Auto reply' => '自动回复',
|
||||||
|
'Reply statements' => '回复语句',
|
||||||
|
'Reply statements tip'=> '一行填写一条回复语句,保存后与当前分流链接关联',
|
||||||
|
'Auto reply saved' => '自动回复已保存',
|
||||||
|
'Reply statements column' => '回复语',
|
||||||
|
];
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\admin\model\split;
|
||||||
|
|
||||||
|
use app\common\library\CountryIso;
|
||||||
|
use think\Model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分流链接模型
|
||||||
|
*/
|
||||||
|
class Link extends Model
|
||||||
|
{
|
||||||
|
protected $name = 'split_link';
|
||||||
|
|
||||||
|
protected $autoWriteTimestamp = 'integer';
|
||||||
|
|
||||||
|
protected $createTime = 'createtime';
|
||||||
|
protected $updateTime = 'updatetime';
|
||||||
|
protected $deleteTime = false;
|
||||||
|
|
||||||
|
protected $append = [
|
||||||
|
'countries_text',
|
||||||
|
'auto_reply_text',
|
||||||
|
'ip_protect_text',
|
||||||
|
'random_shuffle_text',
|
||||||
|
'status_text',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function getIpProtectList(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'0' => __('Ip protect off'),
|
||||||
|
'1' => __('Ip protect on'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRandomShuffleList(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'0' => __('Random shuffle off'),
|
||||||
|
'1' => __('Random shuffle on'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getStatusList(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'normal' => __('Status normal'),
|
||||||
|
'hidden' => __('Status hidden'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setCountriesAttr($value): string
|
||||||
|
{
|
||||||
|
if (is_array($value)) {
|
||||||
|
return CountryIso::codesToStorage($value);
|
||||||
|
}
|
||||||
|
return CountryIso::codesToStorage(explode(',', (string)$value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分流链接码统一为小写
|
||||||
|
*/
|
||||||
|
public function setLinkCodeAttr($value): string
|
||||||
|
{
|
||||||
|
return strtolower(trim((string) $value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动回复:存储为一行一条(换行分隔)
|
||||||
|
*
|
||||||
|
* @param mixed $value
|
||||||
|
*/
|
||||||
|
public function setAutoReplyAttr($value): string
|
||||||
|
{
|
||||||
|
return \app\common\service\SplitAutoReplyService::formatStorage($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动回复语句数组
|
||||||
|
*
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
public function getAutoReplyLines(): array
|
||||||
|
{
|
||||||
|
return \app\common\service\SplitAutoReplyService::parseLines((string) $this->getAttr('auto_reply'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCountriesTextAttr($value, $data): string
|
||||||
|
{
|
||||||
|
return CountryIso::codesToText((string)($data['countries'] ?? ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列表「回复语」列展示(多行合并为单行,供省略号截断)
|
||||||
|
*/
|
||||||
|
public function getAutoReplyTextAttr($value, $data): string
|
||||||
|
{
|
||||||
|
$lines = \app\common\service\SplitAutoReplyService::parseLines((string)($data['auto_reply'] ?? ''));
|
||||||
|
if ($lines === []) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return implode(';', $lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getIpProtectTextAttr($value, $data): string
|
||||||
|
{
|
||||||
|
$value = $value !== '' && $value !== null ? $value : ($data['ip_protect'] ?? '');
|
||||||
|
$list = $this->getIpProtectList();
|
||||||
|
return $list[(string)$value] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRandomShuffleTextAttr($value, $data): string
|
||||||
|
{
|
||||||
|
$value = $value !== '' && $value !== null ? $value : ($data['random_shuffle'] ?? '');
|
||||||
|
$list = $this->getRandomShuffleList();
|
||||||
|
return $list[(string)$value] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getStatusTextAttr($value, $data): string
|
||||||
|
{
|
||||||
|
$value = $value ?: ($data['status'] ?? '');
|
||||||
|
$list = $this->getStatusList();
|
||||||
|
return $list[$value] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已选国家 ISO 代码数组(供编辑表单多选回显)
|
||||||
|
*
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
public function getSelectedCountries(): array
|
||||||
|
{
|
||||||
|
$countries = (string)$this->getAttr('countries');
|
||||||
|
if ($countries === '') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return CountryIso::normalizeCodes(explode(',', $countries));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\admin\validate\split;
|
||||||
|
|
||||||
|
use app\common\library\CountryIso;
|
||||||
|
use app\common\service\SplitLinkCodeService;
|
||||||
|
use think\Validate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分流链接验证器
|
||||||
|
*/
|
||||||
|
class Link extends Validate
|
||||||
|
{
|
||||||
|
protected $rule = [
|
||||||
|
'countries' => 'require|checkCountries',
|
||||||
|
'link_code' => 'require|checkLinkCode',
|
||||||
|
'description' => 'require|max:255',
|
||||||
|
'ip_protect' => 'in:0,1',
|
||||||
|
'random_shuffle' => 'in:0,1',
|
||||||
|
'status' => 'in:normal,hidden',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $message = [
|
||||||
|
'countries.require' => '请至少选择一个投放国家',
|
||||||
|
'link_code.require' => '请填写分流链接',
|
||||||
|
'description.require' => '请填写链接描述',
|
||||||
|
'description.max' => '链接描述不能超过255个字符',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $scene = [
|
||||||
|
'add' => ['countries', 'link_code', 'description', 'ip_protect', 'random_shuffle', 'status'],
|
||||||
|
'edit' => ['countries', 'description', 'ip_protect', 'random_shuffle', 'status'],
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed $value
|
||||||
|
* @return bool|string
|
||||||
|
*/
|
||||||
|
protected function checkCountries($value)
|
||||||
|
{
|
||||||
|
$codes = is_array($value) ? $value : explode(',', (string)$value);
|
||||||
|
if (CountryIso::normalizeCodes($codes) === []) {
|
||||||
|
return '请至少选择一个有效的投放国家';
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验分流链接格式与唯一性(仅新增)
|
||||||
|
*
|
||||||
|
* @param mixed $value
|
||||||
|
* @return bool|string
|
||||||
|
*/
|
||||||
|
protected function checkLinkCode($value)
|
||||||
|
{
|
||||||
|
$code = SplitLinkCodeService::normalize((string) $value);
|
||||||
|
if (!SplitLinkCodeService::isValidFormat($code)) {
|
||||||
|
return '分流链接须为9位小写字母(a-z)';
|
||||||
|
}
|
||||||
|
if (!SplitLinkCodeService::isAvailable($code)) {
|
||||||
|
return '该分流链接已被使用,请更换';
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<form id="add-form" class="form-horizontal" role="form" data-toggle="validator" method="POST" action="">
|
||||||
|
{:token()}
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="c-countries" class="control-label col-xs-12 col-sm-2">{:__('Countries')}:</label>
|
||||||
|
<div class="col-xs-12 col-sm-8">
|
||||||
|
<select id="c-countries" name="row[countries][]" class="form-control selectpicker" multiple data-live-search="true" data-rule="required">
|
||||||
|
{foreach name="countryList" item="vo" key="code"}
|
||||||
|
<option value="{$code|htmlentities}">{$vo|htmlentities}</option>
|
||||||
|
{/foreach}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="c-link_code" class="control-label col-xs-12 col-sm-2">{:__('Link_code')}:</label>
|
||||||
|
<div class="col-xs-12 col-sm-8">
|
||||||
|
<input id="c-link_code" data-rule="required" class="form-control" name="row[link_code]" type="text" value="{$defaultLinkCode|htmlentities}" maxlength="9" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="c-description" class="control-label col-xs-12 col-sm-2">{:__('Description')}:</label>
|
||||||
|
<div class="col-xs-12 col-sm-8">
|
||||||
|
<input id="c-description" data-rule="required" class="form-control" name="row[description]" type="text" value="" maxlength="255">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="c-ip_protect" class="control-label col-xs-12 col-sm-2">{:__('Ip_protect')}:</label>
|
||||||
|
<div class="col-xs-12 col-sm-8">
|
||||||
|
<select id="c-ip_protect" class="form-control selectpicker" name="row[ip_protect]">
|
||||||
|
{foreach name="ipProtectList" item="vo"}
|
||||||
|
<option value="{$key|htmlentities}" {in name="key" value="0"}selected{/in}>{$vo|htmlentities}</option>
|
||||||
|
{/foreach}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="c-random_shuffle" class="control-label col-xs-12 col-sm-2">{:__('Random_shuffle')}:</label>
|
||||||
|
<div class="col-xs-12 col-sm-8">
|
||||||
|
<select id="c-random_shuffle" class="form-control selectpicker" name="row[random_shuffle]">
|
||||||
|
{foreach name="randomShuffleList" item="vo"}
|
||||||
|
<option value="{$key|htmlentities}" {in name="key" value="0"}selected{/in}>{$vo|htmlentities}</option>
|
||||||
|
{/foreach}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="control-label col-xs-12 col-sm-2">{:__('Status')}:</label>
|
||||||
|
<div class="col-xs-12 col-sm-8">
|
||||||
|
<div class="radio">
|
||||||
|
{foreach name="statusList" item="vo"}
|
||||||
|
<label for="row[status]-{$key}"><input id="row[status]-{$key}" name="row[status]" type="radio" value="{$key|htmlentities}" {in name="key" value="normal"}checked{/in} /> {$vo|htmlentities}</label>
|
||||||
|
{/foreach}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group layer-footer">
|
||||||
|
<label class="control-label col-xs-12 col-sm-2"></label>
|
||||||
|
<div class="col-xs-12 col-sm-8">
|
||||||
|
<button type="submit" class="btn btn-primary btn-embossed disabled">{:__('OK')}</button>
|
||||||
|
<button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<form id="edit-form" class="form-horizontal" role="form" data-toggle="validator" method="POST" action="">
|
||||||
|
{:token()}
|
||||||
|
<input type="hidden" name="row[id]" value="{$row.id|htmlentities}">
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="c-countries" class="control-label col-xs-12 col-sm-2">{:__('Countries')}:</label>
|
||||||
|
<div class="col-xs-12 col-sm-8">
|
||||||
|
<select id="c-countries" name="row[countries][]" class="form-control selectpicker" multiple data-live-search="true" data-rule="required">
|
||||||
|
{foreach name="countryList" item="vo" key="code"}
|
||||||
|
<option value="{$code|htmlentities}" {in name="code" value="$selectedCountries"}selected{/in}>{$vo|htmlentities}</option>
|
||||||
|
{/foreach}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="c-link_code" class="control-label col-xs-12 col-sm-2">{:__('Link_code')}:</label>
|
||||||
|
<div class="col-xs-12 col-sm-8">
|
||||||
|
<input id="c-link_code" class="form-control" type="text" value="{$row.link_code|htmlentities}" readonly>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="c-description" class="control-label col-xs-12 col-sm-2">{:__('Description')}:</label>
|
||||||
|
<div class="col-xs-12 col-sm-8">
|
||||||
|
<input id="c-description" data-rule="required" class="form-control" name="row[description]" type="text" value="{$row.description|htmlentities}" maxlength="255">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="c-ip_protect" class="control-label col-xs-12 col-sm-2">{:__('Ip_protect')}:</label>
|
||||||
|
<div class="col-xs-12 col-sm-8">
|
||||||
|
<select id="c-ip_protect" class="form-control selectpicker" name="row[ip_protect]">
|
||||||
|
{foreach name="ipProtectList" item="vo"}
|
||||||
|
<option value="{$key|htmlentities}" {in name="key" value="$row.ip_protect"}selected{/in}>{$vo|htmlentities}</option>
|
||||||
|
{/foreach}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="c-random_shuffle" class="control-label col-xs-12 col-sm-2">{:__('Random_shuffle')}:</label>
|
||||||
|
<div class="col-xs-12 col-sm-8">
|
||||||
|
<select id="c-random_shuffle" class="form-control selectpicker" name="row[random_shuffle]">
|
||||||
|
{foreach name="randomShuffleList" item="vo"}
|
||||||
|
<option value="{$key|htmlentities}" {in name="key" value="$row.random_shuffle"}selected{/in}>{$vo|htmlentities}</option>
|
||||||
|
{/foreach}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="control-label col-xs-12 col-sm-2">{:__('Status')}:</label>
|
||||||
|
<div class="col-xs-12 col-sm-8">
|
||||||
|
<div class="radio">
|
||||||
|
{foreach name="statusList" item="vo"}
|
||||||
|
<label for="row[status]-{$key}"><input id="row[status]-{$key}" name="row[status]" type="radio" value="{$key|htmlentities}" {in name="key" value="$row.status"}checked{/in} /> {$vo|htmlentities}</label>
|
||||||
|
{/foreach}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group layer-footer">
|
||||||
|
<label class="control-label col-xs-12 col-sm-2"></label>
|
||||||
|
<div class="col-xs-12 col-sm-8">
|
||||||
|
<button type="submit" class="btn btn-primary btn-embossed disabled">{:__('OK')}</button>
|
||||||
|
<button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<div class="panel panel-default panel-intro">
|
||||||
|
|
||||||
|
<div class="panel-heading">
|
||||||
|
{:build_heading(null,FALSE)}
|
||||||
|
<ul class="nav nav-tabs" data-field="status">
|
||||||
|
<li class="{:$Think.get.status === null ? 'active' : ''}"><a href="#t-all" data-value="" data-toggle="tab">{:__('All')}</a></li>
|
||||||
|
{foreach name="statusList" item="vo"}
|
||||||
|
<li class="{:$Think.get.status === (string)$key ? 'active' : ''}"><a href="#t-{$key}" data-value="{$key}" data-toggle="tab">{$vo}</a></li>
|
||||||
|
{/foreach}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="panel-body">
|
||||||
|
<div id="myTabContent" class="tab-content">
|
||||||
|
<div class="tab-pane fade active in" id="one">
|
||||||
|
<div class="widget-body no-padding">
|
||||||
|
<div id="toolbar" class="toolbar">
|
||||||
|
<a href="javascript:;" class="btn btn-primary btn-refresh" title="{:__('Refresh')}" ><i class="fa fa-refresh"></i> </a>
|
||||||
|
<a href="javascript:;" class="btn btn-success btn-add {:$auth->check('split.link/add')?'':'hide'}" title="{:__('Add')}" ><i class="fa fa-plus"></i> {:__('Add')}</a>
|
||||||
|
<a href="javascript:;" class="btn btn-success btn-edit btn-disabled disabled {:$auth->check('split.link/edit')?'':'hide'}" title="{:__('Edit')}" ><i class="fa fa-pencil"></i> {:__('Edit')}</a>
|
||||||
|
<a href="javascript:;" class="btn btn-danger btn-del btn-disabled disabled {:$auth->check('split.link/del')?'':'hide'}" title="{:__('Delete')}" ><i class="fa fa-trash"></i> {:__('Delete')}</a>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="dropdown btn-group {:$auth->check('split.link/multi')?'':'hide'}">
|
||||||
|
<a class="btn btn-primary btn-more dropdown-toggle btn-disabled disabled" data-toggle="dropdown"><i class="fa fa-cog"></i> {:__('More')}</a>
|
||||||
|
<ul class="dropdown-menu text-left" role="menu">
|
||||||
|
{foreach name="statusList" item="vo"}
|
||||||
|
<li><a class="btn btn-link btn-multi btn-disabled disabled" href="javascript:" data-params="status={$key}">{:__('Set status to ' . $key)}</a></li>
|
||||||
|
{/foreach}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<table id="table" class="table table-striped table-bordered table-hover table-nowrap"
|
||||||
|
data-operate-edit="{:$auth->check('split.link/edit')}"
|
||||||
|
data-operate-del="{:$auth->check('split.link/del')}"
|
||||||
|
width="100%">
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\common\library;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ISO 3166-1 alpha-2 国家代码与中文名称
|
||||||
|
*/
|
||||||
|
class CountryIso
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 常用投放国家 ISO2 => 中文名
|
||||||
|
*
|
||||||
|
* @var array<string, string>
|
||||||
|
*/
|
||||||
|
private const COUNTRIES = [
|
||||||
|
'CN' => '中国',
|
||||||
|
'HK' => '中国香港',
|
||||||
|
'MO' => '中国澳门',
|
||||||
|
'TW' => '中国台湾',
|
||||||
|
'US' => '美国',
|
||||||
|
'CA' => '加拿大',
|
||||||
|
'GB' => '英国',
|
||||||
|
'DE' => '德国',
|
||||||
|
'FR' => '法国',
|
||||||
|
'IT' => '意大利',
|
||||||
|
'ES' => '西班牙',
|
||||||
|
'NL' => '荷兰',
|
||||||
|
'BE' => '比利时',
|
||||||
|
'CH' => '瑞士',
|
||||||
|
'AT' => '奥地利',
|
||||||
|
'SE' => '瑞典',
|
||||||
|
'NO' => '挪威',
|
||||||
|
'DK' => '丹麦',
|
||||||
|
'FI' => '芬兰',
|
||||||
|
'IE' => '爱尔兰',
|
||||||
|
'PT' => '葡萄牙',
|
||||||
|
'PL' => '波兰',
|
||||||
|
'CZ' => '捷克',
|
||||||
|
'HU' => '匈牙利',
|
||||||
|
'RO' => '罗马尼亚',
|
||||||
|
'GR' => '希腊',
|
||||||
|
'RU' => '俄罗斯',
|
||||||
|
'UA' => '乌克兰',
|
||||||
|
'TR' => '土耳其',
|
||||||
|
'IL' => '以色列',
|
||||||
|
'SA' => '沙特阿拉伯',
|
||||||
|
'AE' => '阿联酋',
|
||||||
|
'QA' => '卡塔尔',
|
||||||
|
'KW' => '科威特',
|
||||||
|
'IN' => '印度',
|
||||||
|
'PK' => '巴基斯坦',
|
||||||
|
'BD' => '孟加拉国',
|
||||||
|
'TH' => '泰国',
|
||||||
|
'VN' => '越南',
|
||||||
|
'MY' => '马来西亚',
|
||||||
|
'SG' => '新加坡',
|
||||||
|
'ID' => '印度尼西亚',
|
||||||
|
'PH' => '菲律宾',
|
||||||
|
'JP' => '日本',
|
||||||
|
'KR' => '韩国',
|
||||||
|
'AU' => '澳大利亚',
|
||||||
|
'NZ' => '新西兰',
|
||||||
|
'BR' => '巴西',
|
||||||
|
'MX' => '墨西哥',
|
||||||
|
'AR' => '阿根廷',
|
||||||
|
'CL' => '智利',
|
||||||
|
'CO' => '哥伦比亚',
|
||||||
|
'PE' => '秘鲁',
|
||||||
|
'ZA' => '南非',
|
||||||
|
'EG' => '埃及',
|
||||||
|
'NG' => '尼日利亚',
|
||||||
|
'KE' => '肯尼亚',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下拉选项 ISO2 => 中文名
|
||||||
|
*
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public static function getOptions(): array
|
||||||
|
{
|
||||||
|
return self::COUNTRIES;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验是否为合法 ISO2 代码
|
||||||
|
*/
|
||||||
|
public static function isValidCode(string $code): bool
|
||||||
|
{
|
||||||
|
$code = strtoupper(trim($code));
|
||||||
|
return isset(self::COUNTRIES[$code]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 规范化并过滤国家代码列表
|
||||||
|
*
|
||||||
|
* @param array<int, string>|string $codes
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
public static function normalizeCodes($codes): array
|
||||||
|
{
|
||||||
|
if (is_string($codes)) {
|
||||||
|
$codes = explode(',', $codes);
|
||||||
|
}
|
||||||
|
if (!is_array($codes)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$result = [];
|
||||||
|
foreach ($codes as $code) {
|
||||||
|
$code = strtoupper(trim((string)$code));
|
||||||
|
if ($code !== '' && self::isValidCode($code) && !in_array($code, $result, true)) {
|
||||||
|
$result[] = $code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort($result);
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 逗号分隔 ISO 代码转中文展示
|
||||||
|
*/
|
||||||
|
public static function codesToText(string $codes): string
|
||||||
|
{
|
||||||
|
if ($codes === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
$parts = [];
|
||||||
|
foreach (explode(',', $codes) as $code) {
|
||||||
|
$code = strtoupper(trim($code));
|
||||||
|
if ($code === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$parts[] = self::COUNTRIES[$code] ?? $code;
|
||||||
|
}
|
||||||
|
return implode(',', $parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数组转存储字符串
|
||||||
|
*
|
||||||
|
* @param array<int, string> $codes
|
||||||
|
*/
|
||||||
|
public static function codesToStorage(array $codes): string
|
||||||
|
{
|
||||||
|
return implode(',', self::normalizeCodes($codes));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace app\common\model;
|
namespace app\common\model;
|
||||||
|
|
||||||
use think\Model;
|
use think\Model;
|
||||||
@@ -92,12 +94,25 @@ class Config extends Model
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 读取分类分组列表
|
* 读取分类分组列表(优先数据库 configgroup,避免 site.php 未同步时分组缺失)
|
||||||
|
*
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
public static function getGroupList()
|
public static function getGroupList()
|
||||||
{
|
{
|
||||||
$groupList = config('site.configgroup');
|
$groupList = config('site.configgroup');
|
||||||
|
if (!is_array($groupList)) {
|
||||||
|
$groupList = [];
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$dbJson = \think\Db::name('config')->where('name', 'configgroup')->value('value');
|
||||||
|
$dbList = json_decode((string) $dbJson, true);
|
||||||
|
if (is_array($dbList) && $dbList !== []) {
|
||||||
|
$groupList = $dbList;
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// 数据库不可用时回退 site.php
|
||||||
|
}
|
||||||
foreach ($groupList as $k => &$v) {
|
foreach ($groupList as $k => &$v) {
|
||||||
$v = __($v);
|
$v = __($v);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\common\service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分流链接自动回复语句(一行一条)
|
||||||
|
*/
|
||||||
|
class SplitAutoReplyService
|
||||||
|
{
|
||||||
|
/** 最多条数 */
|
||||||
|
private const MAX_LINES = 200;
|
||||||
|
|
||||||
|
/** 单条最大字符数 */
|
||||||
|
private const MAX_LINE_LENGTH = 500;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析为多行文本数组
|
||||||
|
*
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
public static function parseLines(string $raw): array
|
||||||
|
{
|
||||||
|
$raw = trim($raw);
|
||||||
|
if ($raw === '') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$parts = preg_split('/\r\n|\r|\n/', $raw) ?: [];
|
||||||
|
$lines = [];
|
||||||
|
foreach ($parts as $part) {
|
||||||
|
$line = trim((string) $part);
|
||||||
|
if ($line === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (strlen($line) > self::MAX_LINE_LENGTH) {
|
||||||
|
$line = mb_substr($line, 0, self::MAX_LINE_LENGTH, 'UTF-8');
|
||||||
|
}
|
||||||
|
$lines[] = $line;
|
||||||
|
if (count($lines) >= self::MAX_LINES) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化为数据库存储(换行分隔)
|
||||||
|
*
|
||||||
|
* @param string|array<int, string> $value
|
||||||
|
*/
|
||||||
|
public static function formatStorage($value): string
|
||||||
|
{
|
||||||
|
if (is_array($value)) {
|
||||||
|
$lines = $value;
|
||||||
|
} else {
|
||||||
|
$lines = self::parseLines((string) $value);
|
||||||
|
}
|
||||||
|
return implode("\n", $lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 供表单 textarea 回显
|
||||||
|
*/
|
||||||
|
public static function formatDisplay(string $stored): string
|
||||||
|
{
|
||||||
|
$lines = self::parseLines($stored);
|
||||||
|
return implode("\n", $lines);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\common\service;
|
||||||
|
|
||||||
|
use app\admin\model\split\Link;
|
||||||
|
use think\Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分流链接码生成服务
|
||||||
|
*/
|
||||||
|
class SplitLinkCodeService
|
||||||
|
{
|
||||||
|
private const CODE_LENGTH = 9;
|
||||||
|
|
||||||
|
private const CHARSET = 'abcdefghijklmnopqrstuvwxyz';
|
||||||
|
|
||||||
|
private const MAX_ATTEMPTS = 50;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 规范化链接码(小写、去空格)
|
||||||
|
*/
|
||||||
|
public static function normalize(string $code): string
|
||||||
|
{
|
||||||
|
return strtolower(trim($code));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否为合法 9 位小写字母链接码
|
||||||
|
*/
|
||||||
|
public static function isValidFormat(string $code): bool
|
||||||
|
{
|
||||||
|
return (bool) preg_match('/^[a-z]{9}$/', $code);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 链接码是否未被占用
|
||||||
|
*/
|
||||||
|
public static function isAvailable(string $code, int $excludeId = 0): bool
|
||||||
|
{
|
||||||
|
$code = self::normalize($code);
|
||||||
|
$query = Link::where('link_code', $code);
|
||||||
|
if ($excludeId > 0) {
|
||||||
|
$query->where('id', '<>', $excludeId);
|
||||||
|
}
|
||||||
|
return !$query->find();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成唯一 9 位小写字母链接码
|
||||||
|
*
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function generateUnique(): string
|
||||||
|
{
|
||||||
|
for ($i = 0; $i < self::MAX_ATTEMPTS; $i++) {
|
||||||
|
$code = $this->generateRandom();
|
||||||
|
if (!Link::where('link_code', $code)->find()) {
|
||||||
|
return $code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Exception('分流链接生成失败,请稍后重试');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateRandom(): string
|
||||||
|
{
|
||||||
|
$chars = self::CHARSET;
|
||||||
|
$max = strlen($chars) - 1;
|
||||||
|
$code = '';
|
||||||
|
for ($i = 0; $i < self::CODE_LENGTH; $i++) {
|
||||||
|
$code .= $chars[random_int(0, $max)];
|
||||||
|
}
|
||||||
|
return $code;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\common\service;
|
||||||
|
|
||||||
|
use app\admin\model\Domain as DomainModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 平台分配域名解析(支持多域名,一行一个)
|
||||||
|
*/
|
||||||
|
class SplitPlatformDomainService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 解析配置文本为合法根域名列表(去重、小写)
|
||||||
|
*
|
||||||
|
* @param string $raw 换行或逗号分隔的域名文本
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
public static function parseList(string $raw): array
|
||||||
|
{
|
||||||
|
$raw = trim($raw);
|
||||||
|
if ($raw === '') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$parts = preg_split('/[\r\n,]+/', $raw) ?: [];
|
||||||
|
$domains = [];
|
||||||
|
foreach ($parts as $part) {
|
||||||
|
$domain = DomainModel::normalizeDomain(trim((string) $part));
|
||||||
|
if ($domain === '' || isset($domains[$domain])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!DomainModel::isValidRootDomain($domain)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$domains[$domain] = $domain;
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values($domains);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将域名列表格式化为配置存储文本(一行一个)
|
||||||
|
*
|
||||||
|
* @param array<int, string> $domains
|
||||||
|
*/
|
||||||
|
public static function formatList(array $domains): string
|
||||||
|
{
|
||||||
|
$lines = [];
|
||||||
|
foreach ($domains as $domain) {
|
||||||
|
$domain = DomainModel::normalizeDomain((string) $domain);
|
||||||
|
if ($domain !== '' && DomainModel::isValidRootDomain($domain)) {
|
||||||
|
$lines[$domain] = $domain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return implode("\n", array_values($lines));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,6 +27,7 @@ return array (
|
|||||||
'dictionary' => 'Dictionary',
|
'dictionary' => 'Dictionary',
|
||||||
'user' => 'User',
|
'user' => 'User',
|
||||||
'example' => 'Example',
|
'example' => 'Example',
|
||||||
|
'split' => '分流设置',
|
||||||
),
|
),
|
||||||
'mail_type' => '1',
|
'mail_type' => '1',
|
||||||
'mail_smtp_host' => 'smtp.qq.com',
|
'mail_smtp_host' => 'smtp.qq.com',
|
||||||
@@ -41,4 +42,8 @@ return array (
|
|||||||
'category2' => 'Category2',
|
'category2' => 'Category2',
|
||||||
'custom' => 'Custom',
|
'custom' => 'Custom',
|
||||||
),
|
),
|
||||||
|
'split_platform_domain' => 'link1.com
|
||||||
|
link2.com
|
||||||
|
link3.com
|
||||||
|
link4.com',
|
||||||
);
|
);
|
||||||
|
|||||||
Executable
+13
@@ -0,0 +1,13 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 部署分流管理-链接管理模块(需 root)
|
||||||
|
set -e
|
||||||
|
BASE="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
|
if [ "$(id -u)" -ne 0 ]; then
|
||||||
|
echo "请使用 root 执行: sudo bash $0"
|
||||||
|
echo "或: sudo php $BASE/runtime/deploy_split_link.php"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
php "$BASE/runtime/deploy_split_link.php"
|
||||||
|
echo "菜单已写入数据库,请在权限管理中勾选「分流管理」相关节点。"
|
||||||
Executable
+19
@@ -0,0 +1,19 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 修复系统配置中「分流设置 / 平台分配域名」不显示的问题(需 root)
|
||||||
|
set -e
|
||||||
|
BASE="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
|
if [ "$(id -u)" -ne 0 ]; then
|
||||||
|
echo "请使用 root 执行: sudo bash $0"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
php "$BASE/runtime/install_split_config_standalone.php"
|
||||||
|
|
||||||
|
cp "$BASE/patches/application/common/model/Config.php" "$BASE/application/common/model/Config.php"
|
||||||
|
chown www:www "$BASE/application/common/model/Config.php"
|
||||||
|
php -l "$BASE/application/common/model/Config.php"
|
||||||
|
|
||||||
|
chown www:www "$BASE/application/extra/site.php" 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "完成。请刷新后台「常规管理 -> 系统配置」,应能看到「分流设置」标签及「平台分配域名」字段。"
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
-- 分流链接:平台分配域名(支持多个,一行一个)
|
||||||
|
SET NAMES utf8mb4;
|
||||||
|
|
||||||
|
INSERT INTO `fa_config` (`name`, `group`, `title`, `tip`, `type`, `visible`, `value`, `content`, `rule`, `extend`, `setting`)
|
||||||
|
SELECT 'split_platform_domain', 'split', '平台分配域名', '一行填写一个根域名,如 example.com;不含 http:// 与路径,保存后在分流链接弹窗中可选用', 'text', '', '', '', '', ' rows="8" placeholder="example.com another.com"', NULL
|
||||||
|
FROM DUAL
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM `fa_config` WHERE `name` = 'split_platform_domain' LIMIT 1);
|
||||||
|
|
||||||
|
UPDATE `fa_config`
|
||||||
|
SET `group` = 'split',
|
||||||
|
`type` = 'text',
|
||||||
|
`title` = '平台分配域名',
|
||||||
|
`tip` = '一行填写一个根域名,如 example.com;不含 http:// 与路径,保存后在分流链接弹窗中可选用',
|
||||||
|
`extend` = ' rows="8" placeholder="example.com another.com"'
|
||||||
|
WHERE `name` = 'split_platform_domain';
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
-- 分流管理 - 链接管理表
|
||||||
|
SET NAMES utf8mb4;
|
||||||
|
SET FOREIGN_KEY_CHECKS = 0;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS `fa_split_link`;
|
||||||
|
CREATE TABLE `fa_split_link` (
|
||||||
|
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
|
||||||
|
`admin_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '管理员ID',
|
||||||
|
`countries` varchar(500) NOT NULL DEFAULT '' COMMENT '投放国家(ISO2逗号分隔)',
|
||||||
|
`link_code` char(9) NOT NULL DEFAULT '' COMMENT '分流链接(9位小写字母)',
|
||||||
|
`description` varchar(255) NOT NULL DEFAULT '' COMMENT '链接描述',
|
||||||
|
`auto_reply` text COMMENT '自动回复语句(一行一条)',
|
||||||
|
`ip_protect` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT 'IP防护:0=关闭,1=开启',
|
||||||
|
`random_shuffle` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '随机打乱:0=关闭,1=开启',
|
||||||
|
`status` enum('normal','hidden') NOT NULL DEFAULT 'normal' COMMENT '状态:normal=正常,hidden=停用',
|
||||||
|
`createtime` bigint(16) DEFAULT NULL COMMENT '创建时间',
|
||||||
|
`updatetime` bigint(16) DEFAULT NULL COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `link_code` (`link_code`),
|
||||||
|
KEY `admin_id` (`admin_id`),
|
||||||
|
KEY `status` (`status`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='分流链接表';
|
||||||
|
|
||||||
|
SET FOREIGN_KEY_CHECKS = 1;
|
||||||
|
|
||||||
|
INSERT INTO `fa_auth_rule` (`type`, `pid`, `name`, `title`, `icon`, `condition`, `remark`, `ismenu`, `createtime`, `updatetime`, `weigh`, `status`)
|
||||||
|
SELECT 'file', 0, 'split', '分流管理', 'fa fa-random', '', '', 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), 0, 'normal'
|
||||||
|
FROM DUAL
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM `fa_auth_rule` WHERE `name` = 'split' LIMIT 1);
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
-- 分流链接:自动回复语句字段(一行一条,关联 link_code)
|
||||||
|
SET NAMES utf8mb4;
|
||||||
|
|
||||||
|
ALTER TABLE `fa_split_link`
|
||||||
|
ADD COLUMN `auto_reply` text COMMENT '自动回复语句(一行一条)' AFTER `description`;
|
||||||
|
|
||||||
|
INSERT INTO `fa_auth_rule` (`type`, `pid`, `name`, `title`, `icon`, `condition`, `remark`, `ismenu`, `createtime`, `updatetime`, `weigh`, `status`)
|
||||||
|
SELECT 'file', r.id, 'split.link/autoreply', '自动回复', 'fa fa-circle-o', '', '', 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), 0, 'normal'
|
||||||
|
FROM `fa_auth_rule` r
|
||||||
|
WHERE r.name = 'split.link' AND r.ismenu = 0
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM `fa_auth_rule` WHERE `name` = 'split.link/autoreply' LIMIT 1)
|
||||||
|
LIMIT 1;
|
||||||
@@ -0,0 +1,367 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\admin\controller\split;
|
||||||
|
|
||||||
|
use app\admin\model\Domain as DomainModel;
|
||||||
|
use app\common\controller\Backend;
|
||||||
|
use app\common\library\CountryIso;
|
||||||
|
use app\common\service\SplitAutoReplyService;
|
||||||
|
use app\common\service\SplitLinkCodeService;
|
||||||
|
use app\common\service\SplitPlatformDomainService;
|
||||||
|
use think\Db;
|
||||||
|
use think\Exception;
|
||||||
|
use think\exception\DbException;
|
||||||
|
use think\exception\PDOException;
|
||||||
|
use think\exception\ValidateException;
|
||||||
|
use think\response\Json;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 链接管理
|
||||||
|
*
|
||||||
|
* @icon fa fa-link
|
||||||
|
* @remark 分流链接管理,自动生成9位字母链接码
|
||||||
|
*/
|
||||||
|
class Link extends Backend
|
||||||
|
{
|
||||||
|
/** @var \app\admin\model\split\Link */
|
||||||
|
protected $model = null;
|
||||||
|
|
||||||
|
protected $searchFields = 'link_code,description';
|
||||||
|
|
||||||
|
protected $dataLimit = 'personal';
|
||||||
|
|
||||||
|
protected $modelValidate = true;
|
||||||
|
|
||||||
|
protected $modelSceneValidate = true;
|
||||||
|
|
||||||
|
/** @var string[] 无需鉴权的操作 */
|
||||||
|
protected $noNeedRight = ['script', 'copyinfo'];
|
||||||
|
|
||||||
|
/** @var string patches 视图目录(未完整部署时使用) */
|
||||||
|
private const PATCH_VIEW_DIR = 'patches/application/admin/view/split/link/';
|
||||||
|
|
||||||
|
public function _initialize()
|
||||||
|
{
|
||||||
|
parent::_initialize();
|
||||||
|
|
||||||
|
$lang = $this->request->langset();
|
||||||
|
$lang = preg_match('/^([a-zA-Z\-_]{2,10})$/i', $lang) ? $lang : 'zh-cn';
|
||||||
|
$langFile = ROOT_PATH . 'patches/application/admin/lang/' . $lang . '/split/link.php';
|
||||||
|
if (is_file($langFile)) {
|
||||||
|
\think\Lang::load($langFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->model = new \app\admin\model\split\Link();
|
||||||
|
$countryList = CountryIso::getOptions();
|
||||||
|
$this->view->assign('countryList', $countryList);
|
||||||
|
$this->view->assign('ipProtectList', $this->model->getIpProtectList());
|
||||||
|
$this->view->assign('randomShuffleList', $this->model->getRandomShuffleList());
|
||||||
|
$this->view->assign('statusList', $this->model->getStatusList());
|
||||||
|
$this->assignconfig('countryList', $countryList);
|
||||||
|
$this->assignconfig('ipProtectList', $this->model->getIpProtectList());
|
||||||
|
$this->assignconfig('randomShuffleList', $this->model->getRandomShuffleList());
|
||||||
|
$this->assignconfig('statusList', $this->model->getStatusList());
|
||||||
|
|
||||||
|
$this->setupPatchFrontend();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 未部署 JS 到 public 时,或 patches 版本较新时,jsname 指向 script 接口
|
||||||
|
*/
|
||||||
|
private function setupPatchFrontend(): void
|
||||||
|
{
|
||||||
|
$patchJs = ROOT_PATH . 'patches/public/assets/js/backend/split/link.js';
|
||||||
|
$publicJs = ROOT_PATH . 'public/assets/js/backend/split/link.js';
|
||||||
|
$usePatchJs = is_file($patchJs) && (
|
||||||
|
!is_file($publicJs) || filemtime($patchJs) >= filemtime($publicJs)
|
||||||
|
);
|
||||||
|
if (!$usePatchJs) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$cfg = is_array($this->view->config ?? null) ? $this->view->config : [];
|
||||||
|
$version = (string) \think\Config::get('site.version');
|
||||||
|
$scriptUrl = (string) url('split.link/script', ['v' => $version], false, true);
|
||||||
|
if (strpos($scriptUrl, '?') === false) {
|
||||||
|
$scriptUrl .= '?v=' . $version;
|
||||||
|
}
|
||||||
|
if (strpos($scriptUrl, '://') === false) {
|
||||||
|
$scriptUrl = $this->request->domain() . $scriptUrl;
|
||||||
|
}
|
||||||
|
$cfg['jsname'] = $scriptUrl;
|
||||||
|
$this->view->assign('config', $cfg);
|
||||||
|
$this->view->config = $cfg;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染模板(优先 patches 视图,回退 application)
|
||||||
|
*/
|
||||||
|
private function fetchPatch(string $template): string
|
||||||
|
{
|
||||||
|
$patchFile = ROOT_PATH . self::PATCH_VIEW_DIR . $template . '.html';
|
||||||
|
$appFile = APP_PATH . 'admin/view/split/link/' . $template . '.html';
|
||||||
|
if (is_file($patchFile)) {
|
||||||
|
$file = $patchFile;
|
||||||
|
} elseif (is_file($appFile)) {
|
||||||
|
$file = $appFile;
|
||||||
|
} else {
|
||||||
|
$this->error('模板文件不存在');
|
||||||
|
}
|
||||||
|
return (string) $this->view->fetch($file);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列表
|
||||||
|
*
|
||||||
|
* @return string|Json
|
||||||
|
* @throws DbException
|
||||||
|
* @throws \think\Exception
|
||||||
|
*/
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$this->request->filter(['strip_tags', 'trim']);
|
||||||
|
if (false === $this->request->isAjax()) {
|
||||||
|
return $this->fetchPatch('index');
|
||||||
|
}
|
||||||
|
if ($this->request->request('keyField')) {
|
||||||
|
return $this->selectpage();
|
||||||
|
}
|
||||||
|
[$where, $sort, $order, $offset, $limit] = $this->buildparams();
|
||||||
|
$list = $this->model
|
||||||
|
->where($where)
|
||||||
|
->order($sort, $order)
|
||||||
|
->paginate($limit);
|
||||||
|
$result = ['total' => $list->total(), 'rows' => $list->items()];
|
||||||
|
return json($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加(打开表单时预生成分流链接,可编辑后保存)
|
||||||
|
*/
|
||||||
|
public function add()
|
||||||
|
{
|
||||||
|
if (false === $this->request->isPost()) {
|
||||||
|
try {
|
||||||
|
$defaultLinkCode = (new SplitLinkCodeService())->generateUnique();
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->error($e->getMessage());
|
||||||
|
}
|
||||||
|
$this->view->assign('defaultLinkCode', $defaultLinkCode);
|
||||||
|
return $this->fetchPatch('add');
|
||||||
|
}
|
||||||
|
|
||||||
|
$params = $this->request->post('row/a', []);
|
||||||
|
if ($params === []) {
|
||||||
|
$this->error(__('Parameter %s can not be empty', ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
$params = $this->preExcludeFields($params);
|
||||||
|
|
||||||
|
if (isset($params['link_code'])) {
|
||||||
|
$params['link_code'] = SplitLinkCodeService::normalize((string) $params['link_code']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->dataLimit && $this->dataLimitFieldAutoFill) {
|
||||||
|
$params[$this->dataLimitField] = $this->auth->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = false;
|
||||||
|
Db::startTrans();
|
||||||
|
try {
|
||||||
|
if ($this->modelValidate) {
|
||||||
|
$name = str_replace('\\model\\', '\\validate\\', get_class($this->model));
|
||||||
|
$validate = $this->modelSceneValidate ? $name . '.add' : $name;
|
||||||
|
$this->model->validateFailException()->validate($validate, $params);
|
||||||
|
}
|
||||||
|
$result = $this->model->allowField(true)->save($params);
|
||||||
|
Db::commit();
|
||||||
|
} catch (ValidateException $e) {
|
||||||
|
Db::rollback();
|
||||||
|
$this->error($e->getMessage());
|
||||||
|
} catch (PDOException|Exception $e) {
|
||||||
|
Db::rollback();
|
||||||
|
$this->error($e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($result === false) {
|
||||||
|
$this->error(__('No rows were inserted'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->success();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 复制分流链接弹窗所需数据(平台域名 + 我的域名列表)
|
||||||
|
*
|
||||||
|
* @return Json
|
||||||
|
*/
|
||||||
|
public function copyinfo(): Json
|
||||||
|
{
|
||||||
|
$platformDomains = $this->getPlatformDomains();
|
||||||
|
|
||||||
|
$domainQuery = (new DomainModel())->order('id', 'desc');
|
||||||
|
if ($this->dataLimit) {
|
||||||
|
$adminIds = $this->getDataLimitAdminIds();
|
||||||
|
if (is_array($adminIds)) {
|
||||||
|
$domainQuery->where($this->dataLimitField, 'in', $adminIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$myDomains = [];
|
||||||
|
foreach ($domainQuery->select() as $domainRow) {
|
||||||
|
$myDomains[] = [
|
||||||
|
'id' => (int) $domainRow['id'],
|
||||||
|
'domain' => (string) $domainRow['domain'],
|
||||||
|
'ns_status' => (string) $domainRow['ns_status'],
|
||||||
|
'ns_status_text' => (string) $domainRow['ns_status_text'],
|
||||||
|
'dns_status' => (string) $domainRow['dns_status'],
|
||||||
|
'dns_status_text' => (string) $domainRow['dns_status_text'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->success('', null, [
|
||||||
|
'platform_domains' => $platformDomains,
|
||||||
|
'my_domains' => $myDomains,
|
||||||
|
'domain_index_url' => (string) url('domain/index'),
|
||||||
|
'domain_add_url' => (string) url('domain/add'),
|
||||||
|
'config_index_url' => (string) url('general/config/index'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取平台分配域名列表(优先 site 配置,回退数据库)
|
||||||
|
*
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
private function getPlatformDomains(): array
|
||||||
|
{
|
||||||
|
$raw = trim((string) \think\Config::get('site.split_platform_domain'));
|
||||||
|
if ($raw === '') {
|
||||||
|
$raw = trim((string) Db::name('config')->where('name', 'split_platform_domain')->value('value'));
|
||||||
|
}
|
||||||
|
return SplitPlatformDomainService::parseList($raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动回复:读取 / 保存与当前链接关联的回复语句
|
||||||
|
*
|
||||||
|
* @param string|null $ids 链接 ID
|
||||||
|
* @return Json
|
||||||
|
* @throws DbException
|
||||||
|
*/
|
||||||
|
public function autoreply($ids = null): Json
|
||||||
|
{
|
||||||
|
$row = $this->model->get($ids);
|
||||||
|
if (!$row) {
|
||||||
|
$this->error(__('No Results were found'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$adminIds = $this->getDataLimitAdminIds();
|
||||||
|
if (is_array($adminIds) && !in_array((int) $row[$this->dataLimitField], $adminIds, true)) {
|
||||||
|
$this->error(__('You have no permission'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->request->isPost()) {
|
||||||
|
$autoReply = (string) $this->request->post('auto_reply', '');
|
||||||
|
$lines = SplitAutoReplyService::parseLines($autoReply);
|
||||||
|
try {
|
||||||
|
$row->save([
|
||||||
|
'auto_reply' => SplitAutoReplyService::formatStorage($lines),
|
||||||
|
]);
|
||||||
|
} catch (PDOException|Exception $e) {
|
||||||
|
$this->error($e->getMessage());
|
||||||
|
}
|
||||||
|
$this->success(__('Auto reply saved'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->success('', null, [
|
||||||
|
'id' => (int) $row['id'],
|
||||||
|
'link_code' => (string) $row['link_code'],
|
||||||
|
'auto_reply' => SplitAutoReplyService::formatDisplay((string) $row->getAttr('auto_reply')),
|
||||||
|
'line_count' => count($row->getAutoReplyLines()),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑(分流链接码不可修改)
|
||||||
|
*
|
||||||
|
* @param string|null $ids
|
||||||
|
* @return string
|
||||||
|
* @throws DbException
|
||||||
|
*/
|
||||||
|
public function edit($ids = null)
|
||||||
|
{
|
||||||
|
$row = $this->model->get($ids);
|
||||||
|
if (!$row) {
|
||||||
|
$this->error(__('No Results were found'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$adminIds = $this->getDataLimitAdminIds();
|
||||||
|
if (is_array($adminIds) && !in_array($row[$this->dataLimitField], $adminIds)) {
|
||||||
|
$this->error(__('You have no permission'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (false === $this->request->isPost()) {
|
||||||
|
$this->view->assign('row', $row);
|
||||||
|
$this->view->assign('selectedCountries', $row->getSelectedCountries());
|
||||||
|
return $this->fetchPatch('edit');
|
||||||
|
}
|
||||||
|
|
||||||
|
$params = $this->request->post('row/a', []);
|
||||||
|
if ($params === []) {
|
||||||
|
$this->error(__('Parameter %s can not be empty', ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
$params = $this->preExcludeFields($params);
|
||||||
|
unset($params['link_code']);
|
||||||
|
|
||||||
|
$result = false;
|
||||||
|
Db::startTrans();
|
||||||
|
try {
|
||||||
|
if ($this->modelValidate) {
|
||||||
|
$name = str_replace('\\model\\', '\\validate\\', get_class($this->model));
|
||||||
|
$validate = $this->modelSceneValidate ? $name . '.edit' : $name;
|
||||||
|
$row->validateFailException()->validate($validate, $params);
|
||||||
|
}
|
||||||
|
$result = $row->allowField(true)->save($params);
|
||||||
|
Db::commit();
|
||||||
|
} catch (ValidateException $e) {
|
||||||
|
Db::rollback();
|
||||||
|
$this->error($e->getMessage());
|
||||||
|
} catch (PDOException|Exception $e) {
|
||||||
|
Db::rollback();
|
||||||
|
$this->error($e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($result === false) {
|
||||||
|
$this->error(__('No rows were updated'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->success();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 输出后台列表/表单 JS(patches 未部署到 public 时由 RequireJS 加载)
|
||||||
|
*/
|
||||||
|
public function script(): void
|
||||||
|
{
|
||||||
|
$jsFile = ROOT_PATH . 'patches/public/assets/js/backend/split/link.js';
|
||||||
|
if (!is_file($jsFile)) {
|
||||||
|
$jsFile = ROOT_PATH . 'public/assets/js/backend/split/link.js';
|
||||||
|
}
|
||||||
|
if (!is_file($jsFile)) {
|
||||||
|
$this->error('脚本文件不存在');
|
||||||
|
}
|
||||||
|
$content = file_get_contents($jsFile);
|
||||||
|
if ($content === false) {
|
||||||
|
$this->error('读取脚本失败');
|
||||||
|
}
|
||||||
|
$response = response($content, 200, [
|
||||||
|
'Content-Type' => 'application/javascript; charset=utf-8',
|
||||||
|
'Cache-Control' => 'public, max-age=3600',
|
||||||
|
]);
|
||||||
|
throw new \think\exception\HttpResponseException($response);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
'Countries' => '投放国家',
|
||||||
|
'Countries help' => '可多选,存储为 ISO 两位国家代码',
|
||||||
|
'Link_code' => '分流链接',
|
||||||
|
'Link code auto tip' => '保存后系统自动生成 9 位小写字母链接码',
|
||||||
|
'Description' => '链接描述',
|
||||||
|
'Ip_protect' => 'IP防护',
|
||||||
|
'Random_shuffle' => '随机打乱',
|
||||||
|
'Status' => '状态',
|
||||||
|
'Createtime' => '创建时间',
|
||||||
|
'Ip protect off' => '关闭',
|
||||||
|
'Ip protect on' => '开启',
|
||||||
|
'Random shuffle off' => '关闭',
|
||||||
|
'Random shuffle on' => '开启',
|
||||||
|
'Status normal' => '正常',
|
||||||
|
'Status hidden' => '停用',
|
||||||
|
'Copy split link' => '复制分流链接',
|
||||||
|
'Manage my domains' => '管理我的域名',
|
||||||
|
'Select main domain' => '选择主域名',
|
||||||
|
'Platform assigned domain' => '平台分配域名',
|
||||||
|
'My domains' => '我的域名',
|
||||||
|
'Split platform domain empty' => '尚未配置平台分配域名,请前往',
|
||||||
|
'Go system config' => '系统设置',
|
||||||
|
'Split my domain empty' => '暂无域名,请先在域名管理中添加',
|
||||||
|
'Split domain prefix tip' => '想使用自己的域名作为分流前缀,可以先去「我的域名」完成接入。',
|
||||||
|
'Go add domain' => '去添加',
|
||||||
|
'Generated result' => '生成结果',
|
||||||
|
'Copy' => '复制',
|
||||||
|
'Copy success' => '复制成功',
|
||||||
|
'Copy failed' => '复制失败',
|
||||||
|
'Open test' => '打开测试',
|
||||||
|
'Close' => '关闭',
|
||||||
|
'Split url empty' => '请先选择有效主域名',
|
||||||
|
'System config' => '系统配置',
|
||||||
|
'Auto reply' => '自动回复',
|
||||||
|
'Reply statements' => '回复语句',
|
||||||
|
'Reply statements tip'=> '一行填写一条回复语句,保存后与当前分流链接关联',
|
||||||
|
'Auto reply saved' => '自动回复已保存',
|
||||||
|
'Reply statements column' => '回复语',
|
||||||
|
];
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\admin\model\split;
|
||||||
|
|
||||||
|
use app\common\library\CountryIso;
|
||||||
|
use think\Model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分流链接模型
|
||||||
|
*/
|
||||||
|
class Link extends Model
|
||||||
|
{
|
||||||
|
protected $name = 'split_link';
|
||||||
|
|
||||||
|
protected $autoWriteTimestamp = 'integer';
|
||||||
|
|
||||||
|
protected $createTime = 'createtime';
|
||||||
|
protected $updateTime = 'updatetime';
|
||||||
|
protected $deleteTime = false;
|
||||||
|
|
||||||
|
protected $append = [
|
||||||
|
'countries_text',
|
||||||
|
'auto_reply_text',
|
||||||
|
'ip_protect_text',
|
||||||
|
'random_shuffle_text',
|
||||||
|
'status_text',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function getIpProtectList(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'0' => __('Ip protect off'),
|
||||||
|
'1' => __('Ip protect on'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRandomShuffleList(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'0' => __('Random shuffle off'),
|
||||||
|
'1' => __('Random shuffle on'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getStatusList(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'normal' => __('Status normal'),
|
||||||
|
'hidden' => __('Status hidden'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setCountriesAttr($value): string
|
||||||
|
{
|
||||||
|
if (is_array($value)) {
|
||||||
|
return CountryIso::codesToStorage($value);
|
||||||
|
}
|
||||||
|
return CountryIso::codesToStorage(explode(',', (string)$value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分流链接码统一为小写
|
||||||
|
*/
|
||||||
|
public function setLinkCodeAttr($value): string
|
||||||
|
{
|
||||||
|
return strtolower(trim((string) $value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动回复:存储为一行一条(换行分隔)
|
||||||
|
*
|
||||||
|
* @param mixed $value
|
||||||
|
*/
|
||||||
|
public function setAutoReplyAttr($value): string
|
||||||
|
{
|
||||||
|
return \app\common\service\SplitAutoReplyService::formatStorage($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动回复语句数组
|
||||||
|
*
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
public function getAutoReplyLines(): array
|
||||||
|
{
|
||||||
|
return \app\common\service\SplitAutoReplyService::parseLines((string) $this->getAttr('auto_reply'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCountriesTextAttr($value, $data): string
|
||||||
|
{
|
||||||
|
return CountryIso::codesToText((string)($data['countries'] ?? ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列表「回复语」列预览(最多 50 字 + ...,悬停用原始 auto_reply 换行展示)
|
||||||
|
*/
|
||||||
|
public function getAutoReplyTextAttr($value, $data): string
|
||||||
|
{
|
||||||
|
return \app\common\service\SplitAutoReplyService::previewForList((string) ($data['auto_reply'] ?? ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getIpProtectTextAttr($value, $data): string
|
||||||
|
{
|
||||||
|
$value = $value !== '' && $value !== null ? $value : ($data['ip_protect'] ?? '');
|
||||||
|
$list = $this->getIpProtectList();
|
||||||
|
return $list[(string)$value] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRandomShuffleTextAttr($value, $data): string
|
||||||
|
{
|
||||||
|
$value = $value !== '' && $value !== null ? $value : ($data['random_shuffle'] ?? '');
|
||||||
|
$list = $this->getRandomShuffleList();
|
||||||
|
return $list[(string)$value] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getStatusTextAttr($value, $data): string
|
||||||
|
{
|
||||||
|
$value = $value ?: ($data['status'] ?? '');
|
||||||
|
$list = $this->getStatusList();
|
||||||
|
return $list[$value] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已选国家 ISO 代码数组(供编辑表单多选回显)
|
||||||
|
*
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
public function getSelectedCountries(): array
|
||||||
|
{
|
||||||
|
$countries = (string)$this->getAttr('countries');
|
||||||
|
if ($countries === '') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return CountryIso::normalizeCodes(explode(',', $countries));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\admin\validate\split;
|
||||||
|
|
||||||
|
use app\common\library\CountryIso;
|
||||||
|
use app\common\service\SplitLinkCodeService;
|
||||||
|
use think\Validate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分流链接验证器
|
||||||
|
*/
|
||||||
|
class Link extends Validate
|
||||||
|
{
|
||||||
|
protected $rule = [
|
||||||
|
'countries' => 'require|checkCountries',
|
||||||
|
'link_code' => 'require|checkLinkCode',
|
||||||
|
'description' => 'require|max:255',
|
||||||
|
'ip_protect' => 'in:0,1',
|
||||||
|
'random_shuffle' => 'in:0,1',
|
||||||
|
'status' => 'in:normal,hidden',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $message = [
|
||||||
|
'countries.require' => '请至少选择一个投放国家',
|
||||||
|
'link_code.require' => '请填写分流链接',
|
||||||
|
'description.require' => '请填写链接描述',
|
||||||
|
'description.max' => '链接描述不能超过255个字符',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $scene = [
|
||||||
|
'add' => ['countries', 'link_code', 'description', 'ip_protect', 'random_shuffle', 'status'],
|
||||||
|
'edit' => ['countries', 'description', 'ip_protect', 'random_shuffle', 'status'],
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed $value
|
||||||
|
* @return bool|string
|
||||||
|
*/
|
||||||
|
protected function checkCountries($value)
|
||||||
|
{
|
||||||
|
$codes = is_array($value) ? $value : explode(',', (string)$value);
|
||||||
|
if (CountryIso::normalizeCodes($codes) === []) {
|
||||||
|
return '请至少选择一个有效的投放国家';
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验分流链接格式与唯一性(仅新增)
|
||||||
|
*
|
||||||
|
* @param mixed $value
|
||||||
|
* @return bool|string
|
||||||
|
*/
|
||||||
|
protected function checkLinkCode($value)
|
||||||
|
{
|
||||||
|
$code = SplitLinkCodeService::normalize((string) $value);
|
||||||
|
if (!SplitLinkCodeService::isValidFormat($code)) {
|
||||||
|
return '分流链接须为9位小写字母(a-z)';
|
||||||
|
}
|
||||||
|
if (!SplitLinkCodeService::isAvailable($code)) {
|
||||||
|
return '该分流链接已被使用,请更换';
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<form id="add-form" class="form-horizontal" role="form" data-toggle="validator" method="POST" action="">
|
||||||
|
{:token()}
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="c-countries" class="control-label col-xs-12 col-sm-2">{:__('Countries')}:</label>
|
||||||
|
<div class="col-xs-12 col-sm-8">
|
||||||
|
<select id="c-countries" name="row[countries][]" class="form-control selectpicker" multiple data-live-search="true" data-rule="required">
|
||||||
|
{foreach name="countryList" item="vo" key="code"}
|
||||||
|
<option value="{$code|htmlentities}">{$vo|htmlentities}</option>
|
||||||
|
{/foreach}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="c-link_code" class="control-label col-xs-12 col-sm-2">{:__('Link_code')}:</label>
|
||||||
|
<div class="col-xs-12 col-sm-8">
|
||||||
|
<input id="c-link_code" data-rule="required" class="form-control" name="row[link_code]" type="text" value="{$defaultLinkCode|htmlentities}" maxlength="9" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="c-description" class="control-label col-xs-12 col-sm-2">{:__('Description')}:</label>
|
||||||
|
<div class="col-xs-12 col-sm-8">
|
||||||
|
<input id="c-description" data-rule="required" class="form-control" name="row[description]" type="text" value="" maxlength="255">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="c-ip_protect" class="control-label col-xs-12 col-sm-2">{:__('Ip_protect')}:</label>
|
||||||
|
<div class="col-xs-12 col-sm-8">
|
||||||
|
<select id="c-ip_protect" class="form-control selectpicker" name="row[ip_protect]">
|
||||||
|
{foreach name="ipProtectList" item="vo"}
|
||||||
|
<option value="{$key|htmlentities}" {in name="key" value="0"}selected{/in}>{$vo|htmlentities}</option>
|
||||||
|
{/foreach}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="c-random_shuffle" class="control-label col-xs-12 col-sm-2">{:__('Random_shuffle')}:</label>
|
||||||
|
<div class="col-xs-12 col-sm-8">
|
||||||
|
<select id="c-random_shuffle" class="form-control selectpicker" name="row[random_shuffle]">
|
||||||
|
{foreach name="randomShuffleList" item="vo"}
|
||||||
|
<option value="{$key|htmlentities}" {in name="key" value="0"}selected{/in}>{$vo|htmlentities}</option>
|
||||||
|
{/foreach}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="control-label col-xs-12 col-sm-2">{:__('Status')}:</label>
|
||||||
|
<div class="col-xs-12 col-sm-8">
|
||||||
|
<div class="radio">
|
||||||
|
{foreach name="statusList" item="vo"}
|
||||||
|
<label for="row[status]-{$key}"><input id="row[status]-{$key}" name="row[status]" type="radio" value="{$key|htmlentities}" {in name="key" value="normal"}checked{/in} /> {$vo|htmlentities}</label>
|
||||||
|
{/foreach}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group layer-footer">
|
||||||
|
<label class="control-label col-xs-12 col-sm-2"></label>
|
||||||
|
<div class="col-xs-12 col-sm-8">
|
||||||
|
<button type="submit" class="btn btn-primary btn-embossed disabled">{:__('OK')}</button>
|
||||||
|
<button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<form id="edit-form" class="form-horizontal" role="form" data-toggle="validator" method="POST" action="">
|
||||||
|
{:token()}
|
||||||
|
<input type="hidden" name="row[id]" value="{$row.id|htmlentities}">
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="c-countries" class="control-label col-xs-12 col-sm-2">{:__('Countries')}:</label>
|
||||||
|
<div class="col-xs-12 col-sm-8">
|
||||||
|
<select id="c-countries" name="row[countries][]" class="form-control selectpicker" multiple data-live-search="true" data-rule="required">
|
||||||
|
{foreach name="countryList" item="vo" key="code"}
|
||||||
|
<option value="{$code|htmlentities}" {in name="code" value="$selectedCountries"}selected{/in}>{$vo|htmlentities}</option>
|
||||||
|
{/foreach}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="c-link_code" class="control-label col-xs-12 col-sm-2">{:__('Link_code')}:</label>
|
||||||
|
<div class="col-xs-12 col-sm-8">
|
||||||
|
<input id="c-link_code" class="form-control" type="text" value="{$row.link_code|htmlentities}" readonly>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="c-description" class="control-label col-xs-12 col-sm-2">{:__('Description')}:</label>
|
||||||
|
<div class="col-xs-12 col-sm-8">
|
||||||
|
<input id="c-description" data-rule="required" class="form-control" name="row[description]" type="text" value="{$row.description|htmlentities}" maxlength="255">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="c-ip_protect" class="control-label col-xs-12 col-sm-2">{:__('Ip_protect')}:</label>
|
||||||
|
<div class="col-xs-12 col-sm-8">
|
||||||
|
<select id="c-ip_protect" class="form-control selectpicker" name="row[ip_protect]">
|
||||||
|
{foreach name="ipProtectList" item="vo"}
|
||||||
|
<option value="{$key|htmlentities}" {in name="key" value="$row.ip_protect"}selected{/in}>{$vo|htmlentities}</option>
|
||||||
|
{/foreach}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="c-random_shuffle" class="control-label col-xs-12 col-sm-2">{:__('Random_shuffle')}:</label>
|
||||||
|
<div class="col-xs-12 col-sm-8">
|
||||||
|
<select id="c-random_shuffle" class="form-control selectpicker" name="row[random_shuffle]">
|
||||||
|
{foreach name="randomShuffleList" item="vo"}
|
||||||
|
<option value="{$key|htmlentities}" {in name="key" value="$row.random_shuffle"}selected{/in}>{$vo|htmlentities}</option>
|
||||||
|
{/foreach}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="control-label col-xs-12 col-sm-2">{:__('Status')}:</label>
|
||||||
|
<div class="col-xs-12 col-sm-8">
|
||||||
|
<div class="radio">
|
||||||
|
{foreach name="statusList" item="vo"}
|
||||||
|
<label for="row[status]-{$key}"><input id="row[status]-{$key}" name="row[status]" type="radio" value="{$key|htmlentities}" {in name="key" value="$row.status"}checked{/in} /> {$vo|htmlentities}</label>
|
||||||
|
{/foreach}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group layer-footer">
|
||||||
|
<label class="control-label col-xs-12 col-sm-2"></label>
|
||||||
|
<div class="col-xs-12 col-sm-8">
|
||||||
|
<button type="submit" class="btn btn-primary btn-embossed disabled">{:__('OK')}</button>
|
||||||
|
<button type="reset" class="btn btn-default btn-embossed">{:__('Reset')}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<div class="panel panel-default panel-intro">
|
||||||
|
|
||||||
|
<div class="panel-heading">
|
||||||
|
{:build_heading(null,FALSE)}
|
||||||
|
<ul class="nav nav-tabs" data-field="status">
|
||||||
|
<li class="{:$Think.get.status === null ? 'active' : ''}"><a href="#t-all" data-value="" data-toggle="tab">{:__('All')}</a></li>
|
||||||
|
{foreach name="statusList" item="vo"}
|
||||||
|
<li class="{:$Think.get.status === (string)$key ? 'active' : ''}"><a href="#t-{$key}" data-value="{$key}" data-toggle="tab">{$vo}</a></li>
|
||||||
|
{/foreach}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="panel-body">
|
||||||
|
<div id="myTabContent" class="tab-content">
|
||||||
|
<div class="tab-pane fade active in" id="one">
|
||||||
|
<div class="widget-body no-padding">
|
||||||
|
<div id="toolbar" class="toolbar">
|
||||||
|
<a href="javascript:;" class="btn btn-primary btn-refresh" title="{:__('Refresh')}" ><i class="fa fa-refresh"></i> </a>
|
||||||
|
<a href="javascript:;" class="btn btn-success btn-add {:$auth->check('split.link/add')?'':'hide'}" title="{:__('Add')}" ><i class="fa fa-plus"></i> {:__('Add')}</a>
|
||||||
|
<a href="javascript:;" class="btn btn-success btn-edit btn-disabled disabled {:$auth->check('split.link/edit')?'':'hide'}" title="{:__('Edit')}" ><i class="fa fa-pencil"></i> {:__('Edit')}</a>
|
||||||
|
<a href="javascript:;" class="btn btn-danger btn-del btn-disabled disabled {:$auth->check('split.link/del')?'':'hide'}" title="{:__('Delete')}" ><i class="fa fa-trash"></i> {:__('Delete')}</a>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="dropdown btn-group {:$auth->check('split.link/multi')?'':'hide'}">
|
||||||
|
<a class="btn btn-primary btn-more dropdown-toggle btn-disabled disabled" data-toggle="dropdown"><i class="fa fa-cog"></i> {:__('More')}</a>
|
||||||
|
<ul class="dropdown-menu text-left" role="menu">
|
||||||
|
{foreach name="statusList" item="vo"}
|
||||||
|
<li><a class="btn btn-link btn-multi btn-disabled disabled" href="javascript:" data-params="status={$key}">{:__('Set status to ' . $key)}</a></li>
|
||||||
|
{/foreach}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<table id="table" class="table table-striped table-bordered table-hover table-nowrap"
|
||||||
|
data-operate-edit="{:$auth->check('split.link/edit')}"
|
||||||
|
data-operate-del="{:$auth->check('split.link/del')}"
|
||||||
|
width="100%">
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\common\library;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ISO 3166-1 alpha-2 国家代码与中文名称
|
||||||
|
*/
|
||||||
|
class CountryIso
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 常用投放国家 ISO2 => 中文名
|
||||||
|
*
|
||||||
|
* @var array<string, string>
|
||||||
|
*/
|
||||||
|
private const COUNTRIES = [
|
||||||
|
'CN' => '中国',
|
||||||
|
'HK' => '中国香港',
|
||||||
|
'MO' => '中国澳门',
|
||||||
|
'TW' => '中国台湾',
|
||||||
|
'US' => '美国',
|
||||||
|
'CA' => '加拿大',
|
||||||
|
'GB' => '英国',
|
||||||
|
'DE' => '德国',
|
||||||
|
'FR' => '法国',
|
||||||
|
'IT' => '意大利',
|
||||||
|
'ES' => '西班牙',
|
||||||
|
'NL' => '荷兰',
|
||||||
|
'BE' => '比利时',
|
||||||
|
'CH' => '瑞士',
|
||||||
|
'AT' => '奥地利',
|
||||||
|
'SE' => '瑞典',
|
||||||
|
'NO' => '挪威',
|
||||||
|
'DK' => '丹麦',
|
||||||
|
'FI' => '芬兰',
|
||||||
|
'IE' => '爱尔兰',
|
||||||
|
'PT' => '葡萄牙',
|
||||||
|
'PL' => '波兰',
|
||||||
|
'CZ' => '捷克',
|
||||||
|
'HU' => '匈牙利',
|
||||||
|
'RO' => '罗马尼亚',
|
||||||
|
'GR' => '希腊',
|
||||||
|
'RU' => '俄罗斯',
|
||||||
|
'UA' => '乌克兰',
|
||||||
|
'TR' => '土耳其',
|
||||||
|
'IL' => '以色列',
|
||||||
|
'SA' => '沙特阿拉伯',
|
||||||
|
'AE' => '阿联酋',
|
||||||
|
'QA' => '卡塔尔',
|
||||||
|
'KW' => '科威特',
|
||||||
|
'IN' => '印度',
|
||||||
|
'PK' => '巴基斯坦',
|
||||||
|
'BD' => '孟加拉国',
|
||||||
|
'TH' => '泰国',
|
||||||
|
'VN' => '越南',
|
||||||
|
'MY' => '马来西亚',
|
||||||
|
'SG' => '新加坡',
|
||||||
|
'ID' => '印度尼西亚',
|
||||||
|
'PH' => '菲律宾',
|
||||||
|
'JP' => '日本',
|
||||||
|
'KR' => '韩国',
|
||||||
|
'AU' => '澳大利亚',
|
||||||
|
'NZ' => '新西兰',
|
||||||
|
'BR' => '巴西',
|
||||||
|
'MX' => '墨西哥',
|
||||||
|
'AR' => '阿根廷',
|
||||||
|
'CL' => '智利',
|
||||||
|
'CO' => '哥伦比亚',
|
||||||
|
'PE' => '秘鲁',
|
||||||
|
'ZA' => '南非',
|
||||||
|
'EG' => '埃及',
|
||||||
|
'NG' => '尼日利亚',
|
||||||
|
'KE' => '肯尼亚',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下拉选项 ISO2 => 中文名
|
||||||
|
*
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public static function getOptions(): array
|
||||||
|
{
|
||||||
|
return self::COUNTRIES;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验是否为合法 ISO2 代码
|
||||||
|
*/
|
||||||
|
public static function isValidCode(string $code): bool
|
||||||
|
{
|
||||||
|
$code = strtoupper(trim($code));
|
||||||
|
return isset(self::COUNTRIES[$code]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 规范化并过滤国家代码列表
|
||||||
|
*
|
||||||
|
* @param array<int, string>|string $codes
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
public static function normalizeCodes($codes): array
|
||||||
|
{
|
||||||
|
if (is_string($codes)) {
|
||||||
|
$codes = explode(',', $codes);
|
||||||
|
}
|
||||||
|
if (!is_array($codes)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$result = [];
|
||||||
|
foreach ($codes as $code) {
|
||||||
|
$code = strtoupper(trim((string)$code));
|
||||||
|
if ($code !== '' && self::isValidCode($code) && !in_array($code, $result, true)) {
|
||||||
|
$result[] = $code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort($result);
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 逗号分隔 ISO 代码转中文展示
|
||||||
|
*/
|
||||||
|
public static function codesToText(string $codes): string
|
||||||
|
{
|
||||||
|
if ($codes === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
$parts = [];
|
||||||
|
foreach (explode(',', $codes) as $code) {
|
||||||
|
$code = strtoupper(trim($code));
|
||||||
|
if ($code === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$parts[] = self::COUNTRIES[$code] ?? $code;
|
||||||
|
}
|
||||||
|
return implode(',', $parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数组转存储字符串
|
||||||
|
*
|
||||||
|
* @param array<int, string> $codes
|
||||||
|
*/
|
||||||
|
public static function codesToStorage(array $codes): string
|
||||||
|
{
|
||||||
|
return implode(',', self::normalizeCodes($codes));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\common\model;
|
||||||
|
|
||||||
|
use think\Model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配置模型
|
||||||
|
*/
|
||||||
|
class Config extends Model
|
||||||
|
{
|
||||||
|
|
||||||
|
// 表名,不含前缀
|
||||||
|
protected $name = 'config';
|
||||||
|
// 自动写入时间戳字段
|
||||||
|
protected $autoWriteTimestamp = false;
|
||||||
|
// 定义时间戳字段名
|
||||||
|
protected $createTime = false;
|
||||||
|
protected $updateTime = false;
|
||||||
|
// 追加属性
|
||||||
|
protected $append = [
|
||||||
|
'extend_html'
|
||||||
|
];
|
||||||
|
protected $type = [
|
||||||
|
'setting' => 'json',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取配置类型
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function getTypeList()
|
||||||
|
{
|
||||||
|
$typeList = [
|
||||||
|
'string' => __('String'),
|
||||||
|
'password' => __('Password'),
|
||||||
|
'text' => __('Text'),
|
||||||
|
'editor' => __('Editor'),
|
||||||
|
'number' => __('Number'),
|
||||||
|
'date' => __('Date'),
|
||||||
|
'time' => __('Time'),
|
||||||
|
'datetime' => __('Datetime'),
|
||||||
|
'datetimerange' => __('Datetimerange'),
|
||||||
|
'select' => __('Select'),
|
||||||
|
'selects' => __('Selects'),
|
||||||
|
'image' => __('Image'),
|
||||||
|
'images' => __('Images'),
|
||||||
|
'file' => __('File'),
|
||||||
|
'files' => __('Files'),
|
||||||
|
'switch' => __('Switch'),
|
||||||
|
'checkbox' => __('Checkbox'),
|
||||||
|
'radio' => __('Radio'),
|
||||||
|
'city' => __('City'),
|
||||||
|
'selectpage' => __('Selectpage'),
|
||||||
|
'selectpages' => __('Selectpages'),
|
||||||
|
'array' => __('Array'),
|
||||||
|
'custom' => __('Custom'),
|
||||||
|
];
|
||||||
|
return $typeList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getRegexList()
|
||||||
|
{
|
||||||
|
$regexList = [
|
||||||
|
'required' => '必选',
|
||||||
|
'digits' => '数字',
|
||||||
|
'letters' => '字母',
|
||||||
|
'date' => '日期',
|
||||||
|
'time' => '时间',
|
||||||
|
'email' => '邮箱',
|
||||||
|
'url' => '网址',
|
||||||
|
'qq' => 'QQ号',
|
||||||
|
'IDcard' => '身份证',
|
||||||
|
'tel' => '座机电话',
|
||||||
|
'mobile' => '手机号',
|
||||||
|
'zipcode' => '邮编',
|
||||||
|
'chinese' => '中文',
|
||||||
|
'username' => '用户名',
|
||||||
|
'password' => '密码'
|
||||||
|
];
|
||||||
|
return $regexList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getExtendHtmlAttr($value, $data)
|
||||||
|
{
|
||||||
|
$result = preg_replace_callback("/\{([a-zA-Z]+)\}/", function ($matches) use ($data) {
|
||||||
|
if (isset($data[$matches[1]])) {
|
||||||
|
return $data[$matches[1]];
|
||||||
|
}
|
||||||
|
}, $data['extend']);
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取分类分组列表(优先数据库 configgroup,避免 site.php 未同步时分组缺失)
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function getGroupList()
|
||||||
|
{
|
||||||
|
$groupList = config('site.configgroup');
|
||||||
|
if (!is_array($groupList)) {
|
||||||
|
$groupList = [];
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$dbJson = \think\Db::name('config')->where('name', 'configgroup')->value('value');
|
||||||
|
$dbList = json_decode((string) $dbJson, true);
|
||||||
|
if (is_array($dbList) && $dbList !== []) {
|
||||||
|
$groupList = $dbList;
|
||||||
|
}
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// 数据库不可用时回退 site.php
|
||||||
|
}
|
||||||
|
foreach ($groupList as $k => &$v) {
|
||||||
|
$v = __($v);
|
||||||
|
}
|
||||||
|
return $groupList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getArrayData($data)
|
||||||
|
{
|
||||||
|
if (!isset($data['value'])) {
|
||||||
|
$result = [];
|
||||||
|
foreach ($data as $index => $datum) {
|
||||||
|
$result['field'][$index] = $datum['key'];
|
||||||
|
$result['value'][$index] = $datum['value'];
|
||||||
|
}
|
||||||
|
$data = $result;
|
||||||
|
}
|
||||||
|
$fieldarr = $valuearr = [];
|
||||||
|
$field = $data['field'] ?? ($data['key'] ?? []);
|
||||||
|
$value = $data['value'] ?? [];
|
||||||
|
foreach ($field as $m => $n) {
|
||||||
|
if ($n != '') {
|
||||||
|
$fieldarr[] = $field[$m];
|
||||||
|
$valuearr[] = $value[$m];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $fieldarr ? array_combine($fieldarr, $valuearr) : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将字符串解析成键值数组
|
||||||
|
* @param string $text
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function decode($text, $split = "\r\n")
|
||||||
|
{
|
||||||
|
$content = explode($split, $text);
|
||||||
|
$arr = [];
|
||||||
|
foreach ($content as $k => $v) {
|
||||||
|
if (stripos($v, "|") !== false) {
|
||||||
|
$item = explode('|', $v);
|
||||||
|
$arr[$item[0]] = $item[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将键值数组转换为字符串
|
||||||
|
* @param array $array
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public static function encode($array, $split = "\r\n")
|
||||||
|
{
|
||||||
|
$content = '';
|
||||||
|
if ($array && is_array($array)) {
|
||||||
|
$arr = [];
|
||||||
|
foreach ($array as $k => $v) {
|
||||||
|
$arr[] = "{$k}|{$v}";
|
||||||
|
}
|
||||||
|
$content = implode($split, $arr);
|
||||||
|
}
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 本地上传配置信息
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function upload()
|
||||||
|
{
|
||||||
|
$uploadcfg = config('upload');
|
||||||
|
|
||||||
|
$uploadurl = request()->module() ? $uploadcfg['uploadurl'] : ($uploadcfg['uploadurl'] === 'ajax/upload' ? 'index/' . $uploadcfg['uploadurl'] : $uploadcfg['uploadurl']);
|
||||||
|
|
||||||
|
if (!preg_match("/^((?:[a-z]+:)?\/\/)(.*)/i", $uploadurl) && substr($uploadurl, 0, 1) !== '/') {
|
||||||
|
$uploadurl = url($uploadurl, '', false);
|
||||||
|
}
|
||||||
|
$uploadcfg['fullmode'] = isset($uploadcfg['fullmode']) && $uploadcfg['fullmode'];
|
||||||
|
$uploadcfg['thumbstyle'] = $uploadcfg['thumbstyle'] ?? '';
|
||||||
|
|
||||||
|
$upload = [
|
||||||
|
'cdnurl' => $uploadcfg['cdnurl'],
|
||||||
|
'uploadurl' => $uploadurl,
|
||||||
|
'bucket' => 'local',
|
||||||
|
'maxsize' => $uploadcfg['maxsize'],
|
||||||
|
'mimetype' => $uploadcfg['mimetype'],
|
||||||
|
'chunking' => $uploadcfg['chunking'],
|
||||||
|
'chunksize' => $uploadcfg['chunksize'],
|
||||||
|
'savekey' => $uploadcfg['savekey'],
|
||||||
|
'multipart' => [],
|
||||||
|
'multiple' => $uploadcfg['multiple'],
|
||||||
|
'fullmode' => $uploadcfg['fullmode'],
|
||||||
|
'thumbstyle' => $uploadcfg['thumbstyle'],
|
||||||
|
'storage' => 'local'
|
||||||
|
];
|
||||||
|
return $upload;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新配置文件
|
||||||
|
*/
|
||||||
|
public static function refreshFile()
|
||||||
|
{
|
||||||
|
//如果没有配置权限无法进行修改
|
||||||
|
if (!\app\admin\library\Auth::instance()->check('general/config/edit')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$config = [];
|
||||||
|
$configList = self::all();
|
||||||
|
foreach ($configList as $k => $v) {
|
||||||
|
$value = $v->toArray();
|
||||||
|
if (in_array($value['type'], ['selects', 'checkbox', 'images', 'files'])) {
|
||||||
|
$value['value'] = explode(',', $value['value']);
|
||||||
|
}
|
||||||
|
if ($value['type'] == 'array') {
|
||||||
|
$value['value'] = (array)json_decode($value['value'], true);
|
||||||
|
}
|
||||||
|
$config[$value['name']] = $value['value'];
|
||||||
|
}
|
||||||
|
file_put_contents(
|
||||||
|
CONF_PATH . 'extra' . DS . 'site.php',
|
||||||
|
'<?php' . "\n\nreturn " . var_export($config, true) . ";\n"
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\common\service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分流链接自动回复语句(一行一条)
|
||||||
|
*/
|
||||||
|
class SplitAutoReplyService
|
||||||
|
{
|
||||||
|
/** 最多条数 */
|
||||||
|
private const MAX_LINES = 200;
|
||||||
|
|
||||||
|
/** 单条最大字符数 */
|
||||||
|
private const MAX_LINE_LENGTH = 500;
|
||||||
|
|
||||||
|
/** 列表预览最大字符数 */
|
||||||
|
private const LIST_PREVIEW_MAX = 20;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列表单元格预览(单行最多 50 字,超出加 ...)
|
||||||
|
*/
|
||||||
|
public static function previewForList(string $raw, int $max = self::LIST_PREVIEW_MAX): string
|
||||||
|
{
|
||||||
|
$display = self::formatDisplay($raw);
|
||||||
|
if ($display === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
$flat = preg_replace('/\s+/u', ' ', str_replace(["\r\n", "\r", "\n"], ' ', $display));
|
||||||
|
$flat = trim((string) $flat);
|
||||||
|
if ($flat === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (mb_strlen($flat, 'UTF-8') <= $max) {
|
||||||
|
return $flat;
|
||||||
|
}
|
||||||
|
return mb_substr($flat, 0, $max, 'UTF-8') . '...';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析为多行文本数组
|
||||||
|
*
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
public static function parseLines(string $raw): array
|
||||||
|
{
|
||||||
|
$raw = trim($raw);
|
||||||
|
if ($raw === '') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
$parts = preg_split('/\r\n|\r|\n/', $raw) ?: [];
|
||||||
|
$lines = [];
|
||||||
|
foreach ($parts as $part) {
|
||||||
|
$line = trim((string) $part);
|
||||||
|
if ($line === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (strlen($line) > self::MAX_LINE_LENGTH) {
|
||||||
|
$line = mb_substr($line, 0, self::MAX_LINE_LENGTH, 'UTF-8');
|
||||||
|
}
|
||||||
|
$lines[] = $line;
|
||||||
|
if (count($lines) >= self::MAX_LINES) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化为数据库存储(换行分隔)
|
||||||
|
*
|
||||||
|
* @param string|array<int, string> $value
|
||||||
|
*/
|
||||||
|
public static function formatStorage($value): string
|
||||||
|
{
|
||||||
|
if (is_array($value)) {
|
||||||
|
$lines = $value;
|
||||||
|
} else {
|
||||||
|
$lines = self::parseLines((string) $value);
|
||||||
|
}
|
||||||
|
return implode("\n", $lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 供表单 textarea 回显
|
||||||
|
*/
|
||||||
|
public static function formatDisplay(string $stored): string
|
||||||
|
{
|
||||||
|
$lines = self::parseLines($stored);
|
||||||
|
return implode("\n", $lines);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\common\service;
|
||||||
|
|
||||||
|
use app\admin\model\split\Link;
|
||||||
|
use think\Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分流链接码生成服务
|
||||||
|
*/
|
||||||
|
class SplitLinkCodeService
|
||||||
|
{
|
||||||
|
private const CODE_LENGTH = 9;
|
||||||
|
|
||||||
|
private const CHARSET = 'abcdefghijklmnopqrstuvwxyz';
|
||||||
|
|
||||||
|
private const MAX_ATTEMPTS = 50;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 规范化链接码(小写、去空格)
|
||||||
|
*/
|
||||||
|
public static function normalize(string $code): string
|
||||||
|
{
|
||||||
|
return strtolower(trim($code));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否为合法 9 位小写字母链接码
|
||||||
|
*/
|
||||||
|
public static function isValidFormat(string $code): bool
|
||||||
|
{
|
||||||
|
return (bool) preg_match('/^[a-z]{9}$/', $code);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 链接码是否未被占用
|
||||||
|
*/
|
||||||
|
public static function isAvailable(string $code, int $excludeId = 0): bool
|
||||||
|
{
|
||||||
|
$code = self::normalize($code);
|
||||||
|
$query = Link::where('link_code', $code);
|
||||||
|
if ($excludeId > 0) {
|
||||||
|
$query->where('id', '<>', $excludeId);
|
||||||
|
}
|
||||||
|
return !$query->find();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成唯一 9 位小写字母链接码
|
||||||
|
*
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public function generateUnique(): string
|
||||||
|
{
|
||||||
|
for ($i = 0; $i < self::MAX_ATTEMPTS; $i++) {
|
||||||
|
$code = $this->generateRandom();
|
||||||
|
if (!Link::where('link_code', $code)->find()) {
|
||||||
|
return $code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Exception('分流链接生成失败,请稍后重试');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateRandom(): string
|
||||||
|
{
|
||||||
|
$chars = self::CHARSET;
|
||||||
|
$max = strlen($chars) - 1;
|
||||||
|
$code = '';
|
||||||
|
for ($i = 0; $i < self::CODE_LENGTH; $i++) {
|
||||||
|
$code .= $chars[random_int(0, $max)];
|
||||||
|
}
|
||||||
|
return $code;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\common\service;
|
||||||
|
|
||||||
|
use app\admin\model\Domain as DomainModel;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 平台分配域名解析(支持多域名,一行一个)
|
||||||
|
*/
|
||||||
|
class SplitPlatformDomainService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 解析配置文本为合法根域名列表(去重、小写)
|
||||||
|
*
|
||||||
|
* @param string $raw 换行或逗号分隔的域名文本
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
public static function parseList(string $raw): array
|
||||||
|
{
|
||||||
|
$raw = trim($raw);
|
||||||
|
if ($raw === '') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$parts = preg_split('/[\r\n,]+/', $raw) ?: [];
|
||||||
|
$domains = [];
|
||||||
|
foreach ($parts as $part) {
|
||||||
|
$domain = DomainModel::normalizeDomain(trim((string) $part));
|
||||||
|
if ($domain === '' || isset($domains[$domain])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!DomainModel::isValidRootDomain($domain)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$domains[$domain] = $domain;
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values($domains);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将域名列表格式化为配置存储文本(一行一个)
|
||||||
|
*
|
||||||
|
* @param array<int, string> $domains
|
||||||
|
*/
|
||||||
|
public static function formatList(array $domains): string
|
||||||
|
{
|
||||||
|
$lines = [];
|
||||||
|
foreach ($domains as $domain) {
|
||||||
|
$domain = DomainModel::normalizeDomain((string) $domain);
|
||||||
|
if ($domain !== '' && DomainModel::isValidRootDomain($domain)) {
|
||||||
|
$lines[$domain] = $domain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return implode("\n", array_values($lines));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,550 @@
|
|||||||
|
define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) {
|
||||||
|
|
||||||
|
var Controller = {
|
||||||
|
index: function () {
|
||||||
|
Table.api.init({
|
||||||
|
extend: {
|
||||||
|
index_url: 'split.link/index' + location.search,
|
||||||
|
add_url: 'split.link/add',
|
||||||
|
edit_url: 'split.link/edit',
|
||||||
|
del_url: 'split.link/del',
|
||||||
|
multi_url: 'split.link/multi',
|
||||||
|
table: 'split_link',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var table = $("#table");
|
||||||
|
|
||||||
|
table.bootstrapTable({
|
||||||
|
url: $.fn.bootstrapTable.defaults.extend.index_url,
|
||||||
|
pk: 'id',
|
||||||
|
sortName: 'id',
|
||||||
|
sortOrder: 'desc',
|
||||||
|
fixedColumns: true,
|
||||||
|
fixedRightNumber: 1,
|
||||||
|
columns: [
|
||||||
|
[
|
||||||
|
{checkbox: true},
|
||||||
|
{field: 'countries_text', title: __('Countries'), operate: false, formatter: Table.api.formatter.content},
|
||||||
|
{
|
||||||
|
field: 'link_code',
|
||||||
|
title: __('Link_code'),
|
||||||
|
operate: 'LIKE',
|
||||||
|
formatter: Controller.api.formatter.linkCode
|
||||||
|
},
|
||||||
|
{field: 'description', title: __('Description'), operate: 'LIKE', class: 'autocontent', formatter: Table.api.formatter.content},
|
||||||
|
{
|
||||||
|
field: 'auto_reply_text',
|
||||||
|
title: __('Reply statements column'),
|
||||||
|
operate: false,
|
||||||
|
formatter: Controller.api.formatter.autoReplyText
|
||||||
|
},
|
||||||
|
{field: 'ip_protect', title: __('Ip_protect'), searchList: Config.ipProtectList, formatter: Table.api.formatter.status},
|
||||||
|
{field: 'random_shuffle', title: __('Random_shuffle'), searchList: Config.randomShuffleList, formatter: Table.api.formatter.status},
|
||||||
|
{field: 'status', title: __('Status'), searchList: Config.statusList, formatter: Table.api.formatter.status},
|
||||||
|
{field: 'createtime', title: __('Createtime'), operate: 'RANGE', addclass: 'datetimerange', autocomplete: false, formatter: Table.api.formatter.datetime, sortable: true},
|
||||||
|
{
|
||||||
|
field: 'operate',
|
||||||
|
title: __('Operate'),
|
||||||
|
table: table,
|
||||||
|
events: Table.api.events.operate,
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
name: 'autoreply',
|
||||||
|
text: __('Auto reply'),
|
||||||
|
title: __('Auto reply'),
|
||||||
|
icon: 'fa fa-commenting-o',
|
||||||
|
classname: 'btn btn-warning btn-xs btn-split-autoreply',
|
||||||
|
url: 'javascript:;'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
formatter: Table.api.formatter.operate
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
table.on('click', '.btn-copy-split-link', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
var linkCode = $.trim($(this).data('link-code') || '');
|
||||||
|
if (!linkCode) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Controller.api.openCopyModal(linkCode);
|
||||||
|
});
|
||||||
|
|
||||||
|
table.on('click', '.btn-copy-link-code-inline', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
var linkCode = $.trim($(this).data('link-code') || '');
|
||||||
|
if (linkCode) {
|
||||||
|
Controller.api.copyText(linkCode);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
table.on('click', '.btn-split-autoreply', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
e.stopImmediatePropagation();
|
||||||
|
var rowIndex = $(this).data('row-index');
|
||||||
|
var row = Table.api.getrowbyindex(table, rowIndex);
|
||||||
|
if (!row || !row.id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Controller.api.openAutoReplyModal(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
Controller.api.bindAutoReplyPreviewTips(table);
|
||||||
|
|
||||||
|
Table.api.bindevent(table);
|
||||||
|
},
|
||||||
|
add: function () {
|
||||||
|
Controller.api.bindevent();
|
||||||
|
Controller.api.bindLinkCodeInput();
|
||||||
|
},
|
||||||
|
edit: function () {
|
||||||
|
Controller.api.bindevent();
|
||||||
|
},
|
||||||
|
api: {
|
||||||
|
/** 弹窗样式(仅注入一次) */
|
||||||
|
modalStyleInjected: false,
|
||||||
|
injectModalStyles: function () {
|
||||||
|
if (Controller.api.modalStyleInjected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Controller.api.modalStyleInjected = true;
|
||||||
|
var css = [
|
||||||
|
'.split-link-copy-modal{padding:16px 20px;box-sizing:border-box;}',
|
||||||
|
'.split-link-copy-modal .form-group{margin-bottom:16px;}',
|
||||||
|
'.split-link-copy-modal .control-label{display:block;font-weight:600;color:#444;margin-bottom:8px;}',
|
||||||
|
'.split-link-copy-modal .split-link-code-row{display:flex;align-items:center;flex-wrap:wrap;gap:8px;}',
|
||||||
|
'.split-link-copy-modal .split-link-code-value{font-size:18px;font-weight:600;color:#337ab7;text-decoration:underline;word-break:break-all;}',
|
||||||
|
'.split-link-copy-modal .split-domain-tabs{margin-bottom:0;border-bottom:1px solid #ddd;}',
|
||||||
|
'.split-link-copy-modal .split-domain-tab-box{border:1px solid #ddd;border-top:none;border-radius:0 0 4px 4px;background:#fafafa;overflow:hidden;}',
|
||||||
|
'.split-link-copy-modal .split-domain-tab-box .tab-pane{max-height:280px;overflow-y:auto;overflow-x:hidden;padding:10px 12px;margin:0;}',
|
||||||
|
'.split-link-copy-modal .split-domain-list{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;align-items:stretch;}',
|
||||||
|
'.split-link-copy-modal .split-domain-item{border:1px solid #e8e8e8;border-radius:4px;background:#fff;transition:border-color .2s,box-shadow .2s;height:100%;min-width:0;}',
|
||||||
|
'.split-link-copy-modal .split-domain-item:hover{border-color:#b8d4f0;}',
|
||||||
|
'.split-link-copy-modal .split-domain-item.is-checked{border-color:#337ab7;box-shadow:0 0 0 1px rgba(51,122,183,.15);}',
|
||||||
|
'.split-link-copy-modal .split-domain-item-label{display:flex;align-items:flex-start;gap:8px;margin:0;padding:8px 10px;cursor:pointer;font-weight:normal;width:100%;height:100%;box-sizing:border-box;}',
|
||||||
|
'.split-link-copy-modal .split-domain-item-label input[type=radio]{margin:3px 0 0;flex-shrink:0;}',
|
||||||
|
'.split-link-copy-modal .split-domain-item-body{flex:1;min-width:0;display:flex;flex-direction:column;gap:5px;}',
|
||||||
|
'.split-link-copy-modal .split-domain-name{font-size:13px;font-weight:600;color:#333;word-break:break-all;line-height:1.4;}',
|
||||||
|
'.split-link-copy-modal .split-domain-status{display:flex;flex-direction:column;align-items:flex-start;gap:4px;}',
|
||||||
|
'.split-link-copy-modal .split-domain-status .label{font-size:10px;font-weight:normal;padding:2px 6px;line-height:1.3;white-space:nowrap;margin:0;display:inline-block;max-width:100%;overflow:hidden;text-overflow:ellipsis;}',
|
||||||
|
'.split-link-copy-modal .split-domain-empty{margin:0;padding:20px 12px;color:#999;text-align:center;line-height:1.6;grid-column:1/-1;}',
|
||||||
|
'.split-link-copy-modal .split-tip-box{margin:12px 0;padding:10px 12px;background:#f8f9fa;border-left:3px solid #337ab7;border-radius:0 4px 4px 0;line-height:1.6;color:#666;}',
|
||||||
|
'.split-link-copy-modal .split-generated-url{font-size:12px;}',
|
||||||
|
'.split-link-copy-modal .split-modal-footer{margin-top:16px;padding-top:12px;border-top:1px solid #eee;}'
|
||||||
|
].join('');
|
||||||
|
$('<style id="split-link-copy-modal-style" type="text/css"></style>').text(css).appendTo('head');
|
||||||
|
},
|
||||||
|
formatter: {
|
||||||
|
/**
|
||||||
|
* 回复语:列表最多 50 字 + ...,悬停保留换行显示全文
|
||||||
|
*/
|
||||||
|
autoReplyText: function (value, row, index) {
|
||||||
|
var full = (row.auto_reply != null && row.auto_reply !== '') ? String(row.auto_reply) : '';
|
||||||
|
if (full === '') {
|
||||||
|
return '<span class="text-muted">-</span>';
|
||||||
|
}
|
||||||
|
var preview = value != null && value !== '' ? String(value) : full;
|
||||||
|
var safePreview = Fast.api.escape(preview);
|
||||||
|
var encFull = encodeURIComponent(full);
|
||||||
|
return '<span class="split-auto-reply-preview" data-full="' + encFull + '" style="display:inline-block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:middle;cursor:default;">'
|
||||||
|
+ safePreview + '</span>';
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 分流链接列:链接样式 + COPY 图标,点击链接打开弹窗
|
||||||
|
*/
|
||||||
|
linkCode: function (value) {
|
||||||
|
value = value == null ? '' : value.toString();
|
||||||
|
if (value === '') {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
var safe = Fast.api.escape(value);
|
||||||
|
return '<span class="split-link-code-cell">'
|
||||||
|
+ '<a href="javascript:;" class="btn-copy-split-link split-link-code-link" data-link-code="' + safe + '" style="font-weight:600;color:#337ab7;text-decoration:underline;">' + safe + '</a>'
|
||||||
|
+ ' <a href="javascript:;" class="btn-copy-link-code-inline text-primary" data-link-code="' + safe + '" title="' + __('Copy') + '"><i class="fa fa-copy"></i></a>'
|
||||||
|
+ '</span>';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
openCopyModal: function (linkCode) {
|
||||||
|
Controller.api.loadCopyModalData(function (data) {
|
||||||
|
Controller.api.renderCopyModal(linkCode, data);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/** 回复语列悬停提示(保留换行) */
|
||||||
|
autoReplyTipIndex: null,
|
||||||
|
bindAutoReplyPreviewTips: function (table) {
|
||||||
|
table.off('mouseenter.splitAutoReply mouseleave.splitAutoReply')
|
||||||
|
.on('mouseenter.splitAutoReply', '.split-auto-reply-preview', function () {
|
||||||
|
var enc = $(this).attr('data-full');
|
||||||
|
if (!enc) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var full = '';
|
||||||
|
try {
|
||||||
|
full = decodeURIComponent(enc);
|
||||||
|
} catch (e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var html = '<div style="white-space:pre-wrap;word-break:break-word;padding:10px 12px;line-height:1.6;max-width:420px;font-size:13px;text-align:left;">'
|
||||||
|
+ Controller.api.escapeHtmlWithNewlines(full) + '</div>';
|
||||||
|
Controller.api.autoReplyTipIndex = Layer.tips(html, this, {
|
||||||
|
tips: [1, ''],
|
||||||
|
time: 0,
|
||||||
|
maxWidth: 450
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.on('mouseleave.splitAutoReply', '.split-auto-reply-preview', function () {
|
||||||
|
if (Controller.api.autoReplyTipIndex != null) {
|
||||||
|
Layer.close(Controller.api.autoReplyTipIndex);
|
||||||
|
Controller.api.autoReplyTipIndex = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
escapeHtmlWithNewlines: function (text) {
|
||||||
|
return $('<div/>').text(text).html().replace(/\n/g, '<br>');
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 打开自动回复弹窗
|
||||||
|
*
|
||||||
|
* @param {object} row 列表行数据
|
||||||
|
*/
|
||||||
|
openAutoReplyModal: function (row) {
|
||||||
|
Fast.api.ajax({
|
||||||
|
url: 'split.link/autoreply/ids/' + row.id,
|
||||||
|
type: 'get'
|
||||||
|
}, function (data, ret) {
|
||||||
|
var info = ret.data || data || {};
|
||||||
|
Controller.api.renderAutoReplyModal(row.id, info);
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
renderAutoReplyModal: function (linkId, info) {
|
||||||
|
var linkCode = Fast.api.escape(info.link_code || '');
|
||||||
|
var autoReply = info.auto_reply || '';
|
||||||
|
var html = [
|
||||||
|
'<div class="split-auto-reply-modal" style="padding:16px 20px;">',
|
||||||
|
' <div class="form-group">',
|
||||||
|
' <label class="control-label">' + __('Link_code') + '</label>',
|
||||||
|
' <p class="form-control-static" style="font-size:16px;font-weight:600;color:#337ab7;margin:0;">' + linkCode + '</p>',
|
||||||
|
' </div>',
|
||||||
|
' <div class="form-group" style="margin-bottom:0;">',
|
||||||
|
' <label class="control-label" for="split-auto-reply-text">' + __('Reply statements') + '</label>',
|
||||||
|
' <p class="help-block" style="margin-top:0;">' + __('Reply statements tip') + '</p>',
|
||||||
|
' <textarea id="split-auto-reply-text" class="form-control" rows="10" placeholder="">' + Fast.api.escape(autoReply) + '</textarea>',
|
||||||
|
' </div>',
|
||||||
|
'</div>'
|
||||||
|
].join('');
|
||||||
|
|
||||||
|
Layer.open({
|
||||||
|
type: 1,
|
||||||
|
title: __('Auto reply'),
|
||||||
|
area: ['520px', 'auto'],
|
||||||
|
shadeClose: false,
|
||||||
|
content: html,
|
||||||
|
btn: [__('OK'), __('Close')],
|
||||||
|
yes: function (index, layero) {
|
||||||
|
var text = layero.find('#split-auto-reply-text').val();
|
||||||
|
Fast.api.ajax({
|
||||||
|
url: 'split.link/autoreply/ids/' + linkId,
|
||||||
|
type: 'post',
|
||||||
|
data: {auto_reply: text}
|
||||||
|
}, function () {
|
||||||
|
Layer.close(index);
|
||||||
|
Toastr.success(__('Auto reply saved'));
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
loadCopyModalData: function (callback) {
|
||||||
|
Fast.api.ajax({
|
||||||
|
url: 'split.link/copyinfo',
|
||||||
|
type: 'get'
|
||||||
|
}, function (data, ret) {
|
||||||
|
callback(ret.data || data || {});
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
renderCopyModal: function (linkCode, data) {
|
||||||
|
Controller.api.injectModalStyles();
|
||||||
|
|
||||||
|
var platformDomains = $.isArray(data.platform_domains) ? data.platform_domains : [];
|
||||||
|
if (!platformDomains.length && data.platform_domain) {
|
||||||
|
var rawSingle = $.trim(data.platform_domain);
|
||||||
|
platformDomains = rawSingle.split(/[\r\n,]+/).map(function (d) {
|
||||||
|
return $.trim(d);
|
||||||
|
}).filter(function (d) {
|
||||||
|
return d.length > 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var myDomains = $.isArray(data.my_domains) ? data.my_domains : [];
|
||||||
|
var domainIndexUrl = data.domain_index_url || 'domain/index';
|
||||||
|
var configIndexUrl = data.config_index_url || 'general/config/index';
|
||||||
|
var defaultType = platformDomains.length ? 'platform' : 'my';
|
||||||
|
var defaultDomain = platformDomains.length ? platformDomains[0] : (myDomains.length ? myDomains[0].domain : '');
|
||||||
|
var activeTab = defaultType === 'platform' ? 'platform' : 'my';
|
||||||
|
|
||||||
|
var platformHtml = '';
|
||||||
|
if (platformDomains.length) {
|
||||||
|
var platformItems = [];
|
||||||
|
$.each(platformDomains, function (i, domain) {
|
||||||
|
platformItems.push(Controller.api.buildDomainRadio({
|
||||||
|
domain: domain,
|
||||||
|
type: 'platform',
|
||||||
|
checked: defaultType === 'platform' && domain === defaultDomain,
|
||||||
|
showStatus: false
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
platformHtml = '<div class="split-domain-list">' + platformItems.join('') + '</div>';
|
||||||
|
} else {
|
||||||
|
platformHtml = '<p class="split-domain-empty">'
|
||||||
|
+ __('Split platform domain empty')
|
||||||
|
+ ' <a href="javascript:;" class="btn-goto-config">' + __('Go system config') + '</a>'
|
||||||
|
+ '</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
var myDomainsHtml = '';
|
||||||
|
if (myDomains.length) {
|
||||||
|
var items = [];
|
||||||
|
$.each(myDomains, function (i, item) {
|
||||||
|
items.push(Controller.api.buildDomainRadio({
|
||||||
|
domain: item.domain,
|
||||||
|
type: 'my',
|
||||||
|
checked: defaultType === 'my' && item.domain === defaultDomain,
|
||||||
|
nsText: item.ns_status_text || '',
|
||||||
|
dnsText: item.dns_status_text || '',
|
||||||
|
nsStatus: item.ns_status || '',
|
||||||
|
dnsStatus: item.dns_status || '',
|
||||||
|
showStatus: true
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
myDomainsHtml = '<div class="split-domain-list">' + items.join('') + '</div>';
|
||||||
|
} else {
|
||||||
|
myDomainsHtml = '<p class="split-domain-empty">' + __('Split my domain empty') + '</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
var initialUrl = Controller.api.buildSplitUrl(defaultDomain, linkCode);
|
||||||
|
var safeLinkCode = Fast.api.escape(linkCode);
|
||||||
|
var html = [
|
||||||
|
'<div class="split-link-copy-modal">',
|
||||||
|
' <div class="form-group">',
|
||||||
|
' <label class="control-label">' + __('Link_code') + '</label>',
|
||||||
|
' <div class="split-link-code-row">',
|
||||||
|
' <a href="javascript:;" class="split-link-code-value" data-link-code="' + safeLinkCode + '">' + safeLinkCode + '</a>',
|
||||||
|
' <button type="button" class="btn btn-default btn-sm btn-copy-link-code" data-link-code="' + safeLinkCode + '" title="' + __('Copy') + '"><i class="fa fa-copy"></i></button>',
|
||||||
|
' <button type="button" class="btn btn-default btn-sm btn-manage-domain">' + __('Manage my domains') + '</button>',
|
||||||
|
' </div>',
|
||||||
|
' </div>',
|
||||||
|
' <div class="form-group">',
|
||||||
|
' <label class="control-label">' + __('Select main domain') + '</label>',
|
||||||
|
' <ul class="nav nav-tabs split-domain-tabs" role="tablist">',
|
||||||
|
' <li role="presentation"' + (activeTab === 'platform' ? ' class="active"' : '') + '>',
|
||||||
|
' <a href="#split-tab-platform" role="tab" data-toggle="tab">' + __('Platform assigned domain') + '</a>',
|
||||||
|
' </li>',
|
||||||
|
' <li role="presentation"' + (activeTab === 'my' ? ' class="active"' : '') + '>',
|
||||||
|
' <a href="#split-tab-my" role="tab" data-toggle="tab">' + __('My domains') + '</a>',
|
||||||
|
' </li>',
|
||||||
|
' </ul>',
|
||||||
|
' <div class="tab-content split-domain-tab-box">',
|
||||||
|
' <div role="tabpanel" class="tab-pane' + (activeTab === 'platform' ? ' active' : '') + '" id="split-tab-platform">' + platformHtml + '</div>',
|
||||||
|
' <div role="tabpanel" class="tab-pane' + (activeTab === 'my' ? ' active' : '') + '" id="split-tab-my">' + myDomainsHtml + '</div>',
|
||||||
|
' </div>',
|
||||||
|
' </div>',
|
||||||
|
' <div class="split-tip-box">',
|
||||||
|
__('Split domain prefix tip'),
|
||||||
|
' <button type="button" class="btn btn-xs btn-primary btn-go-add-domain">' + __('Go add domain') + '</button>',
|
||||||
|
' </div>',
|
||||||
|
' <div class="form-group" style="margin-bottom:0;">',
|
||||||
|
' <label class="control-label">' + __('Generated result') + '</label>',
|
||||||
|
' <div class="input-group">',
|
||||||
|
' <input type="text" class="form-control split-generated-url" readonly value="' + Fast.api.escape(initialUrl) + '">',
|
||||||
|
' <span class="input-group-btn">',
|
||||||
|
' <button type="button" class="btn btn-default btn-copy-generated-url"><i class="fa fa-copy"></i> ' + __('Copy') + '</button>',
|
||||||
|
' </span>',
|
||||||
|
' </div>',
|
||||||
|
' </div>',
|
||||||
|
' <div class="text-center split-modal-footer">',
|
||||||
|
' <button type="button" class="btn btn-success btn-open-test"' + (initialUrl ? '' : ' disabled') + '>' + __('Open test') + '</button>',
|
||||||
|
' <button type="button" class="btn btn-default btn-close-copy-modal">' + __('Close') + '</button>',
|
||||||
|
' </div>',
|
||||||
|
'</div>'
|
||||||
|
].join('');
|
||||||
|
|
||||||
|
Layer.open({
|
||||||
|
type: 1,
|
||||||
|
title: __('Copy split link'),
|
||||||
|
area: ['780px', 'auto'],
|
||||||
|
maxmin: false,
|
||||||
|
resize: false,
|
||||||
|
shadeClose: true,
|
||||||
|
content: html,
|
||||||
|
success: function (layero, index) {
|
||||||
|
var $box = layero.find('.split-link-copy-modal');
|
||||||
|
|
||||||
|
var refreshGeneratedUrl = function () {
|
||||||
|
var $checked = $box.find('input[name="split_main_domain"]:checked');
|
||||||
|
var domain = $.trim($checked.val() || '');
|
||||||
|
var url = Controller.api.buildSplitUrl(domain, linkCode);
|
||||||
|
$box.find('.split-generated-url').val(url);
|
||||||
|
$box.find('.btn-open-test').prop('disabled', url === '');
|
||||||
|
$box.find('.split-domain-item').removeClass('is-checked');
|
||||||
|
$checked.closest('.split-domain-item').addClass('is-checked');
|
||||||
|
};
|
||||||
|
|
||||||
|
var ensureTabSelection = function ($pane) {
|
||||||
|
var $radios = $pane.find('input[name="split_main_domain"]');
|
||||||
|
if ($radios.length && !$radios.filter(':checked').length) {
|
||||||
|
$radios.first().prop('checked', true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$box.on('change', 'input[name="split_main_domain"]', refreshGeneratedUrl);
|
||||||
|
|
||||||
|
$box.on('shown.bs.tab', 'a[data-toggle="tab"]', function (e) {
|
||||||
|
var target = $(e.target).attr('href');
|
||||||
|
if (target) {
|
||||||
|
ensureTabSelection($box.find(target));
|
||||||
|
refreshGeneratedUrl();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$box.on('click', '.btn-copy-link-code', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
Controller.api.copyText(linkCode);
|
||||||
|
});
|
||||||
|
|
||||||
|
$box.on('click', '.btn-manage-domain, .btn-go-add-domain', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
Layer.close(index);
|
||||||
|
Backend.api.addtabs(Fast.api.fixurl(domainIndexUrl), __('Manage my domains'), 'fa fa-globe');
|
||||||
|
});
|
||||||
|
|
||||||
|
$box.on('click', '.btn-goto-config', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
Layer.close(index);
|
||||||
|
Backend.api.addtabs(Fast.api.fixurl(configIndexUrl), __('System config'), 'fa fa-cogs');
|
||||||
|
});
|
||||||
|
|
||||||
|
$box.on('click', '.btn-copy-generated-url', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var url = $.trim($box.find('.split-generated-url').val());
|
||||||
|
if (!url) {
|
||||||
|
Toastr.error(__('Split url empty'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Controller.api.copyText(url);
|
||||||
|
});
|
||||||
|
|
||||||
|
$box.on('click', '.btn-open-test', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var url = $.trim($box.find('.split-generated-url').val());
|
||||||
|
if (url) {
|
||||||
|
window.open(url, '_blank');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$box.on('click', '.btn-close-copy-modal', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
Layer.close(index);
|
||||||
|
});
|
||||||
|
|
||||||
|
ensureTabSelection($box.find('.tab-pane.active'));
|
||||||
|
refreshGeneratedUrl();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
buildDomainRadio: function (options) {
|
||||||
|
var domain = Fast.api.escape(options.domain || '');
|
||||||
|
var type = options.type || 'my';
|
||||||
|
var checked = options.checked ? ' checked' : '';
|
||||||
|
var checkedClass = options.checked ? ' is-checked' : '';
|
||||||
|
var statusHtml = '';
|
||||||
|
if (options.showStatus) {
|
||||||
|
statusHtml = '<span class="split-domain-status">'
|
||||||
|
+ '<span class="label ' + Controller.api.statusLabelClass(options.nsStatus) + '">NS:' + Fast.api.escape(options.nsText || '-') + '</span>'
|
||||||
|
+ '<span class="label ' + Controller.api.statusLabelClass(options.dnsStatus, true) + '">DNS:' + Fast.api.escape(options.dnsText || '-') + '</span>'
|
||||||
|
+ '</span>';
|
||||||
|
}
|
||||||
|
return '<div class="split-domain-item' + checkedClass + '">'
|
||||||
|
+ '<label class="split-domain-item-label">'
|
||||||
|
+ '<input type="radio" name="split_main_domain" value="' + domain + '" data-type="' + type + '"' + checked + '>'
|
||||||
|
+ '<span class="split-domain-item-body">'
|
||||||
|
+ '<span class="split-domain-name">' + domain + '</span>'
|
||||||
|
+ statusHtml
|
||||||
|
+ '</span>'
|
||||||
|
+ '</label>'
|
||||||
|
+ '</div>';
|
||||||
|
},
|
||||||
|
statusLabelClass: function (status, isDns) {
|
||||||
|
if (isDns) {
|
||||||
|
if (status === 'created') {
|
||||||
|
return 'label-success';
|
||||||
|
}
|
||||||
|
if (status === 'failed') {
|
||||||
|
return 'label-danger';
|
||||||
|
}
|
||||||
|
return 'label-warning';
|
||||||
|
}
|
||||||
|
if (status === 'verified') {
|
||||||
|
return 'label-success';
|
||||||
|
}
|
||||||
|
if (status === 'failed') {
|
||||||
|
return 'label-danger';
|
||||||
|
}
|
||||||
|
return 'label-warning';
|
||||||
|
},
|
||||||
|
buildSplitUrl: function (domain, linkCode) {
|
||||||
|
domain = $.trim(domain || '').replace(/^https?:\/\//i, '').replace(/\/+$/, '');
|
||||||
|
linkCode = $.trim(linkCode || '');
|
||||||
|
if (!domain || !linkCode) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return 'https://' + domain + '/s/' + linkCode;
|
||||||
|
},
|
||||||
|
copyText: function (text) {
|
||||||
|
if (navigator.clipboard && window.isSecureContext) {
|
||||||
|
navigator.clipboard.writeText(text).then(function () {
|
||||||
|
Toastr.success(__('Copy success'));
|
||||||
|
}).catch(function () {
|
||||||
|
Controller.api.copyTextFallback(text);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Controller.api.copyTextFallback(text);
|
||||||
|
},
|
||||||
|
copyTextFallback: function (text) {
|
||||||
|
var $temp = $('<textarea>').css({position: 'fixed', left: '-9999px', top: '0'}).val(text);
|
||||||
|
$('body').append($temp);
|
||||||
|
$temp[0].select();
|
||||||
|
try {
|
||||||
|
document.execCommand('copy');
|
||||||
|
Toastr.success(__('Copy success'));
|
||||||
|
} catch (e) {
|
||||||
|
Toastr.error(__('Copy failed'));
|
||||||
|
}
|
||||||
|
$temp.remove();
|
||||||
|
},
|
||||||
|
bindLinkCodeInput: function () {
|
||||||
|
var $input = $('#c-link_code');
|
||||||
|
if (!$input.length || $input.prop('readonly')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$input.on('input', function () {
|
||||||
|
var val = $(this).val().replace(/[^a-zA-Z]/g, '').toLowerCase().slice(0, 9);
|
||||||
|
if ($(this).val() !== val) {
|
||||||
|
$(this).val(val);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
bindevent: function () {
|
||||||
|
Form.api.bindevent($("form[role=form]"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return Controller;
|
||||||
|
});
|
||||||
@@ -0,0 +1,515 @@
|
|||||||
|
define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefined, Backend, Table, Form) {
|
||||||
|
|
||||||
|
var Controller = {
|
||||||
|
index: function () {
|
||||||
|
Table.api.init({
|
||||||
|
extend: {
|
||||||
|
index_url: 'split.link/index' + location.search,
|
||||||
|
add_url: 'split.link/add',
|
||||||
|
edit_url: 'split.link/edit',
|
||||||
|
del_url: 'split.link/del',
|
||||||
|
multi_url: 'split.link/multi',
|
||||||
|
table: 'split_link',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var table = $("#table");
|
||||||
|
|
||||||
|
table.bootstrapTable({
|
||||||
|
url: $.fn.bootstrapTable.defaults.extend.index_url,
|
||||||
|
pk: 'id',
|
||||||
|
sortName: 'id',
|
||||||
|
sortOrder: 'desc',
|
||||||
|
fixedColumns: true,
|
||||||
|
fixedRightNumber: 1,
|
||||||
|
columns: [
|
||||||
|
[
|
||||||
|
{checkbox: true},
|
||||||
|
{field: 'countries_text', title: __('Countries'), operate: false, formatter: Table.api.formatter.content},
|
||||||
|
{
|
||||||
|
field: 'link_code',
|
||||||
|
title: __('Link_code'),
|
||||||
|
operate: 'LIKE',
|
||||||
|
formatter: Controller.api.formatter.linkCode
|
||||||
|
},
|
||||||
|
{field: 'description', title: __('Description'), operate: 'LIKE', class: 'autocontent', formatter: Table.api.formatter.content},
|
||||||
|
{
|
||||||
|
field: 'auto_reply_text',
|
||||||
|
title: __('Reply statements column'),
|
||||||
|
operate: false,
|
||||||
|
class: 'autocontent',
|
||||||
|
hover: true,
|
||||||
|
formatter: Controller.api.formatter.autoReplyText
|
||||||
|
},
|
||||||
|
{field: 'ip_protect', title: __('Ip_protect'), searchList: Config.ipProtectList, formatter: Table.api.formatter.status},
|
||||||
|
{field: 'random_shuffle', title: __('Random_shuffle'), searchList: Config.randomShuffleList, formatter: Table.api.formatter.status},
|
||||||
|
{field: 'status', title: __('Status'), searchList: Config.statusList, formatter: Table.api.formatter.status},
|
||||||
|
{field: 'createtime', title: __('Createtime'), operate: 'RANGE', addclass: 'datetimerange', autocomplete: false, formatter: Table.api.formatter.datetime, sortable: true},
|
||||||
|
{
|
||||||
|
field: 'operate',
|
||||||
|
title: __('Operate'),
|
||||||
|
table: table,
|
||||||
|
events: Table.api.events.operate,
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
name: 'autoreply',
|
||||||
|
text: __('Auto reply'),
|
||||||
|
title: __('Auto reply'),
|
||||||
|
icon: 'fa fa-commenting-o',
|
||||||
|
classname: 'btn btn-warning btn-xs btn-split-autoreply',
|
||||||
|
url: 'javascript:;'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
formatter: Table.api.formatter.operate
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
table.on('click', '.btn-copy-split-link', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
var linkCode = $.trim($(this).data('link-code') || '');
|
||||||
|
if (!linkCode) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Controller.api.openCopyModal(linkCode);
|
||||||
|
});
|
||||||
|
|
||||||
|
table.on('click', '.btn-copy-link-code-inline', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
var linkCode = $.trim($(this).data('link-code') || '');
|
||||||
|
if (linkCode) {
|
||||||
|
Controller.api.copyText(linkCode);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
table.on('click', '.btn-split-autoreply', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
e.stopImmediatePropagation();
|
||||||
|
var rowIndex = $(this).data('row-index');
|
||||||
|
var row = Table.api.getrowbyindex(table, rowIndex);
|
||||||
|
if (!row || !row.id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Controller.api.openAutoReplyModal(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
Table.api.bindevent(table);
|
||||||
|
},
|
||||||
|
add: function () {
|
||||||
|
Controller.api.bindevent();
|
||||||
|
Controller.api.bindLinkCodeInput();
|
||||||
|
},
|
||||||
|
edit: function () {
|
||||||
|
Controller.api.bindevent();
|
||||||
|
},
|
||||||
|
api: {
|
||||||
|
/** 弹窗样式(仅注入一次) */
|
||||||
|
modalStyleInjected: false,
|
||||||
|
injectModalStyles: function () {
|
||||||
|
if (Controller.api.modalStyleInjected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Controller.api.modalStyleInjected = true;
|
||||||
|
var css = [
|
||||||
|
'.split-link-copy-modal{padding:16px 20px;box-sizing:border-box;}',
|
||||||
|
'.split-link-copy-modal .form-group{margin-bottom:16px;}',
|
||||||
|
'.split-link-copy-modal .control-label{display:block;font-weight:600;color:#444;margin-bottom:8px;}',
|
||||||
|
'.split-link-copy-modal .split-link-code-row{display:flex;align-items:center;flex-wrap:wrap;gap:8px;}',
|
||||||
|
'.split-link-copy-modal .split-link-code-value{font-size:18px;font-weight:600;color:#337ab7;text-decoration:underline;word-break:break-all;}',
|
||||||
|
'.split-link-copy-modal .split-domain-tabs{margin-bottom:0;border-bottom:1px solid #ddd;}',
|
||||||
|
'.split-link-copy-modal .split-domain-tab-box{border:1px solid #ddd;border-top:none;border-radius:0 0 4px 4px;background:#fafafa;overflow:hidden;}',
|
||||||
|
'.split-link-copy-modal .split-domain-tab-box .tab-pane{max-height:280px;overflow-y:auto;overflow-x:hidden;padding:10px 12px;margin:0;}',
|
||||||
|
'.split-link-copy-modal .split-domain-list{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;align-items:stretch;}',
|
||||||
|
'.split-link-copy-modal .split-domain-item{border:1px solid #e8e8e8;border-radius:4px;background:#fff;transition:border-color .2s,box-shadow .2s;height:100%;min-width:0;}',
|
||||||
|
'.split-link-copy-modal .split-domain-item:hover{border-color:#b8d4f0;}',
|
||||||
|
'.split-link-copy-modal .split-domain-item.is-checked{border-color:#337ab7;box-shadow:0 0 0 1px rgba(51,122,183,.15);}',
|
||||||
|
'.split-link-copy-modal .split-domain-item-label{display:flex;align-items:flex-start;gap:8px;margin:0;padding:8px 10px;cursor:pointer;font-weight:normal;width:100%;height:100%;box-sizing:border-box;}',
|
||||||
|
'.split-link-copy-modal .split-domain-item-label input[type=radio]{margin:3px 0 0;flex-shrink:0;}',
|
||||||
|
'.split-link-copy-modal .split-domain-item-body{flex:1;min-width:0;display:flex;flex-direction:column;gap:5px;}',
|
||||||
|
'.split-link-copy-modal .split-domain-name{font-size:13px;font-weight:600;color:#333;word-break:break-all;line-height:1.4;}',
|
||||||
|
'.split-link-copy-modal .split-domain-status{display:flex;flex-direction:column;align-items:flex-start;gap:4px;}',
|
||||||
|
'.split-link-copy-modal .split-domain-status .label{font-size:10px;font-weight:normal;padding:2px 6px;line-height:1.3;white-space:nowrap;margin:0;display:inline-block;max-width:100%;overflow:hidden;text-overflow:ellipsis;}',
|
||||||
|
'.split-link-copy-modal .split-domain-empty{margin:0;padding:20px 12px;color:#999;text-align:center;line-height:1.6;grid-column:1/-1;}',
|
||||||
|
'.split-link-copy-modal .split-tip-box{margin:12px 0;padding:10px 12px;background:#f8f9fa;border-left:3px solid #337ab7;border-radius:0 4px 4px 0;line-height:1.6;color:#666;}',
|
||||||
|
'.split-link-copy-modal .split-generated-url{font-size:12px;}',
|
||||||
|
'.split-link-copy-modal .split-modal-footer{margin-top:16px;padding-top:12px;border-top:1px solid #eee;}'
|
||||||
|
].join('');
|
||||||
|
$('<style id="split-link-copy-modal-style" type="text/css"></style>').text(css).appendTo('head');
|
||||||
|
},
|
||||||
|
formatter: {
|
||||||
|
/**
|
||||||
|
* 回复语:单元格内省略,悬停显示完整内容
|
||||||
|
*/
|
||||||
|
autoReplyText: function (value, row, index) {
|
||||||
|
value = value == null ? '' : value.toString();
|
||||||
|
if (value === '') {
|
||||||
|
return '<span class="text-muted">-</span>';
|
||||||
|
}
|
||||||
|
var safe = Fast.api.escape(value);
|
||||||
|
return "<div class='autocontent-item autocontent-hover' style='white-space:nowrap;text-overflow:ellipsis;overflow:hidden;max-width:100%;display:block;' title='"
|
||||||
|
+ safe + "'>" + safe + "</div>";
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 分流链接列:链接样式 + COPY 图标,点击链接打开弹窗
|
||||||
|
*/
|
||||||
|
linkCode: function (value) {
|
||||||
|
value = value == null ? '' : value.toString();
|
||||||
|
if (value === '') {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
var safe = Fast.api.escape(value);
|
||||||
|
return '<span class="split-link-code-cell">'
|
||||||
|
+ '<a href="javascript:;" class="btn-copy-split-link split-link-code-link" data-link-code="' + safe + '" style="font-weight:600;color:#337ab7;text-decoration:underline;">' + safe + '</a>'
|
||||||
|
+ ' <a href="javascript:;" class="btn-copy-link-code-inline text-primary" data-link-code="' + safe + '" title="' + __('Copy') + '"><i class="fa fa-copy"></i></a>'
|
||||||
|
+ '</span>';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
openCopyModal: function (linkCode) {
|
||||||
|
Controller.api.loadCopyModalData(function (data) {
|
||||||
|
Controller.api.renderCopyModal(linkCode, data);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 打开自动回复弹窗
|
||||||
|
*
|
||||||
|
* @param {object} row 列表行数据
|
||||||
|
*/
|
||||||
|
openAutoReplyModal: function (row) {
|
||||||
|
Fast.api.ajax({
|
||||||
|
url: 'split.link/autoreply/ids/' + row.id,
|
||||||
|
type: 'get'
|
||||||
|
}, function (data, ret) {
|
||||||
|
var info = ret.data || data || {};
|
||||||
|
Controller.api.renderAutoReplyModal(row.id, info);
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
renderAutoReplyModal: function (linkId, info) {
|
||||||
|
var linkCode = Fast.api.escape(info.link_code || '');
|
||||||
|
var autoReply = info.auto_reply || '';
|
||||||
|
var html = [
|
||||||
|
'<div class="split-auto-reply-modal" style="padding:16px 20px;">',
|
||||||
|
' <div class="form-group">',
|
||||||
|
' <label class="control-label">' + __('Link_code') + '</label>',
|
||||||
|
' <p class="form-control-static" style="font-size:16px;font-weight:600;color:#337ab7;margin:0;">' + linkCode + '</p>',
|
||||||
|
' </div>',
|
||||||
|
' <div class="form-group" style="margin-bottom:0;">',
|
||||||
|
' <label class="control-label" for="split-auto-reply-text">' + __('Reply statements') + '</label>',
|
||||||
|
' <p class="help-block" style="margin-top:0;">' + __('Reply statements tip') + '</p>',
|
||||||
|
' <textarea id="split-auto-reply-text" class="form-control" rows="10" placeholder="">' + Fast.api.escape(autoReply) + '</textarea>',
|
||||||
|
' </div>',
|
||||||
|
'</div>'
|
||||||
|
].join('');
|
||||||
|
|
||||||
|
Layer.open({
|
||||||
|
type: 1,
|
||||||
|
title: __('Auto reply'),
|
||||||
|
area: ['520px', 'auto'],
|
||||||
|
shadeClose: false,
|
||||||
|
content: html,
|
||||||
|
btn: [__('OK'), __('Close')],
|
||||||
|
yes: function (index, layero) {
|
||||||
|
var text = layero.find('#split-auto-reply-text').val();
|
||||||
|
Fast.api.ajax({
|
||||||
|
url: 'split.link/autoreply/ids/' + linkId,
|
||||||
|
type: 'post',
|
||||||
|
data: {auto_reply: text}
|
||||||
|
}, function () {
|
||||||
|
Layer.close(index);
|
||||||
|
Toastr.success(__('Auto reply saved'));
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
loadCopyModalData: function (callback) {
|
||||||
|
Fast.api.ajax({
|
||||||
|
url: 'split.link/copyinfo',
|
||||||
|
type: 'get'
|
||||||
|
}, function (data, ret) {
|
||||||
|
callback(ret.data || data || {});
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
renderCopyModal: function (linkCode, data) {
|
||||||
|
Controller.api.injectModalStyles();
|
||||||
|
|
||||||
|
var platformDomains = $.isArray(data.platform_domains) ? data.platform_domains : [];
|
||||||
|
if (!platformDomains.length && data.platform_domain) {
|
||||||
|
var rawSingle = $.trim(data.platform_domain);
|
||||||
|
platformDomains = rawSingle.split(/[\r\n,]+/).map(function (d) {
|
||||||
|
return $.trim(d);
|
||||||
|
}).filter(function (d) {
|
||||||
|
return d.length > 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var myDomains = $.isArray(data.my_domains) ? data.my_domains : [];
|
||||||
|
var domainIndexUrl = data.domain_index_url || 'domain/index';
|
||||||
|
var configIndexUrl = data.config_index_url || 'general/config/index';
|
||||||
|
var defaultType = platformDomains.length ? 'platform' : 'my';
|
||||||
|
var defaultDomain = platformDomains.length ? platformDomains[0] : (myDomains.length ? myDomains[0].domain : '');
|
||||||
|
var activeTab = defaultType === 'platform' ? 'platform' : 'my';
|
||||||
|
|
||||||
|
var platformHtml = '';
|
||||||
|
if (platformDomains.length) {
|
||||||
|
var platformItems = [];
|
||||||
|
$.each(platformDomains, function (i, domain) {
|
||||||
|
platformItems.push(Controller.api.buildDomainRadio({
|
||||||
|
domain: domain,
|
||||||
|
type: 'platform',
|
||||||
|
checked: defaultType === 'platform' && domain === defaultDomain,
|
||||||
|
showStatus: false
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
platformHtml = '<div class="split-domain-list">' + platformItems.join('') + '</div>';
|
||||||
|
} else {
|
||||||
|
platformHtml = '<p class="split-domain-empty">'
|
||||||
|
+ __('Split platform domain empty')
|
||||||
|
+ ' <a href="javascript:;" class="btn-goto-config">' + __('Go system config') + '</a>'
|
||||||
|
+ '</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
var myDomainsHtml = '';
|
||||||
|
if (myDomains.length) {
|
||||||
|
var items = [];
|
||||||
|
$.each(myDomains, function (i, item) {
|
||||||
|
items.push(Controller.api.buildDomainRadio({
|
||||||
|
domain: item.domain,
|
||||||
|
type: 'my',
|
||||||
|
checked: defaultType === 'my' && item.domain === defaultDomain,
|
||||||
|
nsText: item.ns_status_text || '',
|
||||||
|
dnsText: item.dns_status_text || '',
|
||||||
|
nsStatus: item.ns_status || '',
|
||||||
|
dnsStatus: item.dns_status || '',
|
||||||
|
showStatus: true
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
myDomainsHtml = '<div class="split-domain-list">' + items.join('') + '</div>';
|
||||||
|
} else {
|
||||||
|
myDomainsHtml = '<p class="split-domain-empty">' + __('Split my domain empty') + '</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
var initialUrl = Controller.api.buildSplitUrl(defaultDomain, linkCode);
|
||||||
|
var safeLinkCode = Fast.api.escape(linkCode);
|
||||||
|
var html = [
|
||||||
|
'<div class="split-link-copy-modal">',
|
||||||
|
' <div class="form-group">',
|
||||||
|
' <label class="control-label">' + __('Link_code') + '</label>',
|
||||||
|
' <div class="split-link-code-row">',
|
||||||
|
' <a href="javascript:;" class="split-link-code-value" data-link-code="' + safeLinkCode + '">' + safeLinkCode + '</a>',
|
||||||
|
' <button type="button" class="btn btn-default btn-sm btn-copy-link-code" data-link-code="' + safeLinkCode + '" title="' + __('Copy') + '"><i class="fa fa-copy"></i></button>',
|
||||||
|
' <button type="button" class="btn btn-default btn-sm btn-manage-domain">' + __('Manage my domains') + '</button>',
|
||||||
|
' </div>',
|
||||||
|
' </div>',
|
||||||
|
' <div class="form-group">',
|
||||||
|
' <label class="control-label">' + __('Select main domain') + '</label>',
|
||||||
|
' <ul class="nav nav-tabs split-domain-tabs" role="tablist">',
|
||||||
|
' <li role="presentation"' + (activeTab === 'platform' ? ' class="active"' : '') + '>',
|
||||||
|
' <a href="#split-tab-platform" role="tab" data-toggle="tab">' + __('Platform assigned domain') + '</a>',
|
||||||
|
' </li>',
|
||||||
|
' <li role="presentation"' + (activeTab === 'my' ? ' class="active"' : '') + '>',
|
||||||
|
' <a href="#split-tab-my" role="tab" data-toggle="tab">' + __('My domains') + '</a>',
|
||||||
|
' </li>',
|
||||||
|
' </ul>',
|
||||||
|
' <div class="tab-content split-domain-tab-box">',
|
||||||
|
' <div role="tabpanel" class="tab-pane' + (activeTab === 'platform' ? ' active' : '') + '" id="split-tab-platform">' + platformHtml + '</div>',
|
||||||
|
' <div role="tabpanel" class="tab-pane' + (activeTab === 'my' ? ' active' : '') + '" id="split-tab-my">' + myDomainsHtml + '</div>',
|
||||||
|
' </div>',
|
||||||
|
' </div>',
|
||||||
|
' <div class="split-tip-box">',
|
||||||
|
__('Split domain prefix tip'),
|
||||||
|
' <button type="button" class="btn btn-xs btn-primary btn-go-add-domain">' + __('Go add domain') + '</button>',
|
||||||
|
' </div>',
|
||||||
|
' <div class="form-group" style="margin-bottom:0;">',
|
||||||
|
' <label class="control-label">' + __('Generated result') + '</label>',
|
||||||
|
' <div class="input-group">',
|
||||||
|
' <input type="text" class="form-control split-generated-url" readonly value="' + Fast.api.escape(initialUrl) + '">',
|
||||||
|
' <span class="input-group-btn">',
|
||||||
|
' <button type="button" class="btn btn-default btn-copy-generated-url"><i class="fa fa-copy"></i> ' + __('Copy') + '</button>',
|
||||||
|
' </span>',
|
||||||
|
' </div>',
|
||||||
|
' </div>',
|
||||||
|
' <div class="text-center split-modal-footer">',
|
||||||
|
' <button type="button" class="btn btn-success btn-open-test"' + (initialUrl ? '' : ' disabled') + '>' + __('Open test') + '</button>',
|
||||||
|
' <button type="button" class="btn btn-default btn-close-copy-modal">' + __('Close') + '</button>',
|
||||||
|
' </div>',
|
||||||
|
'</div>'
|
||||||
|
].join('');
|
||||||
|
|
||||||
|
Layer.open({
|
||||||
|
type: 1,
|
||||||
|
title: __('Copy split link'),
|
||||||
|
area: ['780px', 'auto'],
|
||||||
|
maxmin: false,
|
||||||
|
resize: false,
|
||||||
|
shadeClose: true,
|
||||||
|
content: html,
|
||||||
|
success: function (layero, index) {
|
||||||
|
var $box = layero.find('.split-link-copy-modal');
|
||||||
|
|
||||||
|
var refreshGeneratedUrl = function () {
|
||||||
|
var $checked = $box.find('input[name="split_main_domain"]:checked');
|
||||||
|
var domain = $.trim($checked.val() || '');
|
||||||
|
var url = Controller.api.buildSplitUrl(domain, linkCode);
|
||||||
|
$box.find('.split-generated-url').val(url);
|
||||||
|
$box.find('.btn-open-test').prop('disabled', url === '');
|
||||||
|
$box.find('.split-domain-item').removeClass('is-checked');
|
||||||
|
$checked.closest('.split-domain-item').addClass('is-checked');
|
||||||
|
};
|
||||||
|
|
||||||
|
var ensureTabSelection = function ($pane) {
|
||||||
|
var $radios = $pane.find('input[name="split_main_domain"]');
|
||||||
|
if ($radios.length && !$radios.filter(':checked').length) {
|
||||||
|
$radios.first().prop('checked', true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$box.on('change', 'input[name="split_main_domain"]', refreshGeneratedUrl);
|
||||||
|
|
||||||
|
$box.on('shown.bs.tab', 'a[data-toggle="tab"]', function (e) {
|
||||||
|
var target = $(e.target).attr('href');
|
||||||
|
if (target) {
|
||||||
|
ensureTabSelection($box.find(target));
|
||||||
|
refreshGeneratedUrl();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$box.on('click', '.btn-copy-link-code', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
Controller.api.copyText(linkCode);
|
||||||
|
});
|
||||||
|
|
||||||
|
$box.on('click', '.btn-manage-domain, .btn-go-add-domain', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
Layer.close(index);
|
||||||
|
Backend.api.addtabs(Fast.api.fixurl(domainIndexUrl), __('Manage my domains'), 'fa fa-globe');
|
||||||
|
});
|
||||||
|
|
||||||
|
$box.on('click', '.btn-goto-config', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
Layer.close(index);
|
||||||
|
Backend.api.addtabs(Fast.api.fixurl(configIndexUrl), __('System config'), 'fa fa-cogs');
|
||||||
|
});
|
||||||
|
|
||||||
|
$box.on('click', '.btn-copy-generated-url', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var url = $.trim($box.find('.split-generated-url').val());
|
||||||
|
if (!url) {
|
||||||
|
Toastr.error(__('Split url empty'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Controller.api.copyText(url);
|
||||||
|
});
|
||||||
|
|
||||||
|
$box.on('click', '.btn-open-test', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var url = $.trim($box.find('.split-generated-url').val());
|
||||||
|
if (url) {
|
||||||
|
window.open(url, '_blank');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$box.on('click', '.btn-close-copy-modal', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
Layer.close(index);
|
||||||
|
});
|
||||||
|
|
||||||
|
ensureTabSelection($box.find('.tab-pane.active'));
|
||||||
|
refreshGeneratedUrl();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
buildDomainRadio: function (options) {
|
||||||
|
var domain = Fast.api.escape(options.domain || '');
|
||||||
|
var type = options.type || 'my';
|
||||||
|
var checked = options.checked ? ' checked' : '';
|
||||||
|
var checkedClass = options.checked ? ' is-checked' : '';
|
||||||
|
var statusHtml = '';
|
||||||
|
if (options.showStatus) {
|
||||||
|
statusHtml = '<span class="split-domain-status">'
|
||||||
|
+ '<span class="label ' + Controller.api.statusLabelClass(options.nsStatus) + '">NS:' + Fast.api.escape(options.nsText || '-') + '</span>'
|
||||||
|
+ '<span class="label ' + Controller.api.statusLabelClass(options.dnsStatus, true) + '">DNS:' + Fast.api.escape(options.dnsText || '-') + '</span>'
|
||||||
|
+ '</span>';
|
||||||
|
}
|
||||||
|
return '<div class="split-domain-item' + checkedClass + '">'
|
||||||
|
+ '<label class="split-domain-item-label">'
|
||||||
|
+ '<input type="radio" name="split_main_domain" value="' + domain + '" data-type="' + type + '"' + checked + '>'
|
||||||
|
+ '<span class="split-domain-item-body">'
|
||||||
|
+ '<span class="split-domain-name">' + domain + '</span>'
|
||||||
|
+ statusHtml
|
||||||
|
+ '</span>'
|
||||||
|
+ '</label>'
|
||||||
|
+ '</div>';
|
||||||
|
},
|
||||||
|
statusLabelClass: function (status, isDns) {
|
||||||
|
if (isDns) {
|
||||||
|
if (status === 'created') {
|
||||||
|
return 'label-success';
|
||||||
|
}
|
||||||
|
if (status === 'failed') {
|
||||||
|
return 'label-danger';
|
||||||
|
}
|
||||||
|
return 'label-warning';
|
||||||
|
}
|
||||||
|
if (status === 'verified') {
|
||||||
|
return 'label-success';
|
||||||
|
}
|
||||||
|
if (status === 'failed') {
|
||||||
|
return 'label-danger';
|
||||||
|
}
|
||||||
|
return 'label-warning';
|
||||||
|
},
|
||||||
|
buildSplitUrl: function (domain, linkCode) {
|
||||||
|
domain = $.trim(domain || '').replace(/^https?:\/\//i, '').replace(/\/+$/, '');
|
||||||
|
linkCode = $.trim(linkCode || '');
|
||||||
|
if (!domain || !linkCode) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return 'https://' + domain + '/s/' + linkCode;
|
||||||
|
},
|
||||||
|
copyText: function (text) {
|
||||||
|
if (navigator.clipboard && window.isSecureContext) {
|
||||||
|
navigator.clipboard.writeText(text).then(function () {
|
||||||
|
Toastr.success(__('Copy success'));
|
||||||
|
}).catch(function () {
|
||||||
|
Controller.api.copyTextFallback(text);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Controller.api.copyTextFallback(text);
|
||||||
|
},
|
||||||
|
copyTextFallback: function (text) {
|
||||||
|
var $temp = $('<textarea>').css({position: 'fixed', left: '-9999px', top: '0'}).val(text);
|
||||||
|
$('body').append($temp);
|
||||||
|
$temp[0].select();
|
||||||
|
try {
|
||||||
|
document.execCommand('copy');
|
||||||
|
Toastr.success(__('Copy success'));
|
||||||
|
} catch (e) {
|
||||||
|
Toastr.error(__('Copy failed'));
|
||||||
|
}
|
||||||
|
$temp.remove();
|
||||||
|
},
|
||||||
|
bindLinkCodeInput: function () {
|
||||||
|
var $input = $('#c-link_code');
|
||||||
|
if (!$input.length || $input.prop('readonly')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$input.on('input', function () {
|
||||||
|
var val = $(this).val().replace(/[^a-zA-Z]/g, '').toLowerCase().slice(0, 9);
|
||||||
|
if ($(this).val() !== val) {
|
||||||
|
$(this).val(val);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
bindevent: function () {
|
||||||
|
Form.api.bindevent($("form[role=form]"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return Controller;
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user