Browse Source

调整模块管理

Anyon 4 years ago
parent
commit
5d523feaa5

+ 2 - 0
app/admin/module/change/2020.08.03.00.md

@@ -0,0 +1,2 @@
+# 系统模块初始化成功
+* 这次更新了许多内容哦

+ 1 - 12
app/admin/ver.php → app/admin/module/version.php

@@ -13,20 +13,9 @@
 // | github 代码仓库:https://github.com/zoujingli/ThinkAdmin
 // +----------------------------------------------------------------------
 
-// 模块配置文件
 return [
     'name'    => 'admin',
     'author'  => 'Anyon',
-    'version' => '2020.08.05.00',
+    'version' => '2020.08.03.00',
     'content' => 'ThinkAdmin 系统基础模块',
-    'changes' => [
-        '2020.08.05.00' => [
-            'content'  => '优化系统模块管理',
-            'database' => ['select version()'],
-        ],
-        '2020.08.03.00' => [
-            'content'  => '系统模块初始化提交',
-            'database' => ['select version()'],
-        ],
-    ],
 ];

+ 2 - 0
app/wechat/module/change/2020.08.03.00.md

@@ -0,0 +1,2 @@
+# 微信模块初始化成功
+* 这次更新了许多内容哦

+ 131 - 0
app/wechat/module/database/2020.08.03.00_install.sql

@@ -0,0 +1,131 @@
+-- ----------------------------
+-- Table structure for wechat_fans
+-- ----------------------------
+DROP TABLE IF EXISTS `_PREFIX_wechat_fans`;
+CREATE TABLE `_PREFIX_wechat_fans`  (
+  `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
+  `appid` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '公众号APPID',
+  `unionid` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '粉丝unionid',
+  `openid` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '粉丝openid',
+  `tagid_list` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '粉丝标签id',
+  `is_black` tinyint(1) UNSIGNED NULL DEFAULT 0 COMMENT '是否为黑名单状态',
+  `subscribe` tinyint(1) UNSIGNED NULL DEFAULT 0 COMMENT '关注状态(0未关注,1已关注)',
+  `nickname` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '用户昵称',
+  `sex` tinyint(1) UNSIGNED NULL DEFAULT 0 COMMENT '用户性别(1男性,2女性,0未知)',
+  `country` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '用户所在国家',
+  `province` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '用户所在省份',
+  `city` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '用户所在城市',
+  `language` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '用户的语言(zh_CN)',
+  `headimgurl` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '用户头像',
+  `subscribe_time` bigint(20) UNSIGNED NULL DEFAULT 0 COMMENT '关注时间',
+  `subscribe_at` datetime NULL DEFAULT NULL COMMENT '关注时间',
+  `remark` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '备注',
+  `subscribe_scene` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '扫码关注场景',
+  `qr_scene` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '二维码场景值',
+  `qr_scene_str` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '二维码场景内容',
+  `create_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  PRIMARY KEY (`id`) USING BTREE,
+  INDEX `index_wechat_fans_openid`(`openid`) USING BTREE,
+  INDEX `index_wechat_fans_unionid`(`unionid`) USING BTREE,
+  INDEX `index_wechat_fans_is_back`(`is_black`) USING BTREE,
+  INDEX `index_wechat_fans_subscribe`(`subscribe`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信-粉丝';
+
+-- ----------------------------
+-- Table structure for wechat_fans_tags
+-- ----------------------------
+DROP TABLE IF EXISTS `_PREFIX_wechat_fans_tags`;
+CREATE TABLE `_PREFIX_wechat_fans_tags`  (
+  `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '标签ID',
+  `appid` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '公众号APPID',
+  `name` varchar(35) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '标签名称',
+  `count` bigint(20) UNSIGNED NULL DEFAULT 0 COMMENT '总数',
+  `create_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建日期',
+  INDEX `index_wechat_fans_tags_id`(`id`) USING BTREE,
+  INDEX `index_wechat_fans_tags_appid`(`appid`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信-标签';
+
+-- ----------------------------
+-- Table structure for wechat_keys
+-- ----------------------------
+DROP TABLE IF EXISTS `_PREFIX_wechat_keys`;
+CREATE TABLE `_PREFIX_wechat_keys`  (
+  `id` bigint(20) NOT NULL AUTO_INCREMENT,
+  `appid` char(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '公众号APPID',
+  `type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '类型(text,image,news)',
+  `keys` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '关键字',
+  `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '文本内容',
+  `image_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '图片链接',
+  `voice_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '语音链接',
+  `music_title` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '音乐标题',
+  `music_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '音乐链接',
+  `music_image` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '缩略图片',
+  `music_desc` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '音乐描述',
+  `video_title` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '视频标题',
+  `video_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '视频URL',
+  `video_desc` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '视频描述',
+  `news_id` bigint(20) UNSIGNED NULL DEFAULT NULL COMMENT '图文ID',
+  `sort` bigint(20) UNSIGNED NULL DEFAULT 0 COMMENT '排序字段',
+  `status` tinyint(1) UNSIGNED NULL DEFAULT 1 COMMENT '状态(0禁用,1启用)',
+  `create_by` bigint(20) UNSIGNED NULL DEFAULT 0 COMMENT '创建人',
+  `create_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
+  PRIMARY KEY (`id`) USING BTREE,
+  INDEX `index_wechat_keys_appid`(`appid`) USING BTREE,
+  INDEX `index_wechat_keys_type`(`type`) USING BTREE,
+  INDEX `index_wechat_keys_keys`(`keys`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信-关键字';
+
+-- ----------------------------
+-- Table structure for wechat_media
+-- ----------------------------
+DROP TABLE IF EXISTS `_PREFIX_wechat_media`;
+CREATE TABLE `_PREFIX_wechat_media`  (
+  `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
+  `appid` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '公众号ID',
+  `md5` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '文件md5',
+  `type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '媒体类型',
+  `media_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '永久素材MediaID',
+  `local_url` varchar(300) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '本地文件链接',
+  `media_url` varchar(300) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '远程图片链接',
+  `create_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  PRIMARY KEY (`id`) USING BTREE,
+  INDEX `index_wechat_media_appid`(`appid`) USING BTREE,
+  INDEX `index_wechat_media_md5`(`md5`) USING BTREE,
+  INDEX `index_wechat_media_type`(`type`) USING BTREE,
+  INDEX `index_wechat_media_media_id`(`media_id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信-素材';
+
+-- ----------------------------
+-- Table structure for wechat_news
+-- ----------------------------
+DROP TABLE IF EXISTS `_PREFIX_wechat_news`;
+CREATE TABLE `_PREFIX_wechat_news`  (
+  `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
+  `media_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '永久素材MediaID',
+  `local_url` varchar(300) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '永久素材外网URL',
+  `article_id` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '关联图文ID(用英文逗号做分割)',
+  `is_deleted` tinyint(1) UNSIGNED NULL DEFAULT 0 COMMENT '删除状态(0未删除,1已删除)',
+  `create_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `create_by` bigint(20) NULL DEFAULT NULL COMMENT '创建人',
+  PRIMARY KEY (`id`) USING BTREE,
+  INDEX `index_wechat_news_artcle_id`(`article_id`) USING BTREE,
+  INDEX `index_wechat_news_media_id`(`media_id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信-图文';
+
+-- ----------------------------
+-- Table structure for wechat_news_article
+-- ----------------------------
+DROP TABLE IF EXISTS `_PREFIX_wechat_news_article`;
+CREATE TABLE `_PREFIX_wechat_news_article`  (
+  `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
+  `title` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '素材标题',
+  `local_url` varchar(300) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '永久素材显示URL',
+  `show_cover_pic` tinyint(4) UNSIGNED NULL DEFAULT 0 COMMENT '显示封面(0不显示,1显示)',
+  `author` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '文章作者',
+  `digest` varchar(300) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '摘要内容',
+  `content` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '图文内容',
+  `content_source_url` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '原文地址',
+  `read_num` bigint(20) UNSIGNED NULL DEFAULT 0 COMMENT '阅读数量',
+  `create_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信-文章';

+ 6 - 0
app/wechat/module/database/2020.08.03.00_unset.sql

@@ -0,0 +1,6 @@
+DROP TABLE IF EXISTS `_PREFIX_wechat_fans`;
+DROP TABLE IF EXISTS `_PREFIX_wechat_fans_tags`;
+DROP TABLE IF EXISTS `_PREFIX_wechat_keys`;
+DROP TABLE IF EXISTS `_PREFIX_wechat_media`;
+DROP TABLE IF EXISTS `_PREFIX_wechat_news`;
+DROP TABLE IF EXISTS `_PREFIX_wechat_news_article`;

+ 1 - 0
app/wechat/module/database/2020.08.03.00_update.sql

@@ -0,0 +1 @@
+<?php

+ 0 - 9
app/wechat/ver.php → app/wechat/module/version.php

@@ -13,18 +13,9 @@
 // | github 代码仓库:https://github.com/zoujingli/ThinkAdmin
 // +----------------------------------------------------------------------
 
-// 模块配置文件
 return [
     'name'    => 'wechat',
     'author'  => 'Anyon',
     'version' => '2020.08.03.01',
     'content' => 'ThinkAdmin 微信基础模块',
-    'changes' => [
-        '2020.08.03.00' => [
-            'content'  => '模块初始化提交',
-            'database' => [
-                'select version()',
-            ],
-        ],
-    ],
 ];

+ 5 - 4
composer.lock

@@ -937,12 +937,12 @@
             "source": {
                 "type": "git",
                 "url": "https://github.com/zoujingli/ThinkLibrary.git",
-                "reference": "08aff82bffe0e4b6356373c65ddf7c2d521598a5"
+                "reference": "822d461c4f23e5ab3cd3489cd740a7171706e367"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/zoujingli/ThinkLibrary/zipball/08aff82bffe0e4b6356373c65ddf7c2d521598a5",
-                "reference": "08aff82bffe0e4b6356373c65ddf7c2d521598a5",
+                "url": "https://api.github.com/repos/zoujingli/ThinkLibrary/zipball/822d461c4f23e5ab3cd3489cd740a7171706e367",
+                "reference": "822d461c4f23e5ab3cd3489cd740a7171706e367",
                 "shasum": "",
                 "mirrors": [
                     {
@@ -956,6 +956,7 @@
                 "ext-gd": "*",
                 "ext-iconv": "*",
                 "ext-json": "*",
+                "ext-mbstring": "*",
                 "topthink/framework": "^6.0"
             },
             "type": "library",
@@ -986,7 +987,7 @@
             ],
             "description": "ThinkPHP v6.0 Development Library",
             "homepage": "http://thinkadmin.top",
-            "time": "2020-08-14T09:41:49+00:00"
+            "time": "2020-08-17T07:02:36+00:00"
         },
         {
             "name": "zoujingli/wechat-developer",

+ 1 - 1
config/database.php

@@ -28,7 +28,7 @@ return [
             // 数据库类型
             'type'              => 'mysql',
             // 服务器地址
-            'hostname'          => '127.0.0.1',
+            'hostname'          => 'server.cuci.cc',
             // 数据库名
             'database'          => 'admin_v6',
             // 用户名

+ 1 - 0
vendor/composer/autoload_classmap.php

@@ -298,6 +298,7 @@ return array(
     'think\\admin\\service\\CaptchaService' => $vendorDir . '/zoujingli/think-library/src/service/CaptchaService.php',
     'think\\admin\\service\\ExpressService' => $vendorDir . '/zoujingli/think-library/src/service/ExpressService.php',
     'think\\admin\\service\\InstallService' => $vendorDir . '/zoujingli/think-library/src/service/InstallService.php',
+    'think\\admin\\service\\MarkdownService' => $vendorDir . '/zoujingli/think-library/src/service/MarkdownService.php',
     'think\\admin\\service\\MenuService' => $vendorDir . '/zoujingli/think-library/src/service/MenuService.php',
     'think\\admin\\service\\MessageService' => $vendorDir . '/zoujingli/think-library/src/service/MessageService.php',
     'think\\admin\\service\\ModuleService' => $vendorDir . '/zoujingli/think-library/src/service/ModuleService.php',

+ 1 - 0
vendor/composer/autoload_static.php

@@ -431,6 +431,7 @@ class ComposerStaticInitb911c14a0826c73d9f097343fd33a252
         'think\\admin\\service\\CaptchaService' => __DIR__ . '/..' . '/zoujingli/think-library/src/service/CaptchaService.php',
         'think\\admin\\service\\ExpressService' => __DIR__ . '/..' . '/zoujingli/think-library/src/service/ExpressService.php',
         'think\\admin\\service\\InstallService' => __DIR__ . '/..' . '/zoujingli/think-library/src/service/InstallService.php',
+        'think\\admin\\service\\MarkdownService' => __DIR__ . '/..' . '/zoujingli/think-library/src/service/MarkdownService.php',
         'think\\admin\\service\\MenuService' => __DIR__ . '/..' . '/zoujingli/think-library/src/service/MenuService.php',
         'think\\admin\\service\\MessageService' => __DIR__ . '/..' . '/zoujingli/think-library/src/service/MessageService.php',
         'think\\admin\\service\\ModuleService' => __DIR__ . '/..' . '/zoujingli/think-library/src/service/ModuleService.php',

+ 5 - 4
vendor/composer/installed.json

@@ -963,12 +963,12 @@
         "source": {
             "type": "git",
             "url": "https://github.com/zoujingli/ThinkLibrary.git",
-            "reference": "08aff82bffe0e4b6356373c65ddf7c2d521598a5"
+            "reference": "822d461c4f23e5ab3cd3489cd740a7171706e367"
         },
         "dist": {
             "type": "zip",
-            "url": "https://api.github.com/repos/zoujingli/ThinkLibrary/zipball/08aff82bffe0e4b6356373c65ddf7c2d521598a5",
-            "reference": "08aff82bffe0e4b6356373c65ddf7c2d521598a5",
+            "url": "https://api.github.com/repos/zoujingli/ThinkLibrary/zipball/822d461c4f23e5ab3cd3489cd740a7171706e367",
+            "reference": "822d461c4f23e5ab3cd3489cd740a7171706e367",
             "shasum": "",
             "mirrors": [
                 {
@@ -982,9 +982,10 @@
             "ext-gd": "*",
             "ext-iconv": "*",
             "ext-json": "*",
+            "ext-mbstring": "*",
             "topthink/framework": "^6.0"
         },
-        "time": "2020-08-14T09:41:49+00:00",
+        "time": "2020-08-17T07:02:36+00:00",
         "type": "library",
         "extra": {
             "think": {

+ 1 - 1
vendor/services.php

@@ -1,5 +1,5 @@
 <?php 
-// This file is automatically generated at:2020-08-17 10:22:34
+// This file is automatically generated at:2020-08-17 15:04:49
 declare (strict_types = 1);
 return array (
   0 => 'think\\admin\\Library',

+ 1 - 0
vendor/zoujingli/think-library/composer.json

@@ -15,6 +15,7 @@
     "ext-curl": "*",
     "ext-json": "*",
     "ext-iconv": "*",
+    "ext-mbstring": "*",
     "topthink/framework": "^6.0"
   },
   "autoload": {

+ 4 - 4
vendor/zoujingli/think-library/src/command/Queue.php

@@ -94,7 +94,7 @@ class Queue extends Command
         $host = $this->input->getOption('host') ?: '127.0.0.1';
         $root = $this->app->getRootPath() . 'public' . DIRECTORY_SEPARATOR;
         $command = "php -S {$host}:{$port} -t {$root} {$root}router.php";
-        $this->output->highlight("># {$command}");
+        $this->output->comment("># {$command}");
         if (count($result = $this->process->query($command)) > 0) {
             if ($this->process->iswin()) $this->process->exec("start http://{$host}:{$port}");
             $this->output->writeln(">> WebServer process already exist for pid {$result[0]['pid']}");
@@ -116,7 +116,7 @@ class Queue extends Command
     {
         $root = $this->app->getRootPath() . 'public' . DIRECTORY_SEPARATOR;
         if (count($result = $this->process->query("-t {$root} {$root}router.php")) > 0) {
-            $this->output->highlight("># {$result[0]['cmd']}");
+            $this->output->comment("># {$result[0]['cmd']}");
             $this->output->writeln(">> WebServer process {$result[0]['pid']} running");
         } else {
             $this->output->writeln(">> The WebServer process is not running");
@@ -144,7 +144,7 @@ class Queue extends Command
     {
         $this->app->db->name($this->table)->count();
         $command = $this->process->think('xadmin:queue listen');
-        $this->output->highlight("># {$command}");
+        $this->output->comment("># {$command}");
         if (count($result = $this->process->query($command)) > 0) {
             $this->output->writeln(">> Asynchronous daemons already exist for pid {$result[0]['pid']}");
         } else {
@@ -234,7 +234,7 @@ class Queue extends Command
             [$start, $where] = [microtime(true), [['status', '=', 1], ['exec_time', '<=', time()]]];
             foreach ($this->app->db->name($this->table)->where($where)->order('exec_time asc')->select()->toArray() as $vo) try {
                 $command = $this->process->think("xadmin:queue dorun {$vo['code']} -");
-                $this->output->highlight("># {$command}");
+                $this->output->comment("># {$command}");
                 if (count($this->process->query($command)) > 0) {
                     $this->output->writeln(">> Already in progress -> [{$vo['code']}] {$vo['title']}");
                 } else {

+ 1504 - 0
vendor/zoujingli/think-library/src/extend/Parsedown.php

@@ -0,0 +1,1504 @@
+<?php
+
+// +----------------------------------------------------------------------
+// | Library for ThinkAdmin
+// +----------------------------------------------------------------------
+// | 版权所有 2014~2020 广州楚才信息科技有限公司 [ http://www.cuci.cc ]
+// +----------------------------------------------------------------------
+// | 官方网站: https://gitee.com/zoujingli/ThinkLibrary
+// +----------------------------------------------------------------------
+// | 开源协议 ( https://mit-license.org )
+// +----------------------------------------------------------------------
+// | gitee 仓库地址 :https://gitee.com/zoujingli/ThinkLibrary
+// | github 仓库地址 :https://github.com/zoujingli/ThinkLibrary
+// +----------------------------------------------------------------------
+// | 以下源代码来自 https://github.com/erusev/parsedown ,基于 MIT 协议开源
+// | 为减少 Composer 组件数量优化加载速度,直接将源码复制于此,在此表示感谢原作者 !
+// +----------------------------------------------------------------------
+
+namespace think\admin\extend;
+
+/**
+ * Markdown 文档解析服务
+ * Class Parsedown
+ * @package think\admin\extend
+ */
+class Parsedown
+{
+    const version = '1.7.4';
+
+    function text($text)
+    {
+        # make sure no definitions are set
+        $this->DefinitionData = [];
+        # standardize line breaks
+        $text = str_replace(["\r\n", "\r"], "\n", $text);
+        # remove surrounding line breaks
+        $text = trim($text, "\n");
+        # split text into lines
+        $lines = explode("\n", $text);
+        # iterate through lines to identify blocks
+        $markup = $this->lines($lines);
+        # trim line breaks
+        $markup = trim($markup, "\n");
+        return $markup;
+    }
+
+    #
+    # Setters
+    #
+
+    function setBreaksEnabled($breaksEnabled)
+    {
+        $this->breaksEnabled = $breaksEnabled;
+        return $this;
+    }
+
+    protected $breaksEnabled;
+
+    function setMarkupEscaped($markupEscaped)
+    {
+        $this->markupEscaped = $markupEscaped;
+        return $this;
+    }
+
+    protected $markupEscaped;
+
+    function setUrlsLinked($urlsLinked)
+    {
+        $this->urlsLinked = $urlsLinked;
+        return $this;
+    }
+
+    protected $urlsLinked = true;
+
+    function setSafeMode($safeMode)
+    {
+        $this->safeMode = (bool)$safeMode;
+        return $this;
+    }
+
+    protected $safeMode;
+
+    protected $safeLinksWhitelist = [
+        'http://',
+        'https://',
+        'ftp://',
+        'ftps://',
+        'mailto:',
+        'data:image/png;base64,',
+        'data:image/gif;base64,',
+        'data:image/jpeg;base64,',
+        'irc:',
+        'ircs:',
+        'git:',
+        'ssh:',
+        'news:',
+        'steam:',
+    ];
+
+    #
+    # Lines
+    #
+
+    protected $BlockTypes = [
+        '#' => ['Header'],
+        '*' => ['Rule', 'List'],
+        '+' => ['List'],
+        '-' => ['SetextHeader', 'Table', 'Rule', 'List'],
+        '0' => ['List'],
+        '1' => ['List'],
+        '2' => ['List'],
+        '3' => ['List'],
+        '4' => ['List'],
+        '5' => ['List'],
+        '6' => ['List'],
+        '7' => ['List'],
+        '8' => ['List'],
+        '9' => ['List'],
+        ':' => ['Table'],
+        '<' => ['Comment', 'Markup'],
+        '=' => ['SetextHeader'],
+        '>' => ['Quote'],
+        '[' => ['Reference'],
+        '_' => ['Rule'],
+        '`' => ['FencedCode'],
+        '|' => ['Table'],
+        '~' => ['FencedCode'],
+    ];
+
+    # ~
+
+    protected $unmarkedBlockTypes = ['Code'];
+
+    #
+    # Blocks
+    #
+
+    protected function lines(array $lines)
+    {
+        $CurrentBlock = null;
+        foreach ($lines as $line) {
+            if (chop($line) === '') {
+                if (isset($CurrentBlock)) {
+                    $CurrentBlock['interrupted'] = true;
+                }
+                continue;
+            }
+            if (strpos($line, "\t") !== false) {
+                $parts = explode("\t", $line);
+                $line = $parts[0];
+                unset($parts[0]);
+                foreach ($parts as $part) {
+                    $shortage = 4 - mb_strlen($line, 'utf-8') % 4;
+                    $line .= str_repeat(' ', $shortage);
+                    $line .= $part;
+                }
+            }
+            $indent = 0;
+            while (isset($line[$indent]) and $line[$indent] === ' ') {
+                $indent++;
+            }
+            $text = $indent > 0 ? substr($line, $indent) : $line;
+            $Line = ['body' => $line, 'indent' => $indent, 'text' => $text];
+            if (isset($CurrentBlock['continuable'])) {
+                $Block = $this->{'block' . $CurrentBlock['type'] . 'Continue'}($Line, $CurrentBlock);
+                if (isset($Block)) {
+                    $CurrentBlock = $Block;
+                    continue;
+                } else {
+                    if ($this->isBlockCompletable($CurrentBlock['type'])) {
+                        $CurrentBlock = $this->{'block' . $CurrentBlock['type'] . 'Complete'}($CurrentBlock);
+                    }
+                }
+            }
+            $marker = $text[0];
+            $blockTypes = $this->unmarkedBlockTypes;
+            if (isset($this->BlockTypes[$marker])) {
+                foreach ($this->BlockTypes[$marker] as $blockType) {
+                    $blockTypes [] = $blockType;
+                }
+            }
+            foreach ($blockTypes as $blockType) {
+                $Block = $this->{'block' . $blockType}($Line, $CurrentBlock);
+                if (isset($Block)) {
+                    $Block['type'] = $blockType;
+                    if (!isset($Block['identified'])) {
+                        $Blocks [] = $CurrentBlock;
+                        $Block['identified'] = true;
+                    }
+                    if ($this->isBlockContinuable($blockType)) {
+                        $Block['continuable'] = true;
+                    }
+                    $CurrentBlock = $Block;
+                    continue 2;
+                }
+            }
+
+            # ~
+
+            if (isset($CurrentBlock) and !isset($CurrentBlock['type']) and !isset($CurrentBlock['interrupted'])) {
+                $CurrentBlock['element']['text'] .= "\n" . $text;
+            } else {
+                $Blocks [] = $CurrentBlock;
+                $CurrentBlock = $this->paragraph($Line);
+                $CurrentBlock['identified'] = true;
+            }
+        }
+
+        if (isset($CurrentBlock['continuable']) and $this->isBlockCompletable($CurrentBlock['type'])) {
+            $CurrentBlock = $this->{'block' . $CurrentBlock['type'] . 'Complete'}($CurrentBlock);
+        }
+
+        $Blocks [] = $CurrentBlock;
+
+        unset($Blocks[0]);
+
+        # ~
+
+        $markup = '';
+
+        foreach ($Blocks as $Block) {
+            if (isset($Block['hidden'])) {
+                continue;
+            }
+
+            $markup .= "\n";
+            $markup .= isset($Block['markup']) ? $Block['markup'] : $this->element($Block['element']);
+        }
+
+        $markup .= "\n";
+
+        # ~
+
+        return $markup;
+    }
+
+    protected function isBlockContinuable($Type)
+    {
+        return method_exists($this, 'block' . $Type . 'Continue');
+    }
+
+    protected function isBlockCompletable($Type)
+    {
+        return method_exists($this, 'block' . $Type . 'Complete');
+    }
+
+    #
+    # Code
+
+    protected function blockCode($Line, $Block = null)
+    {
+        if (isset($Block) and !isset($Block['type']) and !isset($Block['interrupted'])) {
+            return;
+        }
+
+        if ($Line['indent'] >= 4) {
+            $text = substr($Line['body'], 4);
+
+            $Block = [
+                'element' => [
+                    'name'    => 'pre',
+                    'handler' => 'element',
+                    'text'    => [
+                        'name' => 'code',
+                        'text' => $text,
+                    ],
+                ],
+            ];
+
+            return $Block;
+        }
+    }
+
+    protected function blockCodeContinue($Line, $Block)
+    {
+        if ($Line['indent'] >= 4) {
+            if (isset($Block['interrupted'])) {
+                $Block['element']['text']['text'] .= "\n";
+
+                unset($Block['interrupted']);
+            }
+
+            $Block['element']['text']['text'] .= "\n";
+
+            $text = substr($Line['body'], 4);
+
+            $Block['element']['text']['text'] .= $text;
+
+            return $Block;
+        }
+    }
+
+    protected function blockCodeComplete($Block)
+    {
+        $text = $Block['element']['text']['text'];
+
+        $Block['element']['text']['text'] = $text;
+
+        return $Block;
+    }
+
+    #
+    # Comment
+
+    protected function blockComment($Line)
+    {
+        if ($this->markupEscaped or $this->safeMode) {
+            return;
+        }
+
+        if (isset($Line['text'][3]) and $Line['text'][3] === '-' and $Line['text'][2] === '-' and $Line['text'][1] === '!') {
+            $Block = [
+                'markup' => $Line['body'],
+            ];
+
+            if (preg_match('/-->$/', $Line['text'])) {
+                $Block['closed'] = true;
+            }
+
+            return $Block;
+        }
+    }
+
+    protected function blockCommentContinue($Line, array $Block)
+    {
+        if (isset($Block['closed'])) {
+            return;
+        }
+
+        $Block['markup'] .= "\n" . $Line['body'];
+
+        if (preg_match('/-->$/', $Line['text'])) {
+            $Block['closed'] = true;
+        }
+
+        return $Block;
+    }
+
+    #
+    # Fenced Code
+
+    protected function blockFencedCode($Line)
+    {
+        if (preg_match('/^[' . $Line['text'][0] . ']{3,}[ ]*([^`]+)?[ ]*$/', $Line['text'], $matches)) {
+            $Element = [
+                'name' => 'code',
+                'text' => '',
+            ];
+
+            if (isset($matches[1])) {
+                /**
+                 * https://www.w3.org/TR/2011/WD-html5-20110525/elements.html#classes
+                 * Every HTML element may have a class attribute specified.
+                 * The attribute, if specified, must have a value that is a set
+                 * of space-separated tokens representing the various classes
+                 * that the element belongs to.
+                 * [...]
+                 * The space characters, for the purposes of this specification,
+                 * are U+0020 SPACE, U+0009 CHARACTER TABULATION (tab),
+                 * U+000A LINE FEED (LF), U+000C FORM FEED (FF), and
+                 * U+000D CARRIAGE RETURN (CR).
+                 */
+                $language = substr($matches[1], 0, strcspn($matches[1], " \t\n\f\r"));
+
+                $class = 'language-' . $language;
+
+                $Element['attributes'] = [
+                    'class' => $class,
+                ];
+            }
+
+            $Block = [
+                'char'    => $Line['text'][0],
+                'element' => [
+                    'name'    => 'pre',
+                    'handler' => 'element',
+                    'text'    => $Element,
+                ],
+            ];
+
+            return $Block;
+        }
+    }
+
+    protected function blockFencedCodeContinue($Line, $Block)
+    {
+        if (isset($Block['complete'])) {
+            return;
+        }
+
+        if (isset($Block['interrupted'])) {
+            $Block['element']['text']['text'] .= "\n";
+
+            unset($Block['interrupted']);
+        }
+
+        if (preg_match('/^' . $Block['char'] . '{3,}[ ]*$/', $Line['text'])) {
+            $Block['element']['text']['text'] = substr($Block['element']['text']['text'], 1);
+
+            $Block['complete'] = true;
+
+            return $Block;
+        }
+
+        $Block['element']['text']['text'] .= "\n" . $Line['body'];
+
+        return $Block;
+    }
+
+    protected function blockFencedCodeComplete($Block)
+    {
+        $text = $Block['element']['text']['text'];
+
+        $Block['element']['text']['text'] = $text;
+
+        return $Block;
+    }
+
+    #
+    # Header
+
+    protected function blockHeader($Line)
+    {
+        if (isset($Line['text'][1])) {
+            $level = 1;
+
+            while (isset($Line['text'][$level]) and $Line['text'][$level] === '#') {
+                $level++;
+            }
+
+            if ($level > 6) {
+                return;
+            }
+
+            $text = trim($Line['text'], '# ');
+
+            $Block = [
+                'element' => [
+                    'name'    => 'h' . min(6, $level),
+                    'text'    => $text,
+                    'handler' => 'line',
+                ],
+            ];
+
+            return $Block;
+        }
+    }
+
+    #
+    # List
+
+    protected function blockList($Line)
+    {
+        [$name, $pattern] = $Line['text'][0] <= '-' ? ['ul', '[*+-]'] : ['ol', '[0-9]+[.]'];
+
+        if (preg_match('/^(' . $pattern . '[ ]+)(.*)/', $Line['text'], $matches)) {
+            $Block = [
+                'indent'  => $Line['indent'],
+                'pattern' => $pattern,
+                'element' => [
+                    'name'    => $name,
+                    'handler' => 'elements',
+                ],
+            ];
+
+            if ($name === 'ol') {
+                $listStart = stristr($matches[0], '.', true);
+
+                if ($listStart !== '1') {
+                    $Block['element']['attributes'] = ['start' => $listStart];
+                }
+            }
+
+            $Block['li'] = [
+                'name'    => 'li',
+                'handler' => 'li',
+                'text'    => [
+                    $matches[2],
+                ],
+            ];
+
+            $Block['element']['text'] [] = &$Block['li'];
+
+            return $Block;
+        }
+    }
+
+    protected function blockListContinue($Line, array $Block)
+    {
+        if ($Block['indent'] === $Line['indent'] and preg_match('/^' . $Block['pattern'] . '(?:[ ]+(.*)|$)/', $Line['text'], $matches)) {
+            if (isset($Block['interrupted'])) {
+                $Block['li']['text'] [] = '';
+
+                $Block['loose'] = true;
+
+                unset($Block['interrupted']);
+            }
+
+            unset($Block['li']);
+
+            $text = isset($matches[1]) ? $matches[1] : '';
+
+            $Block['li'] = [
+                'name'    => 'li',
+                'handler' => 'li',
+                'text'    => [
+                    $text,
+                ],
+            ];
+
+            $Block['element']['text'] [] = &$Block['li'];
+
+            return $Block;
+        }
+
+        if ($Line['text'][0] === '[' and $this->blockReference($Line)) {
+            return $Block;
+        }
+
+        if (!isset($Block['interrupted'])) {
+            $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']);
+
+            $Block['li']['text'] [] = $text;
+
+            return $Block;
+        }
+
+        if ($Line['indent'] > 0) {
+            $Block['li']['text'] [] = '';
+
+            $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']);
+
+            $Block['li']['text'] [] = $text;
+
+            unset($Block['interrupted']);
+
+            return $Block;
+        }
+    }
+
+    protected function blockListComplete(array $Block)
+    {
+        if (isset($Block['loose'])) {
+            foreach ($Block['element']['text'] as &$li) {
+                if (end($li['text']) !== '') {
+                    $li['text'] [] = '';
+                }
+            }
+        }
+
+        return $Block;
+    }
+
+    #
+    # Quote
+
+    protected function blockQuote($Line)
+    {
+        if (preg_match('/^>[ ]?(.*)/', $Line['text'], $matches)) {
+            $Block = [
+                'element' => [
+                    'name'    => 'blockquote',
+                    'handler' => 'lines',
+                    'text'    => (array)$matches[1],
+                ],
+            ];
+
+            return $Block;
+        }
+    }
+
+    protected function blockQuoteContinue($Line, array $Block)
+    {
+        if ($Line['text'][0] === '>' and preg_match('/^>[ ]?(.*)/', $Line['text'], $matches)) {
+            if (isset($Block['interrupted'])) {
+                $Block['element']['text'] [] = '';
+
+                unset($Block['interrupted']);
+            }
+
+            $Block['element']['text'] [] = $matches[1];
+
+            return $Block;
+        }
+
+        if (!isset($Block['interrupted'])) {
+            $Block['element']['text'] [] = $Line['text'];
+
+            return $Block;
+        }
+    }
+
+    #
+    # Rule
+
+    protected function blockRule($Line)
+    {
+        if (preg_match('/^([' . $Line['text'][0] . '])([ ]*\1){2,}[ ]*$/', $Line['text'])) {
+            $Block = [
+                'element' => [
+                    'name' => 'hr',
+                ],
+            ];
+
+            return $Block;
+        }
+    }
+
+    #
+    # Setext
+
+    protected function blockSetextHeader($Line, array $Block = null)
+    {
+        if (!isset($Block) or isset($Block['type']) or isset($Block['interrupted'])) {
+            return;
+        }
+
+        if (chop($Line['text'], $Line['text'][0]) === '') {
+            $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2';
+
+            return $Block;
+        }
+    }
+
+    #
+    # Markup
+
+    protected function blockMarkup($Line)
+    {
+        if ($this->markupEscaped or $this->safeMode) {
+            return;
+        }
+
+        if (preg_match('/^<(\w[\w-]*)(?:[ ]*' . $this->regexHtmlAttribute . ')*[ ]*(\/)?>/', $Line['text'], $matches)) {
+            $element = strtolower($matches[1]);
+
+            if (in_array($element, $this->textLevelElements)) {
+                return;
+            }
+
+            $Block = [
+                'name'   => $matches[1],
+                'depth'  => 0,
+                'markup' => $Line['text'],
+            ];
+
+            $length = strlen($matches[0]);
+
+            $remainder = substr($Line['text'], $length);
+
+            if (trim($remainder) === '') {
+                if (isset($matches[2]) or in_array($matches[1], $this->voidElements)) {
+                    $Block['closed'] = true;
+
+                    $Block['void'] = true;
+                }
+            } else {
+                if (isset($matches[2]) or in_array($matches[1], $this->voidElements)) {
+                    return;
+                }
+
+                if (preg_match('/<\/' . $matches[1] . '>[ ]*$/i', $remainder)) {
+                    $Block['closed'] = true;
+                }
+            }
+
+            return $Block;
+        }
+    }
+
+    protected function blockMarkupContinue($Line, array $Block)
+    {
+        if (isset($Block['closed'])) {
+            return;
+        }
+
+        if (preg_match('/^<' . $Block['name'] . '(?:[ ]*' . $this->regexHtmlAttribute . ')*[ ]*>/i', $Line['text'])) # open
+        {
+            $Block['depth']++;
+        }
+
+        if (preg_match('/(.*?)<\/' . $Block['name'] . '>[ ]*$/i', $Line['text'], $matches)) # close
+        {
+            if ($Block['depth'] > 0) {
+                $Block['depth']--;
+            } else {
+                $Block['closed'] = true;
+            }
+        }
+
+        if (isset($Block['interrupted'])) {
+            $Block['markup'] .= "\n";
+
+            unset($Block['interrupted']);
+        }
+
+        $Block['markup'] .= "\n" . $Line['body'];
+
+        return $Block;
+    }
+
+    #
+    # Reference
+
+    protected function blockReference($Line)
+    {
+        if (preg_match('/^\[(.+?)\]:[ ]*<?(\S+?)>?(?:[ ]+["\'(](.+)["\')])?[ ]*$/', $Line['text'], $matches)) {
+            $id = strtolower($matches[1]);
+
+            $Data = [
+                'url'   => $matches[2],
+                'title' => null,
+            ];
+
+            if (isset($matches[3])) {
+                $Data['title'] = $matches[3];
+            }
+
+            $this->DefinitionData['Reference'][$id] = $Data;
+
+            $Block = [
+                'hidden' => true,
+            ];
+
+            return $Block;
+        }
+    }
+
+    #
+    # Table
+
+    protected function blockTable($Line, array $Block = null)
+    {
+        if (!isset($Block) or isset($Block['type']) or isset($Block['interrupted'])) {
+            return;
+        }
+
+        if (strpos($Block['element']['text'], '|') !== false and chop($Line['text'], ' -:|') === '') {
+            $alignments = [];
+
+            $divider = $Line['text'];
+
+            $divider = trim($divider);
+            $divider = trim($divider, '|');
+
+            $dividerCells = explode('|', $divider);
+
+            foreach ($dividerCells as $dividerCell) {
+                $dividerCell = trim($dividerCell);
+
+                if ($dividerCell === '') {
+                    continue;
+                }
+
+                $alignment = null;
+
+                if ($dividerCell[0] === ':') {
+                    $alignment = 'left';
+                }
+
+                if (substr($dividerCell, -1) === ':') {
+                    $alignment = $alignment === 'left' ? 'center' : 'right';
+                }
+
+                $alignments [] = $alignment;
+            }
+
+            # ~
+
+            $HeaderElements = [];
+
+            $header = $Block['element']['text'];
+
+            $header = trim($header);
+            $header = trim($header, '|');
+
+            $headerCells = explode('|', $header);
+
+            foreach ($headerCells as $index => $headerCell) {
+                $headerCell = trim($headerCell);
+
+                $HeaderElement = [
+                    'name'    => 'th',
+                    'text'    => $headerCell,
+                    'handler' => 'line',
+                ];
+
+                if (isset($alignments[$index])) {
+                    $alignment = $alignments[$index];
+
+                    $HeaderElement['attributes'] = [
+                        'style' => 'text-align: ' . $alignment . ';',
+                    ];
+                }
+
+                $HeaderElements [] = $HeaderElement;
+            }
+
+            # ~
+
+            $Block = [
+                'alignments' => $alignments,
+                'identified' => true,
+                'element'    => [
+                    'name'    => 'table',
+                    'handler' => 'elements',
+                ],
+            ];
+
+            $Block['element']['text'] [] = [
+                'name'    => 'thead',
+                'handler' => 'elements',
+            ];
+
+            $Block['element']['text'] [] = [
+                'name'    => 'tbody',
+                'handler' => 'elements',
+                'text'    => [],
+            ];
+
+            $Block['element']['text'][0]['text'] [] = [
+                'name'    => 'tr',
+                'handler' => 'elements',
+                'text'    => $HeaderElements,
+            ];
+
+            return $Block;
+        }
+    }
+
+    protected function blockTableContinue($Line, array $Block)
+    {
+        if (isset($Block['interrupted'])) {
+            return;
+        }
+
+        if ($Line['text'][0] === '|' or strpos($Line['text'], '|')) {
+            $Elements = [];
+
+            $row = $Line['text'];
+
+            $row = trim($row);
+            $row = trim($row, '|');
+
+            preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]+`|`)+/', $row, $matches);
+
+            foreach ($matches[0] as $index => $cell) {
+                $cell = trim($cell);
+
+                $Element = [
+                    'name'    => 'td',
+                    'handler' => 'line',
+                    'text'    => $cell,
+                ];
+
+                if (isset($Block['alignments'][$index])) {
+                    $Element['attributes'] = [
+                        'style' => 'text-align: ' . $Block['alignments'][$index] . ';',
+                    ];
+                }
+
+                $Elements [] = $Element;
+            }
+
+            $Element = [
+                'name'    => 'tr',
+                'handler' => 'elements',
+                'text'    => $Elements,
+            ];
+
+            $Block['element']['text'][1]['text'] [] = $Element;
+
+            return $Block;
+        }
+    }
+
+    #
+    # ~
+    #
+
+    protected function paragraph($Line)
+    {
+        $Block = [
+            'element' => [
+                'name'    => 'p',
+                'text'    => $Line['text'],
+                'handler' => 'line',
+            ],
+        ];
+
+        return $Block;
+    }
+
+    #
+    # Inline Elements
+    #
+
+    protected $InlineTypes = [
+        '"'  => ['SpecialCharacter'],
+        '!'  => ['Image'],
+        '&'  => ['SpecialCharacter'],
+        '*'  => ['Emphasis'],
+        ':'  => ['Url'],
+        '<'  => ['UrlTag', 'EmailTag', 'Markup', 'SpecialCharacter'],
+        '>'  => ['SpecialCharacter'],
+        '['  => ['Link'],
+        '_'  => ['Emphasis'],
+        '`'  => ['Code'],
+        '~'  => ['Strikethrough'],
+        '\\' => ['EscapeSequence'],
+    ];
+
+    # ~
+
+    protected $inlineMarkerList = '!"*_&[:<>`~\\';
+
+    #
+    # ~
+    #
+
+    public function line($text, $nonNestables = [])
+    {
+        $markup = '';
+
+        # $excerpt is based on the first occurrence of a marker
+
+        while ($excerpt = strpbrk($text, $this->inlineMarkerList)) {
+            $marker = $excerpt[0];
+
+            $markerPosition = strpos($text, $marker);
+
+            $Excerpt = ['text' => $excerpt, 'context' => $text];
+
+            foreach ($this->InlineTypes[$marker] as $inlineType) {
+                # check to see if the current inline type is nestable in the current context
+
+                if (!empty($nonNestables) and in_array($inlineType, $nonNestables)) {
+                    continue;
+                }
+
+                $Inline = $this->{'inline' . $inlineType}($Excerpt);
+
+                if (!isset($Inline)) {
+                    continue;
+                }
+
+                # makes sure that the inline belongs to "our" marker
+
+                if (isset($Inline['position']) and $Inline['position'] > $markerPosition) {
+                    continue;
+                }
+
+                # sets a default inline position
+
+                if (!isset($Inline['position'])) {
+                    $Inline['position'] = $markerPosition;
+                }
+
+                # cause the new element to 'inherit' our non nestables
+
+                foreach ($nonNestables as $non_nestable) {
+                    $Inline['element']['nonNestables'][] = $non_nestable;
+                }
+
+                # the text that comes before the inline
+                $unmarkedText = substr($text, 0, $Inline['position']);
+
+                # compile the unmarked text
+                $markup .= $this->unmarkedText($unmarkedText);
+
+                # compile the inline
+                $markup .= isset($Inline['markup']) ? $Inline['markup'] : $this->element($Inline['element']);
+
+                # remove the examined text
+                $text = substr($text, $Inline['position'] + $Inline['extent']);
+
+                continue 2;
+            }
+
+            # the marker does not belong to an inline
+
+            $unmarkedText = substr($text, 0, $markerPosition + 1);
+
+            $markup .= $this->unmarkedText($unmarkedText);
+
+            $text = substr($text, $markerPosition + 1);
+        }
+
+        $markup .= $this->unmarkedText($text);
+
+        return $markup;
+    }
+
+    #
+    # ~
+    #
+
+    protected function inlineCode($Excerpt)
+    {
+        $marker = $Excerpt['text'][0];
+
+        if (preg_match('/^(' . $marker . '+)[ ]*(.+?)[ ]*(?<!' . $marker . ')\1(?!' . $marker . ')/s', $Excerpt['text'], $matches)) {
+            $text = $matches[2];
+            $text = preg_replace("/[ ]*\n/", ' ', $text);
+
+            return [
+                'extent'  => strlen($matches[0]),
+                'element' => [
+                    'name' => 'code',
+                    'text' => $text,
+                ],
+            ];
+        }
+    }
+
+    protected function inlineEmailTag($Excerpt)
+    {
+        if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<((mailto:)?\S+?@\S+?)>/i', $Excerpt['text'], $matches)) {
+            $url = $matches[1];
+
+            if (!isset($matches[2])) {
+                $url = 'mailto:' . $url;
+            }
+
+            return [
+                'extent'  => strlen($matches[0]),
+                'element' => [
+                    'name'       => 'a',
+                    'text'       => $matches[1],
+                    'attributes' => [
+                        'href' => $url,
+                    ],
+                ],
+            ];
+        }
+    }
+
+    protected function inlineEmphasis($Excerpt)
+    {
+        if (!isset($Excerpt['text'][1])) {
+            return;
+        }
+
+        $marker = $Excerpt['text'][0];
+
+        if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches)) {
+            $emphasis = 'strong';
+        } elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches)) {
+            $emphasis = 'em';
+        } else {
+            return;
+        }
+
+        return [
+            'extent'  => strlen($matches[0]),
+            'element' => [
+                'name'    => $emphasis,
+                'handler' => 'line',
+                'text'    => $matches[1],
+            ],
+        ];
+    }
+
+    protected function inlineEscapeSequence($Excerpt)
+    {
+        if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters)) {
+            return [
+                'markup' => $Excerpt['text'][1],
+                'extent' => 2,
+            ];
+        }
+    }
+
+    protected function inlineImage($Excerpt)
+    {
+        if (!isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[') {
+            return;
+        }
+
+        $Excerpt['text'] = substr($Excerpt['text'], 1);
+
+        $Link = $this->inlineLink($Excerpt);
+
+        if ($Link === null) {
+            return;
+        }
+
+        $Inline = [
+            'extent'  => $Link['extent'] + 1,
+            'element' => [
+                'name'       => 'img',
+                'attributes' => [
+                    'src' => $Link['element']['attributes']['href'],
+                    'alt' => $Link['element']['text'],
+                ],
+            ],
+        ];
+
+        $Inline['element']['attributes'] += $Link['element']['attributes'];
+
+        unset($Inline['element']['attributes']['href']);
+
+        return $Inline;
+    }
+
+    protected function inlineLink($Excerpt)
+    {
+        $Element = [
+            'name'         => 'a',
+            'handler'      => 'line',
+            'nonNestables' => ['Url', 'Link'],
+            'text'         => null,
+            'attributes'   => [
+                'href'  => null,
+                'title' => null,
+            ],
+        ];
+
+        $extent = 0;
+
+        $remainder = $Excerpt['text'];
+
+        if (preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches)) {
+            $Element['text'] = $matches[1];
+
+            $extent += strlen($matches[0]);
+
+            $remainder = substr($remainder, $extent);
+        } else {
+            return;
+        }
+
+        if (preg_match('/^[(]\s*+((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*"|\'[^\']*\'))?\s*[)]/', $remainder, $matches)) {
+            $Element['attributes']['href'] = $matches[1];
+
+            if (isset($matches[2])) {
+                $Element['attributes']['title'] = substr($matches[2], 1, -1);
+            }
+
+            $extent += strlen($matches[0]);
+        } else {
+            if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches)) {
+                $definition = strlen($matches[1]) ? $matches[1] : $Element['text'];
+                $definition = strtolower($definition);
+
+                $extent += strlen($matches[0]);
+            } else {
+                $definition = strtolower($Element['text']);
+            }
+
+            if (!isset($this->DefinitionData['Reference'][$definition])) {
+                return;
+            }
+
+            $Definition = $this->DefinitionData['Reference'][$definition];
+
+            $Element['attributes']['href'] = $Definition['url'];
+            $Element['attributes']['title'] = $Definition['title'];
+        }
+
+        return [
+            'extent'  => $extent,
+            'element' => $Element,
+        ];
+    }
+
+    protected function inlineMarkup($Excerpt)
+    {
+        if ($this->markupEscaped or $this->safeMode or strpos($Excerpt['text'], '>') === false) {
+            return;
+        }
+
+        if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w[\w-]*[ ]*>/s', $Excerpt['text'], $matches)) {
+            return [
+                'markup' => $matches[0],
+                'extent' => strlen($matches[0]),
+            ];
+        }
+
+        if ($Excerpt['text'][1] === '!' and preg_match('/^<!---?[^>-](?:-?[^-])*-->/s', $Excerpt['text'], $matches)) {
+            return [
+                'markup' => $matches[0],
+                'extent' => strlen($matches[0]),
+            ];
+        }
+
+        if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w[\w-]*(?:[ ]*' . $this->regexHtmlAttribute . ')*[ ]*\/?>/s', $Excerpt['text'], $matches)) {
+            return [
+                'markup' => $matches[0],
+                'extent' => strlen($matches[0]),
+            ];
+        }
+    }
+
+    protected function inlineSpecialCharacter($Excerpt)
+    {
+        if ($Excerpt['text'][0] === '&' and !preg_match('/^&#?\w+;/', $Excerpt['text'])) {
+            return [
+                'markup' => '&amp;',
+                'extent' => 1,
+            ];
+        }
+
+        $SpecialCharacter = ['>' => 'gt', '<' => 'lt', '"' => 'quot'];
+
+        if (isset($SpecialCharacter[$Excerpt['text'][0]])) {
+            return [
+                'markup' => '&' . $SpecialCharacter[$Excerpt['text'][0]] . ';',
+                'extent' => 1,
+            ];
+        }
+    }
+
+    protected function inlineStrikethrough($Excerpt)
+    {
+        if (!isset($Excerpt['text'][1])) {
+            return;
+        }
+
+        if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches)) {
+            return [
+                'extent'  => strlen($matches[0]),
+                'element' => [
+                    'name'    => 'del',
+                    'text'    => $matches[1],
+                    'handler' => 'line',
+                ],
+            ];
+        }
+    }
+
+    protected function inlineUrl($Excerpt)
+    {
+        if ($this->urlsLinked !== true or !isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/') {
+            return;
+        }
+
+        if (preg_match('/\bhttps?:[\/]{2}[^\s<]+\b\/*/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE)) {
+            $url = $matches[0][0];
+
+            $Inline = [
+                'extent'   => strlen($matches[0][0]),
+                'position' => $matches[0][1],
+                'element'  => [
+                    'name'       => 'a',
+                    'text'       => $url,
+                    'attributes' => [
+                        'href' => $url,
+                    ],
+                ],
+            ];
+
+            return $Inline;
+        }
+    }
+
+    protected function inlineUrlTag($Excerpt)
+    {
+        if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w+:\/{2}[^ >]+)>/i', $Excerpt['text'], $matches)) {
+            $url = $matches[1];
+
+            return [
+                'extent'  => strlen($matches[0]),
+                'element' => [
+                    'name'       => 'a',
+                    'text'       => $url,
+                    'attributes' => [
+                        'href' => $url,
+                    ],
+                ],
+            ];
+        }
+    }
+
+    # ~
+
+    protected function unmarkedText($text)
+    {
+        if ($this->breaksEnabled) {
+            $text = preg_replace('/[ ]*\n/', "<br />\n", $text);
+        } else {
+            $text = preg_replace('/(?:[ ][ ]+|[ ]*\\\\)\n/', "<br />\n", $text);
+            $text = str_replace(" \n", "\n", $text);
+        }
+
+        return $text;
+    }
+
+    #
+    # Handlers
+    #
+
+    protected function element(array $Element)
+    {
+        if ($this->safeMode) {
+            $Element = $this->sanitiseElement($Element);
+        }
+
+        $markup = '<' . $Element['name'];
+
+        if (isset($Element['attributes'])) {
+            foreach ($Element['attributes'] as $name => $value) {
+                if ($value === null) {
+                    continue;
+                }
+
+                $markup .= ' ' . $name . '="' . self::escape($value) . '"';
+            }
+        }
+
+        $permitRawHtml = false;
+
+        if (isset($Element['text'])) {
+            $text = $Element['text'];
+        }
+        // very strongly consider an alternative if you're writing an
+        // extension
+        elseif (isset($Element['rawHtml'])) {
+            $text = $Element['rawHtml'];
+            $allowRawHtmlInSafeMode = isset($Element['allowRawHtmlInSafeMode']) && $Element['allowRawHtmlInSafeMode'];
+            $permitRawHtml = !$this->safeMode || $allowRawHtmlInSafeMode;
+        }
+
+        if (isset($text)) {
+            $markup .= '>';
+
+            if (!isset($Element['nonNestables'])) {
+                $Element['nonNestables'] = [];
+            }
+
+            if (isset($Element['handler'])) {
+                $markup .= $this->{$Element['handler']}($text, $Element['nonNestables']);
+            } elseif (!$permitRawHtml) {
+                $markup .= self::escape($text, true);
+            } else {
+                $markup .= $text;
+            }
+
+            $markup .= '</' . $Element['name'] . '>';
+        } else {
+            $markup .= ' />';
+        }
+
+        return $markup;
+    }
+
+    protected function elements(array $Elements)
+    {
+        $markup = '';
+
+        foreach ($Elements as $Element) {
+            $markup .= "\n" . $this->element($Element);
+        }
+
+        $markup .= "\n";
+
+        return $markup;
+    }
+
+    # ~
+
+    protected function li($lines)
+    {
+        $markup = $this->lines($lines);
+
+        $trimmedMarkup = trim($markup);
+
+        if (!in_array('', $lines) and substr($trimmedMarkup, 0, 3) === '<p>') {
+            $markup = $trimmedMarkup;
+            $markup = substr($markup, 3);
+
+            $position = strpos($markup, "</p>");
+
+            $markup = substr_replace($markup, '', $position, 4);
+        }
+
+        return $markup;
+    }
+
+    #
+    # Deprecated Methods
+    #
+
+    function parse($text)
+    {
+        $markup = $this->text($text);
+
+        return $markup;
+    }
+
+    protected function sanitiseElement(array $Element)
+    {
+        static $goodAttribute = '/^[a-zA-Z0-9][a-zA-Z0-9-_]*+$/';
+        static $safeUrlNameToAtt = [
+            'a'   => 'href',
+            'img' => 'src',
+        ];
+
+        if (isset($safeUrlNameToAtt[$Element['name']])) {
+            $Element = $this->filterUnsafeUrlInAttribute($Element, $safeUrlNameToAtt[$Element['name']]);
+        }
+
+        if (!empty($Element['attributes'])) {
+            foreach ($Element['attributes'] as $att => $val) {
+                # filter out badly parsed attribute
+                if (!preg_match($goodAttribute, $att)) {
+                    unset($Element['attributes'][$att]);
+                } # dump onevent attribute
+                elseif (self::striAtStart($att, 'on')) {
+                    unset($Element['attributes'][$att]);
+                }
+            }
+        }
+
+        return $Element;
+    }
+
+    protected function filterUnsafeUrlInAttribute(array $Element, $attribute)
+    {
+        foreach ($this->safeLinksWhitelist as $scheme) {
+            if (self::striAtStart($Element['attributes'][$attribute], $scheme)) {
+                return $Element;
+            }
+        }
+
+        $Element['attributes'][$attribute] = str_replace(':', '%3A', $Element['attributes'][$attribute]);
+
+        return $Element;
+    }
+
+    #
+    # Static Methods
+    #
+
+    protected static function escape($text, $allowQuotes = false)
+    {
+        return htmlspecialchars($text, $allowQuotes ? ENT_NOQUOTES : ENT_QUOTES, 'UTF-8');
+    }
+
+    protected static function striAtStart($string, $needle)
+    {
+        $len = strlen($needle);
+
+        if ($len > strlen($string)) {
+            return false;
+        } else {
+            return strtolower(substr($string, 0, $len)) === strtolower($needle);
+        }
+    }
+
+    static function instance($name = 'default')
+    {
+        if (isset(self::$instances[$name])) {
+            return self::$instances[$name];
+        }
+
+        $instance = new static();
+
+        self::$instances[$name] = $instance;
+
+        return $instance;
+    }
+
+    private static $instances = [];
+
+    #
+    # Fields
+    #
+
+    protected $DefinitionData;
+
+    #
+    # Read-Only
+
+    protected $specialCharacters = [
+        '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|',
+    ];
+
+    protected $StrongRegex = [
+        '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*[*])+?)[*]{2}(?![*])/s',
+        '_' => '/^__((?:\\\\_|[^_]|_[^_]*_)+?)__(?!_)/us',
+    ];
+
+    protected $EmRegex = [
+        '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s',
+        '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us',
+    ];
+
+    protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*(?:\s*=\s*(?:[^"\'=<>`\s]+|"[^"]*"|\'[^\']*\'))?';
+
+    protected $voidElements = ['area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source'];
+
+    protected $textLevelElements = [
+        'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont',
+        'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing',
+        'i', 'rp', 'del', 'code', 'strike', 'marquee',
+        'q', 'rt', 'ins', 'font', 'strong',
+        's', 'tt', 'kbd', 'mark',
+        'u', 'xm', 'sub', 'nobr',
+        'sup', 'ruby',
+        'var', 'span',
+        'wbr', 'time',
+    ];
+}

+ 8 - 4
vendor/zoujingli/think-library/src/service/ModuleService.php

@@ -16,6 +16,7 @@
 namespace think\admin\service;
 
 use think\admin\extend\HttpExtend;
+use think\admin\extend\Parsedown;
 use think\admin\Service;
 
 /**
@@ -104,9 +105,12 @@ class ModuleService extends Service
     public function getModules(array $data = []): array
     {
         foreach (NodeService::instance()->getModules() as $name) {
-            if (is_array($version = $this->getModuleVersion($name))) {
-                if (preg_match('|^\d{4}\.\d{2}\.\d{2}\.\d{2}$|', $version['version'])) {
-                    $data[$name] = $version;
+            if (is_array($vars = $this->getModuleVersion($name)) && isset($vars['version'])) {
+                if (preg_match('|^\d{4}\.\d{2}\.\d{2}\.\d{2}$|', $vars['version'])) {
+                    $data[$name] = $vars;
+                    foreach (glob("{$this->app->getBasePath()}{$name}/module/change/*.md") as $file) {
+                        $data[$name]['change'][pathinfo($file, PATHINFO_FILENAME)] = Parsedown::instance()->parse(file_get_contents($file));
+                    }
                 }
             }
         }
@@ -153,7 +157,7 @@ class ModuleService extends Service
      */
     private function getModuleVersion($name)
     {
-        $file = $this->app->getBasePath() . $name . DIRECTORY_SEPARATOR . 'ver.php';
+        $file = "{$this->app->getBasePath()}{$name}/module/version.php";
         if (file_exists($file) && is_file($file) && is_array($vars = @include $file)) {
             return isset($vars['name']) && isset($vars['version']) ? $vars : null;
         } else {