├── .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 |
7 |
8 |
9 |
10 |
11 |
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 |
2 |
3 |
4 |
5 |
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 |
20 |
35 |
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 |
22 |
23 |
34 | {{ item.label }}
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/src/views/home/components/intro.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
WALL-BLOG
5 |
6 | 享受编程和技术所带来的快乐
Coding Your Ambition
7 |
8 |
9 |

10 |

11 |
12 |
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 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | {{ replyItem.replyUser }}
25 | @ {{ replyItem.byReplyUser }}:
26 |
27 |
28 |
29 |
30 | {{ formatTime(replyItem.replyTime, 'yyyy-MM-dd hh:mm:ss') }}
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/src/views/label/index.vue:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
18 |
29 | (params.type = val)"
31 | ref="label"
32 | >
33 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
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 |
48 |
49 |
50 |
59 |
60 |
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/src/views/message/index.vue:
--------------------------------------------------------------------------------
1 |
77 |
78 |
79 |
80 |
81 |
88 |
95 |
96 |
97 |
98 |
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 |
5 |
6 |
7 |

8 |

9 |
10 |
11 |
wall | 苏s哈
12 |
Web前端工程师
13 |
14 | 一个热爱篮球与前端技术的95后!20年入行,
15 | 热衷于研究web前端技术,一边工作一边积累经验,分享一些自己整理的笔记和优选文章。
16 |
17 |
18 |
50 |
51 |
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 |
46 |
47 |
48 |
{{ detail.title }}
49 |
50 |
51 |
52 |
53 | {{ formatTime(detail.releaseTime, 'yyyy-MM-dd') }}
54 |
55 |
56 |
57 |
58 |
{{ formatNumber(detail.pv) }}
59 |
60 |
61 |
62 |
63 | {{ detail.auth }}
64 |
65 |
66 |
67 |
68 |
69 | 标签:
70 |
76 | {{ label }}
77 |
78 |
79 |
80 |
81 |
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 |
74 |
131 |
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 |
93 |
97 |
102 |
103 |
104 | 最新文章({{ total }})
105 |
106 |
113 |
119 |
120 |
![]()
121 |
122 |
123 |
{{ item.title }}
124 |
{{ item.desc }}
125 |
126 |
127 |
133 | {{ label }}
134 |
135 |
136 |
137 |
138 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # vue3-vite2-blog-h5
2 |
3 | 一款简约的移动端博客。前端项目主要是采用`Vue3`语法糖`