开发注意事项
本地开发介绍
nvPress的主题完全属于前端开发,如果你是一位前端开发工作者,你可以使用任何你喜欢的框架进行开发,官方示例是通过vue.js进行开发的,所以本文将以vue.js为例展开介绍。
创建项目
你可以使用命令yarn create vite创建一个全新的项目,或者从nvPress官方下载主题开发示例。
项目目录结构
- front-end-web:主题打包后的代码,也可以是其他的目录名称
- front-end-source:主题源代码
- function.js:主题驱动文件,类似于vue项目中的main.js
- theme.json:主题信息文件,包含主题的版本以及作者信息等
- 其他的目录及文件
源码结构说明
主题分为:首页、分类页、内容页 三个部分。各部分代码分别在以下文件夹:
- 首页:front-end-source\src\pages\home\home.vue
- 分类页:front-end-source\src\pages\term\term.vue
- 文章或页面的内容页:front-end-source\pages\post\post.vue
这是主题的基础内容,其他的内容比如封装的组件,自定义的编辑器模块等,都可以根据自己的代码风格进行定义,当然如果你不喜欢默认的三个部分的目录结构,也可以进行修改。
只需要修改 front-end-source\src\router.js 中的组件引入定义即可
项目运行与部署
- 安装依赖:npm install
- 本地运行:npm run dev
- 项目打包:npm run build
打包说明
打包后需要将除了源代码(此处为/front-end-source)之外的所有目录都放进主题目录中
示例结构:
theme.json
front-end-web
functions.js
...
运行nvPress进行本地开发
1、将打包后的文件夹剪切到nvPress本地开发版的nv-themes文件夹中
2、nvpress启动方法
Windows 系统启动 nvPress 方法
- 在资源管理器中打开nvPress本地开发文件夹
- 路径定位到nvPress本地开发文件夹:在空白处按住shift点击鼠标右键,选择:在此处打开命令行
- 输入nvpress.exe然后回车
macOS 系统启动 nvPress 方法
- 打开终端app
- 路径定位到nvPress本地开发文件夹,输入./nvpress 回车
3、此时,终端中会显示nvPress启动成功并告诉你后台地址。
4、访问nvPress后台地址http://localhost:8081/nv-admin/
5、完成简单的注册流程后,登录后台打开外观-主题
6、启动你开发的主题,然后重启 nvPress
使用functions开发主题
nvPress官方为我们提供了restApi和functions api,在此基础上可以完成与数据的交互和主题的配置
配置渲染模式
set_frontstage_rendering_mode(renderType, dirPath);
renderType:SSR服务器端渲染模式或者CSR客户端渲染模式
dirPath:配置主题打包文件目录,默认为/web
注册导航菜单
register_nav_menus(options);
options:导航菜单
register_nav_menus({
topNav: '顶部菜单',
catNav: '分类菜单',
})
增加后台配置菜单
add_submenu_page 官方文档
示例
add_submenu_page({
parent_slug: 'appearance',
page_title: '主题设置',
menu_title: '主题设置',
menu_slug: 'theme-settings',
power: 10,
position: 9,
component_url: '/srcs/page-settings/index.vue',
});
注册静态资源根路径
register_static_url(url_path, folder_path)
url_path:URL地址,如:"/srcs"
folder_path:指向的本地文件夹地址,如:path.join(__dirname,"./srcs/")
比如我们需要开发一些在编辑器中使用的小组件,并且打算将其放在根目录下的/srcs目录中,就可以在function.js中使用这个钩子注册
注册REST API
register_rest_route(namespace, name, args)
示例
// 查询最近5条已发布公告列表,按照默认排序倒序查询
register_rest_route('salary', 'bulletin-post', {
methods: 'post',
callback(data, req) {
var posts = query_posts({
post_type: 'bulletin',
status: 'publish',
order: 'DESC',
posts_per_page: 5,
});
return posts.data;
},
});
注册自定义编辑器模块
register_editor_block(block_id, supports, script_url)
示例:
register_editor_block('scottstudio/alert', ['page', 'article'], '/srcs/block-alert/index.js');
nvPress使用的编辑器是editor.js,一般的自定义模块包含三个文件
- index.js 注册文件
- settings_ui.vue 自定义模块的工具配置文件(比如可以修改模块的文字排版和颜色)
- ui.vue 自定义模块的ui渲染文件
示例代码:
// index.js
export default ({ register_block_type }) => {
register_block_type('scottstudio/title', {
// 编辑器内显示的标题
name: '小标题',
// 编辑器内显示的图标
icon: ``,
// 模块的属性
attributes: {
tag: 'h2',
style: 1,
text: '',
},
sanitize(editor) {
return {};
},
// 模块主要界面
editor: {
url: '/srcs/block-title/ui.vue',
},
// 模块的配置项
settings: {
url: '/srcs/block-title/settings_ui.vue',
},
});
};
// ui.vue
<template>
<div class="salary-title" :data-style="style" :data-tag="tag">
<div class="flex items-center justify-start relative overflow-hidden">
<div class="icon"><i class="fa-solid fa-hashtag"></i></div>
<richText class="title" :tag="tag" v-model:value="text" nowrap />
</div>
</div>
</template>
<script>
export default {
name: 'scottstudio-title-block',
components: {
richText: nv.components.richText,
},
data() {
// 这里是index.js中设置的attributes
return {
tag: 'h2',
style: 1,
text: '',
};
},
mounted() {
// 加载默认数据
nv.block.loadDefaultData.bind(this)();
},
};
</script>
<style scoped>
</style>
// settings_ui.vue
<template>
<div>
<div class="tune-buttons">
<button class="cdx-settings-button" :class="{ 'cdx-settings-button--active': tag == 'h2' }" @click="tag = 'h2'"><strong>H2</strong></button>
<button class="cdx-settings-button" :class="{ 'cdx-settings-button--active': tag == 'h3' }" @click="tag = 'h3'"><strong>H3</strong></button>
<button class="cdx-settings-button" :class="{ 'cdx-settings-button--active': tag == 'p' }" @click="tag = 'p'"><strong>P</strong></button>
</div>
<div class="tune-buttons border-bottom">
<button class="cdx-settings-button" :class="{ 'cdx-settings-button--active': style == 1 }" @click="style = 1">样式1</button>
<button class="cdx-settings-button" :class="{ 'cdx-settings-button--active': style == 2 }" @click="style = 2">样式2</button>
</div>
</div>
</template>
<script>
export default {
name: 'scottstudio-title-settings',
components: {},
data() {
return {
tag: 'h2',
style: 1,
text: '',
};
},
mounted() {
// 加载默认数据
nv.block.loadDefaultData.bind(this)();
},
methods: {
handleTypeSelect(type) {
this.type = type;
},
},
};
</script>
<style scoped>
</style>
同时我们的源代码中也需要对模块进行渲染
// /src/components/block-parser/scott-studio-title.vue
<template>
<div class="salary-category">
<div class="flex items-center">
<div class="icon"><i class="fa-solid fa-hashtag"></i></div>
<component class="title" :is="data.tag" v-html="data.text" />
</div>
</div>
</template>
<script setup lang="ts">
interface ITitleProps {
style: number;
tag: string;
text: string;
}
defineProps<{ data: ITitleProps }>();
</script>
<style lang="less" scoped>
</style>
这里我们定义一个blockParser组件,统一对所有的编辑器模块进行渲染
// /src/components/block-parser/parser.vue
<template>
<component :is="is" class="nv-blocks">
<p v-if="blocks.length == 0">暂无数据</p>
<template v-for="block in blocks">
<component :is="`block-${block.type.replace(/\//g, '-')}`" :data="block.data" :data-block-id="block.id" />
</template>
</component>
</template>
<script>
import { defineComponent, defineAsyncComponent, computed } from 'vue';
const files = import.meta.glob('./block-*.vue');
const modules = {};
for (const key in files) {
modules[key.replace(/(\.\/|\.vue)/g, '')] = defineAsyncComponent(files[key]);
}
export default defineComponent({
name: 'block-parser',
components: {
...modules,
},
props: {
is: {
type: String,
default: 'div',
},
blocks: {
type: Array,
},
},
});
</script>
<style>
.nv-blocks a {
color: var(--primary-color);
text-decoration: none;
text-shadow: 2px 2px 2px var(--primary-opacity-3);
}
.nv-blocks mark {
background: rgb(var(--orangered-1));
border-radius: 5px;
color: rgb(var(--orangered-6));
padding: 3px 5px;
margin: 0 3px;
}
</style>
在页面中使用:
import blockParser from '@/components/block-parser/parser.vue';
<blockParser :blocks="data.content.blocks" />
增加后台配置菜单
以主题设置为例:
获取主题设置的REST API /nv/get-options
更新主题设置的REST API /nv/set-options
就像写普通的vue页面一样,先获取到已有的配置,并且塞入到表单内,同时保存表单时提交更新设置
<template>
<div class="nv-admin-page">
<div class="page-title">
<span>{{ $route.meta.title }}</span>
</div>
<div class="page-content flex-grow">
<pd-form :config="formConfig" :data="formData" @submit="handleSubmit">
<template v-slot:rewardLinks>
<nvSettingTable add-label="添加图片" :columns="rewardLinksColumns" v-model:data="formData.scott_reward_links">
<template v-slot:column-image="row">
<nv-thumbnail-selector :height="40" v-model:value="formData.scott_reward_links[row.$index].image" />
</template>
<template v-slot:column-text="row">
<n-input v-model:value="formData.scott_reward_links[row.$index].text" />
</template>
<template v-slot:column-url="row">
<n-input placeholder="http(s)://" v-model:value="formData.scott_reward_links[row.$index].url" />
</template>
</nvSettingTable>
</template>
<template v-slot:personalLinks>
<nvSettingTable add-label="添加链接" :columns="personalLinksColumns" v-model:data="formData.scott_personal_links">
<template v-slot:column-image="row">
<nv-thumbnail-selector :height="40" v-model:value="formData.scott_personal_links[row.$index].image" />
</template>
<template v-slot:column-text="row">
<n-input v-model:value="formData.scott_personal_links[row.$index].text" />
</template>
<template v-slot:column-url="row">
<n-input placeholder="http(s)://" v-model:value="formData.scott_personal_links[row.$index].url" />
</template>
</nvSettingTable>
</template>
</pd-form>
</div>
</div>
</template>
<script>
export default {
name: 'niRvana-theme-settings',
data() {
return {
formConfig: {
form: {
labelWidth: '7em',
size: 'large',
submitText: '保存设置',
},
items: [
{
label: '打赏图片设置',
type: 'FormSubtitle',
},
{
custom_type: 'rewardLinks',
prop: 'scott_reward_links',
},
{
label: '个人外链设置',
type: 'FormSubtitle',
},
{
custom_type: 'personalLinks',
prop: 'scott_personal_links',
},
{
label: '卡片样式设置',
type: 'Select',
prop: 'scott_card_theme',
config: {
clearable: true,
style: 'width: 200px',
defaultValue: 1,
options: [
{ value: 1, label: '样式一' },
{ value: 2, label: '样式二' },
],
},
},
],
},
formData: {
scott_reward_links: [],
scott_personal_links: [],
},
rewardLinksColumns: [
{ title: '图片', key: 'image' },
{ title: '文本', key: 'text' },
{ title: '链接地址', key: 'url' },
],
personalLinksColumns: [
{ title: '文本', key: 'text' },
{ title: '链接地址', key: 'url' },
],
};
},
mounted() {
this.requestData();
},
methods: {
requestData() {
//从formConfig里面读出需要从后端得到的options数据
var names = [];
this.formConfig.items.forEach((item) => {
var prop = item.prop;
if (prop) {
names.push(prop);
}
});
$fullLoading.start();
this.$axios({
method: 'post',
url: this.$API + '/nv/get-options',
data: {
names,
},
})
.then(({ data }) => {
if (!this.$isSuccess(data)) {
return;
}
this.formData = data;
this.$nextTick(() => {
this.formDataChanged = false;
});
})
.catch((error) => {
$message.warning('读取设置请求失败');
console.log(error);
})
.finally(() => {
$fullLoading.end();
});
},
handleSubmit() {
$fullLoading.start();
this.$axios({
method: 'post',
url: this.$API + '/nv/set-options',
data: this.formData,
})
.then(({ data }) => {
if (!this.$isSuccess(data)) {
return;
}
$message.success('保存成功');
this.formDataChanged = false;
})
.catch((error) => {
$message.warning('保存设置请求失败');
console.log(error);
})
.finally(() => {
$fullLoading.end();
});
},
},
};
</script>
<style scoped></style>
如果想添加其他的配置页面也是一样的方法。
示例主题源码获取
示例主题由nvPress官方提供,传送门
当然,如果你觉得示例主题太过于简单,你也可以下载官方免费提供的主题niRvana进行二次开发
也可以使用我二次开发的主题,SCOTT-THEME
如果你觉得本文对你有所帮助,可以帮我GitHub点个star或者请我喝杯奶茶~万分感谢🎉🎉🎉