├── public
├── favicon.ico
└── static
│ ├── admin
│ ├── layui
│ │ ├── font
│ │ │ ├── iconfont.eot
│ │ │ ├── iconfont.ttf
│ │ │ ├── iconfont.woff
│ │ │ └── iconfont.woff2
│ │ └── css
│ │ │ └── modules
│ │ │ ├── layer
│ │ │ └── default
│ │ │ │ ├── icon.png
│ │ │ │ ├── icon-ext.png
│ │ │ │ ├── loading-0.gif
│ │ │ │ ├── loading-1.gif
│ │ │ │ └── loading-2.gif
│ │ │ ├── code.css
│ │ │ └── laydate
│ │ │ └── default
│ │ │ └── laydate.css
│ ├── spedit.css
│ └── meedit.css
│ ├── common
│ ├── highlight
│ │ └── default.min.css
│ └── bmap
│ │ └── 1.0.1.css
│ ├── map.js
│ ├── melog.js
│ └── content.js
├── config
├── cache.js
├── view.js
├── db.js
├── app.js
└── routes.js
├── docker
├── Dockerfile.dockerignore
├── Dockerfile
├── Run.sh
└── Dev.md
├── server.js
├── app
├── model
│ ├── base.js
│ ├── cate.js
│ ├── link.js
│ ├── special.js
│ ├── comment.js
│ └── article.js
├── pagination
│ ├── index.js
│ └── cate.js
├── controller
│ ├── index.js
│ ├── cate.js
│ ├── search.js
│ ├── base.js
│ ├── article.js
│ ├── special.js
│ └── comment.js
└── view
│ ├── cate_cate.htm
│ ├── search_search.htm
│ ├── aside.htm
│ ├── index_index.htm
│ ├── layout.htm
│ ├── article_article.htm
│ └── special_special.htm
├── admin
├── model
│ ├── cate.js
│ ├── link.js
│ ├── site.js
│ ├── article.js
│ ├── special.js
│ ├── upload.js
│ ├── comment.js
│ ├── user.js
│ └── specialItem.js
├── middleware
│ ├── cache.js
│ └── auth.js
├── controller
│ ├── base.js
│ ├── comment.js
│ ├── cate.js
│ ├── user.js
│ ├── index.js
│ ├── link.js
│ ├── article.js
│ ├── site.js
│ └── special.js
├── view
│ ├── components
│ │ ├── module-html.htm
│ │ ├── form-html.htm
│ │ ├── form-markdown.htm
│ │ ├── module-markdown.htm
│ │ ├── form-list.htm
│ │ ├── me-point-select.htm
│ │ ├── me-article-select.htm
│ │ ├── form-map.htm
│ │ ├── me-scroll.htm
│ │ ├── module-list.htm
│ │ ├── module-map.htm
│ │ └── me-point.htm
│ ├── upload_form.htm
│ ├── index_login.htm
│ ├── user_index.htm
│ ├── cate_index.htm
│ ├── link_form.htm
│ ├── user_form.htm
│ ├── comment_index.htm
│ ├── cate_form.htm
│ ├── index_index.htm
│ ├── special_index.htm
│ ├── site_form.htm
│ ├── link_index.htm
│ ├── special_special.htm
│ ├── layout.htm
│ ├── article_index.htm
│ └── upload_index.htm
├── service
│ └── cookie.js
└── utils.js
├── package.json
├── LICENSE
├── CHANGE.md
├── README.md
└── install
├── controller
└── index.js
└── view
└── index_index.htm
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yafoo/melog/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/config/cache.js:
--------------------------------------------------------------------------------
1 | const cache = {
2 | app_sql_cache_time: 600
3 | }
4 |
5 | module.exports = cache;
--------------------------------------------------------------------------------
/public/static/admin/layui/font/iconfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yafoo/melog/HEAD/public/static/admin/layui/font/iconfont.eot
--------------------------------------------------------------------------------
/public/static/admin/layui/font/iconfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yafoo/melog/HEAD/public/static/admin/layui/font/iconfont.ttf
--------------------------------------------------------------------------------
/public/static/admin/layui/font/iconfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yafoo/melog/HEAD/public/static/admin/layui/font/iconfont.woff
--------------------------------------------------------------------------------
/public/static/admin/layui/font/iconfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yafoo/melog/HEAD/public/static/admin/layui/font/iconfont.woff2
--------------------------------------------------------------------------------
/docker/Dockerfile.dockerignore:
--------------------------------------------------------------------------------
1 | /.vscode
2 | /.git
3 | /.gitignore
4 | /node_modules
5 | /public/upload/*
6 | /config/install.js
7 | /v2_to_v3.sql
--------------------------------------------------------------------------------
/public/static/admin/layui/css/modules/layer/default/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yafoo/melog/HEAD/public/static/admin/layui/css/modules/layer/default/icon.png
--------------------------------------------------------------------------------
/public/static/admin/layui/css/modules/layer/default/icon-ext.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yafoo/melog/HEAD/public/static/admin/layui/css/modules/layer/default/icon-ext.png
--------------------------------------------------------------------------------
/public/static/admin/layui/css/modules/layer/default/loading-0.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yafoo/melog/HEAD/public/static/admin/layui/css/modules/layer/default/loading-0.gif
--------------------------------------------------------------------------------
/public/static/admin/layui/css/modules/layer/default/loading-1.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yafoo/melog/HEAD/public/static/admin/layui/css/modules/layer/default/loading-1.gif
--------------------------------------------------------------------------------
/public/static/admin/layui/css/modules/layer/default/loading-2.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yafoo/melog/HEAD/public/static/admin/layui/css/modules/layer/default/loading-2.gif
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | const {app, Logger} = require('jj.js');
2 |
3 | //server
4 | app.run(3003, '0.0.0.0', function(err){
5 | !err && Logger.info('http server is ready on 3003');
6 | });
--------------------------------------------------------------------------------
/config/view.js:
--------------------------------------------------------------------------------
1 | const view = {
2 | view_depr: '_', // 模版文件名分割符,'/'代表二级目录
3 | view_filter: {
4 | Date,
5 | dateFormat: (value, format) => require('jj.js').utils.date.format(format, value)
6 | }
7 | }
8 |
9 | module.exports = view;
--------------------------------------------------------------------------------
/docker/Dockerfile:
--------------------------------------------------------------------------------
1 | # syntax=docker/dockerfile:1
2 |
3 | FROM node:12-alpine
4 | ENV NODE_ENV=production
5 |
6 | EXPOSE 3003
7 |
8 | WORKDIR /melog
9 | COPY . .
10 | COPY config config.demo
11 | RUN npm i --production
12 |
13 | CMD [ "sh", "docker/Run.sh" ]
--------------------------------------------------------------------------------
/app/model/base.js:
--------------------------------------------------------------------------------
1 | const {Model} = require('jj.js');
2 |
3 | class Base extends Model
4 | {
5 | constructor(...args) {
6 | super(...args);
7 | this.cacheTime = this.$config.cache.app_sql_cache_time;
8 | }
9 | }
10 |
11 | module.exports = Base;
--------------------------------------------------------------------------------
/admin/model/cate.js:
--------------------------------------------------------------------------------
1 | const {Model} = require('jj.js');
2 |
3 | class Cate extends Model
4 | {
5 | async getCateList(condition, rows=100, order='sort', sort='asc') {
6 | return await this.db.where(condition).order(order, sort).limit(rows).select();
7 | }
8 | }
9 |
10 | module.exports = Cate;
--------------------------------------------------------------------------------
/app/pagination/index.js:
--------------------------------------------------------------------------------
1 | const {Pagination} = require('jj.js');
2 |
3 | class Index extends Pagination
4 | {
5 | init(opts) {
6 | super.init({
7 | url_index: '/',
8 | ...opts
9 | });
10 | return this;
11 | }
12 | }
13 |
14 | module.exports = Index;
--------------------------------------------------------------------------------
/admin/model/link.js:
--------------------------------------------------------------------------------
1 | const {Model} = require('jj.js');
2 |
3 | class Link extends Model
4 | {
5 | async getLinkList(condition, pid = 0) {
6 | const list = await this.db.where(condition).order('sort', 'asc').select();
7 | return this.$utils.toTree(list, pid);
8 | }
9 | }
10 |
11 | module.exports = Link;
--------------------------------------------------------------------------------
/admin/middleware/cache.js:
--------------------------------------------------------------------------------
1 | const {Middleware} = require('jj.js');
2 |
3 | class Cache extends Middleware
4 | {
5 | async clear() {
6 | // 兼容直接调用
7 | if(this.$next) {
8 | await this.$next();
9 | }
10 |
11 | this.$cache.delete();
12 | this.$db.deleteCache();
13 | }
14 | }
15 |
16 | module.exports = Cache;
--------------------------------------------------------------------------------
/docker/Run.sh:
--------------------------------------------------------------------------------
1 | #! /bin/bash
2 | [ ! -f config/app.js ] && cp config.demo/app.js config/app.js
3 | [ ! -f config/cache.js ] && cp config.demo/cache.js config/cache.js
4 | [ ! -f config/db.js ] && cp config.demo/db.js config/db.js
5 | [ ! -f config/routes.js ] && cp config.demo/routes.js config/routes.js
6 | [ ! -f config/view.js ] && cp config.demo/view.js config/view.js
7 | node server.js
--------------------------------------------------------------------------------
/app/pagination/cate.js:
--------------------------------------------------------------------------------
1 | const {Pagination} = require('jj.js');
2 |
3 | class Cate extends Pagination
4 | {
5 | init(opts) {
6 | super.init({
7 | key_origin: 'params',
8 | url_index: ':cate',
9 | url_page: ':cate_page',
10 | ...opts
11 | });
12 | return this;
13 | }
14 | }
15 |
16 | module.exports = Cate;
--------------------------------------------------------------------------------
/config/db.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | default: {
3 | type : 'mysql', // 数据库类型
4 | host : '127.0.0.1', // 服务器地址
5 | database : 'melog', // 数据库名
6 | user : 'root', // 数据库用户名
7 | password : '', // 数据库密码
8 | port : '3306', // 数据库连接端口
9 | charset : 'utf8', // 数据库编码默认采用utf8
10 | prefix : 'melog_' // 数据库表前缀
11 | }
12 | };
--------------------------------------------------------------------------------
/config/app.js:
--------------------------------------------------------------------------------
1 | const app = {
2 | app_debug: false, //调试模式
3 | app_multi: true, //是否开启多应用
4 |
5 | default_app: 'app', //默认应用
6 | default_controller: 'index', //默认控制器
7 | default_action: 'index', //默认方法
8 |
9 | common_app: 'admin', //公共应用,存放公共模型及逻辑
10 | controller_folder: 'controller', //控制器目录名
11 |
12 | static_dir: './public', //静态文件目录,相对于应用根目录,为空或false时,关闭静态访问
13 |
14 | koa_body: {multipart: true, formidable: {keepExtensions: true, maxFieldsSize: 10 * 1024 * 1024}} //koa-body配置参数,为false时,关闭koa-body
15 | }
16 |
17 | module.exports = app;
--------------------------------------------------------------------------------
/docker/Dev.md:
--------------------------------------------------------------------------------
1 | ## 镜像制作
2 | ```bash
3 | docker build -f ./docker/Dockerfile --tag yafoo/melog .
4 | ```
5 |
6 | ## 镜像发布
7 | ```bash
8 | docker tag yafoo/melog yafoo/melog:version
9 | docker push yafoo/melog:version
10 | docker push yafoo/melog
11 | ```
12 |
13 | ## 容器运行
14 | ```bash
15 | docker run -p 3003:3003 --restart unless-stopped --name melog -d yafoo/melog
16 | ```
17 |
18 | ## 容器运行(配置文件、站点数据保存到宿主机)
19 | ```bash
20 | docker run -p 3003:3003 --restart unless-stopped --name melog -d -v $PWD/melog/config:/melog/config -v $PWD/melog/upload:/melog/public/upload yafoo/melog
21 | ```
--------------------------------------------------------------------------------
/app/model/cate.js:
--------------------------------------------------------------------------------
1 | const Base = require('./base');
2 |
3 | class Cate extends Base
4 | {
5 | // 获取一个分类
6 | async getCateInfo(condition) {
7 | return await this.db.where(condition).cache(this.cacheTime).find();
8 | }
9 |
10 | // 博客顶部导航
11 | async getNavList(rows) {
12 | return await this.db.order('sort', 'asc').where({is_show: 1}).limit(rows).cache(this.cacheTime).select();
13 | }
14 |
15 | // 分类目录地址
16 | async getCateDirs() {
17 | return await this.db.cache(this.cacheTime).column('cate_dir');
18 | }
19 | }
20 |
21 | module.exports = Cate;
--------------------------------------------------------------------------------
/app/model/link.js:
--------------------------------------------------------------------------------
1 | const Base = require('./base');
2 |
3 | class Link extends Base
4 | {
5 | // 获取列表
6 | async getLinkList(pid, rows=100) {
7 | const link = await this.db.order('sort', 'asc').limit(rows).cache(this.cacheTime).select();
8 | return this.$utils.toTreeArray(link, pid);
9 | }
10 |
11 | // 友情链接
12 | async getFriendLinks(rows=100) {
13 | return await this.getLinkList(1, rows);
14 | }
15 |
16 | // 底部导航
17 | async getFootLinks(rows=100) {
18 | return await this.getLinkList(2, rows);
19 | }
20 | }
21 |
22 | module.exports = Link;
--------------------------------------------------------------------------------
/app/model/special.js:
--------------------------------------------------------------------------------
1 | const Base = require('./base');
2 |
3 | class Special extends Base
4 | {
5 | // 获取专题信息
6 | async getSpecialInfo(condition) {
7 | return await this.db.where(condition).cache(this.cacheTime).find();
8 | }
9 |
10 | // 获取专题列表
11 | async getSpecialList(condition) {
12 | return await this.db.where(condition).cache(this.cacheTime).select();
13 | }
14 |
15 | // 获取顶部专题列表
16 | async getTopList(rows=100) {
17 | return await this.db.where({flag: 1}).order('sort').order('id').limit(rows).cache(this.cacheTime).select();
18 | }
19 | }
20 |
21 | module.exports = Special;
--------------------------------------------------------------------------------
/config/routes.js:
--------------------------------------------------------------------------------
1 | routes = [
2 | {url: '/', path: 'index/index'},
3 | {url: '/article/:id.html', path: 'article/article', name: 'article'},
4 | {url: '/search', path: 'search/search', name: 'search'},
5 | {url: '/special/:id.html', path: 'special/special', name: 'special'},
6 | {url: '/:cate((?!admin|install)[^/]+)/', path: 'cate/cate', name: 'cate'}, // 会匹配到自定义后台地址,所以程序内需执行this.$next()
7 | {url: '/:cate((?!admin|install)[^/]+)/list_:page.html', path: 'cate/cate', name: 'cate_page'},
8 | {url: '/:app((?!install)[^/]+)/:controller?/:action?', path: 'admin/auth/index', type: 'middleware'} // 后台验证中间件
9 | ];
10 |
11 | module.exports = routes;
--------------------------------------------------------------------------------
/admin/controller/base.js:
--------------------------------------------------------------------------------
1 | const {Controller} = require('jj.js');
2 |
3 | class Base extends Controller
4 | {
5 | async _init() {
6 | this.user_id = this.$service.cookie.get('user');
7 | if(this.user_id) {
8 | this.user = await this.$model.user.get({id: this.user_id});
9 | this.$assign('user', this.user);
10 | }
11 |
12 | this.site = await this.$model.site.getConfig();
13 | this.$assign('site', this.site);
14 |
15 | this.$assign('title', '管理中心');
16 | this.$assign('description', this.site.description);
17 | this.$assign('keywords', this.site.keywords);
18 | }
19 | }
20 |
21 | module.exports = Base;
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "melog",
3 | "version": "3.1.1",
4 | "description": "一个基于jj.js构建的简单轻量级blog系统",
5 | "main": "server.js",
6 | "scripts": {
7 | "start": "node server.js"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/yafoo/melog.git"
12 | },
13 | "keywords": [
14 | "melog",
15 | "blog",
16 | "jj.js",
17 | "simple",
18 | "lightweight",
19 | "koa"
20 | ],
21 | "author": "yafoo",
22 | "license": "MIT",
23 | "bugs": {
24 | "url": "https://github.com/yafoo/melog/issues"
25 | },
26 | "homepage": "https://github.com/yafoo/melog#readme",
27 | "dependencies": {
28 | "jimp-compact": "^0.16.1-2",
29 | "jj.js": "^0.8.8",
30 | "markdown-it": "^13.0.1"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/app/controller/index.js:
--------------------------------------------------------------------------------
1 | const Base = require('./base');
2 |
3 | class Index extends Base
4 | {
5 | async index() {
6 | // 首页列表
7 | const [list, pagination] = await this.$model.article.getIndexList(this.site.list_rows, this.site.style == 'blog');
8 | // 友情链接
9 | const friend_links = await this.$model.link.getFriendLinks();
10 | // seo标题
11 | if(this.site.seo_title) {
12 | this.$assign('title', this.site.seo_title + ' - ' + this.site.webname);
13 | }
14 |
15 | this.$assign('list', list);
16 | this.$assign('pagination', pagination ? pagination.render() : '');
17 | this.$assign('friend_links', friend_links);
18 | await this.$fetch();
19 | }
20 | }
21 |
22 | module.exports = Index;
--------------------------------------------------------------------------------
/admin/model/site.js:
--------------------------------------------------------------------------------
1 | const {Model} = require('jj.js');
2 | const pjson = require('../../package.json');
3 |
4 | class Site extends Model
5 | {
6 | constructor(...args) {
7 | super(...args);
8 | this.cacheTime = this.$config.cache.app_sql_cache_time;
9 | }
10 |
11 | async getSiteList(condition, order='sort', sort='asc') {
12 | return await this.db.where(condition).order(order, sort).select();
13 | }
14 |
15 | // 获取站点设置
16 | async getConfig(key) {
17 | const result = await this.db.cache(this.cacheTime).column('value', 'key');
18 | if(key) {
19 | return result[key];
20 | } else {
21 | result.VERSION = pjson.version;
22 | result.APP_TIME = this.ctx.APP_TIME;
23 | return result;
24 | }
25 | }
26 | }
27 |
28 | module.exports = Site;
--------------------------------------------------------------------------------
/admin/view/components/module-html.htm:
--------------------------------------------------------------------------------
1 |
5 |
6 |
--------------------------------------------------------------------------------
/admin/view/components/form-html.htm:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/admin/view/upload_form.htm:
--------------------------------------------------------------------------------
1 | {{extend './layout.htm'}}
2 |
3 | {{block 'main'}}
4 |
19 | {{/block}}
20 |
21 | {{block 'js'}}
22 |
28 | {{/block}}
--------------------------------------------------------------------------------
/admin/view/components/form-markdown.htm:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/admin/controller/comment.js:
--------------------------------------------------------------------------------
1 | const Base = require('./base');
2 |
3 | class Comment extends Base
4 | {
5 | async index() {
6 | const condition = {};
7 | const keyword = this.ctx.query.keyword;
8 | if(keyword) {
9 | condition['concat(comment.uname, comment.email, comment.url, comment.content, comment.ip)'] = ['like', '%' + keyword + '%'];
10 | }
11 | const [list, pagination] = await this.$model.comment.getCommentList(condition);
12 |
13 | this.$assign('keyword', keyword);
14 | this.$assign('list', list);
15 | this.$assign('pagination', pagination.render());
16 |
17 | await this.$fetch();
18 | }
19 |
20 | async delete() {
21 | const id = parseInt(this.ctx.query.id);
22 |
23 | const err = await this.$model.comment.delComment(id);
24 | if(err) {
25 | this.$error(err);
26 | } else {
27 | this.$success('删除成功!', 'index');
28 | }
29 | }
30 | }
31 |
32 | module.exports = Comment;
--------------------------------------------------------------------------------
/admin/model/article.js:
--------------------------------------------------------------------------------
1 | const {Model} = require('jj.js');
2 |
3 | class Article extends Model
4 | {
5 | // 后台文章列表
6 | async getArticleList(condition) {
7 | return await this.db.table('article a').field('a.id,a.cate_id,a.user_id,a.title,a.writer,a.click,a.description,a.add_time,a.thumb,c.cate_name,c.cate_dir').join('cate c', 'a.cate_id=c.id').where(condition).order('a.id', 'desc').pagination();
8 | }
9 |
10 | // 文章新增修改
11 | async saveArticle(data, condition) {
12 | if(!data.id && !data.add_time) {
13 | data.add_time = this.$utils.time();
14 | }
15 | if(data.id && !data.update_time) {
16 | data.update_time = this.$utils.time();
17 | }
18 | return await super.save(data, condition);
19 | }
20 |
21 | // 更新评论总数
22 | async updateCommentTotal(id) {
23 | const comment_count = await this.$model.comment.db.where({article_id: id}).count();
24 | return await this.save({id, comment_count});
25 | }
26 | }
27 |
28 | module.exports = Article;
--------------------------------------------------------------------------------
/admin/model/special.js:
--------------------------------------------------------------------------------
1 | const {Model} = require('jj.js');
2 |
3 | class Special extends Model
4 | {
5 | // 后台专题列表
6 | async getSpecialList(condition) {
7 | return await this.db.where(condition).order('id', 'desc').pagination();
8 | }
9 |
10 | // 专题新增修改
11 | async saveSpecial(data, condition) {
12 | if(!data.id && !data.add_time) {
13 | data.add_time = this.$utils.time();
14 | }
15 | if(data.id > 0) {
16 | return await this.save(data, condition);
17 | } else {
18 | try {
19 | await this.db.startTrans(async () => {
20 | const result = await this.save(data, condition);
21 | await this.$db.table('special_item').update({special_id: result.insertId}, {special_id: 0});
22 | });
23 | return true;
24 | } catch(e) {
25 | this.$logger.error('专题新增失败:' + e.message);
26 | return false;
27 | }
28 | }
29 |
30 | }
31 | }
32 |
33 | module.exports = Special;
--------------------------------------------------------------------------------
/admin/service/cookie.js:
--------------------------------------------------------------------------------
1 | const {Cookie: LibCookie} = require('jj.js');
2 | let cookieEncode = '';
3 |
4 | class Cookie extends LibCookie
5 | {
6 | constructor(...args) {
7 | super(...args);
8 | cookieEncode || (cookieEncode = this.$utils.randomString(16));
9 | this.cookieEncode = this.$config.cookie && this.$config.cookie.cookieEncode
10 | || this.$config.app.app_debug && 'debug_cookie_encode'
11 | || cookieEncode;
12 | }
13 |
14 | set(key, value, options) {
15 | super.set(key, value, options);
16 | super.set(key + '__ck', this.encode(value), options);
17 | }
18 |
19 | get(key) {
20 | const value = super.get(key);
21 | const value__ck = super.get(key + '__ck');
22 | return this.encode(value) == value__ck ? value : undefined;
23 | }
24 |
25 | delete(key) {
26 | super.delete(key);
27 | super.delete(key + '__ck');
28 | }
29 |
30 | encode(value) {
31 | return this.$utils.md5(this.cookieEncode + value).substr(0, 16);
32 | }
33 | }
34 |
35 | module.exports = Cookie;
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 雨思
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/app/controller/cate.js:
--------------------------------------------------------------------------------
1 | const Base = require('./base');
2 |
3 | class Cate extends Base
4 | {
5 | async _init() {
6 | this._cate_dir = this.ctx.params.cate;
7 | const cate_dirs = await this.$model.cate.getCateDirs();
8 |
9 | // 栏目不存在跳过
10 | if(!~cate_dirs.indexOf(this._cate_dir)) {
11 | await this.$next();
12 | return false;
13 | }
14 |
15 | await super._init();
16 | }
17 |
18 | async cate() {
19 | const cate = await this.$model.cate.getCateInfo({cate_dir: this._cate_dir});
20 | const [list, pagination] = await this.$model.article.getPageList({cate_id: cate.id}, this.site.list_rows);
21 |
22 | this.$assign('title', (cate.seo_title || cate.cate_name) + ' - ' + this.site.webname);
23 | this.$assign('description', cate.description);
24 | this.$assign('keywords', cate.keywords);
25 |
26 | this.$assign('cate', cate);
27 | this.$assign('list', list);
28 | this.$assign('pagination', pagination.render());
29 |
30 | await this.$fetch();
31 | }
32 | }
33 |
34 | module.exports = Cate;
--------------------------------------------------------------------------------
/admin/model/upload.js:
--------------------------------------------------------------------------------
1 | const {Model} = require('jj.js');
2 |
3 | class Upload extends Model
4 | {
5 | // 后台文件列表
6 | async getPageList(condition) {
7 | const [list, pagination] = await this.db.where(condition).order('id', 'desc').pagination();
8 |
9 | if(list && list.length) {
10 | const site_config = await this.$model.site.getConfig();
11 | list.map(item => {
12 | item.image = site_config.upload + item.image;
13 | item.thumb = site_config.upload + item.thumb;
14 | if(item.original) {
15 | item.original = site_config.upload + item.original;
16 | }
17 | item.size_text = this.getFileSize(item.size);
18 | item.origin_size_text = this.getFileSize(item.origin_size);
19 | return item;
20 | });
21 | }
22 |
23 | return [list, pagination];
24 | }
25 |
26 | getFileSize(size) {
27 | return !size ? '' : size < 1024 ? size + 'B' : size < 1024 * 1024 ? (size/1024).toFixed(1) + 'KB' : (size/1024/1024).toFixed(1) + 'MB';
28 | }
29 | }
30 |
31 | module.exports = Upload;
--------------------------------------------------------------------------------
/public/static/common/highlight/default.min.css:
--------------------------------------------------------------------------------
1 | /*!
2 | Theme: Default
3 | Description: Original highlight.js style
4 | Author: (c) Ivan Sagalaev
5 | Maintainer: @highlightjs/core-team
6 | Website: https://highlightjs.org/
7 | License: see project LICENSE
8 | Touched: 2021
9 | */pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{background:#f3f3f3;color:#444}.hljs-comment{color:#697070}.hljs-punctuation,.hljs-tag{color:#444a}.hljs-tag .hljs-attr,.hljs-tag .hljs-name{color:#444}.hljs-attribute,.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-name,.hljs-selector-tag{font-weight:700}.hljs-deletion,.hljs-number,.hljs-quote,.hljs-selector-class,.hljs-selector-id,.hljs-string,.hljs-template-tag,.hljs-type{color:#800}.hljs-section,.hljs-title{color:#800;font-weight:700}.hljs-link,.hljs-operator,.hljs-regexp,.hljs-selector-attr,.hljs-selector-pseudo,.hljs-symbol,.hljs-template-variable,.hljs-variable{color:#ab5656}.hljs-literal{color:#695}.hljs-addition,.hljs-built_in,.hljs-bullet,.hljs-code{color:#397300}.hljs-meta{color:#1f7199}.hljs-meta .hljs-string{color:#38a}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}
--------------------------------------------------------------------------------
/admin/view/index_login.htm:
--------------------------------------------------------------------------------
1 | {{extend './layout.htm'}}
2 |
3 | {{block 'main'}}
4 |
7 |
8 |
26 | {{/block}}
27 |
28 | {{block 'js'}}
29 |
34 | {{/block}}
--------------------------------------------------------------------------------
/app/view/cate_cate.htm:
--------------------------------------------------------------------------------
1 | {{extend './layout.htm'}}
2 |
3 | {{block 'header'}}
4 |
5 |
{{cate.cate_name}}
6 |
{{cate.description}}
7 |
8 | {{/block}}
9 |
10 | {{block 'content'}}
11 |
12 |
13 |
14 | {{each list item}}
15 |
16 |
17 | -
18 | {{@item.thumb && '
'}}
19 | {{item.description}}
20 |
21 |
22 | - 时间: {{item.add_time | dateFormat 'YYYY-mm-dd'}}
23 | - 浏览: {{item.click}}
24 | - 关键词: {{item.keywords}}
25 |
26 |
{{/each}}
27 |
28 | {{if pagination}}
{{/if}}
29 | {{@pagination}}
30 |
31 | {{include './aside.htm'}}
32 |
33 | {{/block}}
--------------------------------------------------------------------------------
/app/view/search_search.htm:
--------------------------------------------------------------------------------
1 | {{extend './layout.htm'}}
2 |
3 | {{block 'header'}}
4 |
5 |
{{search_title}}
6 |
{{description}}
7 |
8 | {{/block}}
9 |
10 | {{block 'content'}}
11 |
12 |
13 |
14 | {{each list item}}
15 |
16 |
17 | -
18 | {{@item.thumb && '
'}}
19 | {{item.description}}
20 |
21 |
22 | - 时间: {{item.add_time | dateFormat 'YYYY-mm-dd'}}
23 | - 浏览: {{item.click}}
24 | - 关键词: {{item.keywords}}
25 |
26 |
{{/each}}
27 |
28 | {{if pagination}}
{{/if}}
29 | {{@pagination}}
30 |
31 | {{include './aside.htm'}}
32 |
33 | {{/block}}
--------------------------------------------------------------------------------
/public/static/admin/layui/css/modules/code.css:
--------------------------------------------------------------------------------
1 | html #layuicss-skincodecss{display:none;position:absolute;width:1989px}.layui-code-h3,.layui-code-view{position:relative;font-size:12px}.layui-code-view{display:block;margin:10px 0;padding:0;border:1px solid #eee;border-left-width:6px;background-color:#FAFAFA;color:#333;font-family:Courier New}.layui-code-h3{padding:0 10px;height:40px;line-height:40px;border-bottom:1px solid #eee}.layui-code-h3 a{position:absolute;right:10px;top:0;color:#999}.layui-code-view .layui-code-ol{position:relative;overflow:auto}.layui-code-view .layui-code-ol li{position:relative;margin-left:45px;line-height:20px;padding:0 10px;border-left:1px solid #e2e2e2;list-style-type:decimal-leading-zero;*list-style-type:decimal;background-color:#fff}.layui-code-view .layui-code-ol li:first-child{padding-top:10px}.layui-code-view .layui-code-ol li:last-child{padding-bottom:10px}.layui-code-view pre{margin:0}.layui-code-notepad{border:1px solid #0C0C0C;border-left-color:#3F3F3F;background-color:#0C0C0C;color:#C2BE9E}.layui-code-notepad .layui-code-h3{border-bottom:none}.layui-code-notepad .layui-code-ol li{background-color:#3F3F3F;border-left:none}.layui-code-demo .layui-code{visibility:visible!important;margin:-15px;border-top:none;border-right:none;border-bottom:none}.layui-code-demo .layui-tab-content{padding:15px;border-top:none}
--------------------------------------------------------------------------------
/app/model/comment.js:
--------------------------------------------------------------------------------
1 | const Base = require('./base');
2 |
3 | class Comment extends Base
4 | {
5 | // 评论列表
6 | async getPageList(article_id, page = 1) {
7 | const comment_ids = await this.db.where({article_id, pid: 0}).order('id', 'desc').page(page, 10).cache(this.cacheTime).column('id');
8 | if(!comment_ids.length) {
9 | return [];
10 | }
11 | return await this.db.field('id,pid,article_id,user_id,uname,url,content,add_time').where({comment_id: ['in', comment_ids]}).order('id', 'asc').limit(100).cache(this.cacheTime).select();
12 | }
13 |
14 | // 新增评论
15 | async addComment(data) {
16 | if(!data.add_time) {
17 | data.add_time = this.$utils.time();
18 | }
19 | try {
20 | await this.db.startTrans(async () => {
21 | const result = await this.add(data);
22 | data.comment_id || await this.save({comment_id: result.insertId}, {id: result.insertId});
23 | await this.$admin.model.article.updateCommentTotal(data.article_id);
24 | });
25 |
26 | // 清理数据库缓存
27 | this.db.deleteCache();
28 | } catch (e) {
29 | this.$logger.error('新增评论出错:' + e.message);
30 | return '新增评论出错';
31 | }
32 | }
33 | }
34 |
35 | module.exports = Comment;
--------------------------------------------------------------------------------
/app/controller/search.js:
--------------------------------------------------------------------------------
1 | const Base = require('./base');
2 |
3 | class Search extends Base
4 | {
5 | async search() {
6 | const url = this.ctx.url;
7 | if(!~url.indexOf('/search/')) {
8 | this.$redirect(url.replace('/search', '/search/'), 301);
9 | return false;
10 | }
11 |
12 | const keyword = this.ctx.query.keyword;
13 | let [list, pagination] = [[], ''];
14 |
15 | if(!keyword) {
16 | this.$assign('title', '搜索' + ' - ' + this.site.webname);
17 | this.$assign('search_title', '搜索');
18 | } else {
19 | const condition = {};
20 | condition['concat(a.title, a.writer, a.keywords, a.description)'] = ['like', '%' + keyword + '%'];
21 | [list, pagination] = await this.$model.article.getSearchList(condition);
22 | pagination = pagination.render();
23 |
24 | this.$assign('title', keyword + ' - ' + this.site.webname);
25 | this.$assign('search_title', keyword);
26 | this.$assign('description', `关于 《${keyword}》的博文文章`);
27 | this.$assign('keywords', keyword);
28 | }
29 |
30 | this.$assign('keyword', keyword);
31 | this.$assign('list', list);
32 | this.$assign('pagination', pagination);
33 | await this.$fetch();
34 | }
35 | }
36 |
37 | module.exports = Search;
--------------------------------------------------------------------------------
/app/view/aside.htm:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/admin/middleware/auth.js:
--------------------------------------------------------------------------------
1 | const {Middleware} = require('jj.js');
2 |
3 | class Auth extends Middleware
4 | {
5 | async index() {
6 | await this.checkAlias();
7 | }
8 |
9 | // 后台地址验证
10 | async checkAlias() {
11 | const admin_auth = this.$service.cookie.get('admin_auth');
12 | const admin_alias = await this.$model.site.getConfig('admin_alias');
13 |
14 | if(admin_auth == 1 && this.ctx.params.app == 'admin') {
15 | await this.checkLogin();
16 | } else if(this.ctx.params.app === admin_alias) {
17 | admin_auth != 1 && this.$service.cookie.set('admin_auth', 1);
18 | this.$redirect('index/index');
19 | } else if(this.ctx.params.app != 'admin') {
20 | await this.$next();
21 | }
22 | }
23 |
24 | // 后台登录验证
25 | async checkLogin() {
26 | if(this.$service.cookie.get('user')) {
27 | if(this.ctx.params.controller == 'index' && this.ctx.params.action == 'login') {
28 | this.$redirect('index/index');
29 | } else {
30 | await this.$next();
31 | }
32 | } else {
33 | if(this.ctx.params.controller == 'index' && this.ctx.params.action == 'login') {
34 | await this.$next();
35 | } else {
36 | this.$redirect('index/login');
37 | }
38 | }
39 | }
40 | }
41 |
42 | module.exports = Auth;
--------------------------------------------------------------------------------
/app/view/index_index.htm:
--------------------------------------------------------------------------------
1 | {{extend './layout.htm'}}
2 |
3 | {{block 'header'}}
4 |
5 |
{{site.webname}}
6 |
{{site.description}}
7 |
8 | {{/block}}
9 |
10 | {{block 'content'}}
11 |
12 |
13 |
14 | {{each list item}}
15 |
16 |
17 | -
18 | {{@item.thumb && '
'}}
19 | {{item.description}}
20 |
21 |
22 | - 时间: {{item.add_time | dateFormat 'YYYY-mm-dd'}}
23 | - 浏览: {{item.click}}
24 | - 关键词: {{item.keywords}}
25 |
26 |
{{/each}}
27 |
28 | {{if pagination}}
{{/if}}
29 | {{@pagination}}
30 |
31 | {{include './aside.htm'}}
32 |
33 |
34 | {{if friend_links}}
35 |
39 | {{/if}}
40 | {{/block}}
--------------------------------------------------------------------------------
/admin/controller/cate.js:
--------------------------------------------------------------------------------
1 | const Base = require('./base');
2 |
3 | class Cate extends Base
4 | {
5 | constructor(...args) {
6 | super(...args);
7 | this.middleware = [{middleware: 'cache/clear', accept: ['save', 'delete']}];
8 | }
9 |
10 | async index() {
11 | const list = await this.$model.cate.getCateList();
12 | this.$assign('list', list);
13 | await this.$fetch();
14 | }
15 |
16 | async form() {
17 | const id = parseInt(this.ctx.query.id);
18 | let cate = {};
19 | if(id) {
20 | cate = await this.$model.cate.get({id});
21 | }
22 |
23 | this.$assign('cate', cate);
24 | await this.$fetch();
25 | }
26 |
27 | async save() {
28 | if(this.ctx.method != 'POST') {
29 | return this.$error('非法请求!');
30 | }
31 |
32 | const data = this.ctx.request.body;
33 | const id = data.id;
34 | data.is_show = data.is_show ? 1 : 0;
35 |
36 | const result = await this.$model.cate.save(data);
37 | if(result) {
38 | this.$success(id ? '保存成功!' : '新增成功!', 'index');
39 | } else {
40 | this.$error(id ? '保存失败!' : '新增失败!');
41 | }
42 | }
43 |
44 | async delete() {
45 | const id = parseInt(this.ctx.query.id);
46 |
47 | const result = await this.$model.cate.del({id});
48 | if(result) {
49 | this.$success('删除成功!', 'index');
50 | } else {
51 | this.$error('删除失败!');
52 | }
53 | }
54 | }
55 |
56 | module.exports = Cate;
--------------------------------------------------------------------------------
/admin/view/components/module-markdown.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/admin/utils.js:
--------------------------------------------------------------------------------
1 | // 获取随机字符串
2 | function randomString(len) {
3 | len = len || 32;
4 | var $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
5 | var maxPos = $chars.length;
6 | var pwd = '';
7 | for (i = 0; i < len; i++) {
8 | pwd += $chars.charAt(Math.floor(Math.random() * maxPos));
9 | }
10 | return pwd;
11 | }
12 |
13 | // 按父子孙分级整理
14 | function toTreeArray(list, pid=0){
15 | const arr = [];
16 | list.forEach(v => {
17 | if(v.pid == pid){
18 | v.child = toTreeArray(list, v.id);
19 | arr.push(v);
20 | }
21 | });
22 | return arr;
23 | }
24 |
25 | // 按父子孙平级排列
26 | function toTree(list, pid=0, level=0) {
27 | let arr = [];
28 | list.forEach(v => {
29 | if(v.pid == pid){
30 | v.level = level + 1;
31 | arr.push(v);
32 | arr = arr.concat(toTree(list, v.id, level + 1));
33 | }
34 | });
35 | return arr;
36 | }
37 |
38 | // 获取用户ip地址
39 | function getIP(req) {
40 | return req.headers['x-forwarded-for'] || // 判断是否有反向代理 IP
41 | req.connection.remoteAddress || // 判断 connection 的远程 IP
42 | req.socket.remoteAddress || // 判断后端的 socket 的 IP
43 | req.connection.socket.remoteAddress;
44 | }
45 |
46 | // md5
47 | const md5 = require('jj.js').utils.md5;
48 |
49 | // 获取时间戳
50 | const time = () => Math.round(new Date() / 1000);
51 |
52 | // 日期格式化
53 | const date = (format, value) => require('jj.js').utils.date.format(format, value);
54 |
55 | module.exports = {randomString, toTreeArray, toTree, getIP, md5, time, date}
--------------------------------------------------------------------------------
/admin/controller/user.js:
--------------------------------------------------------------------------------
1 | const Base = require('./base');
2 |
3 | class User extends Base
4 | {
5 | async index() {
6 | const list = await this.$model.user.getUserList(undefined, 100);
7 | this.$assign('list', list);
8 | await this.$fetch();
9 | }
10 |
11 | async form() {
12 | const id = parseInt(this.ctx.query.id);
13 | let user = {};
14 | if(id) {
15 | user = await this.$model.user.get({id});
16 | }
17 |
18 | this.$assign('user', user);
19 | await this.$fetch();
20 | }
21 |
22 | async save() {
23 | if(this.ctx.method != 'POST') {
24 | return this.$error('非法请求!');
25 | }
26 |
27 | const data = this.ctx.request.body;
28 | if(!data.email) {
29 | return this.$error('账号不能为空!');
30 | }
31 | if(!data.id && !data.password) {
32 | return this.$error('密码不能为空!');
33 | }
34 | if(data.password != data.password2) {
35 | return this.$error('两次输入密码不一致!');
36 | }
37 |
38 | const result = await this.$model.user.saveUser(data);
39 | if(result) {
40 | this.$success(data.id ? '保存成功!' : '新增成功!', 'index');
41 | } else {
42 | this.$error(data.id ? '保存失败!' : '新增失败!');
43 | }
44 | }
45 |
46 | async delete() {
47 | const id = parseInt(this.ctx.query.id);
48 | if(id == 1) {
49 | return this.$error('管理员账号请手工在数据库删除!');
50 | }
51 |
52 | const result = await this.$model.user.del({id});
53 | if(result) {
54 | this.$success('删除成功!', 'index');
55 | } else {
56 | this.$error('删除失败!');
57 | }
58 | }
59 | }
60 |
61 | module.exports = User;
--------------------------------------------------------------------------------
/admin/model/comment.js:
--------------------------------------------------------------------------------
1 | const {Model} = require('jj.js');
2 |
3 | class Comment extends Model
4 | {
5 | // 后台评论管理
6 | async getCommentList(condition) {
7 | return await this.db.table('comment comment').field('comment.*,a.title').join('article a', 'comment.article_id=a.id').where(condition).order('comment.id', 'desc').pagination();
8 | }
9 |
10 | // 删除评论
11 | async delComment(id) {
12 | const comment = await this.get({id});
13 | if(!comment) {
14 | return '数据不存在!';
15 | }
16 |
17 | try {
18 | await this.db.startTrans(async () => {
19 | if(comment.pid == 0) {
20 | await this.del({comment_id: id});
21 | return;
22 | }
23 |
24 | const ids = [id];
25 | const children = await this.getChildren(id);
26 | children.forEach(item => {
27 | ids.push(item.id);
28 | });
29 | await this.del({id: ['in', ids]});
30 |
31 | await this.$model.article.updateCommentTotal(comment.article_id);
32 | });
33 |
34 | // 清理数据库缓存
35 | this.db.deleteCache();
36 | } catch (e) {
37 | this.$logger.error('删除失败:' + e.message);
38 | return '删除失败!';
39 | }
40 | }
41 |
42 | // 获取评论回复
43 | async getChildren(pid) {
44 | let data = [];
45 | if(pid == 0) {
46 | return data;
47 | }
48 | const list = await this.all({pid});
49 | data = data.concat(list);
50 | for(const item of list) {
51 | data = data.concat(await this.getChildren(item.id));
52 | }
53 | return data;
54 | }
55 | }
56 |
57 | module.exports = Comment;
--------------------------------------------------------------------------------
/admin/controller/index.js:
--------------------------------------------------------------------------------
1 | const { assign } = require('markdown-it/lib/common/utils');
2 | const Base = require('./base');
3 |
4 | class Index extends Base
5 | {
6 | async index() {
7 | // 系统数据统计
8 | const [article, cate, comment, upload, link, special, user] = await Promise.all([
9 | this.$model.article.db.count(),
10 | this.$model.cate.db.count(),
11 | this.$model.comment.db.count(),
12 | this.$model.upload.db.count(),
13 | this.$model.link.db.count(),
14 | this.$model.special.db.count(),
15 | this.$model.user.db.count()
16 | ]);
17 | this.$assign('count', {article, cate, comment, upload, link, special, user});
18 |
19 | await this.$fetch();
20 | }
21 |
22 | async login() {
23 | if(this.ctx.method == 'POST'){
24 | const email = this.ctx.request.body.email;
25 | const password = this.ctx.request.body.password;
26 | if(!email) {
27 | this.$error('邮箱不能为空!');
28 | } else if(!this.ctx.request.body.password) {
29 | this.$error('密码不能为空!');
30 | }
31 |
32 | const err = await this.$model.user.login(email, password);
33 | if(err) {
34 | this.$error(err);
35 | } else {
36 | this.$success('登录成功!', 'index');
37 | }
38 | } else {
39 | this.$assign('title', '登录');
40 | await this.$fetch();
41 | }
42 | }
43 |
44 | async logout() {
45 | await this.$model.user.logout();
46 | this.$success('退出成功!', 'index')
47 | }
48 |
49 | async register() {
50 | this.$error('注册功能未开放!');
51 | }
52 | }
53 |
54 | module.exports = Index;
--------------------------------------------------------------------------------
/CHANGE.md:
--------------------------------------------------------------------------------
1 | ## v3.1.1(2022-09-07)
2 | - [修复] 修复分类及分页网址错误BUG
3 |
4 | ## v3.1.0(2022-09-06)
5 | - [新增] 新增install模块,不用再手工导入数据库文件了
6 | - [新增] 新增docker部署,部署方式见README.md
7 | - [优化] 优化路由设置
8 | - [优化] 优化专题页显示样式
9 | - [优化] 优化前台tips函数逻辑
10 | - [修改] 默认关闭调试模式
11 | - [修改] 默认绑定ip改为0.0.0.0
12 | - [依赖] 更新依赖jj.js版本到0.8.7
13 | - [依赖] 更换依赖jimp为jimp-compact,大幅减小程序体积
14 |
15 | ## v3.0(2022-08-19)
16 | - 更新依赖jj.js版本到0.8.2
17 | - 新增专题功能(目前有markdown、html源码、文章列表、地图4个模块)
18 | - 新增网站前台导航风格切换(cms或blog)
19 | - 新增网站前台主题切换功能(自定义主题支持共用默认主题文件,方便界面二次开发)
20 | - 新增首页、分类页seo标题字段
21 | - 新增系统设置自定义参数功能
22 | - 修复系统设置保存后提示错误问题
23 | - 优化后台评论列表显示
24 | - 优化静态资源路径
25 | - 优化系统设置界面
26 | - 数据库表字段调整:
27 | ```
28 | 1. article表:comment_total->comment_count, is_comment->comment_set;
29 | 2. link表: lname -> title
30 | 3. site表: kname -> key, intro -> title
31 | 4. user表: tname -> true_name
32 | ```
33 |
34 | > 注意v3.0改动较大,不兼容v2.x版本,升级时要先备份文件和数据库,建议使用git升级文件,然后运行v2_to_v3.sql升级数据库
35 |
36 | ## v2.4.2(2022-05-27)
37 | - 更新依赖jj.js版本到0.7.6,以修复文件上传bug
38 |
39 | ## v2.4.1(2022-05-15)
40 | - 后台layui资源本地化
41 | - 优化界面显示
42 | - 更新依赖jj.js版本到0.7.5、markdown-it版本到13.0.1
43 |
44 | ## v2.4(2022-03-05)
45 | - 后台首页增加数据统计
46 | - cdn js资源本地化
47 | - 升级npm版本到v7,影响package-lock.json文件,lockfileVersion版本变为2
48 |
49 | ## v2.3(2022-01-18)
50 | - 增加文件名编辑功能
51 | - 修复首页样式
52 | - 优化后台缓存清理逻辑
53 | - 升级markdown-it版本到12.3.2
54 |
55 | ## v2.2(2022-01-05)
56 | - 增加编辑器html代码支持
57 | - 增加相关文章模块,box模块增加图片显示
58 | - 增加文章图片点击查看功能
59 | - 增加上传图片后自动填写缩略图功能
60 | - 增加后台文件列表原始图片大小显示
61 | - 修复图片搜索后不能选择bug
62 | - 修复图片上传bug
63 | - 修复文章不能删除的bug
64 | - 优化图片选择页面样式
65 | - 优化列表页分类、文章页关键词布局
66 | - 优化底部及友情链接显示
67 |
68 | > 注意:本次升级不涉及数据库更改
69 |
70 | ## v2.1(2021-10-23)
71 | - 文章列表增加缩略图显示
72 | - 图片上传增加保留原始图片功能
73 | - README,增加演示demo站点
74 | - 修复站点配置保存bug
75 | - 优化markdown编辑器工具栏在手机端显示效果
76 |
77 | > 注意:文件升级请使用git pull拉取;数据库升级,请执行update.sql文件。升级前请先备份文件及数据库。
78 |
79 | ## v2.0(2021-09-16)
80 | - 首个正式版
--------------------------------------------------------------------------------
/admin/model/user.js:
--------------------------------------------------------------------------------
1 | const {Model} = require('jj.js');
2 |
3 | class User extends Model
4 | {
5 | async getUserList(condition, rows=10, order='id', sort='asc') {
6 | return await this.db.where(condition).order(order, sort).limit(rows).select();
7 | }
8 |
9 | async saveUser(data) {
10 | if(data.id) {
11 | data.add_time = this.$utils.time();
12 | } else {
13 | data.update_time = this.$utils.time();
14 | }
15 |
16 | if(data.password) {
17 | data.salt = this.$utils.randomString(8);
18 | data.password = this.passmd5(data.password, data.salt);
19 | } else {
20 | delete data.password;
21 | }
22 |
23 | return await this.save(data);
24 | }
25 |
26 | async lock(id) {
27 | return await this.db.where({id}).inc('is_lock');
28 | }
29 |
30 | async login(email, password) {
31 | const user = await this.get({email});
32 |
33 | if(!user) {
34 | return '账号或密码错误!';
35 | }
36 |
37 | if(this.is_lock(user)) {
38 | return '账号已被锁定,请联系管理员!';
39 | }
40 |
41 | if(user.password != this.passmd5(password, user.salt)) {
42 | await this.lock(user.id);
43 | return user.is_lock < -2 ? '账号或密码错误!' : '密码剩余次数:' + (1 - user.is_lock);
44 | }
45 |
46 | await this.db.update({is_lock: -5, login_time: this.$utils.time()}, {id: user.id});
47 | this.$service.cookie.set('user', user.id);
48 | }
49 |
50 | async logout() {
51 | this.$service.cookie.delete('user');
52 | }
53 |
54 | is_lock(user) {
55 | return user.is_lock > 0;
56 | }
57 |
58 | // 加密密码
59 | passmd5(password, salt) {
60 | const md5 = this.$utils.md5;
61 | return md5(salt + md5(salt + md5(password + salt) + salt));
62 | }
63 | }
64 |
65 | module.exports = User;
--------------------------------------------------------------------------------
/admin/view/user_index.htm:
--------------------------------------------------------------------------------
1 | {{extend './layout.htm'}}
2 |
3 | {{block 'main'}}
4 |
9 |
10 |
11 |
12 | | ID | 昵称 | 本名 | 账号 | 操作 |
13 |
14 |
15 | {{each list item}}
16 |
17 | | {{item.id}} |
18 | {{item.uname}} |
19 | {{item.true_name}} |
20 | {{item.email}} |
21 | 编辑删除 |
22 |
{{/each}}
23 |
24 |
25 |
26 | {{/block}}
27 |
28 | {{block 'js'}}
29 |
53 | {{/block}}
--------------------------------------------------------------------------------
/admin/view/components/form-list.htm:
--------------------------------------------------------------------------------
1 |
20 |
--------------------------------------------------------------------------------
/admin/view/cate_index.htm:
--------------------------------------------------------------------------------
1 | {{extend './layout.htm'}}
2 |
3 | {{block 'main'}}
4 |
9 |
10 |
11 |
12 | | ID | 分类名字 | 目录地址 | 排序 | 显示 | 操作 |
13 |
14 |
15 | {{each list item}}
16 |
17 | | {{item.id}} |
18 | {{item.cate_name}} |
19 | {{item.cate_dir}} |
20 | {{item.sort}} |
21 | {{item.is_show ? '显示' : '隐藏'}} |
22 | 编辑删除 |
23 |
{{/each}}
24 |
25 |
26 |
27 | {{/block}}
28 |
29 | {{block 'js'}}
30 |
54 | {{/block}}
--------------------------------------------------------------------------------
/app/controller/base.js:
--------------------------------------------------------------------------------
1 | const {Controller} = require('jj.js');
2 |
3 | class Base extends Controller
4 | {
5 | async _init() {
6 | // 文章模型
7 | const model_article = this.$model.article;
8 |
9 | // 站点配置、最新、热门列表、底部链接、顶部专题列表
10 | const [site_config, latest, hot, foot_links, special_list] = await Promise.all([
11 | this.$model.site.getConfig(),
12 | model_article.getNew(),
13 | model_article.getHot(),
14 | this.$model.link.getFootLinks(),
15 | this.$model.special.getTopList()
16 | ]);
17 |
18 | // 顶部导航
19 | let nav_list = [];
20 | if(site_config.style == 'cms') {
21 | nav_list = await this.$model.cate.getNavList();
22 | nav_list.forEach(item => {
23 | item.nav_url = this.$url.build(':cate', {cate: item.cate_dir});
24 | item.nav_name = item.cate_name;
25 | });
26 | } else if(site_config.style == 'blog') {
27 | nav_list = await this.$model.link.getLinkList(site_config.nav_id);
28 | nav_list.forEach(item => {
29 | item.nav_url = item.url;
30 | item.nav_name = item.title;
31 | });
32 | }
33 |
34 | this.$assign('site', this.site = site_config);
35 | this.$assign('title', this.site.webname);
36 | this.$assign('description', this.site.description);
37 | this.$assign('keywords', this.site.keywords);
38 | this.$assign('nav_list', nav_list);
39 | this.$assign('latest', latest);
40 | this.$assign('hot', hot);
41 | this.$assign('foot_links', foot_links);
42 | this.$assign('special_list', special_list);
43 | }
44 |
45 | // 支持更换模板主题
46 | async $fetch(template) {
47 | const content = await this.$view.setFolder(this.site.theme).fetch(template);
48 | this.$show(content);
49 | }
50 | }
51 |
52 | module.exports = Base;
--------------------------------------------------------------------------------
/admin/controller/link.js:
--------------------------------------------------------------------------------
1 | const Base = require('./base');
2 |
3 | class Link extends Base
4 | {
5 | constructor(...args) {
6 | super(...args);
7 | this.middleware = [{middleware: 'cache/clear', accept: ['save', 'delete']}];
8 | }
9 |
10 | async index() {
11 | const pid = this.ctx.query.pid || 0;
12 | const list = await this.$model.link.getLinkList(undefined, pid);
13 | const link_list = await this.$model.link.getLinkList({pid: 0});
14 |
15 | this.$assign('pid', pid);
16 | this.$assign('list', list);
17 | this.$assign('link_list', link_list);
18 | await this.$fetch();
19 | }
20 |
21 | async form() {
22 | const link_list = await this.$model.link.getLinkList();
23 | const pid = parseInt(this.ctx.query.pid);
24 | const id = parseInt(this.ctx.query.id);
25 | let link = {};
26 | if(id) {
27 | link = await this.$model.link.get({id});
28 | }
29 |
30 | this.$assign('pid', pid);
31 | this.$assign('link_list', link_list);
32 | this.$assign('link', link);
33 | await this.$fetch();
34 | }
35 |
36 | async save() {
37 | if(this.ctx.method != 'POST') {
38 | return this.$error('非法请求!');
39 | }
40 |
41 | const data = this.ctx.request.body;
42 | const id = data.id;
43 | const result = await this.$model.link.save(data);
44 |
45 | if(result) {
46 | this.$success(id ? '保存成功!' : '新增成功!', 'index');
47 | } else {
48 | this.$error(id ? '保存失败!' : '新增失败!');
49 | }
50 | }
51 |
52 | async delete() {
53 | const id = parseInt(this.ctx.query.id);
54 | if(id == 1 || id == 2) {
55 | return this.$error('系统固定链接不可删除!');
56 | }
57 |
58 | const result = await this.$model.link.del({id});
59 | if(result) {
60 | this.$success('删除成功!', 'index');
61 | } else {
62 | this.$error('删除失败!');
63 | }
64 | }
65 | }
66 |
67 | module.exports = Link;
--------------------------------------------------------------------------------
/app/view/layout.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{title}}
7 |
8 |
9 |
10 | {{block 'css'}}{{/block}}
11 |
12 |
13 |
25 |
26 | {{if special_list && special_list.length}}
27 |
32 | {{/if}}
33 |
34 | {{block 'content'}}{{/block}}
35 |
36 |
45 |
46 |
47 |
48 |
49 | {{block 'js'}}{{/block}}
50 |
51 |
--------------------------------------------------------------------------------
/app/controller/article.js:
--------------------------------------------------------------------------------
1 | const Base = require('./base');
2 | const md = require('markdown-it')({html: true});
3 |
4 | class Article extends Base
5 | {
6 | async article() {
7 | const aid = parseInt(this.ctx.params.id);
8 | const model_article = this.$model.article;
9 |
10 | // 文章信息
11 | const article = await model_article.getArticle({id: aid});
12 | if(!article) return;
13 |
14 | // 栏目信息、上一篇、下一篇、相关文章
15 | const [cate, prevOne, nextOne, related] = await Promise.all([
16 | this.$model.cate.getCateInfo({id: article.cate_id}),
17 | model_article.prevOne(aid),
18 | model_article.nextOne(aid),
19 | model_article.getRelated({'id': ['!=', aid], keywords: article.keywords}, 9)
20 | ]);
21 |
22 | // 更新点击(页面及数据库)
23 | article.click++;
24 | model_article.db.where({id: article.id}).inc('click');
25 | // markdown
26 | article.content = md.render(article.content);
27 | const is_comment = this.site.is_comment + article.comment_set >= 1 ? true : false;
28 |
29 | this.$assign('title', article.title + ' - ' + cate.cate_name + ' - ' + this.site.webname);
30 | this.$assign('description', article.description);
31 | this.$assign('keywords', article.keywords);
32 |
33 | this.$assign('cate', cate);
34 | this.$assign('article', article);
35 | this.$assign('prevOne', prevOne);
36 | this.$assign('nextOne', nextOne);
37 | this.$assign('related', related);
38 |
39 | this.$assign('is_comment', is_comment);
40 |
41 | // 文章关键词列表
42 | const keywords_list = [];
43 | if(article.keywords) {
44 | article.keywords.split(',').forEach(keyword => {
45 | if(keyword) {
46 | keywords_list.push(keyword);
47 | }
48 | });
49 | }
50 | this.$assign('keywords_list', keywords_list);
51 |
52 | // 文章地址
53 | this.$assign('article_link', this.site.basehost + this.$url.build(':article', {id: article.id}));
54 |
55 | await this.$fetch();
56 | }
57 | }
58 |
59 | module.exports = Article;
--------------------------------------------------------------------------------
/app/controller/special.js:
--------------------------------------------------------------------------------
1 | const Base = require('./base');
2 | const md = require('markdown-it')({html: true});
3 |
4 | class Special extends Base
5 | {
6 | async _init() {
7 | const id = this.ctx.params.id;
8 | if((this._sp_id = parseInt(id)) != id) {
9 | this._sp_dir = id;
10 | }
11 |
12 | // 参数为空
13 | if(!this._sp_id && !this._sp_dir) {
14 | return false;
15 | }
16 |
17 | await super._init();
18 | }
19 |
20 | async special() {
21 | const condition = {};
22 | if(this._sp_dir) {
23 | condition.special_dir = this._sp_dir;
24 | } else {
25 | condition.id = this._sp_id;
26 | }
27 |
28 | const special = await this.$model.special.getSpecialInfo(condition);
29 | if(!special) return;
30 |
31 | const special_item = await this.$model.specialItem.specialItemList(special.id, 1);
32 | const special_modules = [];
33 | let map_data = {};
34 | special_item.forEach(item => {
35 | if(!~special_modules.indexOf(item.type)) {
36 | special_modules.push(item.type);
37 | }
38 | if(item.type == 'markdown') {
39 | item.data.content = md.render(item.data.content);
40 | }
41 | if(item.type == 'map') {
42 | map_data[item.id] = item.data;
43 | }
44 | });
45 | map_data = JSON.stringify(map_data);
46 |
47 | // 更新点击(页面及数据库)
48 | special.click++;
49 | this.$model.special.db.where({id: special.id}).inc('click');
50 |
51 | this.$assign('title', (special.seo_title || special.title) + ' - ' + this.site.webname);
52 | this.$assign('description', special.description);
53 | this.$assign('keywords', special.keywords);
54 |
55 | this.$assign('special', special);
56 | this.$assign('special_item', special_item);
57 | this.$assign('special_modules', special_modules);
58 | this.$assign('map_data', map_data);
59 |
60 | this.$assign('map_ak', this.site.map_ak);
61 |
62 | await this.$fetch();
63 | }
64 | }
65 |
66 | module.exports = Special;
--------------------------------------------------------------------------------
/admin/view/link_form.htm:
--------------------------------------------------------------------------------
1 | {{extend './layout.htm'}}
2 |
3 | {{block 'main'}}
4 |
47 | {{/block}}
48 |
49 | {{block 'js'}}
50 |
56 | {{/block}}
--------------------------------------------------------------------------------
/admin/view/user_form.htm:
--------------------------------------------------------------------------------
1 | {{extend './layout.htm'}}
2 |
3 | {{block 'main'}}
4 |
43 | {{/block}}
44 |
45 | {{block 'js'}}
46 |
60 | {{/block}}
--------------------------------------------------------------------------------
/admin/view/comment_index.htm:
--------------------------------------------------------------------------------
1 | {{extend './layout.htm'}}
2 |
3 | {{block 'main'}}
4 |
13 |
14 |
15 |
16 | | ID | 评论内容 | 所属文章 | 昵称 | 邮箱 | 主页 | IP | 时间 | 操作 |
17 |
18 |
19 | {{each list item}}
20 |
21 | | {{item.id}} |
22 | {{item.pid ? '回复:' : '评论:'}}{{item.content}} |
23 | {{item.title}} |
24 | {{item.uname}} |
25 | {{item.email}} |
26 | {{item.url}} |
27 | {{item.ip}} |
28 | {{item.add_time | dateFormat 'YYYY-mm-dd HH:ii'}} |
29 | 删除 |
30 |
{{/each}}
31 |
32 |
33 |
34 | {{@pagination}}
35 | {{/block}}
36 |
37 | {{block 'js'}}
38 |
62 | {{/block}}
--------------------------------------------------------------------------------
/public/static/common/bmap/1.0.1.css:
--------------------------------------------------------------------------------
1 | .el-vue-bmap-container {
2 | height: 100%;
3 | position: relative;
4 | overflow: hidden;
5 | }
6 | .el-vue-bmap-container .el-vue-bmap {
7 | height: 100%;
8 | }
9 |
10 | .bmap-info-window-custom {
11 | position: absolute;
12 | background: #F5F8FF;
13 | box-shadow: 0 5px 10px 0 rgba(65, 84, 102, 0.09);
14 | border-radius: 5px;
15 | z-index: 200;
16 | }
17 | .bmap-info-window-custom.top {
18 | transform: translate(-50%, 11px);
19 | }
20 | .bmap-info-window-custom.top .arrow {
21 | position: absolute;
22 | box-shadow: 0 5px 7px -1px rgba(238, 238, 238, 0.5);
23 | width: 0;
24 | height: 0;
25 | border-left: 6px solid transparent;
26 | border-right: 6px solid transparent;
27 | border-bottom: 11px solid #F5F8FF;
28 | top: -11px;
29 | left: 50%;
30 | transform: translateX(-50%);
31 | }
32 | .bmap-info-window-custom.right {
33 | transform: translate(calc(-100% - 11px), -50%);
34 | }
35 | .bmap-info-window-custom.right .arrow {
36 | position: absolute;
37 | box-shadow: 0 5px 7px -1px rgba(238, 238, 238, 0.5);
38 | width: 0;
39 | height: 0;
40 | border-top: 6px solid transparent;
41 | border-bottom: 6px solid transparent;
42 | border-left: 11px solid #F5F8FF;
43 | right: -11px;
44 | top: 50%;
45 | transform: translateY(-50%);
46 | }
47 | .bmap-info-window-custom.bottom {
48 | transform: translate(-50%, calc(-100% - 11px));
49 | }
50 | .bmap-info-window-custom.bottom .arrow {
51 | position: absolute;
52 | box-shadow: 0 5px 7px -1px rgba(238, 238, 238, 0.5);
53 | width: 0;
54 | height: 0;
55 | border-left: 6px solid transparent;
56 | border-right: 6px solid transparent;
57 | border-top: 11px solid #F5F8FF;
58 | bottom: -11px;
59 | left: 50%;
60 | transform: translateX(-50%);
61 | }
62 | .bmap-info-window-custom.left {
63 | transform: translate(11px, -50%);
64 | }
65 | .bmap-info-window-custom.left .arrow {
66 | position: absolute;
67 | box-shadow: 0 5px 7px -1px rgba(238, 238, 238, 0.5);
68 | width: 0;
69 | height: 0;
70 | border-top: 6px solid transparent;
71 | border-bottom: 6px solid transparent;
72 | border-right: 11px solid #F5F8FF;
73 | left: -11px;
74 | top: 50%;
75 | transform: translateY(-50%);
76 | }
77 | .bmap-info-window-custom.top-right {
78 | transform: translate(-100%, 0);
79 | }
80 | .bmap-info-window-custom.bottom-left {
81 | transform: translate(0, -100%);
82 | }
83 | .bmap-info-window-custom.bottom-right {
84 | transform: translate(-100%, -100%);
85 | }
86 | .bmap-info-window-custom.custom {
87 | box-shadow: none;
88 | border-radius: 0;
89 | background: none;
90 | }
--------------------------------------------------------------------------------
/admin/view/components/me-point-select.htm:
--------------------------------------------------------------------------------
1 |
16 |
--------------------------------------------------------------------------------
/public/static/map.js:
--------------------------------------------------------------------------------
1 | VueBMap.initBMapApiLoader({
2 | ak: map_ak,
3 | });
4 |
5 | const creat_app = data => {
6 | const {createApp, onMounted, ref, computed} = Vue;
7 | const app = {
8 | setup() {
9 | onMounted(() => {
10 | console.log('app mounted');
11 | });
12 |
13 | const mapData = data;
14 |
15 | const point = value => {
16 | return value.split(',');
17 | }
18 |
19 | const color = value => {
20 | if(!value) {
21 | return '';
22 | }
23 | return value.slice(0, 7);
24 | }
25 |
26 | const opacity = value => {
27 | if(!value) {
28 | return 1;
29 | }
30 | value = value.substring(7);
31 | if(!value) {
32 | return 1;
33 | }
34 | return parseInt(value, 16) / 255;
35 | }
36 |
37 | const text = value => {
38 | value = value.split('::');
39 | return value[1] || value[0];
40 | }
41 |
42 | const zoom = ref(data.zoom);
43 | const scale = computed(() => {
44 | return 2 ** (parseFloat(zoom.value).toFixed(2) - data.zoom);
45 | });
46 | const textStyle = computed(() => {
47 | return data => {
48 | const obj = {color: color(data.text_color), opacity: opacity(data.text_color), fontSize: data.text_size + 'px'};
49 | if(data.text_scale == 1) {
50 | obj.transformOrigin = '0 0';
51 | obj.transform = 'scale(' + scale.value + ')';
52 | }
53 | return obj;
54 | }
55 | });
56 |
57 | const zoomEnd = e => {
58 | zoom.value = e.target.getZoom();
59 | }
60 |
61 | return {
62 | mapData,
63 | point,
64 | color,
65 | opacity,
66 | text,
67 | textStyle,
68 | zoomEnd
69 | }
70 | }
71 | }
72 | return createApp(app);
73 | }
74 |
75 | Object.keys(map_data).forEach(id => {
76 | const app = creat_app(map_data[id]);
77 | app.config.compilerOptions.delimiters = ['{$', '}'];
78 | app.use(VueBMap);
79 | app.mount('.map-' + id);
80 | });
--------------------------------------------------------------------------------
/admin/view/cate_form.htm:
--------------------------------------------------------------------------------
1 | {{extend './layout.htm'}}
2 |
3 | {{block 'main'}}
4 |
57 | {{/block}}
--------------------------------------------------------------------------------
/app/controller/comment.js:
--------------------------------------------------------------------------------
1 | const {Controller} = require('jj.js');
2 |
3 | class Comment extends Controller
4 | {
5 | async _init() {
6 | this.site = await this.$model.site.getConfig();
7 | }
8 |
9 | async list() {
10 | const id = parseInt(this.ctx.query.id) || 0;
11 | const page = parseInt(this.ctx.query.page) || 1;
12 | const article = await this.$model.article.getArticle({id}, 'id,comment_set');
13 | if(!article) {
14 | return this.$error('文章不存在或已删除!');
15 | }
16 | if(article.comment_set + this.site.is_comment < 1) {
17 | if(this.site.is_comment == 0) {
18 | return this.$error('系统已关闭评论功能!');
19 | } else {
20 | return this.$error('本文已关闭评论功能!');
21 | }
22 | }
23 |
24 | const list = await this.$model.comment.getPageList(id, page);
25 | this.$success(this.$utils.toTreeArray(list).reverse());
26 | }
27 |
28 | async post() {
29 | if(this.ctx.method != 'POST') {
30 | return this.$error('非法请求!');
31 | }
32 |
33 | const data = this.ctx.request.body;
34 | if(!data.uname) {
35 | return this.$error('昵称不能为空!');
36 | }
37 | if(!data.content) {
38 | return this.$error('评论内容不能为空!');
39 | }
40 |
41 | data.article_id = parseInt(data.article_id);
42 | const article = await this.$model.article.getArticle({id: data.article_id}, 'id,comment_set');
43 | if(!article) {
44 | return this.$error('文章不存在或已删除!');
45 | }
46 | if(article.comment_set + this.site.is_comment < 1) {
47 | if(this.site.is_comment == 0) {
48 | return this.$error('系统已关闭评论功能!');
49 | } else {
50 | return this.$error('本文已关闭评论功能!');
51 | }
52 | }
53 |
54 | data.pid = parseInt(data.pid);
55 | if(data.pid) {
56 | const reply = await this.$model.comment.get({id: data.pid});
57 | if(!reply) {
58 | return this.$error('原评论不存在或已删除!');
59 | }
60 | data.comment_id = reply.comment_id;
61 | }
62 | data.user_id = this.$service.cookie.get('user') || 0;
63 | data.ip = this.$utils.getIP(this.ctx.req);
64 |
65 | const err = await this.$model.comment.addComment(data);
66 | if(err) {
67 | this.$error(err);
68 | } else {
69 | this.$success(data.pid ? '回复成功!' : '评论成功!');
70 | }
71 | }
72 | }
73 |
74 | module.exports = Comment;
--------------------------------------------------------------------------------
/admin/view/index_index.htm:
--------------------------------------------------------------------------------
1 | {{extend './layout.htm'}}
2 |
3 | {{block 'css'}}
4 |
28 | {{/block}}
29 |
30 | {{block 'main'}}
31 |
69 | {{/block}}
70 |
71 | {{block 'js'}}
72 |
77 | {{/block}}
--------------------------------------------------------------------------------
/admin/controller/article.js:
--------------------------------------------------------------------------------
1 | const Base = require('./base');
2 |
3 | class Article extends Base
4 | {
5 | async index() {
6 | const cate_id = this.ctx.query.cate_id;
7 | const keyword = this.ctx.query.keyword;
8 |
9 | const condition = {};
10 | if(cate_id > 0) {
11 | condition['a.cate_id'] = cate_id;
12 | }
13 | if(keyword) {
14 | condition['concat(a.title, a.writer)'] = ['like', '%' + keyword + '%'];
15 | }
16 |
17 | const cate_list = await this.$model.cate.getCateList();
18 | const [list, pagination] = await this.$model.article.getArticleList(condition);
19 |
20 | this.$assign('cate_id', cate_id);
21 | this.$assign('keyword', keyword);
22 | this.$assign('cate_list', cate_list);
23 | this.$assign('list', list);
24 | this.$assign('pagination', pagination.render());
25 | this.$assign('callback', this.ctx.query.callback || 'callback');
26 |
27 | await this.$fetch();
28 | }
29 |
30 | async form() {
31 | const cate_list = await this.$model.cate.getCateList();
32 | const id = parseInt(this.ctx.query.id);
33 |
34 | let article = {};
35 | if(id) {
36 | article = await this.$model.article.get({id});
37 | }
38 |
39 | this.$assign('cate_list', cate_list);
40 | this.$assign('article', article);
41 | this.$assign('uname', this.user.uname);
42 |
43 | const comment_set_options = [
44 | {value: 0, name: '跟随系统'},
45 | {value: 1, name: '强制开启'},
46 | {value: -1, name: '强制关闭'}
47 | ];
48 | this.$assign('comment_set_options', comment_set_options);
49 |
50 | await this.$fetch();
51 | }
52 |
53 | async save() {
54 | if(this.ctx.method != 'POST') {
55 | return this.$error('非法请求!');
56 | }
57 |
58 | const data = this.ctx.request.body;
59 | const id = data.id;
60 | const result = await this.$model.article.saveArticle(data);
61 |
62 | if(result) {
63 | this.$success(id ? '保存成功!' : '新增成功!', 'index');
64 | } else {
65 | this.$error(id ? '保存失败!' : '新增失败!');
66 | }
67 | }
68 |
69 | async delete() {
70 | const id = parseInt(this.ctx.query.id);
71 |
72 | try {
73 | await this.$db.startTrans(async () => {
74 | await this.$model.article.del({id});
75 | await this.$model.comment.del({article_id: id});
76 | });
77 | this.$success('删除成功!', 'index');
78 | } catch (e) {
79 | this.$logger.error('删除失败:' + e.message);
80 | this.$error('删除失败!');
81 | }
82 | }
83 | }
84 |
85 | module.exports = Article;
--------------------------------------------------------------------------------
/admin/view/special_index.htm:
--------------------------------------------------------------------------------
1 | {{extend './layout.htm'}}
2 |
3 | {{block 'css'}}
4 |
10 | {{/block}}
11 |
12 | {{block 'main'}}
13 |
26 |
27 |
28 |
29 | | ID | 标题 | 短标题 | 点击 | 目录地址 | 排序 | 顶部展示 | 侧边栏 | 时间 | 操作 |
30 |
31 |
32 | {{each list item}}
33 |
34 | | {{item.id}} |
35 | {{@item.thumb && ' '}}{{item.title}} |
36 | {{item.short_title}} |
37 | {{item.click}} |
38 | {{item.special_dir}} |
39 | {{item.sort}} |
40 | {{item.flag == 1 ? '展示' : '隐藏'}} |
41 | {{item.aside ? '显示' : '隐藏'}} |
42 | {{item.add_time | dateFormat 'YYYY-mm-dd HH:ii'}} |
43 | 编辑删除 |
44 |
{{/each}}
45 |
46 |
47 |
48 | {{@pagination}}
49 | {{/block}}
50 |
51 | {{block 'js'}}
52 |
76 | {{/block}}
--------------------------------------------------------------------------------
/admin/view/components/me-article-select.htm:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/admin/view/site_form.htm:
--------------------------------------------------------------------------------
1 | {{extend './layout.htm'}}
2 |
3 | {{block 'css'}}
4 |
8 | {{/block}}
9 |
10 | {{block 'main'}}
11 |
58 | {{/block}}
59 |
60 | {{block 'js'}}
61 |
67 | {{/block}}
--------------------------------------------------------------------------------
/app/model/article.js:
--------------------------------------------------------------------------------
1 | const Base = require('./base');
2 |
3 | class Article extends Base
4 | {
5 | // 首页文章列表
6 | async getIndexList(page_size=10, with_page=false) {
7 | const modle = this.db.table('article a').field('a.id,a.cate_id,a.user_id,a.title,a.writer,a.keywords,a.click,a.description,a.add_time,a.thumb,c.cate_name,c.cate_dir').join('cate c', 'a.cate_id=c.id').order('a.id', 'desc').limit(page_size).cache(this.cacheTime);
8 | if(with_page) {
9 | return await modle.pagination({page_size, pagination: this.$pagination.index});
10 | } else {
11 | return [await modle.select()];
12 | }
13 | }
14 |
15 | // 栏目文章列表及分页
16 | async getPageList(condition, page_size=10) {
17 | return await this.db.field('id,cate_id,user_id,title,writer,source,click,keywords,description,add_time,thumb').where(condition).order('id', 'desc').cache(this.cacheTime).pagination({page_size, pagination: this.$pagination.cate});
18 | }
19 |
20 | // 搜索文章列表及分页
21 | async getSearchList(condition, page_size=10) {
22 | return await this.db.table('article a').field('a.id,a.cate_id,a.user_id,a.title,a.writer,a.keywords,a.click,a.description,a.add_time,a.thumb,c.cate_name,c.cate_dir').join('cate c', 'a.cate_id=c.id').where(condition).order('a.id', 'desc').cache(this.cacheTime).pagination({page_size});
23 | }
24 |
25 | // 获取一篇文章
26 | async getArticle(condition, fields) {
27 | return await this.db.field(fields).where(condition).find();
28 | }
29 |
30 | // 最新文章
31 | async getNew(rows=8) {
32 | return await this.db.field('id,title,click,thumb').order('id', 'desc').limit(rows).cache(this.cacheTime).select();
33 | }
34 |
35 | // 热点文章
36 | async getHot(rows=8) {
37 | return await this.db.field('id,title,click,thumb').order('click', 'desc').limit(rows).cache(this.cacheTime).select();
38 | }
39 |
40 | // 上一篇
41 | async prevOne(id, condition) {
42 | return await this.db.field('id,title,thumb').where({id: ['>', id]}).where(condition).order('id', 'asc').cache(this.cacheTime).find();
43 | }
44 |
45 | // 下一篇
46 | async nextOne(id, condition) {
47 | return await this.db.field('id,title,thumb').where({id: ['<', id]}).where(condition).order('id', 'desc').cache(this.cacheTime).find();
48 | }
49 |
50 | // 相关文章
51 | async getRelated(condition, rows=10) {
52 | let keywords = condition.keywords;
53 | if(!keywords) {
54 | return [];
55 | }
56 | typeof keywords != 'array' && (keywords = keywords.split(','));
57 | keywords = keywords.filter(val => val != '').join('|').replace(/"/, '');
58 | if(keywords == '') {
59 | return [];
60 | }
61 | condition.keywords = ['exp', 'CONCAT_WS(`title`, `keywords`) REGEXP ' + `"${keywords}"`];
62 | return await this.db.field('id,title,click,thumb').where(condition).order('id', 'desc').limit(rows).cache(this.cacheTime).select();
63 | }
64 | }
65 |
66 | module.exports = Article;
--------------------------------------------------------------------------------
/admin/view/link_index.htm:
--------------------------------------------------------------------------------
1 | {{extend './layout.htm'}}
2 |
3 | {{block 'css'}}
4 |
17 | {{/block}}
18 |
19 | {{block 'main'}}
20 |
37 |
38 |
39 |
40 | | ID | 链接文字 | 图标 | 链接 | 排序 | 操作 |
41 |
42 |
43 | {{each list item}}
44 |
45 | | {{item.id}} |
46 | <%for (var i=1;i <%}%>├─ {{item.title}} |
47 | {{@~item.icon.indexOf('/') ? ' ' : ~item.icon.indexOf('layui-icon') ? '' : item.icon}} |
48 | {{item.url}} |
49 | {{item.sort}} |
50 | 新增编辑删除 |
51 |
{{/each}}
52 |
53 |
54 |
55 | {{/block}}
56 |
57 | {{block 'js'}}
58 |
82 | {{/block}}
--------------------------------------------------------------------------------
/admin/view/components/form-map.htm:
--------------------------------------------------------------------------------
1 |
34 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # melog
2 |
3 | 
4 |
5 | melog,一个基于jj.js(nodejs)构建的简单轻量级blog系统。代码极简,无需编译,方便二次开发。
6 |
7 | 仓库地址:[https://github.com/yafoo/melog](https://github.com/yafoo/melog "https://github.com/yafoo/melog")
8 |
9 | 码云镜像:[https://gitee.com/yafu/melog](https://gitee.com/yafu/melog "https://gitee.com/yafu/melog")
10 |
11 | 官网地址:[https://me.i-i.me/special/melog.html](https://me.i-i.me/special/melog.html "https://me.i-i.me/special/melog.html")
12 |
13 | 演示demo:[https://js.i-i.me/](https://js.i-i.me/ "https://js.i-i.me/")(后台:[/admin](https://js.i-i.me/admin "https://js.i-i.me/admin"),账号:`melog@i-i.me`,密码:`123456`)
14 |
15 | ## 特性
16 |
17 | 1. 速度极快
18 | 2. 轻量,前台无框架依赖,移动优先,自适应pc
19 | 3. 简单,基于jj.js(类thinkphp)经典mvc框架,方便二次开发
20 | 4. 安全,后台目录可自定义,密码重试次数限制
21 | 5. 支持更换导航风格(cms或blog)
22 | 6. 支持更换主题,自定义主题可以共用默认主题文件
23 | 7. 专题功能,可以定制个性页面
24 | 8. Markdown编辑、实时预览,支持手机端,支持截图、图片文件粘贴上传
25 |
26 | ## 运行环境
27 | nodejs >= v12
28 | mysql >= v5.5
29 |
30 | ## 安装
31 |
32 | ### 1、程序部署
33 |
34 | - Docker方式部署
35 |
36 | ```bash
37 | # 镜像拉取
38 | docker pull yafoo/melog
39 |
40 | # 容器运行
41 | docker run -p 3003:3003 --restart unless-stopped --name melog -d yafoo/melog
42 |
43 | # 容器运行(配置文件、站点数据保存到宿主机)
44 | docker run -p 3003:3003 --restart unless-stopped --name melog -d -v $PWD/melog/config:/melog/config -v $PWD/melog/upload:/melog/public/upload yafoo/melog
45 | ```
46 |
47 | - Git方式部署
48 |
49 | ```bash
50 | # 也可以直接到github或gitee上下载压缩文件
51 | git clone https://github.com/yafoo/melog.git
52 | cd melog
53 | npm i
54 |
55 | # 运行程序,系统默认运行在3003端口
56 | node server.js
57 | ```
58 |
59 | ### 2、配置数据库
60 |
61 | - 浏览器打开网址 `http://127.0.0.1:3003/install`,配置并点击安装
62 |
63 | > 提示:如果网址打开出错,或者安装失败,可以修改 `/config/app.js` 文件,将 `app_debug` 设置为 `true`,打开调试模式,重启程序并重新安装,在控制台可以看到运行日志。
64 |
65 | ## 访问首页
66 |
67 | ```
68 | http://127.0.0.1:3003
69 | ```
70 |
71 | ## 访问后台
72 |
73 | - 后台地址:`http://127.0.0.1:3003/admin`
74 | - 默认账号:`melog@i-i.me`
75 | - 默认密码:`123456`
76 |
77 | > 提示:登录后请及时在后台修改账号密码
78 |
79 | ## 旧版升级
80 |
81 | ### 1、V2版本升级
82 |
83 | v2版本升级v3,请手工运行 `v2_to_v3.sql` 文件升级数据库,然后创建文件 `/config/install.js`,内容如下:
84 |
85 | ```javascript
86 | module.exports = {
87 | install: true
88 | };
89 | ```
90 |
91 | ### 2、V3.0版本升级
92 |
93 | 系统从v3.1版开始支持系统安装。v3.0版升级后,也需手工创建 `/config/install.js` 文件,内容同v2升级。
94 |
95 | ## 其他
96 |
97 | #### 开发者博客
98 | - [https://me.i-i.me/](https://me.i-i.me/ "https://me.i-i.me/")
99 |
100 | #### jj.js MVC框架
101 | - Github: [https://github.com/yafoo/jj.js](https://github.com/yafoo/jj.js "https://github.com/yafoo/jj.js")
102 | - Gitee: [https://gitee.com/yafu/jj.js](https://gitee.com/yafu/jj.js "https://gitee.com/yafu/jj.js")
103 |
104 | #### 爱主页网址导航
105 | - [https://www.i-i.me/](https://www.i-i.me/ "https://www.i-i.me/")
106 |
107 | ## Nginx代理设置
108 |
109 | ```nginx
110 | location / {
111 | proxy_pass http://127.0.0.1:3003;
112 | proxy_http_version 1.1;
113 | proxy_set_header Upgrade $http_upgrade;
114 | proxy_set_header Connection "upgrade";
115 | proxy_set_header Host $host;
116 | proxy_set_header X-Real-IP $remote_addr;
117 | proxy_set_header X-Forwarded-Proto $scheme;
118 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
119 | }
120 | ```
121 |
122 | ## License
123 |
124 | [MIT](LICENSE)
--------------------------------------------------------------------------------
/admin/controller/site.js:
--------------------------------------------------------------------------------
1 | const Base = require('./base');
2 |
3 | class Site extends Base
4 | {
5 | async index() {
6 | if(this.ctx.method == 'POST') {
7 | const data = this.ctx.request.body;
8 | const list = await this.$model.site.db.column('value', 'key');
9 | try {
10 | await this.$model.site.db.startTrans();
11 | const keys = Object.keys(data);
12 | for(const key of keys) {
13 | if((key in list) && data[key] !== list[key]) {
14 | await this.$model.site.save({value: data[key]}, {key: key});
15 | }
16 | }
17 | await this.$model.site.db.commit();
18 | this.clear('保存成功!');
19 | } catch(e) {
20 | await this.$model.site.db.rollback();
21 | this.$logger.error('保存失败:' + e.message);
22 | this.$error(e.msg);
23 | }
24 | } else {
25 | const list = await this.$model.site.getSiteList();
26 | list.forEach(item => {
27 | if(~['radio', 'select'].indexOf(item.type)) {
28 | item.options = item.options.split('||').map(option => option.split('|'));
29 | }
30 | });
31 | this.$assign('list', list);
32 | await this.$fetch();
33 | }
34 | }
35 |
36 | async form() {
37 | const id = this.ctx.query.id;
38 |
39 | let data = {};
40 | if(id) {
41 | data = await this.$model.site.get({key: id});
42 | }
43 |
44 | this.$assign('id', id);
45 | this.$assign('data', data);
46 | this.$assign('uname', this.user.uname);
47 | await this.$fetch();
48 | }
49 |
50 | async save() {
51 | if(this.ctx.method != 'POST') {
52 | return this.$error('非法请求!');
53 | }
54 |
55 | const data = this.ctx.request.body;
56 | const id = data.id;
57 | delete data.id;
58 | data.group = 'self';
59 |
60 | // 判断是否已存在
61 | if(!id || data.key != id) {
62 | const res = await this.$model.site.get({key: data.key});
63 | if(res) {
64 | return this.$error('参数' + data.key + '已存在!');
65 | }
66 | }
67 | const result = await this.$model.site.save(data, id ? {key: id} : undefined);
68 |
69 | if(result) {
70 | this.$success(id ? '保存成功!' : '新增成功!', 'index');
71 | } else {
72 | this.$error(id ? '保存失败!' : '新增失败!');
73 | }
74 | }
75 |
76 | async delete() {
77 | const id = this.ctx.query.id;
78 | const result = await this.$model.site.del({key: id, group: 'self'});
79 | if(result) {
80 | this.$success('删除成功!', 'index');
81 | } else {
82 | this.$error('删除失败!');
83 | }
84 | }
85 |
86 | async clear(msg = '') {
87 | try {
88 | await this.$middleware.cache.clear(); // 不建议这样调用中间件
89 | this.$success(msg || '清理成功!');
90 | } catch(e) {
91 | this.$logger.error('清理失败:' + e.message);
92 | this.$error('清理失败!');
93 | }
94 | }
95 | }
96 |
97 | module.exports = Site;
--------------------------------------------------------------------------------
/public/static/melog.js:
--------------------------------------------------------------------------------
1 | jQuery.prototype.serializeObject = function() {
2 | var obj=new Object();
3 | $.each(this.serializeArray(),function(index,param){
4 | if(!(param.name in obj)){
5 | obj[param.name]=param.value;
6 | }
7 | });
8 | return obj;
9 | };
10 |
11 | function tips(msg, time, callback) {
12 | msg === undefined && (msg = '');
13 | time === undefined && (time = 3000);
14 | tips.index === undefined && (tips.index = 1000);
15 | tips.index++;
16 | var $tips_dom = $('');
17 | $tips_dom.appendTo("body").text(msg).fadeIn();
18 | setTimeout(function(){
19 | $tips_dom.fadeOut(function(){
20 | $tips_dom.remove();
21 | typeof callback == 'function' && callback();
22 | });
23 | }, time);
24 | }
25 |
26 | function htmlEscape(text) {
27 | return text.replace(/[<>&"\n]/g, function(match){
28 | switch(match){
29 | case "<": return "<";
30 | case ">": return ">";
31 | case "&": return "&";
32 | case "\"": return """;
33 | case "\n": return "
";
34 | }
35 | });
36 | }
37 |
38 | function format(fmt, date) {
39 | (typeof date === 'string' || typeof date === 'number') && (date = new Date(parseInt(date + '000')));
40 | date = date || new Date();
41 | let ret;
42 | let opt = {
43 | "Y+": date.getFullYear().toString(), // 年
44 | "y+": date.getFullYear().toString().slice(2),// 年
45 | "m+": (date.getMonth() + 1).toString(), // 月
46 | "d+": date.getDate().toString(), // 日
47 | "H+": date.getHours().toString(), // 时
48 | "h+": (date.getHours() > 12 ? date.getHours() - 12 : date.getHours()).toString(),// 时
49 | "i+": date.getMinutes().toString(), // 分
50 | "s+": date.getSeconds().toString() // 秒
51 | };
52 | for (let k in opt) {
53 | ret = new RegExp("(" + k + ")").exec(fmt);
54 | if (ret) {
55 | fmt = fmt.replace(ret[1], (ret[1].length == 1) ? (opt[k]) : (opt[k].padStart(ret[1].length, "0")))
56 | };
57 | };
58 | return fmt;
59 | }
60 |
61 | $(function() {
62 | // 顶部导航
63 | $(".navbar-menu").click(function() {
64 | if($(".navbar-item").height() == 0) {
65 | $(".navbar-item").height($(".navbar-item").children().length * 41);
66 | } else {
67 | $(".navbar-item").height(0);
68 | }
69 | });
70 |
71 | // 侧边搜索
72 | $(".s-button").click(function() {
73 | if(!$(".s-input").val()) {
74 | tips('请输入关键词!');
75 | } else {
76 | $(".s-form").submit();
77 | }
78 | });
79 |
80 | // 返回顶部
81 | var showGoTop = $('#footer').offset().top - $(window).height() + +$('#footer').css('margin-top').replace(/[^0-9]/g, '');
82 | $(window).scroll(function(e) {
83 | if($(this).scrollTop() > 200){
84 | $('#go-top').fadeIn(400);
85 | $(this).scrollTop() > showGoTop ? $('#go-top').addClass('go-top') : $('#go-top').removeClass('go-top');
86 | }else{
87 | $('#go-top').stop().fadeOut(400);
88 | }
89 | });
90 | $('#go-top').click(function() {
91 | $('html,body').animate({scrollTop:'0px'}, 200);
92 | });
93 | });
--------------------------------------------------------------------------------
/admin/view/components/me-scroll.htm:
--------------------------------------------------------------------------------
1 |
9 |
--------------------------------------------------------------------------------
/public/static/admin/spedit.css:
--------------------------------------------------------------------------------
1 | html, body, .spedit { width: 100%; height: 100%;}
2 | .layui-form-label {width: 45px; padding-left: 0;}
3 | .layui-input-block {margin-left: 75px;}
4 | .layui-input-wrapper {box-sizing: border-box;}
5 | .layui-input {height: 36px; line-height: 36px;}
6 | .layui-input:focus, .layui-textarea:focus {
7 | border-color: #d2d2d2!important;
8 | }
9 | .layui-form-select .layui-input {height: 38px; line-height: 38px;}
10 | .layui-form-item .layui-form-checkbox {margin-top: -2px;}
11 | .layui-color-picker {width: 240px;}
12 |
13 | /* spedit */
14 | .spedit {
15 | display: flex;
16 | }
17 | .spedit .spedit-item {
18 | padding: 0 10px;
19 | box-sizing: border-box;
20 | }
21 | .spedit .spedit-item .label {
22 | display: block;
23 | padding: 10px 0;
24 | font-weight: 400;
25 | line-height: 20px;
26 | }
27 |
28 | /* spedit-module */
29 | .spedit-module {
30 | width: 200px;
31 | }
32 | .module-list {
33 | height: calc(100% - 42px);
34 | overflow-y: overlay;
35 | font-size: 12px;
36 | margin-right: -10px;
37 | padding-right: 10px;
38 | }
39 | .module-list::-webkit-scrollbar{width:6px;}
40 | .module-list::-webkit-scrollbar-thumb{background-color:#ccc;border-radius:3px;}
41 | .module-list .module-item {
42 | padding: 25px 5px;
43 | text-align: center;
44 | border: 1px solid #f4f6fc;
45 | background-color: #f4f6fc;
46 | margin-bottom: 6px;
47 | word-break: break-all;
48 | }
49 | .module-list .module-item:hover {
50 | border: 1px dashed #1890ff;
51 | color: #1890ff;
52 | cursor: move;
53 | }
54 | .module-list .module-item .layui-icon {
55 | vertical-align: -1px;
56 | }
57 |
58 | /* spedit-special */
59 | .spedit .spedit-special {
60 | padding: 0;
61 | }
62 | .spedit .spedit-special .label {
63 | padding-left: 10px;
64 | padding-right: 10px;
65 | }
66 | .spedit-special {
67 | flex: 1;
68 | border-left: 1px solid #eee;
69 | border-right: 1px solid #eee;
70 | min-width: 300px;
71 | }
72 | .special-list {
73 | padding: 0 10px;
74 | margin: 0 auto;
75 | height: calc(100% - 42px);
76 | overflow-y: overlay;
77 | position: relative;
78 | }
79 | .special-list::-webkit-scrollbar{width:6px;}
80 | .special-list::-webkit-scrollbar-thumb{background-color:#ccc;border-radius:3px;}
81 | .special-item {
82 | min-height: 30px;
83 | position: relative;
84 | cursor: move;
85 | overflow: hidden;
86 | }
87 | .special-item::after {
88 | content: '';
89 | position: absolute;
90 | left: 0;
91 | top: 0;
92 | right: 0;
93 | bottom: 0;
94 | border: 1px dashed #059f05;
95 | display: none;
96 | }
97 | .special-item:hover::after,
98 | .special-item.hover::after {
99 | display: block;
100 | }
101 | .special-place {
102 | width: 0;
103 | height: 0;
104 | border-top: 5px solid transparent;
105 | border-left: 10px solid rgb(4, 124, 54);
106 | border-bottom: 5px solid transparent;
107 | position: absolute;
108 | left: 0;
109 | top: 0;
110 | transform: translateY(-5px);
111 | }
112 | .special-list img {
113 | display: block;
114 | width: 100%;
115 | }
116 | .special-list .special-item-empty {
117 | height: 60px;
118 | line-height: 60px;
119 | text-align: center;
120 | }
121 | .special-list .special-item-disable {
122 | opacity: 0.6;
123 | }
124 | .special-list .special-item-disable:hover {
125 | opacity: 1;
126 | }
127 |
128 | /* spedit-form */
129 | .spedit-form {
130 | width: 350px;
131 | min-width: 310px;
132 | }
133 | .form-list {
134 | height: calc(100% - 80px);
135 | overflow-y: overlay;
136 | margin-right: -10px;
137 | padding-right: 10px;
138 | }
139 | .form-list::-webkit-scrollbar{width:6px;}
140 | .form-list::-webkit-scrollbar-thumb{background-color:#ccc;border-radius:3px;}
--------------------------------------------------------------------------------
/app/view/article_article.htm:
--------------------------------------------------------------------------------
1 | {{extend './layout.htm'}}
2 |
3 | {{block 'css'}}
4 |
5 | {{/block}}
6 |
7 | {{block 'header'}}
8 |
9 |
{{article.title}}
10 |
时间: {{article.add_time | dateFormat 'YYYY-mm-dd HH:ii'}} 浏览: {{article.click}} 作者: {{article.writer}} 来源: {{article.source}}
11 |
12 | {{/block}}
13 |
14 | {{block 'content'}}
15 |
16 |
17 |
18 | {{@article.content}}
19 |
20 | 关键词:{{each keywords_list item}}{{item}}{{/each}}
21 | 所属分类:{{cate.cate_name}}
22 | 本文地址:{{article_link}}
23 |
24 |
25 | {{if related && related.length}}
26 |
27 |
28 | 相关文章
29 | -
30 |
34 |
35 |
36 | {{/if}}
37 |
38 |
42 | {{if is_comment}}
43 |
44 |
73 | {{/if}}
74 |
75 | {{include './aside.htm'}}
76 |
77 | {{/block}}
78 |
79 | {{block 'js'}}
80 |
81 |
82 |
83 | {{/block}}
--------------------------------------------------------------------------------
/install/controller/index.js:
--------------------------------------------------------------------------------
1 | const {Controller} = require('jj.js');
2 | const pjson = require('../../package.json');
3 |
4 | class Index extends Controller
5 | {
6 | async _init() {
7 | this.config = {
8 | db: this.$config.db.default,
9 | VERSION: pjson.version,
10 | APP_TIME: this.ctx.APP_TIME
11 | };
12 |
13 | this.base_dir = this.$config.app.base_dir;
14 | this.installFile = this.base_dir + '/config/install.js';
15 | this.dbFile = this.base_dir + '/config/db.js';
16 | this.sqlFile = this.base_dir + '/melog.sql';
17 |
18 | if(await this._isInstalled()) {
19 | this.$error('系统已安装!');
20 | return false;
21 | }
22 |
23 | this.$assign('title', 'Melog系统安装');
24 | this.$assign('description', 'melog,一个基于jj.js(nodejs)构建的简单轻量级blog系统。代码极简,无需编译,方便二次开发。');
25 | this.$assign('keywords', 'melog');
26 | this.$assign('config', this.config);
27 | }
28 |
29 | async _isInstalled() {
30 | if(this.$config.install) {
31 | return true;
32 | }
33 |
34 | if(await this.$.utils.fs.isFile(this.installFile)) {
35 | return true;
36 | }
37 |
38 | return false;
39 | }
40 |
41 | async _writeInstallFile(config_db) {
42 | const install_content = `// 本文件用来标识系统已安装,不可删除。如需重新安装,请删除本文件并重启系统。
43 | module.exports = {
44 | install: true,
45 | version: '${this.config.VERSION}'
46 | };`;
47 | await this.$.utils.fs.writeFile(this.installFile, install_content);
48 |
49 | const db_content = `module.exports = {
50 | default: {
51 | type : 'mysql', // 数据库类型
52 | host : '${config_db.host}', // 服务器地址
53 | database : '${config_db.database}', // 数据库名
54 | user : '${config_db.user}', // 数据库用户名
55 | password : '${config_db.password}', // 数据库密码
56 | port : '${config_db.port}', // 数据库连接端口
57 | charset : 'utf8', // 数据库编码默认采用utf8
58 | prefix : 'melog_' // 数据库表前缀
59 | }
60 | };`;
61 | await this.$.utils.fs.writeFile(this.dbFile, db_content);
62 | }
63 |
64 | async index() {
65 | await this.$fetch();
66 | }
67 |
68 | async install() {
69 | if(this.ctx.method != 'POST') {
70 | return this.$error('非法请求!');
71 | }
72 |
73 | const form_data = this.ctx.request.body;
74 | let db = null;
75 | let error = '';
76 |
77 | try {
78 | const config_db = {...this.config.db, ...form_data};
79 | delete config_db.database;
80 | const database = form_data.database;
81 |
82 | // 新建db实例
83 | db = new this.$db(this.ctx, config_db);
84 | // 创建数据库
85 | await db.query(`create database if not exists \`${database}\` DEFAULT CHARACTER SET utf8mb4;`);
86 | // 设置数据库并重新连接
87 | config_db.database = database;
88 | (await db.close()).connect(config_db);
89 | // 获取sql文件
90 | let sql_data = await this.$.utils.fs.readFile(this.sqlFile);
91 | sql_data = sql_data.split(/;\r\n/);
92 | // 使用事务执行sql语句
93 | await db.startTrans(async () => {
94 | for(let i=0; i
2 |
3 |
4 |
5 |
6 | {{title}} - {{site.webname}}
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | {$item.module_name}
19 |
20 |
21 |
22 |
23 |
24 |
41 |
42 |
43 |
62 |
63 |
64 |
65 |
66 |
67 |
85 | <%component_files.forEach(file => include('./components/' + file))%>
86 |
87 |
88 |
--------------------------------------------------------------------------------
/public/static/content.js:
--------------------------------------------------------------------------------
1 | $(function() {
2 | // 评论
3 | var reg_email = /.+@.+/i;
4 | var reg_url = /^http(s)?:\/\//i;
5 |
6 | var $form = $(".comment-form");
7 | if($form.length) {
8 | // 发表评论
9 | $(".submit").click(function(){
10 | var data = $form.serializeObject();
11 | if(data.uname === '') {
12 | return tips('请填写昵称!');
13 | }
14 | if(data.email && !reg_email.test(data.email)) {
15 | return tips('请填写正确的邮箱!');
16 | }
17 | if(data.url && !reg_url.test(data.url)) {
18 | return tips('网址前缀需写http://或https://!');
19 | }
20 | if(data.content === '') {
21 | return tips('请填写评论内容!');
22 | }
23 |
24 | $.post($form.attr("action"), data, function(re){
25 | if(re.state) {
26 | tips(re.msg, 2000, function(){
27 | history.go(0);
28 | });
29 | } else {
30 | tips(re.msg);
31 | }
32 | });
33 | });
34 |
35 | // 取消回复
36 | $(".cancel").click(function(){
37 | $("input[name=pid]").val(0);
38 | $(this).hide();
39 | $(".comment .title").after($(".comment-form"));
40 | });
41 |
42 | // 加载更多评论
43 | $(".comment-more").click(function(){
44 | if($(this).data('more') == 'none') {
45 | return tips('没有更多了!');
46 | }
47 | if($(this).data('more') == 'loading') {
48 | return tips('评论正在加载中!');
49 | }
50 | getComment();
51 | });
52 |
53 | // 获取评论
54 | function getComment() {
55 | getComment.page || (getComment.page = 0);
56 | getComment.page++;
57 | $(".comment-more").text('评论加载中..').data('more', 'loading');
58 | $.get($(".comment-list").data("url") + '&page=' + getComment.page, function(re){
59 | $(".comment-more").text('加载更多评论').data('more', '');
60 | if(!re.state) {
61 | return tips(re.msg);
62 | }
63 | var list = re.data;
64 | if(list.length == 0) {
65 | $(".comment-more").text('没有更多评论了!').data('more', 'none');
66 | }
67 | list = $('' + parseComment(list) + '
');
68 | list.find(".face").click(function(){
69 | $("input[name=pid]").val($(this).data('id'));
70 | $(".cancel").show();
71 | $(this).parent().after($(".comment-form"));
72 | tips('回复 ' + $(this).data('uname') + ' 的评论', 1000);
73 | });
74 | list.children().appendTo(".comment-list");
75 | }).error(function () {
76 | getComment.page--;
77 | $(".comment-more").text('加载更多评论').data('more', '');
78 | tips('请求出错了!');
79 | });
80 | }
81 |
82 | // 解析评论模板
83 | function parseComment(list) {
84 | parseComment.HTML || (parseComment.HTML = $(".comment-list").html(), $(".comment-list").html(''));
85 | var html = '';
86 | list.forEach(item => {
87 | html += parseComment.HTML.replace(/\[(.*?)\]/g, function(match, pos){
88 | switch(pos){
89 | case "url": return !item.url ? '' : (!reg_url.test(item.url) ? 'http://' : '') + htmlEscape(item.url);
90 | case "face": return jdenticon.toSvg(item.uname, 80);
91 | case "add_time": return format('YYYY-mm-dd HH:ii', item.add_time);
92 | case "reply": return item.child ? parseComment(item.child) : '';
93 | default : return htmlEscape(item[pos].toString());
94 | }
95 | });
96 | });
97 | return html;
98 | }
99 |
100 | getComment();
101 | }
102 |
103 | // 代码高亮
104 | hljs.highlightAll();
105 |
106 | // 文章图片点击查看
107 | $('article.content img').each(function() {
108 | var that = $(this);
109 | if(that.attr('alt')) {
110 | that.attr('title', that.attr('alt'));
111 | }
112 |
113 | that.click(function() {
114 | window.open(that.attr('src'));
115 | });
116 | });
117 | });
--------------------------------------------------------------------------------
/admin/view/layout.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{title}} - {{site.webname}}
7 |
8 |
79 | {{block 'css'}}{{/block}}
80 |
81 |
82 | {{if user}}
83 |
100 | {{/if}}
101 |
102 | {{block 'main'}}{{/block}}
103 |
104 |
105 |
106 |
116 | {{block 'js'}}{{/block}}
117 |
118 |
--------------------------------------------------------------------------------
/admin/view/article_index.htm:
--------------------------------------------------------------------------------
1 | {{extend './layout.htm'}}
2 |
3 | {{block 'css'}}
4 |
10 | {{/block}}
11 |
12 | {{block 'main'}}
13 |
34 |
35 |
36 |
37 | | ID | 标题 | 栏目 | 点击 | 时间 | 操作 |
38 |
39 |
40 | {{each list item}}
41 |
42 | | {{item.id}} |
43 | {{@item.thumb && ' '}}{{item.title}} |
44 | {{item.cate_name}} |
45 | {{item.click}} |
46 | {{item.add_time | dateFormat 'YYYY-mm-dd HH:ii'}} |
47 | 编辑删除 |
48 |
{{/each}}
49 |
50 |
51 |
52 | {{@pagination}}
53 | {{/block}}
54 |
55 | {{block 'js'}}
56 |
105 | {{/block}}
--------------------------------------------------------------------------------
/admin/view/components/module-list.htm:
--------------------------------------------------------------------------------
1 |
82 |
121 |
--------------------------------------------------------------------------------
/admin/view/components/module-map.htm:
--------------------------------------------------------------------------------
1 |
48 |
--------------------------------------------------------------------------------
/install/view/index_index.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{title}}
7 |
8 |
9 |
10 |
43 |
44 |
45 |
60 |
61 |
62 |
63 |
64 |
数据库设置
65 |
89 |
90 |
91 |
92 |
93 |
101 |
102 |
103 |
104 |
105 |
133 |
134 |
--------------------------------------------------------------------------------
/admin/controller/special.js:
--------------------------------------------------------------------------------
1 | const Base = require('./base');
2 |
3 | class Special extends Base
4 | {
5 | async index() {
6 | const keyword = this.ctx.query.keyword;
7 |
8 | const condition = {};
9 | if(keyword) {
10 | condition['concat(title, short_title, seo_title)'] = ['like', '%' + keyword + '%'];
11 | }
12 |
13 | const [list, pagination] = await this.$model.special.getSpecialList(condition);
14 |
15 | this.$assign('keyword', keyword);
16 | this.$assign('list', list);
17 | this.$assign('pagination', pagination.render());
18 |
19 | await this.$fetch();
20 | }
21 |
22 | async form() {
23 | const id = parseInt(this.ctx.query.id);
24 |
25 | let special = {};
26 | if(id) {
27 | special = await this.$model.special.get({id});
28 | }
29 |
30 | this.$assign('special', special);
31 |
32 | await this.$fetch();
33 | }
34 |
35 | async save() {
36 | if(this.ctx.method != 'POST') {
37 | return this.$error('非法请求!');
38 | }
39 |
40 | const data = this.ctx.request.body;
41 | const id = data.id;
42 | data.aside = data.aside ? 1 : 0;
43 | data.flag = data.flag ? 1 : 0;
44 | const result = await this.$model.special.saveSpecial(data);
45 |
46 | if(result) {
47 | this.$success(id ? '保存成功!' : '新增成功!', 'index');
48 | } else {
49 | this.$error(id ? '保存失败!' : '新增失败!');
50 | }
51 | }
52 |
53 | async delete() {
54 | const id = parseInt(this.ctx.query.id);
55 |
56 | const result = await this.$model.special.del({id});
57 | if(result) {
58 | this.$success('删除成功!', 'index');
59 | } else {
60 | this.$error('删除失败!');
61 | }
62 | }
63 |
64 | async special() {
65 | const id = parseInt(this.ctx.query.id) || 0;
66 | this.$assign('id', id);
67 |
68 | const {resolve} = require('path');
69 | const dir = resolve(__dirname, '../view/components');
70 | const component_files = await this.$.utils.fs.readdir(dir);
71 | this.$assign('component_files', component_files);
72 |
73 | // 地图sdk
74 | this.$assign('map_ak', this.site.map_ak);
75 |
76 | await this.$fetch();
77 | }
78 |
79 | async specialItemList() {
80 | const special_id = parseInt(this.ctx.query.special_id);
81 | const item_list = await this.$model.specialItem.specialItemList(special_id);
82 | await this.$success(item_list);
83 | }
84 |
85 | async specialItemAdd() {
86 | if(this.ctx.method == 'POST') {
87 | const data = this.ctx.request.body;
88 | const msg = await this.$model.specialItem.specialItemSave(data);
89 | if(msg === true) {
90 | return this.$success('新增成功!');
91 | } else {
92 | return this.$error(msg);
93 | }
94 | }
95 |
96 | this.$error('非法请求!');
97 | }
98 |
99 | async specialItemSave() {
100 | if(this.ctx.method == 'POST') {
101 | const form_data = this.ctx.request.body;
102 |
103 | const data = {};
104 | data.id = form_data.id;
105 | data.enable = parseInt(form_data.enable);
106 | delete(form_data.id);
107 | delete(form_data.enable);
108 | delete(form_data.type);
109 | data.data = form_data;
110 |
111 | const msg = await this.$model.specialItem.specialItemSave(data);
112 | if(msg === true) {
113 | return this.$success('保存成功!');
114 | } else {
115 | return this.$error(msg);
116 | }
117 | }
118 |
119 | this.$error('非法请求!');
120 | }
121 |
122 | async specialItemSort() {
123 | if(this.ctx.method == 'POST') {
124 | const form_data = this.ctx.request.body;
125 | const post_sort = form_data.sort.split(',');
126 | const item_ids = [];
127 |
128 | post_sort.forEach((sort, index) => {
129 | post_sort[index] = sort.split(':');
130 | item_ids.push(post_sort[index][0]);
131 | });
132 | const item_list = await this.$model.specialItem.db.where({special_id: form_data.special_id, id: ['in', item_ids]}).column('sort', 'id');
133 |
134 | const sort_change = [];
135 | post_sort.forEach(sort => {
136 | if(typeof(item_list[sort[0]]) != 'undefined' && item_list[sort[0]] != sort[1]) {
137 | sort_change.push({id: sort[0], sort: sort[1]});
138 | }
139 | });
140 |
141 | const msg = await this.$model.specialItem.specialItemSort(sort_change);
142 | if(msg === true) {
143 | return this.$success('排序成功!');
144 | } else {
145 | return this.$error(msg);
146 | }
147 | }
148 |
149 | this.$error('非法请求!');
150 | }
151 |
152 | async specialItemDel() {
153 | if(this.ctx.method == 'POST') {
154 | const data = this.ctx.request.body;
155 | const id = data.id;
156 |
157 | const msg = await this.$model.specialItem.specialItemDel(id);
158 | if(msg === true) {
159 | return this.$success('删除成功!');
160 | } else {
161 | return this.$error(msg);
162 | }
163 | }
164 |
165 | this.$error('非法请求!');
166 | }
167 | }
168 |
169 | module.exports = Special;
--------------------------------------------------------------------------------
/public/static/admin/meedit.css:
--------------------------------------------------------------------------------
1 | @media screen and (min-width: 450px) {
2 | .meedit .meedit-area::-webkit-scrollbar,
3 | .meview .meview-content::-webkit-scrollbar {width:6px;}
4 | .meedit .meedit-area::-webkit-scrollbar-thumb,
5 | .meview .meview-content::-webkit-scrollbar-thumb {background-color:#ddd;cursor: pointer;}
6 | }
7 |
8 | .meedit-wrap {
9 | display: flex;
10 | height: 100%;
11 | }
12 | .meedit {
13 | flex: 1;
14 | display: flex;
15 | flex-direction: column;
16 | height: 100%;
17 | }
18 | .meedit-area {
19 | flex: 1;
20 | overflow-y: auto;
21 | overflow-y: overlay;
22 | }
23 | .meedit-tool {
24 | padding: 3px 5px 5px;
25 | border: 1px solid #eee;
26 | border-radius: 2px;
27 | font-size: 0;
28 | margin-bottom: -2px;
29 | }
30 | .meedit-tool .layui-icon,
31 | .meedit-tool .meedit-tool-mid {
32 | display: inline-block;
33 | vertical-align: middle;
34 | text-align: center;
35 | }
36 | .meedit-tool .layui-icon {
37 | width: 30px;
38 | height: 30px;
39 | line-height: 30px;
40 | margin: 3px 5px;
41 | color: #777;
42 | cursor: pointer;
43 | border-radius: 2px;
44 | }
45 | .meedit-tool .layui-icon:hover,
46 | .meedit-tool .layui-icon.hover {
47 | background-color: #eee;
48 | color: #000;
49 | }
50 | .meedit-tool .meedit-tool-mid {
51 | font-size: 14px;
52 | width: 1px;
53 | height: 18px;
54 | margin: 0 10px;
55 | background-color: #d2d2d2;
56 | }
57 | .meedit-tool .meedit-tool-table {
58 | position: relative;
59 | }
60 | .meedit-tool .meedit-tool-table:hover .meedit-table-select {
61 | display: block;
62 | }
63 | .meedit-tool .meedit-tool-table:hover.no-hover .meedit-table-select {
64 | display: none;
65 | }
66 | .meedit-table-select {
67 | position: absolute;
68 | left: 1px;
69 | top: 32px;
70 | padding: 5px;
71 | width: 144px;
72 | box-shadow: 0 0 1px rgba(0, 0, 0, 0.5);
73 | background-color: #fff;
74 | z-index: 1;
75 | display: none;
76 | }
77 | .meedit-table-select-list {
78 | cursor: pointer;
79 | }
80 | .meedit-table-select-row {
81 | display: flex;
82 | }
83 | .meedit-table-select-item {
84 | width: 14px;
85 | height: 14px;
86 | margin: 1px;
87 | background-color: #f7f7f7;
88 | border: 1px solid #f0f0f0;
89 | }
90 | .meedit-table-select-item.hover {
91 | background-color: #46bc99;
92 | border: 1px solid #3fa98a;
93 | }
94 | .meedit-table-select-tips {
95 | height: 24px;
96 | line-height: 24px;
97 | text-align: center;
98 | font-size: 14px;
99 | letter-spacing: 3px;
100 | }
101 |
102 | .meview {
103 | flex: 1;
104 | display: flex;
105 | flex-direction: column;
106 | height: 100%;
107 | }
108 | .meview-title {
109 | padding: 4px 10px 3px 60px;
110 | line-height: 36px;
111 | text-align: center;
112 | font-size: 18px;
113 | font-weight: bold;
114 | overflow: hidden;
115 | border: 1px solid #eee;
116 | border-left: none;
117 | border-right: none;
118 | border-top-right-radius: 2px;
119 | position: relative;
120 | }
121 | .meview-title::after {
122 | content: '\e691';
123 | font-family: layui-icon!important;
124 | font-size: 16px;
125 | font-weight: normal;
126 | -webkit-font-smoothing: antialiased;
127 | -moz-osx-font-smoothing: grayscale;
128 | position: absolute;
129 | top: 3px;
130 | left: 10px;
131 | width: 30px;
132 | height: 30px;
133 | line-height: 30px;
134 | margin: 3px 5px;
135 | background-color: rgba(0,0,0,0.09);
136 | color: #000;
137 | cursor: pointer;
138 | border-radius: 2px;
139 | }
140 |
141 | .meview-content {
142 | flex: 1;
143 | padding: 0 10px;
144 | overflow-y: auto;
145 | overflow-y: overlay;
146 | }
147 | .meview-content>* {
148 | margin: 1em 0;
149 | }
150 | .meview-content>*:first-child {
151 | margin-top: 6px;
152 | }
153 | .meview-content>blockquote+blockquote,
154 | .meview-content>blockquote+pre,
155 | .meview-content>pre+pre,
156 | .meview-content>pre+blockquote {
157 | margin-top: 1.3em;
158 | }
159 | .meview-content h1, .meview-content h2 {
160 | padding-bottom: .3em;
161 | border-bottom: 1px solid #eee;
162 | }
163 | .meview-content h1,
164 | .meview-content h2,
165 | .meview-content h3 {
166 | font-weight: bold;
167 | }
168 | .meview-content blockquote {
169 | padding: .8em 1em;
170 | border-left: 4px solid #ccc;
171 | background-color: #f3f3f3;
172 | }
173 | .meview-content hr {
174 | border: 1px solid #eee;
175 | }
176 | .meview-content code {
177 | color: #f63;
178 | word-break: break-word;
179 | }
180 | .meview-content a {
181 | color: #46bc99;
182 | }
183 | .meview-content pre {
184 | padding: .8em 1em;
185 | max-height: 35em;
186 | line-height: 1.4;
187 | background-color: #f3f3f3;
188 | overflow: auto;
189 | }
190 | .meview-content pre code {
191 | padding: 0;
192 | color: inherit;
193 | overflow-wrap: normal;
194 | word-break: normal;
195 | background: inherit;
196 | overflow-x: initial;
197 | }
198 | .meview-content table {
199 | width: 100%;
200 | border-collapse:collapse;
201 | }
202 | .meview-content table th, .meview-content table td {
203 | border: 1px solid #eee;
204 | padding: 0.5em;
205 | }
206 | .meview-content table th {
207 | background-color: #f9f9f9;
208 | font-weight: bold;
209 | }
210 | .meview-content li {
211 | margin: 0.6em 0 0.6em 20px;
212 | list-style: inherit;
213 | }
214 | .meview-content p {
215 | line-height: 1.8;
216 | }
217 | .meview-content img {
218 | margin-left: auto;
219 | margin-right: auto;
220 | max-width: 100%;
221 | }
--------------------------------------------------------------------------------
/admin/view/components/me-point.htm:
--------------------------------------------------------------------------------
1 |
47 |
--------------------------------------------------------------------------------
/admin/view/upload_index.htm:
--------------------------------------------------------------------------------
1 | {{extend './layout.htm'}}
2 |
3 | {{block 'css'}}
4 |
33 | {{/block}}
34 |
35 | {{block 'main'}}
36 |
50 |
51 |
52 |
53 | | ID | 图片 | 文件名字 | 类型 | 大小 | 原始文件 | 时间 | 操作 |
54 |
55 |
56 | {{each list item}}
57 |
58 | | {{item.id}} |
59 |  |
60 | {{item.title}} |
61 | {{item.extname}} |
62 | {{item.size_text}} |
63 | {{item.origin_size_text}}{{if item.origin_size_text}} (view){{/if}} |
64 | {{item.add_time | dateFormat 'YYYY-mm-dd HH:ii'}} |
65 | 编辑删除 |
66 |
{{/each}}
67 |
68 |
69 |
70 | {{@pagination}}
71 | {{/block}}
72 |
73 | {{block 'js'}}
74 |
162 | {{/block}}
--------------------------------------------------------------------------------
/admin/model/specialItem.js:
--------------------------------------------------------------------------------
1 | const {Model, utils} = require('jj.js');
2 |
3 | class SpecialItem extends Model
4 | {
5 | async specialItemList(special_id, enable=0) {
6 | const condition = {special_id};
7 | if(enable) {
8 | condition.enable = 1;
9 | }
10 | const item_list = await this.db.order('sort', 'asc').order('id', 'asc').select(condition);
11 |
12 | await Promise.all(item_list.map(async item => {
13 | if(item.data) {
14 | item.data = JSON.parse(item.data);
15 | } else {
16 | item.data = {};
17 | }
18 | switch(item.type) {
19 | case 'list':
20 | if(item.data.rows === undefined) {
21 | item.data.rows = 5;
22 | }
23 | if(item.data.source === undefined) {
24 | item.data.source = 'keyword';
25 | }
26 | if(item.data.style === undefined) {
27 | item.data.style = '';
28 | }
29 | if(item.data.ids === undefined) {
30 | item.data.ids = '';
31 | }
32 | item.data.list = [];
33 | let field = 'id,thumb,title,description,click,keywords,add_time,cate_id';
34 | if(item.data.style) {
35 | field = 'id,thumb,title';
36 | }
37 | if(item.data.source == 'keyword' && item.data.keyword) {
38 | const list = await this.$db.table('article').where({title: ['like', '%' + item.data.keyword + '%']}).field(field).order('id', 'desc').limit(item.data.rows).select();
39 | item.data.list = list;
40 | } else if(item.data.source != 'keyword' && item.data.ids) {
41 | const ids = item.data.ids.split(',');
42 | const list = await this.$db.table('article').where({id: ['in', ids]}).field(field).select();
43 | const temp_list = {};
44 | list.forEach(item => {
45 | temp_list[item.id] = item;
46 | });
47 | ids.forEach(id => {
48 | item.data.list.push(temp_list[id]);
49 | });
50 | }
51 | let cate_list = {};
52 | if(!item.data.style) {
53 | const temp_list = await this.$db.table('cate').limit(100).field('id,cate_name,cate_dir').select();
54 | temp_list.forEach(item => {
55 | cate_list[item.id] = item;
56 | });
57 | }
58 | item.data.list.forEach(arc => {
59 | arc.url = this.$url.build(':article', {id: arc.id});
60 | if(arc.add_time) {
61 | arc.add_date = this.$utils.date('YYYY-mm-dd', arc.add_time)
62 | }
63 | if(arc.cate_id) {
64 | arc.cate_name = cate_list[arc.cate_id].cate_name;
65 | arc.cate_dir = cate_list[arc.cate_id].cate_dir;
66 | arc.cate_url = this.$url.build(':cate', {cate: cate_list[arc.cate_id].cate_dir});
67 | }
68 | });
69 | break;
70 | case 'map':
71 | if(item.data.map_type === undefined) {
72 | item.data.map_type = 'normal';
73 | }
74 | if(item.data.zoom === undefined) {
75 | item.data.zoom = 14;
76 | }
77 | if(item.data.center === undefined) {
78 | item.data.center = '';
79 | }
80 | if(item.data.list === undefined) {
81 | item.data.list = [];
82 | }
83 | break;
84 | }
85 | }));
86 |
87 | return item_list;
88 | }
89 |
90 | async specialItemSave(data) {
91 | if(data.data) {
92 | data.data = JSON.stringify(data.data);
93 | } else if(!data.id) {
94 | data.data = '';
95 | }
96 |
97 | let msg = true;
98 |
99 | if(data.id) {
100 | const result = await this.save(data);
101 | if(!result) {
102 | msg = '修改失败!';
103 | }
104 | } else {
105 | data.add_time = this.$utils.time();
106 |
107 | await this.db.startTrans();
108 | try {
109 | const result = await this.save(data);
110 | if(!result) {
111 | throw new Error('新增失败!');
112 | }
113 |
114 | const sort_data = [];
115 | if(data.sort) {
116 | const item_list = await this.db.where({special_id: data.special_id}).order('sort', 'asc').order('id', 'asc').limit(data.sort, 1000).select();
117 | item_list.forEach((item, index) => {
118 | sort_data.push({id: item.id, sort: data.sort + index + 1});
119 | });
120 |
121 | msg = await this.specialItemSort(sort_data);
122 | if(msg !== true) {
123 | throw new Error(msg);
124 | }
125 | }
126 |
127 | await this.db.commit();
128 | } catch(err) {
129 | msg = err.msg || '系统出错!';
130 | await this.db.rollback();
131 | }
132 |
133 | }
134 |
135 | return msg;
136 | }
137 |
138 | async specialItemSort(sort_data) {
139 | let msg = true;
140 | if(!sort_data || !sort_data.length) {
141 | return msg;
142 | }
143 |
144 | await this.db.startTrans();
145 | try {
146 | for(let i=0; i
6 | {{/if}}
7 | {{if ~special_modules.indexOf('map')}}
8 |
9 | {{/if}}
10 | {{if special.page_width}}
11 |
18 | {{/if}}
19 | {{/block}}
20 |
21 | {{block 'header'}}
22 |
23 |
{{special.title}}
24 |
{{special.description}}
25 |
26 | {{/block}}
27 |
28 | {{block 'content'}}
29 |
30 |
31 | {{each special_item sp_item}}
32 |
33 | {{if sp_item.type == 'list'}}
34 | {{if !sp_item.data.style}}
35 |
36 | {{each sp_item.data.list item}}
37 |
38 |
39 | -
40 | {{@item.thumb && '
'}}
41 | {{item.description}}
42 |
43 |
44 | - 时间: {{item.add_time | dateFormat 'YYYY-mm-dd'}}
45 | - 浏览: {{item.click}}
46 | - 关键词: {{item.keywords}}
47 |
48 |
49 | {{/each}}
50 |
51 | {{else if ~['imagetitle', 'image', 'title'].indexOf(sp_item.data.style)}}
52 |
53 | {{each sp_item.data.list item}}
54 |
55 | {{if ~['imagetitle', 'image'].indexOf(sp_item.data.style)}}
56 | - {{@item.thumb && '
'}}
57 | {{/if}}
58 | {{if ~['imagetitle', 'title'].indexOf(sp_item.data.style)}}
59 |
60 | {{/if}}
61 |
62 | {{/each}}
63 |
64 | {{/if}}
65 | {{else if sp_item.type == 'markdown' || sp_item.type == 'html'}}
66 |
67 | {{@sp_item.data.content}}
68 |
69 | {{else if sp_item.type == 'map'}}
70 |
71 |
78 |
79 |
80 |
83 |
84 |
85 |
92 |
93 |
94 |
103 |
104 |
105 |
109 | {$text(item.text)}
110 |
111 |
112 |
113 |
114 | {{/if}}
115 |
116 | {{/each}}
117 |
118 | {{if special.aside}}{{include './aside.htm'}}{{/if}}
119 |
120 | {{/block}}
121 |
122 | {{block 'js'}}
123 | {{if ~special_modules.indexOf('markdown')}}
124 |
125 |
138 | {{/if}}
139 | {{if ~special_modules.indexOf('map')}}
140 |
141 |
142 |
143 |
144 | {{/if}}
145 | {{/block}}
--------------------------------------------------------------------------------
/public/static/admin/layui/css/modules/laydate/default/laydate.css:
--------------------------------------------------------------------------------
1 | .laydate-set-ym,.layui-laydate,.layui-laydate *,.layui-laydate-list{box-sizing:border-box}html #layuicss-laydate{display:none;position:absolute;width:1989px}.layui-laydate *{margin:0;padding:0}.layui-laydate{position:absolute;z-index:66666666;margin:5px 0;border-radius:2px;font-size:14px;-webkit-animation-duration:.2s;animation-duration:.2s;-webkit-animation-fill-mode:both;animation-fill-mode:both;animation-name:laydate-downbit}.layui-laydate-main{width:272px}.layui-laydate-content td,.layui-laydate-header *,.layui-laydate-list li{transition-duration:.3s;-webkit-transition-duration:.3s}@keyframes laydate-downbit{0%{opacity:.3;transform:translate3d(0,-5px,0)}100%{opacity:1;transform:translate3d(0,0,0)}}.layui-laydate-static{position:relative;z-index:0;display:inline-block;margin:0;-webkit-animation:none;animation:none}.laydate-ym-show .laydate-next-m,.laydate-ym-show .laydate-prev-m{display:none!important}.laydate-ym-show .laydate-next-y,.laydate-ym-show .laydate-prev-y{display:inline-block!important}.laydate-time-show .laydate-set-ym span[lay-type=month],.laydate-time-show .laydate-set-ym span[lay-type=year],.laydate-time-show .layui-laydate-header .layui-icon,.laydate-ym-show .laydate-set-ym span[lay-type=month]{display:none!important}.layui-laydate-header{position:relative;line-height:30px;padding:10px 70px 5px}.layui-laydate-header *{display:inline-block;vertical-align:bottom}.layui-laydate-header i{position:absolute;top:10px;padding:0 5px;color:#999;font-size:18px;cursor:pointer}.layui-laydate-header i.laydate-prev-y{left:15px}.layui-laydate-header i.laydate-prev-m{left:45px}.layui-laydate-header i.laydate-next-y{right:15px}.layui-laydate-header i.laydate-next-m{right:45px}.laydate-set-ym{width:100%;text-align:center;text-overflow:ellipsis;overflow:hidden;white-space:nowrap}.laydate-set-ym span{padding:0 10px;cursor:pointer}.laydate-time-text{cursor:default!important}.layui-laydate-content{position:relative;padding:10px;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none}.layui-laydate-content table{border-collapse:collapse;border-spacing:0}.layui-laydate-content td,.layui-laydate-content th{width:36px;height:30px;padding:5px;text-align:center}.layui-laydate-content td{position:relative;cursor:pointer}.laydate-day-mark{position:absolute;left:0;top:0;width:100%;line-height:30px;font-size:12px;overflow:hidden}.laydate-day-mark::after{position:absolute;content:'';right:2px;top:2px;width:5px;height:5px;border-radius:50%}.layui-laydate-footer{position:relative;height:46px;line-height:26px;padding:10px}.layui-laydate-footer span{display:inline-block;vertical-align:top;height:26px;line-height:24px;padding:0 10px;border:1px solid #C9C9C9;border-radius:2px;background-color:#fff;font-size:12px;cursor:pointer;white-space:nowrap;transition:all .3s}.layui-laydate-list>li,.layui-laydate-range .layui-laydate-main{display:inline-block;vertical-align:middle}.layui-laydate-footer span:hover{color:#5FB878}.layui-laydate-footer span.layui-laydate-preview{cursor:default;border-color:transparent!important}.layui-laydate-footer span.layui-laydate-preview:hover{color:#666}.layui-laydate-footer span:first-child.layui-laydate-preview{padding-left:0}.laydate-footer-btns{position:absolute;right:10px;top:10px}.laydate-footer-btns span{margin:0 0 0 -1px}.layui-laydate-list{position:absolute;left:0;top:0;width:100%;height:100%;padding:10px;background-color:#fff}.layui-laydate-list>li{position:relative;width:33.3%;height:36px;line-height:36px;margin:3px 0;text-align:center;cursor:pointer}.laydate-month-list>li{width:25%;margin:17px 0}.laydate-time-list>li{height:100%;margin:0;line-height:normal;cursor:default}.laydate-time-list p{position:relative;top:-4px;line-height:29px}.laydate-time-list ol{height:181px;overflow:hidden}.laydate-time-list>li:hover ol{overflow-y:auto}.laydate-time-list ol li{width:130%;padding-left:33px;height:30px;line-height:30px;text-align:left;cursor:pointer}.layui-laydate-hint{position:absolute;top:115px;left:50%;width:250px;margin-left:-125px;line-height:20px;padding:15px;text-align:center;font-size:12px}.layui-laydate-range{width:546px}.layui-laydate-range .laydate-main-list-1 .layui-laydate-content,.layui-laydate-range .laydate-main-list-1 .layui-laydate-header{border-left:1px solid #e2e2e2}.layui-laydate,.layui-laydate-hint{border:1px solid #d2d2d2;box-shadow:0 2px 4px rgba(0,0,0,.12);background-color:#fff;color:#666}.layui-laydate-header{border-bottom:1px solid #e2e2e2}.layui-laydate-header i:hover,.layui-laydate-header span:hover{color:#5FB878}.layui-laydate-content{border-top:none 0;border-bottom:none 0}.layui-laydate-content th{font-weight:400;color:#333}.layui-laydate-content td{color:#666}.layui-laydate-content td.laydate-selected{background-color:#B5FFF8}.laydate-selected:hover{background-color:#00F7DE!important}.layui-laydate-content td:hover,.layui-laydate-list li:hover{background-color:#eee;color:#333}.laydate-time-list li ol{margin:0;padding:0;border:1px solid #e2e2e2;border-left-width:0}.laydate-time-list li:first-child ol{border-left-width:1px}.laydate-time-list>li:hover{background:0 0}.layui-laydate-content .laydate-day-next,.layui-laydate-content .laydate-day-prev{color:#d2d2d2}.laydate-selected.laydate-day-next,.laydate-selected.laydate-day-prev{background-color:#f8f8f8!important}.layui-laydate-footer{border-top:1px solid #e2e2e2}.layui-laydate-hint{color:#FF5722}.laydate-day-mark::after{background-color:#5FB878}.layui-laydate-content td.layui-this .laydate-day-mark::after{display:none}.layui-laydate-footer span[lay-type=date]{color:#5FB878}.layui-laydate .layui-this{background-color:#009688!important;color:#fff!important}.layui-laydate .laydate-disabled,.layui-laydate .laydate-disabled:hover{background:0 0!important;color:#d2d2d2!important;cursor:not-allowed!important;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none}.laydate-theme-molv{border:none}.laydate-theme-molv.layui-laydate-range{width:548px}.laydate-theme-molv .layui-laydate-main{width:274px}.laydate-theme-molv .layui-laydate-header{border:none;background-color:#009688}.laydate-theme-molv .layui-laydate-header i,.laydate-theme-molv .layui-laydate-header span{color:#f6f6f6}.laydate-theme-molv .layui-laydate-header i:hover,.laydate-theme-molv .layui-laydate-header span:hover{color:#fff}.laydate-theme-molv .layui-laydate-content{border:1px solid #e2e2e2;border-top:none;border-bottom:none}.laydate-theme-molv .laydate-main-list-1 .layui-laydate-content{border-left:none}.laydate-theme-grid .laydate-month-list>li,.laydate-theme-grid .laydate-year-list>li,.laydate-theme-grid .layui-laydate-content td,.laydate-theme-grid .layui-laydate-content thead,.laydate-theme-molv .layui-laydate-footer{border:1px solid #e2e2e2}.laydate-theme-grid .laydate-selected,.laydate-theme-grid .laydate-selected:hover{background-color:#f2f2f2!important;color:#009688!important}.laydate-theme-grid .laydate-selected.laydate-day-next,.laydate-theme-grid .laydate-selected.laydate-day-prev{color:#d2d2d2!important}.laydate-theme-grid .laydate-month-list,.laydate-theme-grid .laydate-year-list{margin:1px 0 0 1px}.laydate-theme-grid .laydate-month-list>li,.laydate-theme-grid .laydate-year-list>li{margin:0 -1px -1px 0}.laydate-theme-grid .laydate-year-list>li{height:43px;line-height:43px}.laydate-theme-grid .laydate-month-list>li{height:71px;line-height:71px}
--------------------------------------------------------------------------------
评论
46 | 61 |