├── .env.production ├── .env.development ├── .browserslistrc ├── .gitignore ├── public ├── my.jpg ├── index.jpg ├── label.jpg ├── detail.jpg ├── message.jpg ├── wallBlog.ico └── wall-blog-h5.png ├── server ├── index.js ├── models │ ├── label.js │ ├── user.js │ ├── blog.js │ └── message.js ├── app.js ├── middleware │ ├── func │ │ ├── get_info.js │ │ ├── index.js │ │ ├── file.js │ │ └── db.js │ ├── log │ │ ├── index.js │ │ ├── access.js │ │ └── log.js │ ├── send │ │ └── index.js │ ├── auth │ │ └── index.js │ ├── rule │ │ └── index.js │ └── index.js ├── .babelrc ├── mongodb.js ├── config.js ├── controller │ ├── client │ │ ├── label.js │ │ ├── message.js │ │ └── blog.js │ └── admin │ │ ├── upload.js │ │ ├── message.js │ │ ├── label.js │ │ ├── blog.js │ │ └── user.js ├── package.json └── router │ └── index.js ├── src ├── assets │ ├── bg4.jpg │ ├── avatar.png │ ├── cloud-left.png │ ├── none-data.jpg │ └── grass-confetti.png ├── components │ ├── tabbar.scss │ ├── noData.tsx │ ├── svgIcon.tsx │ └── tabbar.tsx ├── styles │ ├── index.scss │ ├── common │ │ ├── theme.scss │ │ ├── common.scss │ │ ├── iphone_x.scss │ │ ├── mixin.scss │ │ └── markdown.scss │ └── init.scss ├── store │ ├── index.ts │ └── modules │ │ └── label.ts ├── api │ ├── label.ts │ ├── blog.ts │ └── message.ts ├── views │ ├── home │ │ ├── index.vue │ │ └── components │ │ │ ├── intro.vue │ │ │ └── list.vue │ ├── message │ │ ├── components │ │ │ ├── commentList.vue │ │ │ ├── replyItem.vue │ │ │ ├── commentEditor.vue │ │ │ └── commentItem.vue │ │ └── index.vue │ ├── label │ │ ├── components │ │ │ └── labelSelect.vue │ │ └── index.vue │ ├── myself │ │ └── index.vue │ └── article │ │ └── detail.vue ├── main.ts ├── plugins │ └── vant.ts ├── utils │ ├── base.ts │ └── request.ts ├── App.vue ├── useMixin │ ├── useGetLabelColor.ts │ └── useClickLike.ts ├── env.d.ts ├── models │ └── index.ts ├── filters │ └── index.ts └── router │ └── index.ts ├── .editorconfig ├── postcss.config.js ├── index.html ├── package.json ├── tsconfig.json ├── vite.config.ts ├── README.md └── yarn.lock /.env.production: -------------------------------------------------------------------------------- 1 | # 网站前缀 2 | VITE_BASE_URL = / -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | # 网站前缀 2 | VITE_BASE_URL = / -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | -------------------------------------------------------------------------------- /public/my.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sujb-sus/vue3-vite2-ts-blog-h5/HEAD/public/my.jpg -------------------------------------------------------------------------------- /public/index.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sujb-sus/vue3-vite2-ts-blog-h5/HEAD/public/index.jpg -------------------------------------------------------------------------------- /public/label.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sujb-sus/vue3-vite2-ts-blog-h5/HEAD/public/label.jpg -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | require('babel-core/register') // babel编译 2 | module.exports = require('./app.js') -------------------------------------------------------------------------------- /public/detail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sujb-sus/vue3-vite2-ts-blog-h5/HEAD/public/detail.jpg -------------------------------------------------------------------------------- /public/message.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sujb-sus/vue3-vite2-ts-blog-h5/HEAD/public/message.jpg -------------------------------------------------------------------------------- /src/assets/bg4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sujb-sus/vue3-vite2-ts-blog-h5/HEAD/src/assets/bg4.jpg -------------------------------------------------------------------------------- /public/wallBlog.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sujb-sus/vue3-vite2-ts-blog-h5/HEAD/public/wallBlog.ico -------------------------------------------------------------------------------- /src/assets/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sujb-sus/vue3-vite2-ts-blog-h5/HEAD/src/assets/avatar.png -------------------------------------------------------------------------------- /public/wall-blog-h5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sujb-sus/vue3-vite2-ts-blog-h5/HEAD/public/wall-blog-h5.png -------------------------------------------------------------------------------- /src/assets/cloud-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sujb-sus/vue3-vite2-ts-blog-h5/HEAD/src/assets/cloud-left.png -------------------------------------------------------------------------------- /src/assets/none-data.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sujb-sus/vue3-vite2-ts-blog-h5/HEAD/src/assets/none-data.jpg -------------------------------------------------------------------------------- /src/assets/grass-confetti.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sujb-sus/vue3-vite2-ts-blog-h5/HEAD/src/assets/grass-confetti.png -------------------------------------------------------------------------------- /src/components/tabbar.scss: -------------------------------------------------------------------------------- 1 | @media #{$ipxMedia} { 2 | .van-tabbar { 3 | padding-bottom: $ipxBtm !important; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | -------------------------------------------------------------------------------- /src/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import './init.scss'; 2 | 3 | @import './common/theme.scss'; 4 | @import './common/mixin.scss'; 5 | @import './common/iphone_x.scss'; 6 | @import './common/markdown.scss'; 7 | @import './common/common.scss'; 8 | -------------------------------------------------------------------------------- /server/models/label.js: -------------------------------------------------------------------------------- 1 | import db from "../mongodb"; 2 | let labelSchema = db.Schema({ 3 | label: String, 4 | bgColor: String, 5 | createTime: { type: Date, default: Date.now }, 6 | }); 7 | export default db.model("label", labelSchema); 8 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from "vuex"; 2 | import label from "./modules/label"; 3 | 4 | export default createStore({ 5 | state: {}, 6 | mutations: {}, 7 | actions: {}, 8 | modules: { 9 | label, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /src/api/label.ts: -------------------------------------------------------------------------------- 1 | import axios from "@/utils/request"; 2 | 3 | /** 4 | * 获取标签列表 5 | * @param data 6 | * @returns {AxiosPromise} 7 | */ 8 | export const apiGetLabelList = (params?: object) => { 9 | return axios.get("/label/list", params); 10 | }; 11 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | "postcss-pxtorem": { 4 | rootValue: 37.5, 5 | propList: ["*"], 6 | }, 7 | autoprefixer: { 8 | overrideBrowserslist: ["Android 4.1", "iOS 7.1"], 9 | grid: true, 10 | }, 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /server/models/user.js: -------------------------------------------------------------------------------- 1 | import db from "../mongodb"; 2 | let userSchema = db.Schema({ 3 | username: String, 4 | pwd: String, 5 | avatar: String, 6 | roles: Array, 7 | createTime: { type: Date, default: Date.now }, 8 | loginTime: Date, 9 | }); 10 | export default db.model("user", userSchema); 11 | -------------------------------------------------------------------------------- /src/components/noData.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent } from "vue"; 2 | import img from "/src/assets/none-data.jpg"; 3 | 4 | export default defineComponent({ 5 | name: "noData", 6 | setup() { 7 | return () => ( 8 |
9 | 10 |
11 | ); 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /src/views/home/index.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 12 | 13 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import App from "./App.vue"; 3 | import router from "./router"; 4 | import store from "./store"; 5 | import vant from "./plugins/vant"; 6 | import "amfe-flexible"; 7 | 8 | const app = createApp(App); 9 | // 按需注入vant组件 10 | Object.values(vant).forEach((key) => app.use(key)); 11 | 12 | app.use(store).use(router).mount("#app"); 13 | -------------------------------------------------------------------------------- /src/plugins/vant.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Tabbar, 3 | TabbarItem, 4 | List, 5 | PullRefresh, 6 | Toast, 7 | Search, 8 | Tab, 9 | Tabs, 10 | Field, 11 | CellGroup, 12 | } from "vant"; 13 | 14 | export default { 15 | Tabbar, 16 | TabbarItem, 17 | List, 18 | PullRefresh, 19 | Toast, 20 | Search, 21 | Tab, 22 | Tabs, 23 | Field, 24 | CellGroup, 25 | }; 26 | -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | import Koa from 'koa' 2 | import ip from 'ip' 3 | import conf from './config' 4 | import router from './router' 5 | import middleware from './middleware' 6 | import './mongodb' 7 | 8 | const app = new Koa() 9 | middleware(app) 10 | router(app) 11 | app.listen(conf.port, '0.0.0.0', () => { 12 | console.log(`server is running at http://${ip.address()}:${conf.port}`) 13 | }) -------------------------------------------------------------------------------- /server/middleware/func/get_info.js: -------------------------------------------------------------------------------- 1 | export const get_client_ip = (ctx) => { 2 | return ( 3 | ctx.request.headers["x-forwarded-for"] || 4 | (ctx.request.connection && ctx.request.connection.remoteAddress) || 5 | ctx.request.socket.remoteAddress || 6 | (ctx.request.connection.socket && 7 | ctx.request.connection.socket.remoteAddress) || 8 | null 9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /src/utils/base.ts: -------------------------------------------------------------------------------- 1 | import { Toast } from "vant"; 2 | 3 | const showLoading = (message?: string) => { 4 | Toast.loading({ 5 | duration: 0, 6 | forbidClick: true, 7 | loadingType: "spinner", 8 | message: message, 9 | transition: "", 10 | }); 11 | }; 12 | 13 | const hideLoading = () => { 14 | Toast.clear(); 15 | }; 16 | 17 | export default { 18 | showLoading, 19 | hideLoading, 20 | }; 21 | -------------------------------------------------------------------------------- /server/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | [ 5 | "env", 6 | { 7 | "modules": false, 8 | "targets": { 9 | "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] 10 | } 11 | } 12 | ], 13 | "stage-2" 14 | ], 15 | "plugins": ["transform-runtime"], 16 | "env": { 17 | "test": { 18 | "presets": ["env", "stage-2"] 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /server/middleware/func/index.js: -------------------------------------------------------------------------------- 1 | import * as get_Info_func from "./get_info"; 2 | import * as db_func from "./db"; 3 | import * as file_func from "./file"; 4 | 5 | export default () => { 6 | const func = Object.assign({}, get_Info_func, db_func, file_func); 7 | return async (ctx, next) => { 8 | for (let v in func) { 9 | if (func.hasOwnProperty(v)) ctx[v] = func[v]; 10 | } 11 | await next(); 12 | }; 13 | }; 14 | -------------------------------------------------------------------------------- /server/mongodb.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import conf from "./config"; 3 | const DB_URL = `mongodb://${conf.mongodb.username}:${conf.mongodb.pwd}@${conf.mongodb.address}/${conf.mongodb.db}`; // 账号登陆 4 | mongoose.Promise = global.Promise; 5 | mongoose.connect(DB_URL, { useMongoClient: true }, (err) => { 6 | if (err) { 7 | console.log("数据库连接失败!"); 8 | } else { 9 | console.log("数据库连接成功!"); 10 | } 11 | }); 12 | export default mongoose; 13 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | wallBlog 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /server/middleware/log/index.js: -------------------------------------------------------------------------------- 1 | import logger from './log' 2 | 3 | export default opts => { 4 | let loggerMiddleware = logger(opts); 5 | return async (ctx, next) => { 6 | return loggerMiddleware(ctx, next) 7 | .catch( e => { 8 | if (ctx.status < 500) { 9 | ctx.status = 500; 10 | } 11 | ctx.log.error(e.stack); 12 | ctx.state.logged = true; 13 | ctx.throw(e); 14 | }) 15 | } 16 | } -------------------------------------------------------------------------------- /src/components/svgIcon.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent, computed } from 'vue'; 2 | 3 | export default defineComponent({ 4 | name: 'svgIcon', 5 | props: { 6 | name: { 7 | type: String, 8 | required: true, 9 | } 10 | }, 11 | setup(props) { 12 | const iconName = computed(() => `#${props.name}`); 13 | return () => ( 14 | 18 | ); 19 | }, 20 | }); 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 19 | 20 | -------------------------------------------------------------------------------- /src/styles/common/theme.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * 变量样式 3 | */ 4 | $mainColor: #009688; // 主色0b8df4 5 | $baseFontColor: #595959; //默认字色(正文、常规字色) 6 | $strongFontColor: #262626; //加深字色(标题、主要字色) 7 | $subFontColor: #8c8c8c; //辅助字色(描述、次要字色) 8 | $disableFontColor: #bfbfbf; //禁用、占位文字 9 | $baseLinkColor: #0b8df4; //link字体颜色 10 | 11 | $baseBorderColor: #ededed; //默认边框颜色 12 | $baseLineColor: #f0f0f0; // 分割线颜色 13 | $baseBackColor: #f5f5f5; //默认背景颜色 14 | 15 | $warningColor: #ff6c1a; //提醒 16 | $errorColor: #ff3838; //错误、禁用 17 | $successColor: #54b523; //成功色 18 | -------------------------------------------------------------------------------- /server/models/blog.js: -------------------------------------------------------------------------------- 1 | import db from "../mongodb"; 2 | let blogSchema = db.Schema({ 3 | type: Array, 4 | title: String, 5 | desc: String, 6 | fileCoverImgUrl: String, 7 | html: String, 8 | markdown: String, 9 | level: Number, 10 | github: String, 11 | auth: String, 12 | source: Number, 13 | isVisible: Boolean, 14 | releaseTime: String, 15 | pv: { type: Number, default: 0 }, 16 | likes: { type: Number, default: 0 }, 17 | comments: { type: Number, default: 0 }, 18 | }); 19 | export default db.model("blog", blogSchema); 20 | -------------------------------------------------------------------------------- /server/middleware/log/access.js: -------------------------------------------------------------------------------- 1 | export default (ctx, msg, commonInfo) => { 2 | const { 3 | method, // 请求方法 get post或其他 4 | url, // 请求链接 5 | host, // 发送请求的客户端的host 6 | headers // 请求中的headers 7 | } = ctx.request; 8 | const client = { 9 | method, 10 | url, 11 | host, 12 | msg, 13 | ip: ctx.get_client_ip(ctx), 14 | referer: headers['referer'], // 请求的源地址 15 | userAgent: headers['user-agent'] // 客户端信息 设备及浏览器信息 16 | } 17 | return JSON.stringify(Object.assign(commonInfo, client)); 18 | } -------------------------------------------------------------------------------- /server/models/message.js: -------------------------------------------------------------------------------- 1 | import db from "../mongodb"; 2 | let messageSchema = db.Schema({ 3 | content: String, 4 | headerColor: { type: String, default: "#ff6c1a" }, 5 | nickname: { type: String, default: "匿名网友" }, 6 | createTime: String, 7 | likes: { type: Number, default: 0 }, 8 | comments: { type: Number, default: 0 }, 9 | replyList: [ 10 | { 11 | replyHeaderColor: { type: String, default: "#009688" }, 12 | replyContent: String, 13 | replyUser: { type: String, default: "匿名网友" }, 14 | byReplyUser: String, 15 | replyTime: String, 16 | }, 17 | ], 18 | }); 19 | export default db.model("message", messageSchema); 20 | -------------------------------------------------------------------------------- /src/styles/common/common.scss: -------------------------------------------------------------------------------- 1 | // svg-icon 2 | :deep(svg) { 3 | &.icon { 4 | width: 1em; 5 | height: 1em; 6 | vertical-align: -0.15em; 7 | fill: currentColor; 8 | overflow: hidden; 9 | font-size: 16px; 10 | svg:not(:root) { 11 | overflow: hidden; 12 | } 13 | } 14 | } 15 | // no-data 16 | :deep(.no-data) { 17 | margin-top: 10vh; 18 | @include flex(); 19 | img { 20 | width: 80vw; 21 | } 22 | } 23 | // 公共类 24 | .app-container { 25 | padding-bottom: calc(50px + 16px); 26 | } 27 | // iPhone X系列底部加高 28 | @media #{$ipxMedia} { 29 | .app-container { 30 | padding-bottom: calc(50px + 34px) !important; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /server/config.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | const auth = { 3 | admin_secret: "admin-token", 4 | tokenKey: "Token-Auth", 5 | whiteList: ["login", "client_api"], 6 | blackList: ["admin_api"], 7 | }; 8 | 9 | const log = { 10 | logLevel: "debug", // 指定记录的日志级别 11 | dir: path.resolve(__dirname, "../../logs"), // 指定日志存放的目录名 12 | projectName: "blog", // 项目名,记录在日志中的项目信息 13 | ip: "0.0.0.0", // 默认情况下服务器 ip 地址 14 | }; 15 | const port = process.env.NODE_ENV === "production" ? "80" : "3000"; 16 | 17 | export default { 18 | env: process.env.NODE_ENV, 19 | port, 20 | auth, 21 | log, 22 | mongodb: { 23 | username: "wall", 24 | pwd: 123456, 25 | address: "127.0.0.1:27017", 26 | db: "wallBlog", 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /server/middleware/send/index.js: -------------------------------------------------------------------------------- 1 | export default () => { 2 | let render = (ctx) => { 3 | return (json, msg) => { 4 | ctx.set("Content-Type", "application/json"); 5 | ctx.body = JSON.stringify({ 6 | code: 1, 7 | data: json || {}, 8 | msg: msg || "success", 9 | }); 10 | }; 11 | }; 12 | let renderError = (ctx) => { 13 | return (msg) => { 14 | ctx.set("Content-Type", "application/json"); 15 | ctx.body = JSON.stringify({ 16 | code: 0, 17 | data: {}, 18 | msg: msg.toString(), 19 | }); 20 | }; 21 | }; 22 | return async (ctx, next) => { 23 | ctx.send = render(ctx); 24 | ctx.sendError = renderError(ctx); 25 | await next(); 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /server/middleware/auth/index.js: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | import conf from '../../config'; 3 | 4 | export default () => { 5 | return async (ctx, next) => { 6 | // 白名单就不需要走 jwt 鉴权 7 | if (!conf.auth.whiteList.some((v) => ctx.path.includes(v))) { 8 | let token = ctx.cookies.get(conf.auth.tokenKey); 9 | try { 10 | jwt.verify(token, conf.auth.admin_secret); 11 | } catch (e) { 12 | if ('TokenExpiredError' === e.name) { 13 | ctx.sendError('token已过期, 请重新登录!'); 14 | ctx.throw(401, 'token已过期, 请重新登录!'); 15 | } 16 | ctx.sendError('token验证失败, 请重新登录!'); 17 | ctx.throw(401, 'token验证失败, 请重新登录!'); 18 | } 19 | console.log('鉴权成功'); 20 | } 21 | await next(); 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /server/controller/client/label.js: -------------------------------------------------------------------------------- 1 | import labelModel from "../../models/label"; 2 | 3 | module.exports = { 4 | async list(ctx, next) { 5 | console.log( 6 | "----------------获取标签列表 label/list-----------------------" 7 | ); 8 | let { keyword, pageindex = 1, pagesize = 50 } = ctx.request.query; 9 | try { 10 | let reg = new RegExp(keyword, "i"); 11 | let data = await ctx.findPage( 12 | labelModel, 13 | { 14 | $or: [{ label: { $regex: reg } }, { bgColor: { $regex: reg } }], 15 | }, 16 | null, 17 | { limit: pagesize * 1, skip: (pageindex - 1) * pagesize } 18 | ); 19 | ctx.send(data); 20 | } catch (e) { 21 | console.log(e); 22 | ctx.sendError(e); 23 | } 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /src/useMixin/useGetLabelColor.ts: -------------------------------------------------------------------------------- 1 | import { useStore } from "vuex"; 2 | import { computed } from "vue"; 3 | 4 | /** 5 | * 封装获取标签背景色逻辑 6 | * @description 文章Item、文章详情Detail 7 | */ 8 | const useGetLabelColor = () => { 9 | const store = useStore(); 10 | const labelList = store.getters["label/labelList"]; 11 | 12 | const getLabelColor = computed(() => { 13 | return (labelName: string) => { 14 | if (labelList.length) { 15 | let labelIndex = labelList.findIndex( 16 | (item: { label: string }) => item.label === labelName 17 | ); 18 | return labelList[labelIndex].bgColor; 19 | } 20 | return "rgba(70, 70, 70, 0.9)"; 21 | }; 22 | }); 23 | 24 | return { 25 | getLabelColor, 26 | }; 27 | }; 28 | 29 | export default useGetLabelColor; 30 | -------------------------------------------------------------------------------- /src/api/blog.ts: -------------------------------------------------------------------------------- 1 | import axios from "@/utils/request"; 2 | /** 3 | * 获取博客列表 4 | * @param data 5 | * @returns {AxiosPromise} 6 | */ 7 | export const apiGetBlogList = (params?: object) => { 8 | return axios.get("/blog/list", params); 9 | }; 10 | /** 11 | * 获取博客详情 12 | * @param data 13 | * @returns {AxiosPromise} 14 | */ 15 | export const apiGetBlogDetail = (params?: object) => { 16 | return axios.get("/blog/info", params); 17 | }; 18 | /** 19 | * 点赞 20 | * @param data 21 | * @returns {AxiosPromise} 22 | */ 23 | export const apiUpdateLikes = (params: object) => { 24 | return axios.post("/blog/updateLikes", params); 25 | }; 26 | /** 27 | * 浏览量 28 | * @param data 29 | * @returns {AxiosPromise} 30 | */ 31 | export const apiUpdatePV = (params: object) => { 32 | return axios.post("/blog/updatePV", params); 33 | }; 34 | -------------------------------------------------------------------------------- /src/styles/common/iphone_x.scss: -------------------------------------------------------------------------------- 1 | //iphone X系列底部间距 2 | $ipxBtm: 34px; 3 | $ipxMedia: 'only screen and (min-height:804px) and (device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2), only screen and (min-height:720px) and (device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3), only screen and (min-height:804px) and (device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3)'; 4 | 5 | @supports (bottom: env(safe-area-inset-bottom)) { 6 | .btm-btn-bar-ipx { 7 | bottom: env(safe-area-inset-bottom) !important; 8 | &:after { 9 | content: ''; 10 | height: env(safe-area-inset-bottom); 11 | position: absolute; 12 | top: 100%; 13 | left: 0; 14 | right: 0; 15 | background-color: #fff; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/components/tabbar.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent } from "vue"; 2 | import "./tabbar.scss"; 3 | 4 | export default defineComponent({ 5 | name: "Tabbar", 6 | setup() { 7 | return () => ( 8 | <> 9 | 10 | 11 | 12 | 首页 13 | 14 | 15 | 标签 16 | 17 | 18 | 留言 19 | 20 | 21 | 关于我 22 | 23 | 24 | 25 | ); 26 | }, 27 | }); 28 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module "*.vue" { 4 | import { DefineComponent } from "vue"; 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types 6 | const component: DefineComponent<{}, {}, any>; 7 | export default component; 8 | } 9 | declare module "qs"; 10 | declare module "*.module.css" { 11 | const classes: { readonly [key: string]: string }; 12 | export default classes; 13 | } 14 | declare module "*.module.scss" { 15 | const classes: { readonly [key: string]: string }; 16 | export default classes; 17 | } 18 | declare module "*.jpg" { 19 | const src: string; 20 | export default src; 21 | } 22 | declare module "*.jpeg" { 23 | const src: string; 24 | export default src; 25 | } 26 | declare module "*.png" { 27 | const src: string; 28 | export default src; 29 | } 30 | -------------------------------------------------------------------------------- /server/controller/admin/upload.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | async uploadImage(ctx, next) { 5 | console.log("----------------添加图片 uploadImage-----------------------"); 6 | try { 7 | let opts = { 8 | path: path.resolve(__dirname, "../../../../public"), 9 | }; 10 | let result = await ctx.uploadFile(ctx, opts); 11 | ctx.send(result); 12 | } catch (e) { 13 | ctx.sendError(e); 14 | } 15 | }, 16 | async delUploadImage(ctx, next) { 17 | console.log( 18 | "----------------删除图片 delUploadImage-----------------------" 19 | ); 20 | let fileName = ctx.request.body.fileName; 21 | let fileCoverImgUrl = `public/images/${fileName}`; 22 | try { 23 | ctx.removeFile(fileCoverImgUrl); 24 | ctx.send(); 25 | } catch (e) { 26 | ctx.sendError(e); 27 | } 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /src/store/modules/label.ts: -------------------------------------------------------------------------------- 1 | import { apiGetLabelList } from "@/api/label"; 2 | import { labelModel } from "@/models/index"; 3 | 4 | type State = { 5 | labelList: Array; 6 | }; 7 | export default { 8 | namespaced: true, 9 | state: { 10 | labelList: null, 11 | }, 12 | mutations: { 13 | setLabelList(state: State, data: Array) { 14 | state.labelList = data; 15 | }, 16 | }, 17 | actions: { 18 | getLabelList(context: { commit: (arg0: string, arg1: any) => void }) { 19 | let params = { 20 | pageindex: 1, 21 | pagesize: 50, 22 | }; 23 | return apiGetLabelList(params) 24 | .then((res) => { 25 | context.commit("setLabelList", res?.data?.list); 26 | }) 27 | .catch((err) => { 28 | console.log(err); 29 | }); 30 | }, 31 | }, 32 | getters: { 33 | labelList(state: State) { 34 | return state.labelList; 35 | }, 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /src/styles/common/mixin.scss: -------------------------------------------------------------------------------- 1 | //flex布局 2 | @mixin flex($justify-content: center, $align-items: center, $direction: row) { 3 | display: flex; 4 | flex-direction: $direction; 5 | justify-content: $justify-content; 6 | align-items: $align-items; 7 | } 8 | // 单行文本 9 | @mixin singleText() { 10 | white-space: nowrap; 11 | text-overflow: ellipsis; 12 | overflow: hidden; 13 | } 14 | // 多行文本 15 | @mixin multiText($line) { 16 | display: -webkit-box; 17 | text-overflow: ellipsis; 18 | -webkit-line-clamp: $line; 19 | -webkit-box-orient: vertical; 20 | overflow: hidden; 21 | word-break: break-all; 22 | } 23 | // 0.5px border 24 | // 父元素需要添加 position: relative; 25 | @mixin borderZeroPointFive( 26 | $left: 0, 27 | $bottom: 0, 28 | $width: 100%, 29 | $borderColor: $baseBorderColor 30 | ) { 31 | content: ' '; 32 | position: absolute; 33 | left: $left; 34 | bottom: $bottom; 35 | width: $width; 36 | height: 1px; 37 | background-color: $borderColor; 38 | transform: scaleY(0.5); 39 | } 40 | -------------------------------------------------------------------------------- /src/models/index.ts: -------------------------------------------------------------------------------- 1 | interface articleModel { 2 | _id: string; 3 | title: string; 4 | releaseTime: string; 5 | pv: number; 6 | auth?: string; 7 | type: string[]; 8 | desc?: string; 9 | fileCoverImgUrl: string; 10 | html: string; 11 | markdown?: string; 12 | level?: number; 13 | github?: string; 14 | source?: number; 15 | isVisible: boolean; 16 | likes: number; 17 | comments?: number; 18 | } 19 | 20 | interface labelModel { 21 | _id: string; 22 | label: string; 23 | bgColor: string; 24 | createTime?: string; 25 | } 26 | 27 | type replyItem = { 28 | _id: string; 29 | replyHeaderColor?: string; 30 | replyContent?: string; 31 | replyUser: string; 32 | byReplyUser: string; 33 | replyTime: string; 34 | }; 35 | interface commentModel { 36 | _id: string; 37 | content: string; 38 | headerColor: string; 39 | nickname: string; 40 | createTime: string; 41 | likes: number; 42 | comments: number; 43 | replyList?: Array; 44 | } 45 | 46 | export type { articleModel, labelModel, commentModel, replyItem }; 47 | -------------------------------------------------------------------------------- /src/api/message.ts: -------------------------------------------------------------------------------- 1 | import axios from "@/utils/request"; 2 | 3 | /** 4 | * 获取留言列表 5 | * @param data 6 | * @returns {AxiosPromise} 7 | */ 8 | export const apiGetMessageList = (params?: object) => { 9 | return axios.get("/message/list", params); 10 | }; 11 | 12 | /** 13 | * 获取回复数量 14 | * @param data 15 | * @returns {AxiosPromise} 16 | */ 17 | export const apiGetReplyCount = (params?: object) => { 18 | return axios.get("/message/replyCount", params); 19 | }; 20 | 21 | /** 22 | * 添加留言 23 | * @param data 24 | * @returns {AxiosPromise} 25 | */ 26 | export const apiAddMessage = (params: object) => { 27 | return axios.post("/message/add", params); 28 | }; 29 | 30 | /** 31 | * 点赞 32 | * @param data 33 | * @returns {AxiosPromise} 34 | */ 35 | export const apiUpdateLikes = (params: object) => { 36 | return axios.post("/message/updateLikes", params); 37 | }; 38 | 39 | /** 40 | * 回复 41 | * @param data 42 | * @returns {AxiosPromise} 43 | */ 44 | export const apiUpdateReplys = (params: object) => { 45 | return axios.post("/message/updateReplys", params); 46 | }; 47 | -------------------------------------------------------------------------------- /server/middleware/rule/index.js: -------------------------------------------------------------------------------- 1 | import Path from "path"; 2 | import fs from "fs"; 3 | 4 | export default (opts) => { 5 | let { app, rules = [] } = opts; 6 | if (!app) { 7 | throw new Error("the app params is necessary!"); 8 | } 9 | 10 | app.router = {}; 11 | const appKeys = Object.keys(app); 12 | rules.forEach((item) => { 13 | let { path, name } = item; 14 | if (appKeys.includes(name)) { 15 | throw new Error(`the name of ${name} already exists!`); 16 | } 17 | let content = {}; 18 | //readdirSync: 方法将返回一个包含“指定目录下所有文件名称”的数组对象。 19 | //extname: 返回path路径文件扩展名,如果path以 ‘.' 为结尾,将返回 ‘.',如果无扩展名 又 不以'.'结尾,将返回空值。 20 | //basename: path.basename(p, [ext]) p->要处理的path ext->要过滤的字符 21 | fs.readdirSync(path).forEach((filename) => { 22 | let extname = Path.extname(filename); 23 | if (extname === ".js") { 24 | let name = Path.basename(filename, extname); 25 | content[name] = require(Path.join(path, filename)); 26 | content[name].filename = name; 27 | } 28 | }); 29 | app[name] = content; 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /src/useMixin/useClickLike.ts: -------------------------------------------------------------------------------- 1 | import { ref, computed } from "vue"; 2 | import { Toast } from "vant"; 3 | 4 | /** 5 | * 封装点赞逻辑 6 | * @requestApi api请求的path 7 | * @description 点赞文章、留言 8 | */ 9 | const useClickLike = (requestApi: Function) => { 10 | let likeList = ref([]); // 点过赞列表 11 | 12 | // 获取点赞数 13 | const getLikesNumber = computed( 14 | () => (id: string, likes: number) => 15 | likeList.value.includes(id) ? likes + 1 : likes 16 | ); 17 | // 点赞高亮 18 | const getLikesColor = computed( 19 | () => (id: string) => likeList.value.includes(id) 20 | ); 21 | // 点赞事件 22 | const handleLikes = (id: string) => { 23 | return requestApi({ _id: id, isLike: likeList.value.includes(id) }) 24 | .then(() => { 25 | likeList.value.includes(id) 26 | ? likeList.value.splice(likeList.value.indexOf(id), 1) 27 | : likeList.value.push(id); 28 | }) 29 | .catch((err: any) => { 30 | Toast("点赞失败"); 31 | console.log(err); 32 | }); 33 | }; 34 | 35 | return { 36 | getLikesNumber, 37 | getLikesColor, 38 | handleLikes, 39 | likeList, 40 | }; 41 | }; 42 | 43 | export default useClickLike; 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue3-vite-blog-h5", 3 | "version": "1.0.0", 4 | "description": "A Vite2.x + Vue3 + TypeScript simple mobile blog", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vue-tsc --noEmit && vite build", 8 | "serve": "vite preview" 9 | }, 10 | "dependencies": { 11 | "amfe-flexible": "^2.2.1", 12 | "axios": "^0.21.2", 13 | "postcss-pxtorem": "5.1.1", 14 | "qs": "^6.10.1", 15 | "vant": "^3.2.2", 16 | "vue": "^3.2.6", 17 | "vue-router": "^4.0.0-0", 18 | "vuex": "^4.0.0-0" 19 | }, 20 | "devDependencies": { 21 | "@types/node": "^16.7.10", 22 | "@vitejs/plugin-legacy": "^1.5.2", 23 | "@vitejs/plugin-vue": "^1.6.0", 24 | "@vitejs/plugin-vue-jsx": "^1.1.7", 25 | "@vue/compiler-sfc": "^3.2.6", 26 | "autoprefixer": "^10.3.4", 27 | "postcss": "^8.3.6", 28 | "sass": "^1.39.0", 29 | "sass-loader": "^12.1.0", 30 | "typescript": "^4.3.2", 31 | "vite": "^2.5.2", 32 | "vite-plugin-style-import": "^1.2.1", 33 | "vue-tsc": "^0.2.2" 34 | }, 35 | "keywords": [ 36 | "Vite", 37 | "Vue3", 38 | "TypeScript", 39 | "Vant", 40 | "Blog" 41 | ], 42 | "author": "wall", 43 | "license": "MIT" 44 | } 45 | -------------------------------------------------------------------------------- /src/filters/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 时间日期格式化 3 | * 用法 formatTime(new Date(), 'yyyy-MM-dd hh:mm:ss') 4 | * @param time 5 | * @param fmt 6 | */ 7 | export const formatTime = (time: any, fmt: string) => { 8 | time = parseInt(time); 9 | if (!time) { 10 | return ""; 11 | } 12 | const date = new Date(time); 13 | let o: object = { 14 | "M+": date.getMonth() + 1, // 月份 15 | "d+": date.getDate(), // 日 16 | "h+": date.getHours(), // 小时 17 | "m+": date.getMinutes(), // 分 18 | "s+": date.getSeconds(), // 秒 19 | "q+": Math.floor((date.getMonth() + 3) / 3), // 季度 20 | S: date.getMilliseconds(), // 毫秒 21 | }; 22 | if (/(y+)/.test(fmt)) { 23 | fmt = fmt.replace( 24 | RegExp.$1, 25 | (date.getFullYear() + "").substr(4 - RegExp.$1.length) 26 | ); 27 | } 28 | for (let k in o) { 29 | if (new RegExp("(" + k + ")").test(fmt)) { 30 | fmt = fmt.replace( 31 | RegExp.$1, 32 | RegExp.$1.length === 1 ? o[k] : ("00" + o[k]).substr(("" + o[k]).length) 33 | ); 34 | } 35 | } 36 | return fmt; 37 | }; 38 | 39 | /** 40 | * 数字转成 k、w 方式 41 | * @param num 42 | */ 43 | export const formatNumber = (num: number) => { 44 | return num >= 1e3 && num < 1e4 45 | ? (num / 1e3).toFixed(1) + "k" 46 | : num >= 1e4 47 | ? (num / 1e4).toFixed(1) + "w" 48 | : num; 49 | }; 50 | -------------------------------------------------------------------------------- /src/views/message/components/commentList.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 36 | 37 | -------------------------------------------------------------------------------- /server/middleware/index.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import bodyParser from "koa-bodyparser"; 3 | import staticFiles from "koa-static"; 4 | import Rule from "./rule"; 5 | import Send from "./send"; 6 | import Auth from "./auth"; 7 | import Log from "./log"; 8 | import Func from "./func"; 9 | 10 | export default (app) => { 11 | //缓存拦截器 12 | app.use(async (ctx, next) => { 13 | if (ctx.url == "/favicon.ico") return; 14 | 15 | await next(); 16 | ctx.status = 200; 17 | ctx.set("Cache-Control", "must-revalidation"); 18 | if (ctx.fresh) { 19 | ctx.status = 304; 20 | return; 21 | } 22 | }); 23 | 24 | // 日志中间件 25 | app.use(Log()); 26 | 27 | // 数据返回的封装 28 | app.use(Send()); 29 | 30 | // 方法封装 31 | app.use(Func()); 32 | 33 | //权限中间件 34 | app.use(Auth()); 35 | 36 | //post请求中间件 37 | app.use(bodyParser()); 38 | 39 | //静态文件中间件 40 | app.use(staticFiles(path.resolve(__dirname, "../../../public"))); 41 | 42 | // 规则中间件 43 | Rule({ 44 | app, 45 | rules: [ 46 | { 47 | path: path.join(__dirname, "../controller/admin"), 48 | name: "admin", 49 | }, 50 | { 51 | path: path.join(__dirname, "../controller/client"), 52 | name: "client", 53 | }, 54 | ], 55 | }); 56 | 57 | // 增加错误的监听处理 58 | app.on("error", (err, ctx) => { 59 | if (ctx && !ctx.headerSent && ctx.status < 500) { 60 | ctx.status = 500; 61 | } 62 | if (ctx && ctx.log && ctx.log.error) { 63 | if (!ctx.state.logged) { 64 | ctx.log.error(err.stack); 65 | } 66 | } 67 | }); 68 | }; 69 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", // 指定ECMAScript目标版本 4 | "module": "esnext", // 指定生成哪个模块系统代码 5 | "moduleResolution": "node", // 决定如何处理模块 6 | "jsx": "preserve", // 在 .tsx文件里支持JSX 7 | "baseUrl": ".", // 解析非相对模块名的基准目录 8 | "allowJs": true, // 允许编译javascript文件 9 | "allowSyntheticDefaultImports": true, // 允许从没有设置默认导出的模块中默认导入 10 | "allowUnreachableCode": true, // 不报告执行不到的代码错误 11 | "allowUnusedLabels": true, // 不报告未使用的标签错误 12 | "esModuleInterop": true, // 支持用引入commonjs规范的包 13 | "experimentalDecorators": true, // 启用实验性的ES装饰器 14 | "strict": true, // 启用所有严格类型检查选项 15 | "strictFunctionTypes": false, // 禁用函数参数双向协变检查 16 | "skipLibCheck": true, // 忽略所有的声明文件( *.d.ts)的类型检查 17 | "noUnusedLocals": false, // 若有未使用的局部变量则抛错 18 | "noUnusedParameters": true, // 若有未使用的参数则抛错 19 | "isolatedModules": true, // 将每个文件作为单独的模块 20 | "resolveJsonModule": true, // 允许直接导入JSON文件的 21 | "sourceMap": true, 22 | "suppressImplicitAnyIndexErrors": true, // 阻止 --noImplicitAny对缺少索引签名的索引对象报错 23 | "forceConsistentCasingInFileNames": true, // 禁止对同一个文件的不一致的引用 24 | "typeRoots": ["./node_modules/@types/", "./types"], 25 | "types": ["vite/client"], // 要包含的类型声明文件名 26 | "paths": { 27 | "@/*": ["src/*"] 28 | }, 29 | "lib": ["esnext", "dom", "dom.iterable", "scripthost"] // 编译过程中需要引入的库文件 30 | }, 31 | "include": [ 32 | "src/**/*.ts", 33 | "src/**/*.js", 34 | "src/**/*.d.ts", 35 | "src/**/*.tsx", 36 | "src/**/*.jsx", 37 | "src/**/*.vue" 38 | ], 39 | "exclude": ["dist", "node_modules"] 40 | } 41 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wall-blog", 3 | "version": "1.0.0", 4 | "author": "wall", 5 | "description": "A blog website sharing front-end technology", 6 | "keywords": [ 7 | "vue", 8 | "node", 9 | "koa", 10 | "mongodb" 11 | ], 12 | "main": "index.js", 13 | "scripts": { 14 | "server": "cross-env NODE_ENV=development nodemon index.js", 15 | "start": "pm2 start index.js", 16 | "stop": "pm2 stop index.js", 17 | "restart": "pm2 restart index.js" 18 | }, 19 | "devDependencies": { 20 | "babel-core": "^6.26.0", 21 | "babel-loader": "^7.1.2", 22 | "babel-plugin-transform-runtime": "^6.23.0", 23 | "babel-polyfill": "^6.26.0", 24 | "babel-preset-env": "^1.6.1", 25 | "babel-preset-es2015": "^6.24.1", 26 | "babel-preset-stage-2": "^6.24.1", 27 | "babel-register": "^6.26.0", 28 | "chalk": "^2.3.0", 29 | "cross-env": "^5.1.3", 30 | "node-notifier": "^5.1.2", 31 | "nodemon": "^1.12.1", 32 | "ora": "^1.3.0", 33 | "rimraf": "^2.6.2" 34 | }, 35 | "dependencies": { 36 | "axios": "^0.17.0", 37 | "babel-polyfill": "^6.26.0", 38 | "busboy": "^0.2.14", 39 | "clipboard": "^2.0.8", 40 | "highlight.js": "^10.7.0", 41 | "ip": "^1.1.5", 42 | "js-md5": "^0.7.3", 43 | "jsonwebtoken": "^8.1.0", 44 | "koa": "^2.4.1", 45 | "koa-bodyparser": "^4.2.0", 46 | "koa-router": "^7.3.0", 47 | "koa-static": "^4.0.2", 48 | "log4js": "^2.4.1", 49 | "marked": "^0.3.12", 50 | "mongoose": "^4.13.9", 51 | "nprogress": "^0.2.0", 52 | "qs": "^6.5.1" 53 | }, 54 | "license": "ISC" 55 | } 56 | -------------------------------------------------------------------------------- /src/utils/request.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import qs from "qs"; 3 | import { Toast } from "vant"; 4 | 5 | interface responseStatus { 6 | readonly status: number; 7 | data: any; 8 | statusText: string; 9 | } 10 | interface responseCode { 11 | readonly code: number; 12 | data: any; 13 | msg: string; 14 | } 15 | 16 | axios.defaults.withCredentials = true; 17 | // 发送时 18 | axios.interceptors.request.use( 19 | (config) => config, 20 | (err) => Promise.reject(err) 21 | ); 22 | 23 | // 响应时 24 | axios.interceptors.response.use( 25 | (response) => response, 26 | (err) => Promise.resolve(err.response) 27 | ); 28 | 29 | // 检查状态码 30 | const checkStatus = (res: responseStatus) => { 31 | if (res.status === 200 || res.status === 304) { 32 | return res.data; 33 | } 34 | return { 35 | code: 0, 36 | msg: res.statusText, 37 | data: res.statusText, 38 | }; 39 | }; 40 | 41 | // 检查CODE值 42 | const checkCode = (res: responseCode) => { 43 | if (res.code === 0) { 44 | Toast.fail({ 45 | message: res.msg, 46 | duration: 2 * 1000, 47 | }); 48 | throw new Error(res.msg); 49 | } 50 | return res; 51 | }; 52 | 53 | const prefix = "/client_api"; 54 | export default { 55 | get(url: string, params?: object) { 56 | return axios({ 57 | method: "get", 58 | url: prefix + url, 59 | params, 60 | timeout: 30000, 61 | }) 62 | .then(checkStatus) 63 | .then(checkCode); 64 | }, 65 | post(url: string, data: object) { 66 | return axios({ 67 | method: "post", 68 | url: prefix + url, 69 | data: qs.stringify(data), 70 | timeout: 30000, 71 | }) 72 | .then(checkStatus) 73 | .then(checkCode); 74 | }, 75 | }; 76 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory, RouteRecordRaw } from "vue-router"; 2 | import Tabbar from "../components/tabbar"; 3 | // 先识别所有的views/文件夹name/*.vue文件 4 | // 这里限制性很高,只有路径为/views/文件夹name/*.vue,的文件才能背识别 5 | const modules = import.meta.glob("../views/*/*.vue"); 6 | const loadComponent = (component: string) => 7 | modules[`../views/${component}.vue`]; 8 | 9 | const routes: Array = [ 10 | { 11 | path: "/home", 12 | component: Tabbar, 13 | children: [ 14 | { 15 | path: "/home", 16 | component: loadComponent("home/index"), 17 | meta: { 18 | title: "首页", 19 | }, 20 | }, 21 | { 22 | path: "/label", 23 | component: loadComponent("label/index"), 24 | meta: { 25 | title: "标签", 26 | }, 27 | }, 28 | { 29 | path: "/message", 30 | component: loadComponent("message/index"), 31 | meta: { 32 | title: "留言", 33 | }, 34 | }, 35 | { 36 | path: "/myself", 37 | component: loadComponent("myself/index"), 38 | meta: { 39 | title: "关于我", 40 | }, 41 | }, 42 | ], 43 | }, 44 | { 45 | path: "/article/detail", 46 | component: loadComponent("article/detail"), 47 | meta: { 48 | title: "文章详情", 49 | }, 50 | }, 51 | { 52 | path: "/:pathMatch(.*)*", 53 | redirect: "/home", 54 | }, 55 | ]; 56 | 57 | const router = createRouter({ 58 | history: createWebHashHistory(), 59 | routes, 60 | }); 61 | 62 | router.beforeEach((to) => { 63 | if (to.meta && to.meta.title) { 64 | document.title = to.meta.title as string; 65 | } 66 | }); 67 | 68 | export default router; 69 | -------------------------------------------------------------------------------- /server/controller/admin/message.js: -------------------------------------------------------------------------------- 1 | import messageModel from '../../models/message'; 2 | 3 | module.exports = { 4 | async list(ctx, next) { 5 | console.log( 6 | '----------------获取留言列表 admin_api/message/list-----------------------' 7 | ); 8 | let { keyword, pageindex = 1, pagesize = 10 } = ctx.request.query; 9 | 10 | let reg = new RegExp(keyword, 'i'); 11 | 12 | let conditions = { 13 | $or: [{ nickname: { $regex: reg } }, { content: { $regex: reg } }], 14 | }; 15 | 16 | // 排序参数 17 | let sortParams = { 18 | createTime: -1, 19 | }; 20 | 21 | let options = { 22 | limit: pagesize * 1, 23 | skip: (pageindex - 1) * pagesize, 24 | sort: sortParams, 25 | }; 26 | 27 | try { 28 | let data = await ctx.find(messageModel, conditions, null, options); 29 | return ctx.send(data); 30 | } catch (e) { 31 | console.log(e); 32 | return ctx.sendError(e); 33 | } 34 | }, 35 | 36 | async del(ctx, next) { 37 | console.log( 38 | '----------------删除留言 admin_api/message/del-----------------------' 39 | ); 40 | let id = ctx.request.query.id; 41 | try { 42 | ctx.remove(messageModel, { _id: id }); 43 | ctx.send(); 44 | } catch (e) { 45 | ctx.sendError(e); 46 | } 47 | }, 48 | 49 | async delReply(ctx, next) { 50 | console.log( 51 | '----------------删除回复 admin_api/message/delReply-----------------------' 52 | ); 53 | let { _id } = ctx.request.body; 54 | let options = { 55 | $pull: { replyList: { _id } }, 56 | }; 57 | try { 58 | let data = await ctx.update(messageModel, { _id }, options); 59 | ctx.send(); 60 | } catch (e) { 61 | ctx.sendError(e); 62 | } 63 | }, 64 | }; 65 | -------------------------------------------------------------------------------- /src/views/label/components/labelSelect.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 38 | 39 | -------------------------------------------------------------------------------- /src/views/home/components/intro.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | -------------------------------------------------------------------------------- /server/controller/admin/label.js: -------------------------------------------------------------------------------- 1 | import labelModel from "../../models/label"; 2 | 3 | module.exports = { 4 | async list(ctx, next) { 5 | console.log( 6 | "----------------获取标签列表 label/list-----------------------" 7 | ); 8 | let { keyword, pageindex = 1, pagesize = 50 } = ctx.request.query; 9 | try { 10 | let reg = new RegExp(keyword, "i"); 11 | let data = await ctx.findPage( 12 | labelModel, 13 | { 14 | $or: [{ label: { $regex: reg } }, { bgColor: { $regex: reg } }], 15 | }, 16 | null, 17 | { limit: pagesize * 1, skip: (pageindex - 1) * pagesize } 18 | ); 19 | ctx.send(data); 20 | } catch (e) { 21 | console.log(e); 22 | ctx.sendError(e); 23 | } 24 | }, 25 | 26 | async add(ctx, next) { 27 | console.log("----------------添加标签 label/add-----------------------"); 28 | let paramsData = ctx.request.body; 29 | try { 30 | let data = await ctx.findOne(labelModel, { label: paramsData.label }); 31 | if (data) { 32 | ctx.sendError("数据已经存在, 请重新添加!"); 33 | } else { 34 | let result = await ctx.add(labelModel, paramsData); 35 | ctx.send(result); 36 | } 37 | } catch (e) { 38 | ctx.sendError(e); 39 | } 40 | }, 41 | 42 | async update(ctx, next) { 43 | console.log("----------------更新标签 label/update-----------------------"); 44 | let paramsData = ctx.request.body; 45 | try { 46 | let data = await ctx.update( 47 | labelModel, 48 | { _id: paramsData._id }, 49 | paramsData 50 | ); 51 | ctx.send(data); 52 | } catch (e) { 53 | if (e === "暂无数据") { 54 | ctx.sendError(e); 55 | } 56 | } 57 | }, 58 | 59 | async del(ctx, next) { 60 | console.log("----------------删除标签 label/del-----------------------"); 61 | let id = ctx.request.query.id; 62 | try { 63 | let data = await ctx.remove(labelModel, { _id: id }); 64 | ctx.send(data); 65 | } catch (e) { 66 | ctx.sendError(e); 67 | } 68 | }, 69 | }; 70 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { ConfigEnv, loadEnv, UserConfig } from "vite"; 2 | import vue from "@vitejs/plugin-vue"; 3 | import vueJsx from "@vitejs/plugin-vue-jsx"; 4 | import legacy from "@vitejs/plugin-legacy"; 5 | import { resolve } from "path"; 6 | import styleImport from "vite-plugin-style-import"; // 按需导入ui组件 7 | 8 | const CWD = process.cwd(); // 项目根目录 9 | 10 | export default ({ mode }: ConfigEnv): UserConfig => { 11 | const { VITE_BASE_URL } = loadEnv(mode, CWD); // mode环境变量 12 | 13 | return { 14 | base: VITE_BASE_URL, 15 | css: { 16 | modules: { 17 | localsConvention: "camelCase", // 默认只支持驼峰,修改为同时支持横线和驼峰 18 | }, 19 | preprocessorOptions: { 20 | scss: { 21 | additionalData: '@import "./src/styles/index.scss";', 22 | }, 23 | }, 24 | }, 25 | plugins: [ 26 | vue(), 27 | vueJsx(), 28 | legacy({ 29 | targets: ["defaults", "not IE 11"], 30 | }), 31 | styleImport({ 32 | // 手动导入组件 33 | libs: [ 34 | { 35 | libraryName: "vant", 36 | esModule: true, 37 | resolveStyle: (name) => { 38 | return `vant/es/${name}/style`; 39 | }, 40 | }, 41 | ], 42 | }), 43 | ], 44 | resolve: { 45 | alias: { 46 | "@": resolve(__dirname, "src"), 47 | }, 48 | }, 49 | build: { 50 | target: "modules", 51 | polyfillModulePreload: true, 52 | outDir: "dist", 53 | assetsDir: "static", 54 | minify: "terser", // 混淆器 55 | }, 56 | optimizeDeps: { 57 | include: ["vue", "vue-router", "vant"], 58 | exclude: ["vue-demi"], 59 | }, 60 | server: { 61 | host: "0.0.0.0", 62 | port: 8089, // 设置服务启动端口号 63 | open: true, 64 | cors: true, // 允许跨域 65 | // 设置代理,根据项目实际情况配置 66 | proxy: { 67 | "/client_api": { 68 | target: "http://localhost:3000/client_api/", 69 | changeOrigin: true, 70 | secure: false, 71 | rewrite: (path) => path.replace(/^\/client_api/, "/"), 72 | }, 73 | }, 74 | }, 75 | }; 76 | }; 77 | -------------------------------------------------------------------------------- /src/views/message/components/replyItem.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 35 | 36 | -------------------------------------------------------------------------------- /src/views/label/index.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 51 | 52 | -------------------------------------------------------------------------------- /server/middleware/func/file.js: -------------------------------------------------------------------------------- 1 | import Busboy from "busboy"; 2 | import fs from "fs"; 3 | import path from "path"; 4 | 5 | //检测文件并创建文件 6 | const mkdirSync = (dirname) => { 7 | if (fs.existsSync(dirname)) { 8 | return true; 9 | } else { 10 | if (mkdirSync(path.dirname(dirname))) { 11 | fs.mkdirSync(dirname); 12 | return true; 13 | } 14 | } 15 | }; 16 | // 删除本地图片 17 | export const removeFile = (filePath) => { 18 | fs.unlink(filePath, function(err) { 19 | if (err) { 20 | throw err; 21 | } 22 | console.log("文件:" + filePath + "删除成功!"); 23 | }); 24 | }; 25 | 26 | export const uploadFile = (ctx, opts) => { 27 | //重命名 28 | function rename(fileName) { 29 | return ( 30 | Math.random() 31 | .toString(16) 32 | .substr(2) + 33 | "." + 34 | fileName.split(".").pop() 35 | ); 36 | } 37 | let busboy = new Busboy({ headers: ctx.req.headers }); 38 | console.log("start uploading..."); 39 | /* 40 | filename: 字段名, 41 | file: 文件流, 42 | filename: 文件名 43 | */ 44 | return new Promise((resolve, reject) => { 45 | var fileObj = {}; 46 | busboy.on("file", async (fieldname, file, filename, encoding, mimetype) => { 47 | let filePath = "", 48 | imgPrefix = ""; 49 | 50 | filePath = path.join(opts.path, mimetype.split("/")[0] + "s"); 51 | // 现网图片路径不一样 52 | imgPrefix = `${ctx.protocol}://${ctx.host}/${mimetype.split("/")[0]}s`; 53 | 54 | if (!mkdirSync(filePath)) { 55 | throw new Error("没找到目录"); 56 | } 57 | let fName = rename(filename), 58 | fPath = path.join(path.join(filePath, fName)); 59 | file.pipe(fs.createWriteStream(fPath)); 60 | 61 | console.log("fName =>", fName); 62 | console.log("fPath =>", fPath); 63 | 64 | file.on("end", () => { 65 | fileObj[fieldname] = `${imgPrefix}/${fName}`; 66 | }); 67 | }); 68 | 69 | busboy.on( 70 | "field", 71 | ( 72 | fieldname, 73 | val, 74 | fieldnameTruncated, 75 | valTruncated, 76 | encoding, 77 | mimetype 78 | ) => { 79 | fileObj[fieldname] = val; 80 | } 81 | ); 82 | 83 | busboy.on("finish", async () => { 84 | resolve(fileObj); 85 | console.log("finished...", fileObj); 86 | }); 87 | busboy.on("error", function(err) { 88 | console.log("err:" + err); 89 | reject(err); 90 | }); 91 | 92 | ctx.req.pipe(busboy); 93 | }); 94 | }; 95 | -------------------------------------------------------------------------------- /server/router/index.js: -------------------------------------------------------------------------------- 1 | import koaRouter from "koa-router"; 2 | const router = koaRouter(); 3 | 4 | export default (app) => { 5 | /*----------------------admin-------------------------------*/ 6 | // 用户请求 7 | router.post("/admin_api/user/login", app.admin.user.login); 8 | router.get("/admin_api/user/info", app.admin.user.info); 9 | router.get("/admin_api/user/list", app.admin.user.list); 10 | router.post("/admin_api/user/add", app.admin.user.add); 11 | router.post("/admin_api/user/update", app.admin.user.update); 12 | router.get("/admin_api/user/del", app.admin.user.del); 13 | 14 | // 文章请求 15 | router.get("/admin_api/blog/list", app.admin.blog.list); 16 | router.post("/admin_api/blog/add", app.admin.blog.add); 17 | router.post("/admin_api/blog/update", app.admin.blog.update); 18 | router.get("/admin_api/blog/del", app.admin.blog.del); 19 | router.get("/admin_api/blog/info", app.admin.blog.info); 20 | 21 | // 标签请求 22 | router.get("/admin_api/label/list", app.admin.label.list); 23 | router.post("/admin_api/label/add", app.admin.label.add); 24 | router.post("/admin_api/label/update", app.admin.label.update); 25 | router.get("/admin_api/label/del", app.admin.label.del); 26 | 27 | // 留言请求 28 | router.get("/admin_api/message/list", app.admin.message.list); 29 | router.get("/admin_api/message/del", app.admin.message.del); 30 | router.post("/admin_api/message/delReply", app.admin.message.delReply); 31 | 32 | // 图片请求 33 | router.post("/admin_api/uploadImage", app.admin.upload.uploadImage); 34 | router.post("/admin_api/delUploadImage", app.admin.upload.delUploadImage); 35 | 36 | /*----------------------client-------------------------------*/ 37 | // 文章请求 38 | router.get("/client_api/blog/list", app.client.blog.list); 39 | router.get("/client_api/blog/info", app.client.blog.info); 40 | router.post("/client_api/blog/updateLikes", app.client.blog.updateLikes); 41 | router.post("/client_api/blog/updatePV", app.client.blog.updatePV); 42 | 43 | // 标签请求 44 | router.get("/client_api/label/list", app.client.label.list); 45 | 46 | // 留言请求 47 | router.post("/client_api/message/add", app.client.message.add); 48 | router.get("/client_api/message/list", app.client.message.list); 49 | router.get("/client_api/message/replyCount", app.client.message.replyCount); 50 | router.post( 51 | "/client_api/message/updateLikes", 52 | app.client.message.updateLikes 53 | ); 54 | router.post( 55 | "/client_api/message/updateReplys", 56 | app.client.message.updateReplys 57 | ); 58 | 59 | app.use(router.routes()).use(router.allowedMethods()); 60 | }; 61 | -------------------------------------------------------------------------------- /server/middleware/log/log.js: -------------------------------------------------------------------------------- 1 | import log4js from 'log4js' 2 | import access from './access' // 引入日志输出信息的封装文件 3 | import config from '../../config' 4 | const methods = ["trace", "debug", "info", "warn", "error", "fatal", "mark"]; 5 | 6 | // 提取默认公用参数对象 7 | const baseInfo = config.log 8 | export default (options = {}) => { 9 | let contextLogger = {}, //错误日志等级对象,最后会赋值给ctx上,用于打印各种日志 10 | appenders = {}, //日志配置 11 | opts = Object.assign({}, baseInfo, options), //系统配置 12 | { 13 | logLevel, 14 | dir, 15 | ip, 16 | projectName 17 | } = opts, 18 | commonInfo = { 19 | projectName, 20 | ip 21 | }; //存储公用的日志信息 22 | 23 | //指定要记录的日志分类 24 | appenders.all = { 25 | type: 'dateFile', //日志文件类型,可以使用日期作为文件名的占位符 26 | filename: `${dir}/all/`, //日志文件名,可以设置相对路径或绝对路径 27 | pattern: 'task-yyyy-MM-dd.log', //占位符,紧跟在filename后面 28 | alwaysIncludePattern: true //是否总是有后缀名 29 | } 30 | 31 | // 环境变量为dev local development 认为是开发环境 32 | if (config.env === "dev" || config.env === "local" || config.env === "development") { 33 | appenders.out = { 34 | type: "console" 35 | } 36 | } 37 | 38 | let logConfig = { 39 | appenders, 40 | 41 | /** 42 | * 指定日志的默认配置项 43 | * 如果 log4js.getLogger 中没有指定,默认为 cheese 日志的配置项 44 | */ 45 | categories: { 46 | default: { 47 | appenders: Object.keys(appenders), 48 | level: logLevel 49 | } 50 | } 51 | } 52 | 53 | let logger = log4js.getLogger('cheese'); 54 | return async (ctx, next) => { 55 | const start = Date.now() // 记录请求开始的时间 56 | 57 | // 循环methods将所有方法挂载到ctx 上 58 | methods.forEach((method, i) => { 59 | contextLogger[method] = message => { 60 | logConfig.appenders.cheese = { 61 | type: 'dateFile', //日志文件类型,可以使用日期作为文件名的占位符 62 | filename: `${dir}/${method}/`, 63 | pattern: `${method}-yyyy-MM-dd.log`, 64 | alwaysIncludePattern: true //是否总是有后缀名 65 | } 66 | log4js.configure(logConfig) 67 | logger[method](access(ctx, message, commonInfo)) 68 | } 69 | }) 70 | ctx.log = contextLogger 71 | await next() 72 | // 记录完成的时间 作差 计算响应时间 73 | const responseTime = Date.now() - start 74 | 75 | ctx.log.info(access(ctx, { 76 | responseTime: `响应时间为${responseTime/1000}s` 77 | }, commonInfo)) 78 | 79 | } 80 | 81 | } -------------------------------------------------------------------------------- /src/views/message/components/commentEditor.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 72 | 73 | -------------------------------------------------------------------------------- /src/views/message/index.vue: -------------------------------------------------------------------------------- 1 | 77 | 78 | 99 | 100 | -------------------------------------------------------------------------------- /server/controller/client/message.js: -------------------------------------------------------------------------------- 1 | import messageModel from '../../models/message'; 2 | 3 | module.exports = { 4 | async add(ctx, next) { 5 | console.log('----------------添加留言 message/add-----------------------'); 6 | let paramsData = ctx.request.body; 7 | if (!paramsData.nickname) { 8 | paramsData.nickname = '匿名网友'; 9 | } 10 | try { 11 | await ctx.add(messageModel, paramsData); 12 | ctx.send(paramsData); 13 | } catch (e) { 14 | ctx.sendError(e); 15 | } 16 | }, 17 | 18 | async list(ctx, next) { 19 | console.log( 20 | '----------------获取评论列表 client_api/message/list-----------------------' 21 | ); 22 | let { pageindex = 1, pagesize = 10 } = ctx.request.query; 23 | 24 | // 排序参数 25 | let sortParams = { 26 | createTime: -1, 27 | }; 28 | 29 | let options = { 30 | limit: pagesize * 1, 31 | skip: (pageindex - 1) * pagesize, 32 | sort: sortParams, 33 | }; 34 | 35 | try { 36 | let data = await ctx.find(messageModel, null, null, options); 37 | return ctx.send(data); 38 | } catch (e) { 39 | console.log(e); 40 | return ctx.sendError(e); 41 | } 42 | }, 43 | 44 | async replyCount(ctx, next) { 45 | console.log( 46 | '----------------获取回复列表 client_api/message/replyCount-----------------------' 47 | ); 48 | let conditions = [ 49 | { $project: { replyCount: { $size: '$replyList' } } }, 50 | { $group: { _id: null, replyCount: { $sum: '$replyCount' } } }, 51 | ]; 52 | try { 53 | let data = await ctx.aggregate(messageModel, conditions); 54 | return ctx.send(data); 55 | } catch (e) { 56 | console.log(e); 57 | return ctx.sendError(e); 58 | } 59 | }, 60 | 61 | async updateLikes(ctx, next) { 62 | console.log( 63 | '----------------点赞留言 client_api/message/updateLikes------------------' 64 | ); 65 | let paramsData = ctx.request.body; 66 | let num = JSON.parse(paramsData.isLike) ? -1 : 1; 67 | let options = { $inc: { likes: num } }; 68 | try { 69 | let data = await ctx.update( 70 | messageModel, 71 | { _id: paramsData._id }, 72 | options 73 | ); 74 | ctx.send(); 75 | } catch (e) { 76 | ctx.sendError(e); 77 | } 78 | }, 79 | 80 | async updateReplys(ctx, next) { 81 | console.log( 82 | '----------------回复留言 client_api/message/updateReplys------------------' 83 | ); 84 | let paramsData = ctx.request.body; 85 | if (!paramsData.replyUser) { 86 | paramsData.replyUser = '匿名网友'; 87 | } 88 | let options = { 89 | $push: { replyList: { $each: [paramsData], $position: 0 } }, 90 | }; 91 | try { 92 | let data = await ctx.update( 93 | messageModel, 94 | { _id: paramsData._id }, 95 | options 96 | ); 97 | ctx.send(); 98 | } catch (e) { 99 | ctx.sendError(e); 100 | } 101 | }, 102 | }; 103 | -------------------------------------------------------------------------------- /server/controller/client/blog.js: -------------------------------------------------------------------------------- 1 | import blogModel from "../../models/blog"; 2 | 3 | module.exports = { 4 | async list(ctx, next) { 5 | console.log( 6 | "----------------获取博客列表 client_api/blog/list-----------------------" 7 | ); 8 | let { 9 | keyword = null, 10 | isQuery = null, 11 | pageindex = 1, 12 | pagesize = 9, 13 | sortBy = null, 14 | isMobile = false, 15 | type = null, 16 | } = ctx.request.query; 17 | // 条件参数 18 | let conditions = { isVisible: true }; 19 | // 用isMobile来区分移动端和pc端 20 | let reg = new RegExp(keyword, "i"); 21 | if (isMobile) { 22 | if (type) { 23 | conditions.type = type; 24 | } 25 | if (keyword) { 26 | let searchObj = [{ title: { $regex: reg } }, { desc: { $regex: reg } }]; 27 | conditions["$or"] = [...searchObj]; 28 | } 29 | } else { 30 | if (keyword) { 31 | // 区分搜索框、标签场景 32 | let searchObj = isQuery 33 | ? [{ title: { $regex: reg } }, { desc: { $regex: reg } }] 34 | : [{ type: { $regex: reg } }]; 35 | conditions["$or"] = [...searchObj]; 36 | } 37 | } 38 | // 排序参数 39 | let sortParams = {}; 40 | if (sortBy) { 41 | sortParams[sortBy] = -1; 42 | } else { 43 | sortParams["releaseTime"] = -1; 44 | } 45 | 46 | let options = { 47 | limit: pagesize * 1, 48 | skip: (pageindex - 1) * pagesize, 49 | sort: sortParams, 50 | }; 51 | 52 | try { 53 | let data = await ctx.find(blogModel, conditions, null, options); 54 | return ctx.send(data); 55 | } catch (e) { 56 | console.log(e); 57 | return ctx.sendError(e); 58 | } 59 | }, 60 | 61 | async info(ctx, next) { 62 | console.log( 63 | "----------------获取博客信息 client_api/blog/info-----------------------" 64 | ); 65 | let _id = ctx.request.query._id; 66 | try { 67 | let data = await ctx.findOne(blogModel, { _id }); 68 | return ctx.send(data); 69 | } catch (e) { 70 | return ctx.sendError(e); 71 | } 72 | }, 73 | 74 | async updateLikes(ctx, next) { 75 | console.log( 76 | "----------------点赞文章 client_api/blog/updateLikes------------------" 77 | ); 78 | let paramsData = ctx.request.body; 79 | let num = JSON.parse(paramsData.isLike) ? -1 : 1; 80 | let options = { $inc: { likes: num } }; 81 | try { 82 | let data = await ctx.update(blogModel, { _id: paramsData._id }, options); 83 | ctx.send(); 84 | } catch (e) { 85 | ctx.sendError(e); 86 | } 87 | }, 88 | 89 | async updatePV(ctx, next) { 90 | console.log( 91 | "----------------文章浏览量 client_api/blog/updatePV------------------" 92 | ); 93 | let paramsData = ctx.request.body; 94 | let options = { $inc: { pv: 1 } }; 95 | try { 96 | let data = await ctx.update(blogModel, { _id: paramsData._id }, options); 97 | ctx.send(); 98 | } catch (e) { 99 | ctx.sendError(e); 100 | } 101 | }, 102 | }; 103 | -------------------------------------------------------------------------------- /src/views/myself/index.vue: -------------------------------------------------------------------------------- 1 | 3 | 4 | 52 | 53 | -------------------------------------------------------------------------------- /server/controller/admin/blog.js: -------------------------------------------------------------------------------- 1 | import blogModel from "../../models/blog"; 2 | import marked from "marked"; 3 | 4 | marked.setOptions({ 5 | renderer: new marked.Renderer(), 6 | gfm: true, //允许 Git Hub标准的markdown. 7 | tables: true, //允许支持表格语法。该选项要求 gfm 为true。 8 | breaks: true, //允许回车换行。该选项要求 gfm 为true。 9 | pedantic: false, //尽可能地兼容 markdown.pl的晦涩部分。不纠正原始模型任何的不良行为和错误。 10 | sanitize: true, //对输出进行过滤(清理),将忽略任何已经输入的html代码(标签) 11 | smartLists: true, //使用比原生markdown更时髦的列表。 旧的列表将可能被作为pedantic的处理内容过滤掉. 12 | smartypants: false, //使用更为时髦的标点,比如在引用语法中加入破折号。 13 | highlight: function(code) { 14 | return require("highlight.js").highlightAuto(code).value; 15 | }, 16 | }); 17 | 18 | module.exports = { 19 | async list(ctx, next) { 20 | console.log( 21 | "----------------获取博客列表 blog/list-----------------------" 22 | ); 23 | let { keyword, pageindex = 1, pagesize = 10 } = ctx.request.query; 24 | console.log("ctx.request =>", ctx.request); 25 | console.log( 26 | "keyword:" + 27 | keyword + 28 | "," + 29 | "pageindex:" + 30 | pageindex + 31 | "," + 32 | "pagesize:" + 33 | pagesize 34 | ); 35 | try { 36 | let reg = new RegExp(keyword, "i"); 37 | let data = await ctx.findPage( 38 | blogModel, 39 | { 40 | $or: [{ type: { $regex: reg } }, { title: { $regex: reg } }], 41 | }, 42 | null, 43 | { limit: pagesize * 1, skip: (pageindex - 1) * pagesize } 44 | ); 45 | ctx.send(data); 46 | } catch (e) { 47 | console.log(e); 48 | ctx.sendError(e); 49 | } 50 | }, 51 | 52 | async add(ctx, next) { 53 | console.log("----------------添加博客 blog/add-----------------------"); 54 | let paramsData = ctx.request.body; 55 | try { 56 | let data = await ctx.findOne(blogModel, { title: paramsData.title }); 57 | if (data) { 58 | ctx.sendError("数据已经存在, 请重新添加!"); 59 | } else { 60 | paramsData.html = marked(paramsData.html); 61 | let data = await ctx.add(blogModel, paramsData); 62 | ctx.send(paramsData); 63 | } 64 | } catch (e) { 65 | ctx.sendError(e); 66 | } 67 | }, 68 | 69 | async update(ctx, next) { 70 | console.log("----------------更新博客 blog/update-----------------------"); 71 | let paramsData = ctx.request.body; 72 | try { 73 | paramsData.html = marked(paramsData.html); 74 | let data = await ctx.update( 75 | blogModel, 76 | { _id: paramsData._id }, 77 | paramsData 78 | ); 79 | ctx.send(); 80 | } catch (e) { 81 | if (e === "暂无数据") { 82 | ctx.sendError(e); 83 | } 84 | } 85 | }, 86 | 87 | async del(ctx, next) { 88 | console.log("----------------删除博客 blog/del-----------------------"); 89 | let id = ctx.request.query.id; 90 | try { 91 | ctx.remove(blogModel, { _id: id }); 92 | ctx.send(); 93 | } catch (e) { 94 | ctx.sendError(e); 95 | } 96 | }, 97 | 98 | async info(ctx, next) { 99 | console.log( 100 | "----------------获取博客信息 blog/info-----------------------" 101 | ); 102 | let _id = ctx.request.query._id; 103 | try { 104 | let data = await ctx.findOne(blogModel, { _id }); 105 | return ctx.send(data); 106 | } catch (e) { 107 | return ctx.sendError(e); 108 | } 109 | }, 110 | }; 111 | -------------------------------------------------------------------------------- /src/styles/init.scss: -------------------------------------------------------------------------------- 1 | html { 2 | background: #fff; 3 | overflow-y: scroll; 4 | -webkit-text-size-adjust: 100%; 5 | -ms-text-size-adjust: 100%; 6 | text-size-adjust: 100%; 7 | -webkit-user-select: none; /* 禁止选中文本(如无文本选中需求,此为必选项) */ 8 | user-select: none; 9 | } 10 | html, 11 | body { 12 | position: relative; 13 | height: 100%; 14 | min-height: 100%; 15 | font-size: 12px; 16 | } 17 | /* 内外边距通常让各个浏览器样式的表现位置不同 */ 18 | body, 19 | div, 20 | dl, 21 | dt, 22 | dd, 23 | ul, 24 | ol, 25 | li, 26 | h1, 27 | h2, 28 | h3, 29 | h4, 30 | h5, 31 | h6, 32 | pre, 33 | code, 34 | form, 35 | fieldset, 36 | legend, 37 | input, 38 | textarea, 39 | p, 40 | blockquote, 41 | th, 42 | td, 43 | hr, 44 | button, 45 | article, 46 | aside, 47 | details, 48 | figcaption, 49 | figure, 50 | footer, 51 | header, 52 | hgroup, 53 | menu, 54 | nav, 55 | section { 56 | margin: 0; 57 | padding: 0; 58 | } 59 | 60 | /* 重设 HTML5 标签, IE 需要在 js 中 createElement(TAG) */ 61 | article, 62 | aside, 63 | details, 64 | figcaption, 65 | figure, 66 | footer, 67 | header, 68 | hgroup, 69 | menu, 70 | nav, 71 | section { 72 | display: block; 73 | } 74 | 75 | /* HTML5 媒体文件跟 img 保持一致 */ 76 | audio, 77 | canvas, 78 | video { 79 | display: inline-block; 80 | zoom: 1; 81 | } 82 | 83 | /* 要注意表单元素并不继承父级 font 的问题 */ 84 | body, 85 | button, 86 | input, 87 | select, 88 | textarea { 89 | font-size: 12px; 90 | line-height: 1.5; 91 | font-family: PingFangSC-Regular, 'PingFang SC', 'San Francisco', 92 | 'Helvetica Neue', 'Hiragino Sans GB', 'Hiragino Sans GB W3', 93 | 'Microsoft Yahei', '微软雅黑', sans-serif, Tahoma, Helvetica, Arial, STHeiti; 94 | -webkit-font-smoothing: antialiased; 95 | } 96 | input, 97 | button, 98 | select, 99 | textarea { 100 | outline: none; 101 | resize: none; 102 | } 103 | 104 | /* 去掉各Table cell 的边距并让其边重合 */ 105 | table { 106 | border-collapse: collapse; 107 | border-spacing: 0; 108 | } 109 | 110 | /* IE bug fixed: th 不继承 text-align*/ 111 | th { 112 | text-align: inherit; 113 | } 114 | 115 | /* 去除默认边框 */ 116 | fieldset, 117 | img { 118 | border: 0; 119 | } 120 | 121 | /* ie6 7 8(q) bug 显示为行内表现 */ 122 | iframe { 123 | display: block; 124 | } 125 | 126 | /* 去掉 firefox 下此元素的边框 */ 127 | abbr, 128 | acronym { 129 | border: 0; 130 | font-variant: normal; 131 | } 132 | 133 | /* 一致的 del 样式 */ 134 | del { 135 | text-decoration: line-through; 136 | } 137 | 138 | address, 139 | caption, 140 | cite, 141 | code, 142 | dfn, 143 | em, 144 | th, 145 | var { 146 | font-style: normal; 147 | } 148 | 149 | /* 去掉列表前的标识, li 会继承 */ 150 | ol, 151 | ul { 152 | list-style: none; 153 | } 154 | 155 | /* 来自yahoo, 让标题都自定义, 适应多个系统应用 */ 156 | h1, 157 | h2, 158 | h3, 159 | h4, 160 | h5, 161 | h6 { 162 | font-size: 100%; 163 | font-weight: normal; 164 | } 165 | 166 | b, 167 | strong { 168 | font-weight: normal; 169 | } 170 | 171 | a, 172 | img { 173 | -webkit-touch-callout: none; /* 禁止长按链接与图片弹出菜单 */ 174 | } 175 | a, 176 | input, 177 | * { 178 | /* 去掉outline */ 179 | -webkit-tap-highlight-color: rgba(255, 255, 255, 0) !important; 180 | -webkit-focus-ring-color: rgba(255, 255, 255, 0) !important; 181 | outline: none !important; 182 | } 183 | ins, 184 | a { 185 | text-decoration: none; 186 | } 187 | /*去除IE10下ipunt后面的叉叉*/ 188 | input::-ms-clear { 189 | display: none; 190 | } 191 | 192 | /*webkit渲染bugfix*/ 193 | input[type='text'], 194 | input[type='password'], 195 | input[type='submit'], 196 | input[type='reset'], 197 | input[type='button'], 198 | button { 199 | -webkit-appearance: none; 200 | } 201 | 202 | input[type='number']::-webkit-inner-spin-button { 203 | -webkit-appearance: none; 204 | } 205 | input[type='number']::-webkit-outer-spin-button { 206 | -webkit-appearance: none; 207 | } 208 | -------------------------------------------------------------------------------- /src/views/article/detail.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 82 | 83 | -------------------------------------------------------------------------------- /server/controller/admin/user.js: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | import conf from "../../config"; 3 | import userModel from "../../models/user"; 4 | module.exports = { 5 | async login(ctx, next) { 6 | console.log("----------------登录接口 user/login-----------------------"); 7 | let { username, pwd } = ctx.request.body; 8 | try { 9 | let data = await ctx.findOne(userModel, { username: username }); 10 | console.log(data); 11 | if (!data) { 12 | return ctx.sendError("用户名不存在!"); 13 | } 14 | if (pwd !== data.pwd) { 15 | return ctx.sendError("密码错误,请重新输入!"); 16 | } 17 | await ctx.update( 18 | userModel, 19 | { _id: data._id }, 20 | { $set: { loginTime: new Date() } } 21 | ); //更新登陆时间 22 | 23 | let payload = { 24 | _id: data._id, 25 | username: data.username, 26 | roles: data.roles, 27 | }; 28 | // token签名 有效期为24小时 29 | let token = jwt.sign(payload, conf.auth.admin_secret, { 30 | expiresIn: "24h", 31 | }); 32 | // 是否只用于http请求中获取 33 | ctx.cookies.set(conf.auth.tokenKey, token, { 34 | httpOnly: false, 35 | }); 36 | ctx.send({ message: "登录成功" }); 37 | } catch (e) { 38 | if (e === "暂无数据") { 39 | console.log("用户名不存在"); 40 | return ctx.sendError("用户名不存在"); 41 | } 42 | ctx.throw(e); 43 | ctx.sendError(e); 44 | } 45 | }, 46 | async info(ctx, next) { 47 | console.log( 48 | "----------------获取用户信息接口 user/getUserInfo-----------------------" 49 | ); 50 | let token = ctx.request.query.token; 51 | try { 52 | let tokenInfo = jwt.verify(token, conf.auth.admin_secret); 53 | console.log("log tokenInfo =>", tokenInfo); 54 | ctx.send({ 55 | username: tokenInfo.username, 56 | _id: tokenInfo._id, 57 | roles: tokenInfo.roles, 58 | }); 59 | } catch (e) { 60 | if ("TokenExpiredError" === e.name) { 61 | ctx.sendError("鉴权失败, 请重新登录!"); 62 | ctx.throw(401, "token验证失败, 请重新登录!"); 63 | } 64 | ctx.throw(401, "invalid token"); 65 | ctx.sendError("系统异常!"); 66 | } 67 | }, 68 | 69 | async list(ctx, next) { 70 | console.log( 71 | "----------------获取用户信息列表接口 user/getUserList-----------------------" 72 | ); 73 | let { keyword, pageindex = 1, pagesize = 10 } = ctx.request.query; 74 | console.log( 75 | "keyword:" + 76 | keyword + 77 | "," + 78 | "pageindex:" + 79 | pageindex + 80 | "," + 81 | "pagesize:" + 82 | pagesize 83 | ); 84 | 85 | try { 86 | let reg = new RegExp(keyword, "i"); 87 | let data = await ctx.findPage( 88 | userModel, 89 | { 90 | $or: [{ username: { $regex: reg } }], 91 | }, 92 | { pwd: 0 }, 93 | { limit: pagesize * 1, skip: (pageindex - 1) * pagesize } 94 | ); 95 | 96 | ctx.send(data); 97 | } catch (e) { 98 | console.log(e); 99 | ctx.sendError(e); 100 | } 101 | }, 102 | 103 | async add(ctx, next) { 104 | console.log("----------------添加管理员 user/add-----------------------"); 105 | let paramsData = ctx.request.body; 106 | try { 107 | let data = await ctx.findOne(userModel, { 108 | username: paramsData.username, 109 | }); 110 | if (data) { 111 | ctx.sendError("数据已经存在, 请重新添加!"); 112 | } else { 113 | await ctx.add(userModel, paramsData); 114 | ctx.send(paramsData); 115 | } 116 | } catch (e) { 117 | ctx.sendError(e); 118 | } 119 | }, 120 | 121 | async update(ctx, next) { 122 | console.log( 123 | "----------------更新管理员 user/update-----------------------" 124 | ); 125 | let paramsData = ctx.request.body; 126 | console.log(paramsData); 127 | try { 128 | let data = await ctx.findOne(userModel, { 129 | username: paramsData.username, 130 | }); 131 | if (paramsData.old_pwd !== data.pwd) { 132 | return ctx.sendError("密码不匹配!"); 133 | } 134 | delete paramsData.old_pwd; 135 | await ctx.update(userModel, { _id: paramsData._id }, paramsData); 136 | ctx.send(); 137 | } catch (e) { 138 | if (e === "暂无数据") { 139 | ctx.sendError(e); 140 | } 141 | } 142 | }, 143 | 144 | async del(ctx, next) { 145 | console.log("----------------删除管理员 user/del-----------------------"); 146 | let id = ctx.request.query.id; 147 | try { 148 | ctx.remove(userModel, { _id: id }); 149 | ctx.send(); 150 | } catch (e) { 151 | ctx.sendError(e); 152 | } 153 | }, 154 | }; 155 | -------------------------------------------------------------------------------- /src/styles/common/markdown.scss: -------------------------------------------------------------------------------- 1 | :deep(.fmt) { 2 | line-height: 22px; 3 | font-size: 14px; 4 | word-wrap: break-word; 5 | color: $strongFontColor; 6 | a { 7 | color: #009a61; 8 | text-decoration: none; 9 | } 10 | h1 { 11 | font-size: 24px; 12 | } 13 | 14 | h2 { 15 | font-size: 20px; 16 | } 17 | 18 | h3 { 19 | font-size: 18px; 20 | } 21 | 22 | h4 { 23 | font-size: 16px; 24 | } 25 | 26 | h5 { 27 | font-size: 14px; 28 | } 29 | 30 | h6 { 31 | font-size: 12px; 32 | } 33 | p { 34 | margin-top: 10px; 35 | font-size: 14px; 36 | } 37 | h1, 38 | h2, 39 | h3, 40 | h4, 41 | h5, 42 | h6 { 43 | margin: 10px 0; 44 | } 45 | 46 | h1 + .widget-codetool + pre, 47 | h2 + .widget-codetool + pre, 48 | h3 + .widget-codetool + pre { 49 | margin-top: 12px !important; 50 | } 51 | 52 | h1, 53 | h2 { 54 | border-bottom: 1px solid #eee; 55 | padding-bottom: 10px; 56 | } 57 | 58 | h1:first-child, 59 | h2:first-child, 60 | h3:first-child, 61 | h4:first-child, 62 | p:first-child, 63 | ul:first-child, 64 | ol:first-child, 65 | blockquote:first-child { 66 | margin-top: 0; 67 | } 68 | 69 | ul, 70 | ol { 71 | margin-left: 16px; 72 | margin-top: 8px; 73 | padding-left: 0; 74 | } 75 | 76 | ul li, 77 | ol li { 78 | margin: 4px 0; 79 | list-style: disc; 80 | } 81 | 82 | ul p, 83 | ol p { 84 | margin: 0; 85 | } 86 | 87 | p:last-child { 88 | margin-bottom: 0; 89 | } 90 | 91 | p > p:empty, 92 | div > p:empty, 93 | p > div:empty, 94 | div > div:empty, 95 | div > br:only-child, 96 | p + br, 97 | img + br { 98 | display: none; 99 | } 100 | 101 | img, 102 | video, 103 | audio { 104 | position: static !important; 105 | max-width: 100%; 106 | } 107 | 108 | img { 109 | padding: 3px; 110 | border: 1px solid #ddd; 111 | } 112 | 113 | img.emoji { 114 | padding: 0; 115 | border: none; 116 | } 117 | 118 | blockquote { 119 | border-left: 2px solid #009a61; 120 | background: $baseBackColor; 121 | color: $strongFontColor; 122 | font-size: 12px; 123 | } 124 | 125 | pre, 126 | code { 127 | font-size: 12px; 128 | margin-top: 8px; 129 | } 130 | 131 | pre { 132 | font-family: 'Source Code Pro', Consolas, Menlo, Monaco, 'Courier New', 133 | monospace; 134 | padding: 10px; 135 | margin-top: 8px; 136 | border: none; 137 | overflow: auto; 138 | line-height: 24px; 139 | max-height: 350px; 140 | position: relative; 141 | background-color: $baseBackColor; 142 | font-size: 12px; 143 | -webkit-overflow-scrolling: touch; 144 | border-radius: 5px; 145 | } 146 | 147 | pre code { 148 | background: none; 149 | font-size: 10px; 150 | overflow-wrap: normal; 151 | white-space: inherit; 152 | } 153 | 154 | hr { 155 | margin: 16px auto; 156 | border-top: 2px dotted #eee; 157 | } 158 | 159 | kbd { 160 | margin: 0 4px; 161 | padding: 3px 4px; 162 | background: #eee; 163 | color: $strongFontColor; 164 | } 165 | 166 | .x-scroll { 167 | overflow-x: auto; 168 | } 169 | 170 | table { 171 | width: 100%; 172 | } 173 | 174 | table th, 175 | table td { 176 | border: 1px solid #e6e6e6; 177 | padding: 5px 8px; 178 | word-break: normal; 179 | } 180 | 181 | table th { 182 | background: #f3f3f3; 183 | } 184 | 185 | a:not(.btn) { 186 | border-bottom: 1px solid rgba(0, 154, 97, 0.25); 187 | padding-bottom: 1px; 188 | } 189 | 190 | a:not(.btn):hover { 191 | border-bottom: 1px solid #009a61; 192 | text-decoration: none; 193 | } 194 | 195 | .hljs { 196 | display: block; 197 | overflow-x: auto; 198 | padding: 6px; 199 | color: $mainColor; 200 | background: #f8f8f8; 201 | } 202 | 203 | .hljs-comment, 204 | .hljs-quote { 205 | color: #998; 206 | font-style: italic; 207 | } 208 | 209 | .hljs-keyword, 210 | .hljs-selector-tag, 211 | .hljs-subst { 212 | color: $mainColor; 213 | font-weight: bold; 214 | } 215 | 216 | .hljs-number, 217 | .hljs-literal, 218 | .hljs-variable, 219 | .hljs-template-variable, 220 | .hljs-tag .hljs-attr { 221 | color: #008080; 222 | } 223 | 224 | .hljs-string, 225 | .hljs-doctag { 226 | color: #d14; 227 | } 228 | 229 | .hljs-title, 230 | .hljs-section, 231 | .hljs-selector-id { 232 | color: #900; 233 | font-weight: bold; 234 | } 235 | 236 | .hljs-subst { 237 | font-weight: normal; 238 | } 239 | 240 | .hljs-type, 241 | .hljs-class .hljs-title { 242 | color: #458; 243 | font-weight: bold; 244 | } 245 | 246 | .hljs-tag, 247 | .hljs-name, 248 | .hljs-attribute { 249 | color: #000080; 250 | font-weight: normal; 251 | } 252 | 253 | .hljs-regexp, 254 | .hljs-link { 255 | color: #009926; 256 | } 257 | 258 | .hljs-symbol, 259 | .hljs-bullet { 260 | color: #990073; 261 | } 262 | 263 | .hljs-built_in, 264 | .hljs-builtin-name { 265 | color: #0086b3; 266 | } 267 | 268 | .hljs-meta { 269 | color: $strongFontColor; 270 | font-weight: bold; 271 | } 272 | 273 | .hljs-deletion { 274 | background: #fdd; 275 | } 276 | 277 | .hljs-addition { 278 | background: #dfd; 279 | } 280 | 281 | .hljs-emphasis { 282 | font-style: italic; 283 | } 284 | 285 | .hljs-strong { 286 | font-weight: bold; 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /src/views/message/components/commentItem.vue: -------------------------------------------------------------------------------- 1 | 72 | 73 | 132 | 133 | -------------------------------------------------------------------------------- /server/middleware/func/db.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 公共Add方法 3 | * @param model 要操作数据库的模型 4 | * @param conditions 增加的条件,如{id:xxx} 5 | */ 6 | export const add = (model, conditions) => { 7 | return new Promise((resolve, reject) => { 8 | model.create(conditions, (err, res) => { 9 | if (err) { 10 | console.error("Error: " + JSON.stringify(err)); 11 | reject(err); 12 | return false; 13 | } 14 | console.log("save success!"); 15 | resolve(res); 16 | }); 17 | }); 18 | }; 19 | 20 | /* 21 | * 公共update方法 22 | * @param model 要操作数据库的模型 23 | * @param conditions 增加的条件,如{id:xxx} 24 | * @param update 更新条件{set{id:xxx}} 25 | * @param options 26 | */ 27 | export const update = (model, conditions, update, options) => { 28 | return new Promise((resolve, reject) => { 29 | model.update(conditions, update, options, (err, res) => { 30 | if (err) { 31 | console.error("Error: " + JSON.stringify(err)); 32 | reject(err); 33 | return false; 34 | } 35 | if (res.n !== 0) { 36 | console.log("update success!"); 37 | } else { 38 | console.log("update fail:no this data!"); 39 | return reject("update fail:no this data!"); 40 | } 41 | resolve(res); 42 | }); 43 | }); 44 | }; 45 | 46 | /** 47 | * 公共remove方法 48 | * @param model 49 | * @param conditions 50 | */ 51 | 52 | export const remove = (model, conditions) => { 53 | return new Promise((resolve, reject) => { 54 | model.remove(conditions, function(err, res) { 55 | if (err) { 56 | console.error("Error: " + JSON.stringify(err)); 57 | reject(err); 58 | return false; 59 | } else { 60 | if (res.result.n !== 0) { 61 | console.log("remove success!"); 62 | } else { 63 | console.log("remove fail:no this data!"); 64 | } 65 | resolve(res); 66 | } 67 | }); 68 | }); 69 | }; 70 | 71 | /** 72 | * 公共find方法 非关联查找 73 | * @param model 74 | * @param conditions 75 | * @param fields 查找时限定的条件,如顺序,某些字段不查找等 76 | * @param options 77 | * @param callback 78 | */ 79 | export const find = async (model, conditions, fields, options = {}) => { 80 | let { sort } = options; 81 | delete options.sort; 82 | 83 | const getCount = () => { 84 | return new Promise((resolve, reject) => { 85 | model.find(conditions, fields).count({}, (err, res) => { 86 | if (err) { 87 | console.log("查询长度错误"); 88 | return reject(err); 89 | } 90 | 91 | resolve(res); 92 | }); 93 | }); 94 | }; 95 | 96 | const count = await getCount(); 97 | 98 | return new Promise((resolve, reject) => { 99 | model 100 | .find(conditions, fields, options, function(err, res) { 101 | if (err) { 102 | console.error("Error: " + JSON.stringify(err)); 103 | reject(err); 104 | return false; 105 | } else { 106 | if (res.length !== 0) { 107 | resolve({ 108 | list: res, 109 | total: count, 110 | }); 111 | console.log("find success!"); 112 | } else { 113 | console.log("find fail:no this data!"); 114 | } 115 | // resolve(res); 116 | resolve({ 117 | list: res, 118 | total: count, 119 | }); 120 | } 121 | }) 122 | .sort(sort); 123 | }); 124 | }; 125 | 126 | /** 127 | * 公共findOne方法 非关联查找 128 | * @param model 129 | * @param conditions 130 | * @param fields 查找时限定的条件,如顺序,某些字段不查找等 131 | * @param options 132 | * @param callback 133 | */ 134 | export const findOne = (model, conditions, fields, options = {}) => { 135 | let { sort } = options; 136 | delete options.sort; 137 | return new Promise((resolve, reject) => { 138 | model 139 | .findOne(conditions, fields, options, function(err, res) { 140 | if (err) { 141 | console.error("Error: " + JSON.stringify(err)); 142 | reject(err); 143 | return false; 144 | } else { 145 | if (res) { 146 | console.log("find success!"); 147 | } else { 148 | console.log("find fail:no this data!"); 149 | } 150 | resolve(res); 151 | } 152 | }) 153 | .sort(sort); 154 | }); 155 | }; 156 | 157 | export const findPage = async (model, conditions, fields, options = {}) => { 158 | let { sort } = options; 159 | delete options.sort; 160 | 161 | const getCount = () => { 162 | return new Promise((resolve, reject) => { 163 | model.find(conditions, fields).count({}, (err, res) => { 164 | if (err) { 165 | console.log("查询长度错误"); 166 | return reject(err); 167 | } 168 | resolve(res); 169 | }); 170 | }); 171 | }; 172 | 173 | const count = await getCount(); 174 | 175 | return new Promise((resolve, reject) => { 176 | model.find(conditions, fields, options, function(err, res) { 177 | if (err) { 178 | console.error("Error: " + JSON.stringify(err)); 179 | reject(err); 180 | return false; 181 | } else { 182 | if (res.length !== 0) { 183 | console.log("find success!"); 184 | resolve({ 185 | list: res, 186 | total: count, 187 | }); 188 | } else { 189 | console.log("find fail:no this data!"); 190 | resolve({ 191 | list: res, 192 | total: count, 193 | }); 194 | } 195 | } 196 | }); 197 | }); 198 | }; 199 | 200 | /* 201 | * 公共aggregate方法 202 | * @param model 要操作数据库的模型 203 | * @param conditions 增加的条件,如{id:xxx} 204 | */ 205 | export const aggregate = (model, conditions) => { 206 | return new Promise((resolve, reject) => { 207 | model.aggregate(conditions, (err, res) => { 208 | if (err) { 209 | console.error("Error: " + JSON.stringify(err)); 210 | reject(err); 211 | return false; 212 | } 213 | console.log("aggregate success!"); 214 | resolve(res); 215 | }); 216 | }); 217 | }; 218 | -------------------------------------------------------------------------------- /src/views/home/components/list.vue: -------------------------------------------------------------------------------- 1 | 2 | 91 | 92 | 166 | 167 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue3-vite2-blog-h5 2 | 3 | 一款简约的移动端博客。前端项目主要是采用`Vue3`语法糖`