├── 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 |
5 | 6 |
7 |
8 | 9 |
10 | 11 |
12 |
13 |
14 | 15 |
16 | 17 |
18 |
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 |
5 | {{title}} 6 |
7 | 8 |
9 |
10 | 11 |
12 | 13 |
14 |
15 |
16 | 17 |
18 | 19 |
20 |
21 |
22 | 23 | 没账号?去注册 24 |
25 |
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 |

{{item.title}}

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 |

[{{item.cate_name}}] {{item.title}}

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 |

[{{item.cate_name}}] {{item.title}}

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 |
36 |
37 | 38 |
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 |
5 |
6 | 新增 7 |
8 |
9 |
10 | 11 | 12 | 13 | 14 | 15 | {{each list item}} 16 | 17 | 18 | 19 | 20 | 21 | 22 | {{/each}} 23 | 24 |
ID昵称本名账号操作
{{item.id}}{{item.uname}}{{item.true_name}}{{item.email}}编辑删除
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 |
5 |
6 | 新增 7 |
8 |
9 |
10 | 11 | 12 | 13 | 14 | 15 | {{each list item}} 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | {{/each}} 24 | 25 |
ID分类名字目录地址排序显示操作
{{item.id}}{{item.cate_name}}{{item.cate_dir}}{{item.sort}}{{item.is_show ? '显示' : '隐藏'}}编辑删除
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 |
5 | 6 |
7 |
8 | 9 |
10 | 15 |
16 |
17 |
18 | 19 |
20 | 21 |
22 |
23 |
24 | 25 |
26 | 27 |
28 |
29 |
30 | 31 |
32 | 33 |
34 |
35 |
36 | 37 |
38 | 39 |
40 |
41 |
42 | 43 |
44 | 45 |
46 |
47 | {{/block}} 48 | 49 | {{block 'js'}} 50 | 56 | {{/block}} -------------------------------------------------------------------------------- /admin/view/user_form.htm: -------------------------------------------------------------------------------- 1 | {{extend './layout.htm'}} 2 | 3 | {{block 'main'}} 4 |
5 | 6 |
7 |
8 | 9 |
10 | 11 |
12 |
13 |
14 | 15 |
16 | 17 |
18 |
19 |
20 | 21 |
22 | 23 |
24 |
25 |
26 | 27 |
28 | 29 |
30 |
31 |
32 | 33 |
34 | 35 |
36 |
37 |
38 | 39 |
40 | 41 |
42 |
43 | {{/block}} 44 | 45 | {{block 'js'}} 46 | 60 | {{/block}} -------------------------------------------------------------------------------- /admin/view/comment_index.htm: -------------------------------------------------------------------------------- 1 | {{extend './layout.htm'}} 2 | 3 | {{block 'main'}} 4 |
5 |
6 | 7 |
8 |
9 |
10 | 11 |
12 |
13 |
14 | 15 | 16 | 17 | 18 | 19 | {{each list item}} 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {{/each}} 31 | 32 |
ID评论内容所属文章昵称邮箱主页IP时间操作
{{item.id}}{{item.pid ? '回复:' : '评论:'}}{{item.content}}{{item.title}}{{item.uname}}{{item.email}}{{item.url}}{{item.ip}}{{item.add_time | dateFormat 'YYYY-mm-dd HH:ii'}}删除
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 |
5 | 6 |
7 |
8 | 9 |
10 | 11 |
12 |
13 |
14 | 15 |
16 | 17 |
18 |
19 |
20 | 21 |
22 | 23 |
24 |
25 |
26 | 27 |
28 | 29 |
30 |
31 |
32 | 33 |
34 | 35 |
36 |
37 | 38 |
39 | 40 |
41 | 42 |
43 |
44 | 45 |
46 | 47 |
48 | 49 |
50 |
51 |
52 | 53 |
54 | 55 |
56 |
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 |
32 | 68 |
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 |
14 |
15 | 16 |
17 |
18 |
19 | 20 |
21 |
22 |
23 | 新增 24 |
25 |
26 |
27 | 28 | 29 | 30 | 31 | 32 | {{each list item}} 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | {{/each}} 45 | 46 |
ID标题短标题点击目录地址排序顶部展示侧边栏时间操作
{{item.id}}{{@item.thumb && ''}}{{item.title}}{{item.short_title}}{{item.click}}{{item.special_dir}}{{item.sort}}{{item.flag == 1 ? '展示' : '隐藏'}}{{item.aside ? '显示' : '隐藏'}}{{item.add_time | dateFormat 'YYYY-mm-dd HH:ii'}}编辑删除
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 |
12 | 13 |
14 |
15 | 16 |
17 | 18 |
19 |
20 |
21 | 22 |
23 | 24 |
25 |
26 |
27 | 28 |
29 | 30 |
31 |
32 |
33 | 34 |
35 | 36 | 37 | 38 |
39 |
40 |
41 | 42 |
43 | 44 |
45 |
46 |
47 | 48 |
49 | 50 |
51 |
52 |
53 | 54 |
55 | 56 |
57 |
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 |
21 |
22 | 27 |
28 |
29 |
30 | 31 |
32 |
33 |
34 | 新增 35 |
36 |
37 |
38 | 39 | 40 | 41 | 42 | 43 | {{each list item}} 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | {{/each}} 52 | 53 |
ID链接文字图标链接排序操作
{{item.id}}<%for (var i=1;i    <%}%>├─ {{item.title}}{{@~item.icon.indexOf('/') ? '' : ~item.icon.indexOf('layui-icon') ? '' : item.icon}}{{item.url}}{{item.sort}}新增编辑删除
54 |
55 | {{/block}} 56 | 57 | {{block 'js'}} 58 | 82 | {{/block}} -------------------------------------------------------------------------------- /admin/view/components/form-map.htm: -------------------------------------------------------------------------------- 1 | 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # melog 2 | 3 | ![melog](https://me.i-i.me/static/images/melog_360.png "melog") 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 | 36 | {{/if}} 37 |
38 | 42 | {{if is_comment}} 43 |
44 |
45 |

评论

46 |
47 | 48 | 49 |
50 | 51 | 52 | 53 |
54 |
55 | 56 |
57 |
58 | 提交取消回复 59 |
60 |
61 |
62 |
63 |
[face]@
64 |
65 |
[uname]  [add_time]
66 |
[content]
67 | [reply] 68 |
69 |
70 |
71 |
加载更多评论
72 |
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 |
25 | 26 |
27 |
36 | 37 |
38 |
39 |
40 |
41 | 42 | 43 |
44 | 45 |
46 | 47 | 48 | 49 | 50 | 51 | 52 | 启用 53 | 禁用 54 | 55 | 56 |
57 |
58 | 保存 59 | 删除 60 |
61 |
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 |
84 | 99 |
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 |
14 |
15 | 20 |
21 |
22 |
23 | 24 |
25 |
26 |
27 | 28 |
29 |
30 |
31 | 新增 32 |
33 |
34 |
35 | 36 | 37 | 38 | 39 | 40 | {{each list item}} 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | {{/each}} 49 | 50 |
ID标题栏目点击时间操作
{{item.id}}{{@item.thumb && ''}}{{item.title}}{{item.cate_name}}{{item.click}}{{item.add_time | dateFormat 'YYYY-mm-dd HH:ii'}}编辑删除
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 |
66 |
67 |
数据库类型
mysql
68 |
69 |
70 |
数据库主机
71 |
72 |
73 |
数据库用户
74 |
75 |
76 |
数据库密码
77 |
78 |
79 |
数据库名字
80 |
81 |
82 |
数据库端口
83 |
84 |
85 |
安装
86 |
87 |
88 |
89 |
90 |
91 |
92 | 93 |
94 |
95 |

{{description}}

96 |
97 |
98 | © 2020 Powered by Melog · {{config.VERSION}} · {{new Date() - config.APP_TIME}}ms 99 |
100 |
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 |
37 |
38 | 39 |
40 |
41 |
42 | 43 |
44 |
45 |
46 | 47 |
48 | 49 |
50 |
51 | 52 | 53 | 54 | 55 | 56 | {{each list item}} 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | {{/each}} 67 | 68 |
ID图片文件名字类型大小原始文件时间操作
{{item.id}}
{{item.title}}
{{item.extname}}{{item.size_text}}{{item.origin_size_text}}{{if item.origin_size_text}} (view){{/if}}{{item.add_time | dateFormat 'YYYY-mm-dd HH:ii'}}编辑删除
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 |

{{item.title}}

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 |

{{item.title}}

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 | 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} --------------------------------------------------------------------------------