vue3 + tsrpc +mongodb 实现后台管理系统

前言

之前上线了一个vue后台管理系统,有小伙伴问我有没有后端代码,咱只是个小前端,这就有点为难我了。不过不能辜负小伙伴的信任,nodejs也可以啊,废话不多说,开搞!后端采用 TSRPC 框架实现 API 接口,前端采用 vue-manage-system 后台管理系统框架,数据库采用 mongodb。TSRPC 是专为 TypeScript 设计的 RPC 框架,经千万级用户验证。适用于 HTTP API、WebSocket 实时应用、NodeJS 微服务等场景。有兴趣深入了解可以参考 TSRPC官方文档。

创建项目

用 TSRPC 脚手架快速创建一个项目,会生成 backend 和 frontend 两个文件夹,把 vue-manage-system 前端代码替换到 frontend 中,安装相关依赖,就完成一个基本的前后端完整项目了。

使用 mongodb,在backend/src下创建目录和文件 mongodb/index.ts

import { Db, MongoClient } from "mongodb";export class Global {static db: Db;static async initDb() {const uri = 'mongodb://127.0.0.1:27017/test?authSource=admin';const client = await new MongoClient(uri).connect();this.db = client.db();}
}

在 src/index.ts 中初始化 mongodb 连接

import { Global } from './mongodb/index';async function init() {// ...await Global.initDb();
};

vue-manage-system 是基于vue3实现的一个后台管理系统解决方案,代码简单,上手容易,已经在多个项目中应用。下载代码覆盖到 frontend 文件夹下,保留 src/client.ts 文件,这是 tsrpc 框架提供给客户端调用后端接口的方法。重装依赖,即可运行起来。
接下来实现一个用户管理的前后端功能。

后端接口

在 backend/shared/protocols 下新建一个 users 文件夹,用于定义用户管理的相关接口。在该目录下,新建 db_User.ts 文件,用于定义用户集合的字段类型,先按照vue-manage-system前端框架中已有的表格字段随便定义下吧。

import { ObjectId } from 'mongodb';export interface db_User {_id: ObjectId;name: string;	// 用户名pwd: string;    // 密码thumb?: string;  // 头像money: number;  // 账户余额state: number;  // 账户状态address: string;    // 地址date: Date; // 注册日期
}

一个用户拥有以上的字段,接下来实现用户管理的增删查改操作。在users目录下分别创建 PtlAdd.ts、PtlDel.ts、PtlGet.ts、PtlUpdate.ts文件,TSRPC 完全通过文件名和类型名来识别协议,务必要严格按照 TSRPC 规定的名称前缀来命名,文件名为:Ptl{接口名}.ts,在 src/api/users 目录下,也会生成对应的 Apixxx.ts 文件,就是对应的接口 users/Add、users/Del、users/Get、users/Update。

新增

// PtlAdd.ts
import { BaseRequest, BaseResponse, BaseConf } from "../base";
import { db_User } from "./db_User";export interface ReqAdd extends BaseRequest {query: Omit<db_User, '_id'>		// 除了_id自动生成,db_User其它属性都作为入参
}export interface ResAdd extends BaseResponse {newID: string;		// 请求成功时返回_id
}

TSRPC 有统一的 错误处理 规范,这里不需要考虑成功、失败和错误的情况,不用定义code、data、message等字段,TSRPC 会返回以下格式

{isSucc: true,data: {newID: 'xxx'}
}

在 src/api/users/ApiAdd.ts 中,实现接口的主要逻辑,把数据插入数据库集合中。

import { Global } from './../../mongodb/index';
import { ApiCall } from "tsrpc";
import { ReqAdd, ResAdd } from "../../shared/protocols/users/PtlAdd";export default async function (call: ApiCall<ReqAdd, ResAdd>) {// 这里就省略了各种判断const ret = await Global.db.collection('User').insertOne(call.req.query);return call.succ({ newID: ret.insertedId.toString() })
}

同理,把另外三个接口也加上

删除

// PtlDel.ts
import { ObjectId } from "mongodb";
import { BaseRequest, BaseResponse, BaseConf } from "../base";export interface ReqDel extends BaseRequest {_id: ObjectId
}export interface ResDel extends BaseResponse {matchNum: number;
}// ApiDel.ts
import { ApiCall } from "tsrpc";
import { Global } from "../../mongodb";
import { ReqDel, ResDel } from "../../shared/protocols/users/PtlDel";export default async function (call: ApiCall<ReqDel, ResDel>) {const ret = await Global.db.collection('User').deleteOne({ _id: call.req._id });return call.succ({ matchNum: ret.deletedCount })
}

查询

// PtlGet.ts
import { db_User } from './db_User';
import { BaseRequest, BaseResponse, BaseConf } from "../base";export interface ReqGet extends BaseRequest {query: {pageIndex: number;pageSize: number;name?: string;};
}export interface ResGet extends BaseResponse {data: db_User[],pageTotal: number
}// ApiGet.ts
import { Global } from './../../mongodb/index';
import { ApiCall } from "tsrpc";
import { ReqGet, ResGet } from "../../shared/protocols/users/PtlGet";export default async function (call: ApiCall<ReqGet, ResGet>) {const { pageIndex, pageSize, name } = call.req.query;const filter: any = {}if (name) {filter.filter = new RegExp(name!)}const ret = await Global.db.collection('User').aggregate([{$match: filter},{$facet: {total: [{ $count: 'total' }],data: [{ $sort: { _id: -1 } }, { $skip: (pageIndex - 1) * pageSize }, { $limit: pageSize }],},},]).toArray()return call.succ({data: ret[0].data,pageTotal: ret[0].total[0]?.total || 0})
}

修改

// PtlUpdate.ts
import { BaseRequest, BaseResponse, BaseConf } from "../base";
import { db_User } from "./db_User";export interface ReqUpdate extends BaseRequest {updateObj: Pick<db_User, '_id'> & Partial<Pick<db_User, 'name' | 'money' | 'address' | 'thumb'>>;
}export interface ResUpdate extends BaseResponse {updatedNum: number;
}// ApiUpdate.ts
import { Global } from './../../mongodb/index';
import { ApiCall } from "tsrpc";
import { ReqUpdate, ResUpdate } from "../../shared/protocols/users/PtlUpdate";export default async function (call: ApiCall<ReqUpdate, ResUpdate>) {let { _id, ...reset } = call.req.updateObj;let op = await Global.db.collection('User').updateOne({_id: _id,},{$set: reset,});call.succ({updatedNum: op.matchedCount,});
}

后端的增删查改接口已经完成,接下来在前端中调用接口。

前端调用接口

在 frontend/src/client.ts 中,TSRPC 提供了 client.callApi 来调用 API 接口,在 table.vue 中我们来调用查询接口并加载到表格中。

import { client } from '../client';
const query = reactive({name: '',pageIndex: 1,pageSize: 10
});
const tableData = ref<TableItem[]>([]);
const pageTotal = ref(0);
// 获取表格数据
const getData = async () => {const ret = await client.callApi('users/Get', {query: query});if (ret.isSucc) {tableData.value = ret.res.data;pageTotal.value = ret.res.pageTotal;}
};
getData();

删除操作

const handleDelete = async (id: string) => {const ret = await client.callApi('users/Del', { _id });if (ret.isSucc) {ElMessage.success('删除成功');}
};

接口调用比较简单,新增和修改这里就不多描述了,有需要可以看代码。在用户字段中,有个头像,需要后端提供上传图片的接口,在实际业务中,大多数文件上传都会上传到cdn服务器上,不过这里没钱买cdn存储,就只能直接上传到服务器本地。

上传文件

先实现后端上传文件的接口,在 backend/shared/protocols 下新建一个 upload 文件夹,然后在 upload 里创建 PtlUpload.ts 文件

// PtlUpload.ts
import { BaseRequest, BaseResponse, BaseConf } from "../base";export interface ReqUpload extends BaseRequest {fileName: string;fileData: Uint8Array;
}export interface ResUpload extends BaseResponse {url: string;
}

这里用到了 Uint8Array 类型,它用于表示8位无符号整数的值的数组。Uint8Array主要提供字节级别的处理能力,如文件读写、二进制数据处理等。

import { ApiCall } from "tsrpc";
import { ReqUpload, ResUpload } from "../../shared/protocols/upload/PtlUpload";
import fs from 'fs/promises';export default async function (call: ApiCall<ReqUpload, ResUpload>) {await fs.access('uploads').catch(async () => {await fs.mkdir('uploads')})await fs.writeFile('uploads/' + call.req.fileName, call.req.fileData);call.succ({url: call.req.fileName,});
}

把上传的文件存储到 uploads 目录下,如果该目录不存在,则先创建。如果想要比较细的话,可以多创建出一个日期的目录,按天存储。

注意:这里文件名是由用户传过来的,有可能出现重名的,按上面的逻辑会覆盖到之前的文件,所以这里可以改成文件名由后端自己生成。

在前端结合 element-plus 的上传组件调用api上传

<el-upload class="avatar-uploader" action="#" :show-file-list="false" :http-request="localUpload"><img v-if="form.thumb" :src="UPLOADURL + form.thumb" class="avatar" /><el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
</el-upload>
const localUpload = async (params: UploadRequestOptions) => {const ab = await params.file.arrayBuffer();var array = new Uint8Array(ab);const res = await client.callApi('upload/Upload', {fileName: Date.now() + '__' + params.file.name,fileData: array});if (res.isSucc) {form.value.thumb = res.res.url;} else {ElMessage.error(res.err.message);}
};

可是在上传后会发现,上传接口成功了,服务器的图片文件也存在,但是图片地址加载失败。原来是 TSRPC 默认创建的项目中没有直接支持静态文件服务,需要我们通过中间件简单处理下即可

静态文件服务

创建 getStaticFile.ts 文件,在中间件中自定义 HTTP 响应,对 Get 类型的请求,找到服务器上对应的文件并返回

import { HttpConnection, HttpServer } from 'tsrpc';
import fs from 'fs/promises';
import * as path from 'path';export function getStaticFile(server: HttpServer) {server.flows.preRecvDataFlow.push(async (v) => {let conn = v.conn as HttpConnection;if (conn.httpReq.method === 'GET') {// 静态文件服务if (conn.httpReq.url) {// 检测文件是否存在let resFilePath = path.join('./', decodeURI(conn.httpReq.url));let isExisted = await fs.access(resFilePath).then(() => true).catch(() => false);if (isExisted) {// 返回文件内容let content = await fs.readFile(resFilePath);conn.httpRes.end(content);return undefined;}}// 默认 GET 响应conn.httpRes.end('Not Found');return undefined;}return v;});
}

在 backend/src/index.ts 中使用,让每个网络请求都经过这个工作流

import { HttpServer } from "tsrpc";
import { serviceProto } from "./shared/protocols/serviceProto";
import { getStaticFile } from './models/getStaticFile'
const server = new HttpServer(serviceProto, {port: 3000,json: true
});
getStaticFile(server);

于是图片在前端就可以正常加载出来了。

总结

作为一个小前端,也能做一个完整前后端功能的后台管理系统,再也不用可怜兮兮的等后端接口了,自己一把梭哈,挺适合发展自己的副业余爱好。上面只是个基础的功能,还有许多功能需要慢慢完善,有兴趣可以看代码:tsrpc-manage-system

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/627029.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【深度学习目标检测】十五、基于深度学习的口罩检测系统-含GUI和源码(python,yolov8)

YOLOv8是一种物体检测算法&#xff0c;是YOLO系列算法的最新版本。 YOLO&#xff08;You Only Look Once&#xff09;是一种实时物体检测算法&#xff0c;其优势在于快速且准确的检测结果。YOLOv8在之前的版本基础上进行了一系列改进和优化&#xff0c;提高了检测速度和准确性。…

基于vue+Spring Boot技术的幼儿园管理系统设计与实现4un3j

本系统能为家长、教师提供一个幼儿园管理平台&#xff0c;就能够快速有效的帮助家长在线查询活动名次、学生考勤、成长记录等&#xff0c;教师发布班级信息、课程信息等&#xff0c;并且可以让管理员能够轻松效率地添加所有的信息。系统开发的意义主要在于两个方面&#xff0c;…

FineBI实战项目一(23):订单商品分类词云图分析开发

点击新建组件&#xff0c;创建订单商品分类词云图组件。 选择词云&#xff0c;拖拽catName到颜色和文本&#xff0c;拖拽cat到大小。 将组件拖拽到仪表板。 结果如下&#xff1a;

tim实践系列——去中心化分布式架构特点

前言&#xff1a; tim是去中心化分布式即时通讯引擎。不依赖于任何中心服务器&#xff0c;采用去中心化分布式架构&#xff0c;解决传统中心化通讯方式的问题&#xff0c;去中心化分布式架构的通讯引擎的各个节点之间相互连接&#xff0c;形成一个庞大的分布式网络。可以轻松地…

使用composer构建软件包时文件(夹)权限设置

在构建软件包的时候你可能会需要对包源内文件或文件夹的权限做出相应的调整&#xff0c;以确保软件包在部署到客户端后可以正常运行。在此之前我们先来了解一下Apple文件系统内文件或文件夹的权限设定。 常见的文件或文件夹会有Owner, Group, Everyone这三种类型的所有权&#…

MetaGPT入门(二)

接着MetaGPT入门&#xff08;一&#xff09;&#xff0c;在文件里再添加一个role类 class SimpleCoder(Role):def __init__(self,name:str"Alice",profile:str"SimpleCoder",**kwargs):super().__init__(name,profile,**kwargs)self._init_actions([Write…

1.16寒假集训

A: 解题思路&#xff1a; 题目的意思是小辰实力大于集训队员的实力&#xff0c;成就感就加上该集训队员的实力值。 下面是c代码&#xff1a; #include<iostream> using namespace std; int main() {int n,x,arr[100],num 0;cin >> n >> x;for(int i 0;i…

js 调用动态函数传对象

var obj {"name":"张三","age",14}; var funcName "funcName"; eval(funcName"(obj);");//对象要在括号里不能用引号括着&#xff0c;如果参数是字符串则要用引号括着 --子组件调用父窗口事件 let fn"this.$parent.…

选择安全数据交换系统时 要考虑哪些因素?

安全数据交换系统是一种专门设计用于在不同的网络环境&#xff08;如内部不同网络&#xff0c;内部网络和外部网络&#xff09;之间安全传输数据的解决方案。它通常包括一系列的技术和流程&#xff0c;旨在确保数据在传输过程中的完整性、机密性和可用性。 安全数据交换系统可以…

Python Pandera 用于数据验证和清洗:是一个强大的工具用起来

今天为大家分享一个非常好用的 Python 库 - pandera。 Github地址&#xff1a;https://github.com/unionai-oss/pandera 在数据科学和数据分析中&#xff0c;数据的质量至关重要。不良的数据质量可能导致不准确的分析和决策。为了确保数据的质量&#xff0c;Python Pandera 库…

Flask SQLAlchemy怎么查看参数化查询的实际SQL语句

Flask SQLAlchemy操作数据库时&#xff0c;不管是直接用sql语句还是用orm&#xff0c;有的时候为了调试&#xff0c;都需要知道参数化查询的sql语句生成的实际sql语句是什么。 比如&#xff1a; params {org: 123}# 如下是参数化查询sql语句&#xff0c;如果sql语句很长&…

油烟净化器,餐饮业绿色健康发展的“助推器”

我最近分析了餐饮市场的油烟净化器等产品报告&#xff0c;解决了餐饮业厨房油腻的难题&#xff0c;更加方便了在餐饮业和商业场所有需求的小伙伴们。 随着人们生活水平的提高&#xff0c;餐饮业也得到了快速发展。然而&#xff0c;餐饮油烟污染也成为了人们关注的焦点。 油烟…

第十五讲_css水平垂直居中的技巧

css水平垂直居中的技巧 1. 水平垂直居中&#xff08;场景一&#xff09;2. 水平垂直居中&#xff08;场景二&#xff09;3. 水平垂直居中&#xff08;场景三&#xff09;4. 水平垂直居中&#xff08;场景四&#xff09; 1. 水平垂直居中&#xff08;场景一&#xff09; 条件&a…

c语言for循环和水仙花

c语言for循环和水仙花 c语言for循环和水仙花 c语言for循环和水仙花一、for循环语句格式二、for循环案例水仙花 一、for循环语句格式 for(初始值&#xff1b;表达式&#xff1b;表达式) { 代码 }int main() {for (int i 0; i < 10; i){printf("%d\n", i);} }二、f…

C语言为什么会发⽣数据溢出?如何避免数据溢出?

一、问题 有以下程序&#xff1a;#include <stdio.h> int main() {int i, sum; /*声明变量 */i 2147483647; /*定义变量*/sum i 1; /*变量值加1*/printf("%d,%d", i, sum); /*输出结果*/return 0; }运⾏后 sum 的结果是什么&#xff1f; 二、解答 1、分析…

财政局运维管理平台应用解决方案

随着信息化建设的不断深入&#xff0c;财政局在IT运维中面临的问题也日益凸显。为了解决这些问题&#xff0c;本文将结合财政局的网络和IT基础建设现状&#xff0c;探讨如何选择合适的运维管理平台&#xff0c;以满足财政局的管理需求&#xff0c;并体现个性化需求。 一、财政…

分销商城多端uniapp 可编译5端 - 等级提现额度

等级提现额度 等级提现额度是一种常见的财务管理策略&#xff0c;通常用于在线平台、金融服务或游戏中&#xff0c;用于控制不同等级用户的提现限额。这样的机制有助于平台管理资金流动性&#xff0c;防范欺诈&#xff0c;并鼓励用户提升他们的活跃度或忠诚度。以下是一个简单的…

uniapp css 横向3个按钮 选中变色

<view class"uni-flex uni-row" style"text-align: center;line-height: 30px;" ><view class"itembtn" tabindex"1"> 选中变色1 </view><view class"itembtn" tabindex"2"> 选中变色2 &…

在商城里边怎么做分销功能_微信小程序商城如何开展分销

一、开篇引子 在数字化浪潮的推动下&#xff0c;商城已不再是一个单纯的购物场所&#xff0c;而是一个集购物、社交、娱乐为一体的多元化平台。在这个竞争激烈的市场环境中&#xff0c;如何让自己的商城脱颖而出&#xff0c;成为众多商家关注的焦点。而分销功能的引入&#xf…

uniapp一键换色

需求 : 在我们现有项目基础上, 把原来的颜色替换成另一个颜色, 同时需要为下一个项目预留出来随时更换主题色, 实现一键换色 实现 : 1. 介绍 兼容不同项目对主题色及图标的需求 主要通过以下对css颜色和icon主题色图标两个模块的切换 scss/less的css变量config/index.js中的…