├── LICENSE ├── public └── index.html ├── src ├── static │ ├── 1.txt │ ├── 1.jpg │ ├── 2.jpg │ ├── 3.jpg │ ├── 4.jpg │ ├── logo.png │ ├── cover.png │ ├── favicon.ico │ ├── poster.png │ ├── review.png │ ├── poster │ │ ├── 5dbd8936276aeb0ea379cc26-5YaP2Olwg4_small.jpg │ │ ├── 5dbd8dfa276aeb0ea379cc28-1bf124bb97861b5c768cdc2408034889186997df.jpg │ │ └── 5df4a81f8bf583067be30b4f-7e3e6709c93d70cf3b56f10862786605bba12bb4.jpeg │ └── tip.svg ├── pages │ ├── comics │ │ ├── _id │ │ │ ├── chapters │ │ │ │ └── _cid │ │ │ │ │ ├── edit.vue │ │ │ │ │ └── index.vue │ │ │ └── index.vue │ │ ├── index.vue │ │ └── tag │ │ │ └── _id.vue │ ├── tags │ │ └── _id │ │ │ ├── _type.vue │ │ │ └── index.vue │ ├── beecms │ │ ├── setting │ │ │ ├── index.vue │ │ │ └── cloudserver │ │ │ │ └── index.vue │ │ ├── index.vue │ │ ├── bees │ │ │ └── index.vue │ │ ├── comics │ │ │ └── index.vue │ │ └── novels │ │ │ └── index.vue │ ├── videos │ │ ├── index.vue │ │ └── tag │ │ │ └── _id.vue │ ├── bees │ │ ├── index.vue │ │ ├── tag │ │ │ └── _id.vue │ │ └── _id │ │ │ └── index.vue │ ├── novels │ │ ├── index.vue │ │ ├── tag │ │ │ └── _id.vue │ │ └── _id │ │ │ ├── chapters │ │ │ └── _cid │ │ │ │ ├── edit.vue │ │ │ │ └── index.vue │ │ │ ├── index.vue │ │ │ └── add.vue │ ├── search.vue │ ├── store.vue │ └── index.vue ├── components │ ├── videobox.vue │ ├── headerTitle.vue │ ├── widget.vue │ ├── notification.vue │ ├── plugins │ │ └── image.js │ ├── tab.vue │ ├── itemSection.vue │ ├── hero.vue │ ├── card.vue │ ├── sideCard.vue │ ├── floatMenu.vue │ ├── userInfo.vue │ └── cardTable.vue ├── apollo │ └── allCategories.gql ├── middleware │ └── isadmin.js ├── assets │ ├── variables.scss │ └── app.css ├── plugins │ └── toast.js ├── layouts │ └── error.vue └── store │ └── index.js ├── .gitignore ├── email ├── sendCode.pug └── index.js ├── README.md ├── .envdemo ├── server.js ├── package.json ├── helper ├── sms.js └── common.js ├── controller └── api.js └── index.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/static/1.txt: -------------------------------------------------------------------------------- 1 | 1111 -------------------------------------------------------------------------------- /src/pages/comics/_id/chapters/_cid/edit.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/videobox.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | dump.rdb 4 | yarn.lock 5 | .env -------------------------------------------------------------------------------- /src/pages/tags/_id/_type.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/tags/_id/index.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/static/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookyo/BeeCMS/HEAD/src/static/1.jpg -------------------------------------------------------------------------------- /src/static/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookyo/BeeCMS/HEAD/src/static/2.jpg -------------------------------------------------------------------------------- /src/static/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookyo/BeeCMS/HEAD/src/static/3.jpg -------------------------------------------------------------------------------- /src/static/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookyo/BeeCMS/HEAD/src/static/4.jpg -------------------------------------------------------------------------------- /src/static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookyo/BeeCMS/HEAD/src/static/logo.png -------------------------------------------------------------------------------- /src/static/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookyo/BeeCMS/HEAD/src/static/cover.png -------------------------------------------------------------------------------- /src/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookyo/BeeCMS/HEAD/src/static/favicon.ico -------------------------------------------------------------------------------- /src/static/poster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookyo/BeeCMS/HEAD/src/static/poster.png -------------------------------------------------------------------------------- /src/static/review.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookyo/BeeCMS/HEAD/src/static/review.png -------------------------------------------------------------------------------- /src/apollo/allCategories.gql: -------------------------------------------------------------------------------- 1 | { 2 | allCategories(first: 8) { 3 | name, 4 | id 5 | } 6 | } -------------------------------------------------------------------------------- /src/pages/beecms/setting/index.vue: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /src/static/poster/5dbd8936276aeb0ea379cc26-5YaP2Olwg4_small.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookyo/BeeCMS/HEAD/src/static/poster/5dbd8936276aeb0ea379cc26-5YaP2Olwg4_small.jpg -------------------------------------------------------------------------------- /src/static/poster/5dbd8dfa276aeb0ea379cc28-1bf124bb97861b5c768cdc2408034889186997df.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookyo/BeeCMS/HEAD/src/static/poster/5dbd8dfa276aeb0ea379cc28-1bf124bb97861b5c768cdc2408034889186997df.jpg -------------------------------------------------------------------------------- /src/static/poster/5df4a81f8bf583067be30b4f-7e3e6709c93d70cf3b56f10862786605bba12bb4.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bookyo/BeeCMS/HEAD/src/static/poster/5df4a81f8bf583067be30b4f-7e3e6709c93d70cf3b56f10862786605bba12bb4.jpeg -------------------------------------------------------------------------------- /email/sendCode.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | head 4 | style(type="text/css"). 5 | .firstline { 6 | font-weight: bold; 7 | } 8 | body 9 | p.firstline 欢迎加入beecms社区 10 | p 您的验证码为:#{code}。 11 | p 请使用您验证码进行注册或者修改密码。 -------------------------------------------------------------------------------- /src/middleware/isadmin.js: -------------------------------------------------------------------------------- 1 | export default function ({ store, redirect }) { 2 | if (!store.state.authUser) { 3 | return redirect("/") 4 | } else { 5 | if (!store.state.authUser.isAdmin) { 6 | return redirect("/") 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /src/assets/variables.scss: -------------------------------------------------------------------------------- 1 | $body-font-family: Roboto, "Noto Sans CJK SC", "Noto Sans SC", "Microsoft Yahei", 2 | Arial, Helvetica, sans-serif; 3 | $heading-font-family: Roboto, "Noto Sans CJK SC", "Noto Sans SC", "Microsoft Yahei", 4 | Arial, Helvetica, sans-serif; 5 | @import '~vuetify/src/styles/styles.sass'; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 蜂窝创作平台:BeeCMS,让每个人都能创作,全内容全域UGC平台。所有内容全部用户产生,web2.0全域CMS。 2 | 3 | 技术栈:Graphql,mongodb,nodejs,vuetify,nuxtjs,expressjs,redis。 4 | 5 | - 官网:https://www.iqi360.com/p/BeeCMS 6 | - 演示站:https://beefun.cc https://yize-ad.com 7 | 8 | 功能介绍: 9 | - 一、四大内容模块:视频、小说/教程、图集/漫画、自媒体,可独立使用其中一个或多个模块。 10 | - 二、前后端完全分离,后端完全采用Graphql服务器,完备的API层面的用户权限控制。 11 | - 三、演示前端采用nuxtjs和vuetify开发,效果极佳。 12 | - 四、可单独作为API后端使用,前端可对接安卓/IOS,web,小程序等一切平台,使用同一套API规则。 -------------------------------------------------------------------------------- /email/index.js: -------------------------------------------------------------------------------- 1 | const { emailSender } = require('@keystonejs/email'); 2 | 3 | const pugEmailSender = emailSender.pug({ 4 | root: __dirname, 5 | transport: 'mailgun', 6 | }); 7 | 8 | const sendEmail = (templatePath, rendererProps, options) => { 9 | if (!templatePath) { 10 | console.error('No template path provided'); 11 | } 12 | return pugEmailSender(templatePath).send(rendererProps, options); 13 | }; 14 | 15 | module.exports = { 16 | sendEmail, 17 | }; -------------------------------------------------------------------------------- /src/plugins/toast.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VuetifyNotify from 'vuetify-notify' 3 | 4 | export default ({ app }, inject) => { 5 | window.onNuxtReady(() => { 6 | Vue.use(VuetifyNotify, { 7 | vuetify: app.vuetify, 8 | container: '__nuxt', 9 | options: { 10 | toast: { 11 | x: 'center', 12 | y: 'top', 13 | color: 'primary' 14 | }, 15 | dialog: { 16 | width: 300 17 | } 18 | } 19 | }) 20 | }) 21 | } -------------------------------------------------------------------------------- /.envdemo: -------------------------------------------------------------------------------- 1 | ENDPOINT=http://127.0.0.1:3000/admin/api 2 | MONGOURI=mongodb://beecms:beecms@127.0.0.1:27017/beecms 3 | COOKIESECRET=beecmsdocsmyfever 4 | CODE_EXPIRY=480000 5 | HOST=http://127.0.0.1:3000 6 | 7 | TENCENTCLOUD_SECRET_ID=yoursecret 8 | TENCENTCLOUD_SECRET_KEY=yourkey 9 | 10 | CLOUD_HOST=image.querydata.org 11 | BUCKET=beecms 12 | PUBLICURL=https://image.querydata.org/beecms 13 | ACCESS_ID=79c1bf 14 | SECRET_KEY=3ed8b 15 | USE_SSL=on 16 | 17 | MAILGUN_DOMAIN=mail.bai.moe 18 | MAILGUN_API_KEY=269 19 | MAILGUN_FROM=admin@bai.moe -------------------------------------------------------------------------------- /src/layouts/error.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | -------------------------------------------------------------------------------- /src/components/headerTitle.vue: -------------------------------------------------------------------------------- 1 | 13 | -------------------------------------------------------------------------------- /src/components/widget.vue: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /src/components/notification.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /src/components/plugins/image.js: -------------------------------------------------------------------------------- 1 | import { Image as TiptapImage } from "tiptap-extensions"; 2 | import { TextSelection } from "tiptap"; 3 | 4 | export default class Image extends TiptapImage { 5 | commands({ type }) { 6 | return attrs => (state, dispatch) => { 7 | const { selection,doc,tr,schema } = state 8 | const position = selection.$cursor ? selection.$cursor.pos : selection.$to.pos 9 | const node = type.create(attrs) 10 | const thetype = schema.nodes['paragraph'] 11 | const transaction = state.tr.insert(position, node).insert().insert(doc.content.size, thetype.create()) 12 | dispatch(transaction) 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { keystone, apps } = require('./index.js'); 3 | const api = require('./controller/api'); 4 | keystone 5 | .prepare({ 6 | apps: apps, 7 | dev: process.env.NODE_ENV !== 'production', 8 | }) 9 | .then(async ({ middlewares }) => { 10 | await keystone.connect(); 11 | const app = express(); 12 | app.set('trust proxy', true); 13 | app.use(express.json({ limit: '5mb' })); 14 | app.use(express.urlencoded({ limit: '5mb', extended: false })); 15 | app.get('/api/presign', api.getPreSignUrl); 16 | app.post('/api/payback', api.cloudServerPayback); 17 | app.post('/codepayback', api.codePayback); 18 | app.use(middlewares).listen(3000); 19 | }); 20 | -------------------------------------------------------------------------------- /src/components/tab.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | -------------------------------------------------------------------------------- /src/components/itemSection.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | -------------------------------------------------------------------------------- /src/components/hero.vue: -------------------------------------------------------------------------------- 1 | 26 | -------------------------------------------------------------------------------- /src/components/card.vue: -------------------------------------------------------------------------------- 1 | 26 | -------------------------------------------------------------------------------- /src/components/sideCard.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | -------------------------------------------------------------------------------- /src/components/floatMenu.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | -------------------------------------------------------------------------------- /src/pages/beecms/index.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | -------------------------------------------------------------------------------- /src/pages/videos/index.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | -------------------------------------------------------------------------------- /src/assets/app.css: -------------------------------------------------------------------------------- 1 | .theme--dark.v-navigation-drawer { 2 | background-color: #1E1E1E; 3 | } 4 | a { 5 | text-decoration: none; 6 | } 7 | .v-card__text pre { 8 | line-height: 1.4; 9 | padding: 1.25rem 1.5rem; 10 | margin: .85rem 0; 11 | background-color: #282c34; 12 | border-radius: 6px; 13 | overflow: auto; 14 | } 15 | .v-card__text pre code { 16 | font-weight: 400; 17 | margin: 0; 18 | font-size: .85em; 19 | color: #fff; /* #fff */ 20 | padding: 0; 21 | background-color: transparent; 22 | border-radius: 0; 23 | box-shadow: none; 24 | } 25 | .v-card__text pre code:after, .v-card__text pre code:before { 26 | content: ""; 27 | } 28 | .v-card__text p { 29 | color:#F5F5F5; 30 | /* color: hsla(0,0%,100%,.85); */ 31 | } 32 | .v-card__text p img { 33 | max-width: 100%; 34 | } 35 | .v-card__text h1 { 36 | font-size: 1.25rem !important; 37 | font-weight: 500; 38 | line-height: 2rem; 39 | letter-spacing: 0.0125em !important; 40 | margin-bottom: 12px; 41 | color:#fff; 42 | } 43 | .v-card__text h2,.v-card__text h3,.v-card__text h4 { 44 | font-size: 1rem !important; 45 | font-weight: 400; 46 | letter-spacing: 0.009375em !important; 47 | line-height: 1.75rem; 48 | color:#fff; 49 | margin-bottom:12px; 50 | } 51 | .v-card__text h2 { 52 | font-size: 1.125rem !important; 53 | } 54 | .v-card__text blockquote { 55 | padding: 16px 0 16px 24px; 56 | font-size: .875rem; 57 | font-weight: 400; 58 | border-left:#2196f3 solid 4px; 59 | margin-bottom: 16px; 60 | } 61 | .v-card__text blockquote p { 62 | margin:0; 63 | } 64 | .v-card__text hr { 65 | display: block; 66 | flex: 1 1 0px; 67 | max-width: 100%; 68 | height: 0; 69 | max-height: 0; 70 | border: solid; 71 | border-width: thin 0 0; 72 | transition: inherit; 73 | border-color: hsla(0,0%,100%,.12); 74 | margin-bottom: 16px; 75 | } 76 | .v-card__subtitle, .v-card__text { 77 | font-size: .875rem; 78 | font-weight: 400; 79 | line-height: 1.375rem; 80 | letter-spacing: .0071428571em; 81 | } 82 | .caption1 { 83 | font-weight: 400; 84 | line-height: 1.25rem; 85 | font-size: 0.75rem; 86 | } 87 | .v-card__title h1 { 88 | font-size: 1.25rem; 89 | font-weight: 500; 90 | } 91 | .v-application .webkit-line2 { 92 | display: -webkit-box!important; 93 | -webkit-box-orient: vertical; 94 | -webkit-line-clamp: 2; 95 | word-wrap: break-word!important; 96 | word-break: break-word!important; 97 | white-space: normal!important; 98 | overflow: hidden!important; 99 | -o-text-overflow: ellipsis!important; 100 | text-overflow: ellipsis!important; 101 | } 102 | .v-application .right-title { 103 | display: block; 104 | font-size: 0.9rem!important; 105 | font-weight: 400; 106 | line-height: 1.1rem; 107 | letter-spacing: .009375em!important; 108 | } 109 | .order-warning { 110 | border-radius: 4px; 111 | border:1px dashed rgba(255, 255, 255, 0.12); 112 | padding: 15px; 113 | text-align: center; 114 | } 115 | .chapters-wrapper .v-slide-group__wrapper { 116 | touch-action: unset; 117 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bee_cms", 3 | "description": "A opensource web2.0 cms project", 4 | "private": true, 5 | "version": "1.0.0", 6 | "author": "yuebook", 7 | "license": "MIT", 8 | "engines": { 9 | "node": ">=12.4.0" 10 | }, 11 | "scripts": { 12 | "dev": "cross-env NODE_ENV=development DISABLE_LOGGING=true keystone dev", 13 | "build": "cross-env NODE_ENV=production keystone build", 14 | "update": "manypkg upgrade @keystonejs", 15 | "start": "cross-env NODE_ENV=production keystone start" 16 | }, 17 | "dependencies": { 18 | "@chenfengyuan/vue-qrcode": "^1.0.1", 19 | "@elastic/elasticsearch": "^7.8.0", 20 | "@hapi/joi": "^17.1.1", 21 | "@keystonejs/adapter-mongoose": "10.0.1", 22 | "@keystonejs/app-admin-ui": "7.3.11", 23 | "@keystonejs/app-graphql": "6.1.3", 24 | "@keystonejs/app-nuxt": "5.1.6", 25 | "@keystonejs/app-static": "5.1.3", 26 | "@keystonejs/auth-password": "5.1.17", 27 | "@keystonejs/email": "^5.2.0", 28 | "@keystonejs/fields": "20.1.3", 29 | "@keystonejs/fields-authed-relationship": "1.0.15", 30 | "@keystonejs/fields-datetime-utc": "7.0.0", 31 | "@keystonejs/fields-markdown": "5.2.12", 32 | "@keystonejs/fields-wysiwyg-tinymce": "5.3.14", 33 | "@keystonejs/file-adapters": "7.0.8", 34 | "@keystonejs/keystone": "17.1.2", 35 | "@keystonejs/list-plugins": "7.1.4", 36 | "@keystonejs/server-side-graphql-client": "1.1.2", 37 | "@manypkg/cli": "^0.16.1", 38 | "@mdi/js": "^5.3.45", 39 | "@nuxtjs/apollo": "^4.0.0-rc17", 40 | "@nuxtjs/markdownit": "latest", 41 | "@nuxtjs/moment": "latest", 42 | "@nuxtjs/vuetify": "^1.11.2", 43 | "axios": "^0.19.2", 44 | "body-parser": "^1.19.0", 45 | "connect-mongo": "^3.1.2", 46 | "core-js": "2.5.7", 47 | "cross-env": "^5.2.0", 48 | "dotenv": "latest", 49 | "express": "^4.17.1", 50 | "express-session": "^1.17.0", 51 | "filepond": "^4.21.1", 52 | "filepond-plugin-file-metadata": "^1.0.7", 53 | "filepond-plugin-file-poster": "^2.3.1", 54 | "filepond-plugin-file-validate-size": "^2.2.2", 55 | "filepond-plugin-file-validate-type": "^1.2.5", 56 | "filepond-plugin-image-crop": "^2.0.4", 57 | "filepond-plugin-image-exif-orientation": "^1.0.9", 58 | "filepond-plugin-image-preview": "^4.6.4", 59 | "filepond-plugin-image-resize": "^2.0.7", 60 | "filepond-plugin-image-transform": "^3.7.4", 61 | "filepond-plugin-media-preview": "^1.0.7", 62 | "graphql-tag": "^2.10.1", 63 | "ioredis": "^4.17.3", 64 | "joi-objectid": "^3.0.1", 65 | "lodash": "^4.17.15", 66 | "minio": "^7.0.16", 67 | "moment": "^2.27.0", 68 | "phone": "^2.4.12", 69 | "pug": "^3.0.0", 70 | "rate-limiter-flexible": "^2.1.7", 71 | "sanitize-html": "^1.27.0", 72 | "sharp": "^0.25.4", 73 | "stylus": "^0.54.5", 74 | "stylus-loader": "^3.0.2", 75 | "tencentcloud-sdk-nodejs": "^3.0.196", 76 | "tiptap": "^1.29.1", 77 | "tiptap-extensions": "^1.31.1", 78 | "vue-filepond": "^6.0.3", 79 | "vue-notification": "^1.3.20", 80 | "vue-script2": "^2.1.0", 81 | "vuetify-notify": "^1.0.6" 82 | }, 83 | "resolutions": { 84 | "prosemirror-model": "1.10.0", 85 | "graphql": "15.3.0" 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/pages/videos/tag/_id.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | -------------------------------------------------------------------------------- /src/components/userInfo.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | -------------------------------------------------------------------------------- /src/pages/bees/index.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | -------------------------------------------------------------------------------- /src/pages/novels/index.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | -------------------------------------------------------------------------------- /helper/sms.js: -------------------------------------------------------------------------------- 1 | const tencentcloud = require("tencentcloud-sdk-nodejs"); 2 | require('dotenv').config(); 3 | // 导入对应产品模块的client models。 4 | const smsClient = tencentcloud.sms.v20190711.Client; 5 | const models = tencentcloud.sms.v20190711.Models; 6 | 7 | const Credential = tencentcloud.common.Credential; 8 | const ClientProfile = tencentcloud.common.ClientProfile; 9 | const HttpProfile = tencentcloud.common.HttpProfile; 10 | 11 | exports.sendsms = function (phone, code, callback) { 12 | /* 必要步骤: 13 | * 实例化一个认证对象,入参需要传入腾讯云账户密钥对secretId,secretKey。 14 | * 这里采用的是从环境变量读取的方式,需要在环境变量中先设置这两个值。 15 | * 你也可以直接在代码中写死密钥对,但是小心不要将代码复制、上传或者分享给他人, 16 | * 以免泄露密钥对危及你的财产安全。 17 | * CAM密匙查询: https://console.cloud.tencent.com/cam/capi*/ 18 | //let cred = new Credential(process.env.TENCENTCLOUD_SECRET_ID, process.env.TENCENTCLOUD_SECRET_KEY); 19 | let cred = new Credential(process.env.TENCENTCLOUD_SECRET_ID, process.env.TENCENTCLOUD_SECRET_KEY); 20 | /* 非必要步骤: 21 | * 实例化一个客户端配置对象,可以指定超时时间等配置 */ 22 | let httpProfile = new HttpProfile(); 23 | /* SDK默认使用POST方法。 24 | * 如果你一定要使用GET方法,可以在这里设置。GET方法无法处理一些较大的请求 */ 25 | httpProfile.reqMethod = "POST"; 26 | /* SDK有默认的超时时间,非必要请不要进行调整 27 | * 如有需要请在代码中查阅以获取最新的默认值 */ 28 | httpProfile.reqTimeout = 30; 29 | httpProfile.endpoint = "sms.tencentcloudapi.com"; 30 | 31 | // 实例化一个client选项,可选的,没有特殊需求可以跳过。 32 | let clientProfile = new ClientProfile(); 33 | /* SDK默认用TC3-HMAC-SHA256进行签名,非必要请不要修改这个字段 */ 34 | clientProfile.signMethod = "HmacSHA256"; 35 | clientProfile.httpProfile = httpProfile; 36 | 37 | /* SDK会自动指定域名。通常是不需要特地指定域名的,但是如果你访问的是金融区的服务 38 | * 则必须手动指定域名,例如sms的上海金融区域名: sms.ap-shanghai-fsi.tencentcloudapi.com 39 | * 实例化要请求产品(以sms为例)的client对象 40 | * 第二个参数是地域信息,可以直接填写字符串ap-guangzhou,或者引用预设的常量 */ 41 | let client = new smsClient(cred, "ap-guangzhou", clientProfile); 42 | 43 | /* 实例化一个请求对象,根据调用的接口和实际情况,可以进一步设置请求参数 44 | * 你可以直接查询SDK源码确定SendSmsRequest有哪些属性可以设置 45 | * 属性可能是基本类型,也可能引用了另一个数据结构 46 | * 推荐使用IDE进行开发,可以方便的跳转查阅各个接口和数据结构的文档说明 */ 47 | let req = new models.SendSmsRequest(); 48 | 49 | /* 基本类型的设置: 50 | * SDK采用的是指针风格指定参数,即使对于基本类型你也需要用指针来对参数赋值。 51 | * SDK提供对基本类型的指针引用封装函数 52 | * 帮助链接: 53 | * 短信控制台: https://console.cloud.tencent.com/sms/smslist 54 | * sms helper: https://cloud.tencent.com/document/product/382/3773 */ 55 | 56 | /* 短信应用ID: 短信SdkAppid在 [短信控制台] 添加应用后生成的实际SdkAppid,示例如1400006666 */ 57 | req.SmsSdkAppid = "1400159268"; 58 | /* 短信签名内容: 使用 UTF-8 编码,必须填写已审核通过的签名,签名信息可登录 [短信控制台] 查看 */ 59 | req.Sign = "百萌科技"; 60 | /* 短信码号扩展号: 默认未开通,如需开通请联系 [sms helper] */ 61 | req.ExtendCode = ""; 62 | /* 国际/港澳台短信 senderid: 国内短信填空,默认未开通,如需开通请联系 [sms helper] */ 63 | req.SenderId = ""; 64 | /* 用户的 session 内容: 可以携带用户侧 ID 等上下文信息,server 会原样返回 */ 65 | req.SessionContext = ""; 66 | /* 模板 ID: 必须填写已审核通过的模板 ID。模板ID可登录 [短信控制台] 查看 */ 67 | req.TemplateID = "227530"; 68 | req.PhoneNumberSet = [phone]; 69 | req.TemplateParamSet = [code]; //数组具体的元素个数和模板中变量个数必须一致,例如事例中templateId:5678对应一个变量,参数数组中元素个数也必须是一个 70 | // 通过client对象调用想要访问的接口,需要传入请求对象以及响应回调函数 71 | client.SendSms(req, callback); 72 | } 73 | 74 | // // 通过client对象调用想要访问的接口,需要传入请求对象以及响应回调函数 75 | // client.SendSms(req, function (err, response) { 76 | // // 请求异常返回,打印异常信息 77 | // if (err) { 78 | // console.log(err); 79 | // return; 80 | // } 81 | // // 请求正常返回,打印response对象 82 | // console.log(response.to_json_string()); 83 | // }); 84 | -------------------------------------------------------------------------------- /src/pages/comics/index.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | -------------------------------------------------------------------------------- /src/pages/novels/tag/_id.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | -------------------------------------------------------------------------------- /src/pages/comics/tag/_id.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | -------------------------------------------------------------------------------- /src/pages/search.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | -------------------------------------------------------------------------------- /src/pages/bees/tag/_id.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | -------------------------------------------------------------------------------- /src/components/cardTable.vue: -------------------------------------------------------------------------------- 1 | 44 | -------------------------------------------------------------------------------- /helper/common.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | exports.randomcode = function () { 3 | var num = "" 4 | for (var i = 0; i < 4; i++) { 5 | num += Math.floor(Math.random() * 10) 6 | } 7 | return num 8 | } 9 | exports.validateEmail = function(email) { 10 | const re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 11 | return re.test(String(email).toLowerCase()); 12 | } 13 | exports.combinedata = function (existingItem, resolvedData) { 14 | let data = {}; 15 | if (!existingItem) { 16 | data = resolvedData; 17 | } else { 18 | data = { ...existingItem, ...resolvedData }; 19 | } 20 | return data; 21 | } 22 | exports.deleteHtmlTag = function(html) { 23 | return html.replace(/<[^>]+>/g,"");//去掉所有的html标记 24 | } 25 | exports.getHd = function(width) { 26 | let hd = ''; 27 | if (width === 320) { 28 | hd = '240P'; 29 | } else if (width === 480) { 30 | hd = '360P'; 31 | } else if (width === 640) { 32 | hd = '480P'; 33 | } else if (width === 1138) { 34 | hd = '640P'; 35 | } else if (width === 1280) { 36 | hd = '720P'; 37 | } else if (width === 1920) { 38 | hd = '1080P'; 39 | } else if (width === 2560) { 40 | hd = '2k'; 41 | } else if (width === 20000) { 42 | hd = '原画'; 43 | } 44 | return hd; 45 | } 46 | exports.checkId = function(id) { 47 | var reg = /^[a-f0-9]{24}$/; 48 | return reg.test(id) 49 | } 50 | exports.ksort = function (inputArr, sort_flags) { //JS版对象排序 51 | var tmp_arr = {}, 52 | keys = [], 53 | sorter, i, k, that = this, 54 | strictForIn = false, 55 | populateArr = {}; 56 | 57 | switch (sort_flags) { 58 | case 'SORT_STRING': 59 | // compare items as strings 60 | sorter = function (a, b) { 61 | return that.strnatcmp(a, b); 62 | }; 63 | break; 64 | case 'SORT_LOCALE_STRING': 65 | // compare items as strings, original by the current locale (set with i18n_loc_set_default() as of PHP6) 66 | var loc = this.i18n_loc_get_default(); 67 | sorter = this.php_js.i18nLocales[loc].sorting; 68 | break; 69 | case 'SORT_NUMERIC': 70 | // compare items numerically 71 | sorter = function (a, b) { 72 | return ((a + 0) - (b + 0)); 73 | }; 74 | break; 75 | // case 'SORT_REGULAR': // compare items normally (don't change types) 76 | default: 77 | sorter = function (a, b) { 78 | var aFloat = parseFloat(a), 79 | bFloat = parseFloat(b), 80 | aNumeric = aFloat + '' === a, 81 | bNumeric = bFloat + '' === b; 82 | if (aNumeric && bNumeric) { 83 | return aFloat > bFloat ? 1 : aFloat < bFloat ? -1 : 0; 84 | } else if (aNumeric && !bNumeric) { 85 | return 1; 86 | } else if (!aNumeric && bNumeric) { 87 | return -1; 88 | } 89 | return a > b ? 1 : a < b ? -1 : 0; 90 | }; 91 | break; 92 | } 93 | 94 | // Make a list of key names 95 | for (k in inputArr) { 96 | if (inputArr.hasOwnProperty(k)) { 97 | keys.push(k); 98 | } 99 | } 100 | keys.sort(sorter); 101 | 102 | // BEGIN REDUNDANT 103 | this.php_js = this.php_js || {}; 104 | this.php_js.ini = this.php_js.ini || {}; 105 | // END REDUNDANT 106 | strictForIn = this.php_js.ini['phpjs.strictForIn'] && this.php_js.ini['phpjs.strictForIn'].local_value && this.php_js 107 | .ini['phpjs.strictForIn'].local_value !== 'off'; 108 | populateArr = strictForIn ? inputArr : populateArr; 109 | 110 | // Rebuild array with sorted key names 111 | for (i = 0; i < keys.length; i++) { 112 | k = keys[i]; 113 | tmp_arr[k] = inputArr[k]; 114 | if (strictForIn) { 115 | delete inputArr[k]; 116 | } 117 | } 118 | for (i in tmp_arr) { 119 | if (tmp_arr.hasOwnProperty(i)) { 120 | populateArr[i] = tmp_arr[i]; 121 | } 122 | } 123 | 124 | return strictForIn || populateArr; 125 | } 126 | exports.md5 = function (data, codeBase) { //MD5加密 127 | var buf = typeof data == "string" ? codeBase == 'gbk' ? iconv.encode(data, 'gbk') : Buffer.from(data) : data; 128 | var str = buf.toString("binary"); 129 | return crypto.createHash("md5").update(str).digest("hex"); 130 | } -------------------------------------------------------------------------------- /src/pages/store.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import gql from "graphql-tag"; 2 | 3 | export const state = () => ({ 4 | setting: null, 5 | authUser: null, 6 | }) 7 | 8 | export const mutations = { 9 | SET_SETTING(state, setting) { 10 | state.setting = setting; 11 | }, 12 | SET_USER(state, user) { 13 | state.authUser = user; 14 | }, 15 | } 16 | 17 | export const actions = { 18 | async nuxtServerInit({ commit }, { req, app }) { 19 | const client = app.apolloProvider.defaultClient; 20 | const user = req.user; 21 | if(user) { 22 | commit('SET_USER', { 23 | id: req.user.id, 24 | isAdmin: req.user.isAdmin, 25 | name: req.user.name 26 | }) 27 | } 28 | }, 29 | async signup({ commit }, { email, password, name, code }) { 30 | const client = this.app.apolloProvider.defaultClient; 31 | try { 32 | const response = await client.mutate({ 33 | mutation: gql` 34 | mutation signup($email: String!, $password: String!, $name: String!, $code: String!) { 35 | createUserByCode(email: $email, password: $password, name: $name, code: $code) { 36 | id, 37 | name, 38 | } 39 | } 40 | `, 41 | variables: { 42 | email, password, name, code 43 | }, 44 | fetchPolicy: 'no-cache', 45 | }); 46 | const loginres = await client.mutate({ 47 | mutation: gql` 48 | mutation signin($email: String, $password: String) { 49 | authenticateUserWithPassword(email: $email, password: $password) { 50 | item { 51 | id, 52 | name, 53 | isAdmin 54 | } 55 | token 56 | } 57 | }`, 58 | variables: { email, password }, 59 | fetchPolicy: 'no-cache', 60 | } 61 | ); 62 | await client.resetStore(); 63 | const authenticateUserWithPassword = loginres.data.authenticateUserWithPassword; 64 | if (authenticateUserWithPassword && authenticateUserWithPassword.item) { 65 | commit('SET_USER', authenticateUserWithPassword.item) 66 | await this.$apolloHelpers.onLogin(authenticateUserWithPassword.token) 67 | } 68 | } catch (error) { 69 | throw error; 70 | } 71 | }, 72 | async login({ commit }, { email, password }) { 73 | const client = this.app.apolloProvider.defaultClient; 74 | try { 75 | const response = await client.mutate({ 76 | mutation: gql` 77 | mutation signin($email: String, $password: String) { 78 | authenticateUserWithPassword(email: $email, password: $password) { 79 | item { 80 | id, 81 | name, 82 | isAdmin 83 | } 84 | token 85 | } 86 | }`, 87 | variables: { email, password }, 88 | fetchPolicy: 'no-cache', 89 | } 90 | ) 91 | await client.resetStore(); 92 | const authenticateUserWithPassword = response.data.authenticateUserWithPassword; 93 | if (authenticateUserWithPassword && authenticateUserWithPassword.item) { 94 | commit('SET_USER', authenticateUserWithPassword.item) 95 | await this.$apolloHelpers.onLogin(authenticateUserWithPassword.token); 96 | } 97 | } catch (error) { 98 | throw error; 99 | } 100 | }, 101 | async signout({ commit }) { 102 | const client = this.app.apolloProvider.defaultClient; 103 | try { 104 | const response = await client.mutate({ 105 | mutation: gql` 106 | mutation { 107 | unauthenticateUser { 108 | success 109 | } 110 | } 111 | `, 112 | fetchPolicy: 'no-cache', 113 | }) 114 | const unauthenticateUser = response.data.unauthenticateUser; 115 | if (unauthenticateUser && unauthenticateUser.success) { 116 | commit("SET_USER", null); 117 | await this.$apolloHelpers.onLogout(); 118 | } 119 | } catch (error) { 120 | throw error; 121 | } 122 | } 123 | } -------------------------------------------------------------------------------- /controller/api.js: -------------------------------------------------------------------------------- 1 | const { keystone } = require('../index.js'); 2 | const { getItem, getItems, createItem, createItems, updateItem } = require('@keystonejs/server-side-graphql-client'); 3 | const common = require('../helper/common'); 4 | const { update } = require('lodash'); 5 | 6 | exports.getPreSignUrl = async function (req, res) { 7 | return res.json({ success: 1 }); 8 | } 9 | 10 | // 通知接口,用于转码服务器转码之后传递数据或传递错误信息。 11 | exports.cloudServerPayback = async (req, res) => { 12 | const { success, movie, apikey, apisecret } = req.body; 13 | console.log(req.body); 14 | if (!apikey || !apisecret) { 15 | return res.json({ success: 0 }); 16 | } 17 | const cloudServers = await getItems({ 18 | keystone, 19 | listKey: 'CloudServer', 20 | first: 1, 21 | where: { apiKey: apikey, apiSecret: apisecret }, 22 | returnFields: 'id,domain' 23 | }) 24 | console.log(cloudServers); 25 | if (cloudServers.length == 0) { 26 | return res.json({ success: 0, message: '没有找到云转码服务器数据!' }); 27 | } 28 | const cloudServerId = cloudServers[0].id; 29 | const episode = await getItem({ 30 | keystone, 31 | listKey: 'Episode', 32 | itemId: movie.clientId, 33 | returnFields: 'id,video { id, cover }' 34 | }); 35 | if(!episode) { 36 | return res.json({success: 0, message: '不存在此分集!'}); 37 | } 38 | const videoId = episode.video.id; 39 | const cover = episode.video.cover; 40 | if (success == 0) { 41 | // await updateItem({ 42 | // keystone, 43 | // listKey: 'Video', 44 | // item: { id: movie.clientId, data: { status: 'error' } }, 45 | // returnFields: 'id' 46 | // }); 47 | // 逻辑转移,将视频升级成可以上传多个分集的视频模块,每个分集可自定义价格。 48 | await updateItem({ 49 | keystone, 50 | listKey: 'Episode', 51 | item: { id: movie.clientId, data: { serverId: movie.id, server: { connect: { id: cloudServerId } }, status: 'error' } }, 52 | returnFields: 'id' 53 | }) 54 | return res.json({ success: 0, message: '转码失败!' }); 55 | } 56 | if (!movie) { 57 | return res.json({ success: 0, message: '没有传递视频数据' }); 58 | } 59 | let urls = ""; 60 | for (let i = 0; i < movie.m3u8paths.length; i++) { 61 | const m3u8 = movie.m3u8paths[i]; 62 | const hd = common.getHd(m3u8.hd); 63 | urls += hd + '$' + cloudServers[0].domain + m3u8.path.replace('./public', '') + '#'; 64 | } 65 | const playUrlObj = { 66 | title: movie.originalname, 67 | url: urls, 68 | server: { connect: { id: cloudServerId } } 69 | }; 70 | const newCover = cloudServers[0].domain + movie.poster; 71 | console.log(playUrlObj); 72 | // 占位,更新分集对应的视频的海报和标题。 73 | if(!cover) { 74 | await updateItem({ 75 | keystone, 76 | listKey: 'Video', 77 | item: { 78 | id: videoId, 79 | data: { 80 | cover: newCover 81 | } 82 | }, 83 | returnFields: 'id' 84 | }); 85 | } 86 | await updateItem({ 87 | keystone, 88 | listKey: 'Episode', 89 | item: { 90 | id: movie.clientId, 91 | data: { 92 | status: 'published', 93 | serverId: movie._id, 94 | server: { connect: { id: cloudServerId } }, 95 | url: urls 96 | } 97 | }, 98 | returnFields: 'id' 99 | }); 100 | return res.json({ success: 1 }); 101 | } 102 | 103 | // 码支付payback 104 | exports.codePayback = async (req, res) => { 105 | const body = req.body; 106 | const json = JSON.parse(JSON.stringify(body)); 107 | const data = common.ksort(json); 108 | const pays = await getItems({ 109 | keystone, 110 | listKey: "Pay", 111 | where: { type: "codePay" }, 112 | returnFields: `id,apiUrl,appId,secretKey`, 113 | }); 114 | const pay = pays[0]; 115 | let sign = ''; 116 | const order = await getItem({ 117 | keystone, 118 | listKey: "Order", 119 | itemId: body.pay_id, 120 | returnFields: ` 121 | id 122 | owner { 123 | id 124 | score 125 | } 126 | status 127 | goods { 128 | id 129 | score 130 | price 131 | } 132 | `, 133 | }); 134 | if (order.status == 'finished') { 135 | return res.send('success'); 136 | } 137 | for (var key in data) { 138 | if (data[key] == '' || key == 'sign') continue; 139 | sign += data[key] == '' ? '' : key + "=" + data[key] + "&"; 140 | } 141 | sign = sign.substring(0, sign.length - 1); //去掉最后一个&字符 142 | var key = common.md5(sign + pay.secretKey);//替换为自己的密钥 143 | if (!body.pay_no || key != body.sign) { 144 | res.send('fail'); 145 | } else { 146 | const goods = order.goods; 147 | if (goods.score) { 148 | await updateItem({ 149 | keystone, 150 | listKey: 'Order', 151 | item: { 152 | id: body.pay_id, 153 | data: { 154 | status: 'finished', 155 | } 156 | }, 157 | returnFields: 'id' 158 | }); 159 | const oldScore = order.owner.score; 160 | await updateItem({ 161 | keystone, 162 | listKey: 'User', 163 | item: { 164 | id: order.owner.id, 165 | data: { 166 | score: oldScore + goods.score 167 | } 168 | }, 169 | returnFields: 'id' 170 | }) 171 | res.send('success'); 172 | } 173 | } 174 | } -------------------------------------------------------------------------------- /src/pages/novels/_id/chapters/_cid/edit.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 186 | 187 | -------------------------------------------------------------------------------- /src/pages/comics/_id/index.vue: -------------------------------------------------------------------------------- 1 | 112 | -------------------------------------------------------------------------------- /src/pages/novels/_id/index.vue: -------------------------------------------------------------------------------- 1 | 112 | -------------------------------------------------------------------------------- /src/pages/novels/_id/add.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 254 | 255 | -------------------------------------------------------------------------------- /src/pages/index.vue: -------------------------------------------------------------------------------- 1 | 119 | 131 | -------------------------------------------------------------------------------- /src/static/tip.svg: -------------------------------------------------------------------------------- 1 | experience design -------------------------------------------------------------------------------- /src/pages/comics/_id/chapters/_cid/index.vue: -------------------------------------------------------------------------------- 1 | 63 | -------------------------------------------------------------------------------- /src/pages/bees/_id/index.vue: -------------------------------------------------------------------------------- 1 | 61 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const { Keystone } = require('@keystonejs/keystone'); 2 | const { PasswordAuthStrategy } = require('@keystonejs/auth-password'); 3 | const { GraphQLApp } = require('@keystonejs/app-graphql'); 4 | const { AdminUIApp } = require('@keystonejs/app-admin-ui'); 5 | const { NuxtApp } = require('@keystonejs/app-nuxt'); 6 | const { MongooseAdapter: Adapter } = require('@keystonejs/adapter-mongoose'); 7 | const expressSession = require('express-session'); 8 | const MongoStore = require('connect-mongo')(expressSession); 9 | const { createItems } = require('@keystonejs/server-side-graphql-client'); 10 | const { Image, User, Group, VipGroup, Novel, Comic, ComicChapter, Chapter, Code, customSchema, Tag, Video, CloudServer, PlayUrl, Bee, Setting, PushTag, PointOrder, Follow, Episode, PayType, Exchange, Pay, Store, Order} = require('./schema'); 11 | require('dotenv').config(); 12 | 13 | const config = { 14 | endpoint: process.env.ENDPOINT, 15 | keystoneconfig: { 16 | name: "beecms", 17 | queryLimits: { 18 | maxTotalResults: 1000, 19 | }, 20 | cookie: { 21 | secure: process.env.NODE_ENV === 'production', // Default to true in production 22 | maxAge: 1000 * 60 * 60 * 24 * 30, // 30 days 23 | sameSite: false, 24 | }, 25 | sessionStore: new MongoStore({ url: process.env.MONGOURI }), 26 | adapter: new Adapter({ 27 | mongoUri: process.env.MONGOURI, 28 | }), 29 | cookieSecret: process.env.COOKIESECRET, 30 | onConnect: async () => { 31 | const users = await keystone.lists.User.adapter.findAll(); 32 | if (!users.length) { 33 | // 创建基础用户组 34 | await createItems({ 35 | keystone, 36 | listKey: 'Group', 37 | items: [{ 38 | data: { 39 | name: 'LV0', 40 | score: 0, 41 | limitCreate: 1, 42 | needReview: true, 43 | } 44 | }, { 45 | data: { 46 | name: 'LV1', 47 | score: 600, 48 | limitCreate: 3, 49 | needReview: true, 50 | } 51 | }, { 52 | data: { 53 | name: 'LV2', 54 | score: 1200, 55 | limitCreate: 6, 56 | needReview: false, 57 | } 58 | }] 59 | }); 60 | // 初始化支付方式 61 | await createItems({ 62 | keystone, 63 | listKey: 'PayType', 64 | items: [ 65 | { 66 | data: { 67 | type: 'aliPay', 68 | }, 69 | }, 70 | { 71 | data: { 72 | type: 'qqPay', 73 | }, 74 | }, 75 | { 76 | data: { 77 | type: 'wechatPay', 78 | }, 79 | }, 80 | ] 81 | }); 82 | // 创建一部分初始化标签示例 83 | const tags = ["剧情", "冒险", "奇幻", "歌舞", "战争", "恐怖", "纪录片", "传记", "惊悚", "犯罪", "悬疑", "动作", "科幻", "动画", "喜剧", "爱情"]; 84 | const tagsData = tags.map(function (tag) { return { title: tag } }); 85 | const comicTags = ["热血", "搞笑", "恋爱", "少女", "纯爱", "日常"]; 86 | const comicTagsData = comicTags.map(function (tag) { return { title: tag } }); 87 | const beeTags = ["游戏", "影视", "生活", "科技", "数码"]; 88 | const beeTagsData = beeTags.map(function (tag) { return { title: tag } }); 89 | const novelTags = ["言情", "都市", "玄幻", "仙侠", "轻小说"]; 90 | const novelTagsData = novelTags.map(function (tag) { return { title: tag } }); 91 | await createItems({ 92 | keystone, 93 | listKey: 'PushTag', 94 | items: [{ 95 | data: { 96 | type: 'video', 97 | tags: { create: tagsData } 98 | }, 99 | }, { 100 | data: { 101 | type: 'comic', 102 | tags: { create: comicTagsData } 103 | }, 104 | }, { 105 | data: { 106 | type: 'bee', 107 | tags: { create: beeTagsData } 108 | }, 109 | }, { 110 | data: { 111 | type: 'novel', 112 | tags: { create: novelTagsData } 113 | }, 114 | }] 115 | }); 116 | // 创建初始管理用户 117 | await createItems({ 118 | keystone, 119 | listKey: 'User', 120 | items: [{ 121 | data: { 122 | name: 'beecms', 123 | email: 'admin@admin.com', 124 | isAdmin: true, 125 | status: "verified", 126 | password: 'adminadmin' 127 | } 128 | }] 129 | }); 130 | // 创建初始设置 131 | await createItems({ 132 | keystone, 133 | listKey: 'Setting', 134 | items: [{ 135 | data: { 136 | title: '蜂窝创作平台', 137 | seoTitle: '蜂窝创作平台,让每个人都能创作', 138 | tax: 40 139 | } 140 | }] 141 | }); 142 | } 143 | } 144 | } 145 | }; 146 | const keystone = new Keystone(config.keystoneconfig); 147 | 148 | keystone.createList('Image', Image); 149 | keystone.createList('User', User); 150 | keystone.createList('Code', Code); 151 | keystone.createList('VipGroup', VipGroup); 152 | keystone.createList('Group', Group); 153 | keystone.createList('Novel', Novel); 154 | keystone.createList('Comic', Comic); 155 | keystone.createList('ComicChapter', ComicChapter); 156 | keystone.createList('Chapter', Chapter); 157 | keystone.createList('Tag', Tag); 158 | keystone.createList('PushTag', PushTag); 159 | keystone.createList('Video', Video); 160 | keystone.createList('CloudServer', CloudServer); 161 | keystone.createList('PlayUrl', PlayUrl); 162 | keystone.createList('Bee', Bee); 163 | keystone.createList('Setting', Setting); 164 | keystone.createList('PointOrder', PointOrder); 165 | keystone.createList('Follow', Follow); 166 | keystone.createList('Episode', Episode); 167 | keystone.createList('PayType', PayType); 168 | keystone.createList('Exchange', Exchange); 169 | keystone.createList('Pay', Pay); 170 | keystone.createList('Store', Store); 171 | keystone.createList('Order', Order); 172 | 173 | keystone.extendGraphQLSchema(customSchema); 174 | 175 | const authStrategy = keystone.createAuthStrategy({ 176 | type: PasswordAuthStrategy, 177 | list: 'User', 178 | config: { 179 | identityField: 'email', 180 | secretField: 'password', 181 | }, 182 | }); 183 | 184 | module.exports = { 185 | keystone, 186 | apps: [ 187 | new GraphQLApp({ 188 | apollo: { 189 | tracing: true, 190 | cacheControl: { 191 | defaultMaxAge: 3600, 192 | } 193 | } 194 | }), 195 | new AdminUIApp({ authStrategy }), 196 | new NuxtApp({ 197 | srcDir: 'src', 198 | buildDir: 'dist', 199 | cache: true, 200 | mode: 'universal', 201 | loading: { 202 | color: '#fff', 203 | }, 204 | /* 205 | ** Headers of the page 206 | */ 207 | head: { 208 | title: 'BeeCms', 209 | meta: [ 210 | { charset: 'utf-8' }, 211 | { name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=0' }, 212 | ], 213 | link: [ 214 | { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }, 215 | { rel: 'stylesheet', href: 'https://cdn.jsdelivr.net/npm/simplebar@latest/dist/simplebar.css' }, 216 | // { 217 | // rel: "stylesheet", 218 | // href: 219 | // "https://cdn.jsdelivr.net/npm/@mdi/font@4.x/css/materialdesignicons.min.css" 220 | // } 221 | ], 222 | script: [ 223 | { src: 'https://cdn.jsdelivr.net/npm/simplebar@5.2.1/dist/simplebar.min.js' } 224 | ], 225 | }, 226 | /* 227 | ** Load Vuetify into the app 228 | */ 229 | plugins: [ 230 | { src: "@/plugins/toast.js", ssr: false } 231 | ], 232 | build: { 233 | extractCSS: true, 234 | }, 235 | /* 236 | ** Load Vuetify CSS globally 237 | */ 238 | css: ['~/assets/app.css'], 239 | modules: ['@nuxtjs/apollo', 240 | ["@nuxtjs/moment", ["zh-cn"]], ['@nuxtjs/vuetify', { 241 | theme: { 242 | dark: true 243 | }, 244 | defaultAssets: { 245 | icons: false 246 | }, 247 | // customVariables: ['~/assets/variables.scss'], 248 | icons: { 249 | iconfont: 'mdiSvg', // default - only for display purposes 250 | }, 251 | }]], 252 | // markdownit: { 253 | // injected: true 254 | // }, 255 | apollo: { 256 | clientConfigs: { 257 | default: { 258 | httpEndpoint: config.endpoint, 259 | } 260 | }, 261 | cookieAttributes: { 262 | /** 263 | * Define when the cookie will be removed. Value can be a Number 264 | * which will be interpreted as days from time of creation or a 265 | * Date instance. If omitted, the cookie becomes a session cookie. 266 | */ 267 | expires: 30, 268 | 269 | /** 270 | * A Boolean indicating if the cookie transmission requires a 271 | * secure protocol (https). Defaults to false. 272 | */ 273 | secure: process.env.NODE_ENV === 'production', 274 | }, 275 | }, 276 | }), 277 | ], 278 | configureExpress: app => { 279 | app.set('trust proxy', true); 280 | }, 281 | }; 282 | -------------------------------------------------------------------------------- /src/pages/beecms/setting/cloudserver/index.vue: -------------------------------------------------------------------------------- 1 | 84 | -------------------------------------------------------------------------------- /src/pages/novels/_id/chapters/_cid/index.vue: -------------------------------------------------------------------------------- 1 | 93 | -------------------------------------------------------------------------------- /src/pages/beecms/bees/index.vue: -------------------------------------------------------------------------------- 1 | 127 | 128 | -------------------------------------------------------------------------------- /src/pages/beecms/comics/index.vue: -------------------------------------------------------------------------------- 1 | 135 | 136 | -------------------------------------------------------------------------------- /src/pages/beecms/novels/index.vue: -------------------------------------------------------------------------------- 1 | 137 | 138 | --------------------------------------------------------------------------------