This commit is contained in:
建喵 2022-05-04 11:13:09 +08:00
parent 3d331ee10d
commit 0b76b5935d
68 changed files with 38788 additions and 1 deletions

View File

@ -1,2 +1,2 @@
PORT = 3000
PORT = 4000
TZ = Asia/Taipei

3
backend/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
node_modules
dist
.DS_STORE

31
backend/README.md Normal file
View File

@ -0,0 +1,31 @@
# TSRPC Server
## Usage
### Local dev server
Dev server would restart automatically when code changed.
```
npm run dev
```
### Build
```
npm run build
```
### Generate API document
Generate API document in swagger/openapi and markdown format.
```shell
npm run doc
```
### Run unit Test
Execute `npm run dev` first, then execute:
```
npm run test
```
---

3699
backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

29
backend/package.json Normal file
View File

@ -0,0 +1,29 @@
{
"name": "TSRPC_Badminton-Scoreboard-backend",
"version": "0.1.0",
"main": "index.js",
"private": true,
"scripts": {
"dev": "tsrpc-cli dev",
"build": "tsrpc-cli build",
"doc": "tsrpc-cli doc",
"test": "mocha test/**/*.test.ts",
"proto": "tsrpc-cli proto",
"sync": "tsrpc-cli sync",
"api": "tsrpc-cli api"
},
"devDependencies": {
"@types/mocha": "^8.2.3",
"@types/node": "^15.14.9",
"mocha": "^9.2.2",
"onchange": "^7.1.0",
"ts-node": "^10.7.0",
"tsrpc-cli": "^2.4.3",
"typescript": "^4.6.4"
},
"dependencies": {
"dayjs": "^1.11.1",
"dotenv": "^16.0.0",
"tsrpc": "^3.3.0"
}
}

View File

@ -0,0 +1,26 @@
import { ApiCall } from "tsrpc";
import { server } from "..";
import { ReqSend, ResSend } from "../shared/protocols/PtlSend";
// This is a demo code file
// Feel free to delete it
export async function ApiSend(call: ApiCall<ReqSend, ResSend>): Promise<void> {
// Error
if (call.req.content.length === 0) {
call.error("Content is empty");
return;
}
// Success
let time: Date = new Date();
call.succ({
time: time
});
// Broadcast
server.broadcastMsg("Chat", {
content: "Chat: " + call.req.content,
time: time
});
}

View File

@ -0,0 +1,31 @@
import dayjs from "dayjs";
import { ApiCall } from "tsrpc";
import { server } from "../..";
import { Reqstart, Resstart } from "../../shared/protocols/lobby/Ptlstart";
export async function Apistart(call: ApiCall<Reqstart, Resstart>): Promise<void> {
// Error
// if (call.req.content.length === 0) {
// call.error("Content is empty");
// return;
// }
// Success
let time: Date = new Date();
let dayjstime: string = dayjs().format("YYYY-MM-DD HH:mm:ss");
call.logger.log(`${dayjstime} Apistart`);
call.succ({
code: 1,
data: {
time: dayjstime
}
});
// Broadcast
server.broadcastMsg("lobby/B_start", {
time: time,
data: {
content: `${dayjstime}: B_start`
}
});
}

32
backend/src/index.ts Normal file
View File

@ -0,0 +1,32 @@
import dayjs from "dayjs";
import * as path from "path";
import { WsServer } from "tsrpc";
import { serviceProto, ServiceType } from "./shared/protocols/serviceProto";
require("dotenv").config();
require("dayjs/locale/zh-tw");
dayjs.locale("zh-tw");
// Create the Server
export const server: WsServer<ServiceType> = new WsServer(serviceProto, {
port: +process.env.PORT! || 3000,
// Remove this to use binary mode (remove from the client too)
json: true
});
// Initialize before server start
async function init(): Promise<void> {
await server.autoImplementApi(path.resolve(__dirname, "api"));
// TODO
// let time: Date = new Date();
let time: string = dayjs().format("YYYY-MM-DD HH:mm:ss");
console.log(`${time} Start Server`);
// Prepare something... (e.g. connect the db)
}
// Entry function
async function main(): Promise<void> {
await init();
await server.start();
}
main();

View File

@ -0,0 +1,7 @@
// This is a demo code file
// Feel free to delete it
export interface MsgChat {
content: string;
time: Date;
}

View File

@ -0,0 +1,11 @@
// This is a demo code file
// Feel free to delete it
export interface ReqSend {
content: string;
}
export interface ResSend {
time: Date;
}

View File

@ -0,0 +1,15 @@
export interface BaseRequest {
}
export interface BaseResponse {
}
export interface BaseConf {
}
export interface BaseMessage {
}

View File

@ -0,0 +1,9 @@
// This is a demo code file
// Feel free to delete it
export interface MsgB_start {
data: {
content: string
};
time: Date;
}

View File

@ -0,0 +1,11 @@
// This is a demo code file
// Feel free to delete it
export interface Reqstart { }
export interface Resstart {
code: number; // 与 TSRPC 的错误处理重复
data: {
time: string;
};
}

View File

@ -0,0 +1,152 @@
import { ServiceProto } from 'tsrpc-proto';
import { MsgB_start } from './lobby/MsgB_start';
import { Reqstart, Resstart } from './lobby/Ptlstart';
import { MsgChat } from './MsgChat';
import { ReqSend, ResSend } from './PtlSend';
export interface ServiceType {
api: {
"lobby/start": {
req: Reqstart,
res: Resstart
},
"Send": {
req: ReqSend,
res: ResSend
}
},
msg: {
"lobby/B_start": MsgB_start,
"Chat": MsgChat
}
}
export const serviceProto: ServiceProto<ServiceType> = {
"version": 1,
"services": [
{
"id": 5,
"name": "lobby/B_start",
"type": "msg"
},
{
"id": 1,
"name": "lobby/start",
"type": "api"
},
{
"id": 2,
"name": "Chat",
"type": "msg"
},
{
"id": 4,
"name": "Send",
"type": "api"
}
],
"types": {
"lobby/MsgB_start/MsgB_start": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "data",
"type": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "content",
"type": {
"type": "String"
}
}
]
}
},
{
"id": 1,
"name": "time",
"type": {
"type": "Date"
}
}
]
},
"lobby/Ptlstart/Reqstart": {
"type": "Interface"
},
"lobby/Ptlstart/Resstart": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "code",
"type": {
"type": "Number"
}
},
{
"id": 1,
"name": "data",
"type": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "time",
"type": {
"type": "String"
}
}
]
}
}
]
},
"MsgChat/MsgChat": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "content",
"type": {
"type": "String"
}
},
{
"id": 1,
"name": "time",
"type": {
"type": "Date"
}
}
]
},
"PtlSend/ReqSend": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "content",
"type": {
"type": "String"
}
}
]
},
"PtlSend/ResSend": {
"type": "Interface",
"properties": [
{
"id": 0,
"name": "time",
"type": {
"type": "Date"
}
}
]
}
}
};

View File

@ -0,0 +1,40 @@
import assert from 'assert';
import { TsrpcError, WsClient } from 'tsrpc';
import { serviceProto } from '../../src/shared/protocols/serviceProto';
// 1. EXECUTE `npm run dev` TO START A LOCAL DEV SERVER
// 2. EXECUTE `npm test` TO START UNIT TEST
describe('ApiSend', function () {
let client = new WsClient(serviceProto, {
server: 'ws://127.0.0.1:3000',
json: true,
logger: console
});
before(async function () {
let res = await client.connect();
assert.strictEqual(res.isSucc, true, 'Failed to connect to server, have you executed `npm run dev` already?');
})
it('Success', async function () {
let ret = await client.callApi('Send', {
content: 'Test'
});
assert.ok(ret.isSucc)
});
it('Check content is empty', async function () {
let ret = await client.callApi('Send', {
content: ''
});
assert.deepStrictEqual(ret, {
isSucc: false,
err: new TsrpcError('Content is empty')
});
})
after(async function () {
await client.disconnect();
})
})

View File

@ -0,0 +1,19 @@
{
"compilerOptions": {
"lib": [
"es2018"
],
"module": "commonjs",
"target": "es2018",
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node"
},
"include": [
".",
"../src"
]
}

39
backend/tsrpc.config.ts Normal file
View File

@ -0,0 +1,39 @@
import { CodeTemplate, TsrpcConfig } from "tsrpc-cli";
const tsrpcConf: TsrpcConfig = {
// Generate ServiceProto
proto: [
{
ptlDir: "src/shared/protocols", // Protocol dir
output: "src/shared/protocols/serviceProto.ts", // Path for generated ServiceProto
apiDir: "src/api", // API dir
docDir: "docs", // API documents dir
ptlTemplate: CodeTemplate.getExtendedPtl(),
// msgTemplate: CodeTemplate.getExtendedMsg(),
}
],
// Sync shared code
sync: [
{
from: "src/shared",
to: "../frontend/src/shared",
type: "symlink" // Change this to 'copy' if your environment not support symlink
}
],
// Dev server
dev: {
autoProto: true, // Auto regenerate proto
autoSync: true, // Auto sync when file changed
autoApi: true, // Auto create API when ServiceProto updated
watch: "src", // Restart dev server when these files changed
entry: "src/index.ts", // Dev server command: node -r ts-node/register {entry}
},
// Build config
build: {
autoProto: true, // Auto generate proto before build
autoSync: true, // Auto sync before build
autoApi: true, // Auto generate API before build
outDir: "dist", // Clean this dir before build
}
};
export default tsrpcConf;

2
backend1/.env Normal file
View File

@ -0,0 +1,2 @@
PORT = 4000
TZ = Asia/Taipei

View File

11
backend1/.mocharc.js Normal file
View File

@ -0,0 +1,11 @@
module.exports = {
require: [
'ts-node/register',
],
timeout: 999999,
exit: true,
spec: [
'./test/**/*.test.ts'
],
'preserve-symlinks': true
}

30
backend1/.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,30 @@
{
"configurations": [
{
"type": "node",
"request": "launch",
"name": "mocha current file",
"program": "${workspaceFolder}/node_modules/mocha/bin/_mocha",
"args": [
"${file}"
],
"internalConsoleOptions": "openOnSessionStart",
"cwd": "${workspaceFolder}"
},
{
"type": "node",
"request": "launch",
"name": "ts-node current file",
"protocol": "inspector",
"args": [
"${relativeFile}"
],
"cwd": "${workspaceRoot}",
"runtimeArgs": [
"-r",
"ts-node/register"
],
"internalConsoleOptions": "openOnSessionStart"
}
]
}

3
backend1/.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"typescript.tsdk": "node_modules\\typescript\\lib"
}

30
backend1/Dockerfile Normal file
View File

@ -0,0 +1,30 @@
FROM node
# 使用淘宝 NPM 镜像(国内机器构建推荐启用)
# RUN npm config set registry https://registry.npm.taobao.org/
# npm install
ADD package*.json /src/
WORKDIR /src
RUN npm i
# build
ADD . /src
RUN npm run build
# clean
RUN npm prune --production
# move
RUN rm -rf /app \
&& mv dist /app \
&& mv node_modules /app/ \
&& rm -rf /src
# ENV
ENV NODE_ENV production
EXPOSE 3000
WORKDIR /app
CMD node index.js

18
backend1/tsconfig.json Normal file
View File

@ -0,0 +1,18 @@
{
"compilerOptions": {
"lib": [
"es2018"
],
"module": "commonjs",
"target": "es2018",
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node"
},
"include": [
"src"
]
}

116
backend1/tslint.json Normal file
View File

@ -0,0 +1,116 @@
{
"defaultSeverity": "warning",
"rules": {
"ban": [
true,
[
"_",
"extend"
],
[
"_",
"isNull"
],
[
"_",
"isDefined"
]
],
"class-name": false,
"comment-format": [
true,
"check-space"
],
"curly": true,
"eofline": false,
"forin": false,
"indent": [
true,
4
],
"interface-name": [
true,
"never-prefix"
],
"jsdoc-format": true,
"label-position": true,
"label-undefined": true,
"max-line-length": [
false,
140
],
"no-arg": true,
"no-bitwise": false,
"no-console": [
true,
"debug",
"info",
"time",
"timeEnd",
"trace"
],
"no-construct": true,
"no-debugger": true,
"no-duplicate-key": true,
"no-duplicate-variable": true,
"no-empty": true,
// "no-eval": true,
"no-string-literal": false,
"no-trailing-comma": true,
"no-trailing-whitespace": true,
"no-unused-expression": false,
"no-unused-variable": true,
"no-unreachable": true,
"no-use-before-declare": false,
"one-line": [
true,
"check-open-brace",
"check-catch",
"check-else",
"check-whitespace"
],
"quotemark": [
true,
"double"
],
"radix": true,
"semicolon": [
true,
"always"
],
"triple-equals": [
true,
"allow-null-check"
],
"typedef": [
true,
"call-signature",
"parameter",
"property-declaration",
"variable-declaration"
],
"typedef-whitespace": [
true,
{
"call-signature": "nospace"
},
{
"index-signature": "space"
}
],
"use-strict": [
true,
"check-module",
"check-function"
],
"variable-name": false,
"whitespace": [
false,
"check-branch",
"check-decl",
"check-operator",
"check-separator",
"check-type"
]
}
}

24
frontend/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

3
frontend/.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar"]
}

16
frontend/README.md Normal file
View File

@ -0,0 +1,16 @@
# Vue 3 + TypeScript + Vite
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
## Recommended IDE Setup
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar)
## Type Support For `.vue` Imports in TS
Since TypeScript cannot handle type information for `.vue` imports, they are shimmed to be a generic Vue component type by default. In most cases this is fine if you don't really care about component prop types outside of templates. However, if you wish to get actual prop types in `.vue` imports (for example to get props validation when using manual `h(...)` calls), you can enable Volar's Take Over mode by following these steps:
1. Run `Extensions: Show Built-in Extensions` from VS Code's command palette, look for `TypeScript and JavaScript Language Features`, then right click and select `Disable (Workspace)`. By default, Take Over mode will enable itself if the default TypeScript extension is disabled.
2. Reload the VS Code window by running `Developer: Reload Window` from the command palette.
You can learn more about Take Over mode [here](https://github.com/johnsoncodehk/volar/discussions/471).

13
frontend/index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

33833
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

33
frontend/package.json Normal file
View File

@ -0,0 +1,33 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview"
},
"dependencies": {
"core-js": "^3.22.4",
"tsrpc-browser": "^3.3.0",
"vue": "^3.2.25"
},
"devDependencies": {
"@vitejs/plugin-vue": "^2.3.1",
"typescript": "^4.5.4",
"vite": "^2.9.7",
"vue-tsc": "^0.34.7",
"@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/parser": "^4.33.0",
"@vue/cli-plugin-babel": "^4.5.17",
"@vue/cli-plugin-eslint": "^4.5.17",
"@vue/cli-plugin-typescript": "^4.5.17",
"@vue/cli-service": "^4.5.17",
"@vue/compiler-sfc": "^3.2.33",
"@vue/eslint-config-typescript": "^7.0.0",
"eslint": "^6.8.0",
"eslint-plugin-vue": "^7.20.0",
"less": "^3.13.1",
"less-loader": "^5.0.0"
}
}

BIN
frontend/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

23
frontend/src/App.vue Normal file
View File

@ -0,0 +1,23 @@
<script lang="ts">
import { defineComponent } from "vue";
import Chatroom from "./Chatroom.vue";
export default defineComponent({
name: "app",
components: {
Chatroom,
},
});
</script>
<template>
<Chatroom title="Client #1" />
<Chatroom title="Client #2" />
</template>
<style>
#app {
display: flex;
justify-content: center;
}
</style>

236
frontend/src/Chatroom.vue Normal file
View File

@ -0,0 +1,236 @@
<template>
<div class="Chatroom">
<header>{{ title }}</header>
<ul class="list" ref="ul">
<li v-for="(v, i) in list" :key="i">
<div class="content">{{ v.content }}</div>
<div class="time">{{ v.time.toLocaleTimeString() }}</div>
</li>
</ul>
<div> <button @click="connect">Connect</button><button @click="start">Start</button> </div>
<div class="send">
<input placeholder="Say something..." v-model="input" @keypress.enter="send" />
<button @click="send">Send</button>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, nextTick } from "vue";
import { getClient } from "./getClient";
import { Msgstart } from "./shared/protocols/lobby/Msgstart";
import { MsgChat } from "./shared/protocols/MsgChat";
export default defineComponent({
name: "Chatroom",
props: {
title: String,
},
data() {
return {
input: "",
list: [] as MsgChat[],
client: getClient(),
};
},
mounted(): void {
// Listen Msg
this.client.listenMsg("Chat", (v: MsgChat) => {
console.log("Chat")
this.list.push(v);
// Scroll the list to the bottom
nextTick(() => {
const ul = this.$refs.ul as HTMLElement;
ul.scrollTo(0, ul.scrollHeight);
});
});
// Listen Msg
this.client.listenMsg("lobby/B_start", (v: Msgstart) => {
console.log("lobby/B_start")
let listdata: MsgChat = {
time: v.time,
content: v.data.content,
};
this.list.push(listdata);
// Scroll the list to the bottom
nextTick(() => {
const ul = this.$refs.ul as HTMLElement;
ul.scrollTo(0, ul.scrollHeight);
});
});
// When disconnected
this.client.flows.postDisconnectFlow.push((v) => {
// alert("Server disconnected");
let listdata: MsgChat = {
time: new Date(),
content: "Server disconnected",
};
this.list.push(listdata);
return v;
});
},
methods: {
async connect(): Promise<void> {
// this.client = getClient();
// let listdata: MsgChat = {
// time: new Date(),
// content: `connect`,
// };
// this.list.push(listdata);
this.client.connect().then((v) => {
if (!v.isSucc) {
// alert("= Client Connect Error =\n" + v.errMsg);
let listdata: MsgChat = {
time: new Date(),
content: "= Client Connect Error =\n" + v.errMsg,
};
this.list.push(listdata);
} else {
// alert("= Client Connect Error =\n" + v.errMsg);
let listdata: MsgChat = {
time: new Date(),
content: "Server Connect",
};
this.list.push(listdata);
}
});
},
async start(): Promise<void> {
let ret = await this.client.callApi("lobby/start", {});
// Error
if (!ret.isSucc) {
// alert(ret.err.message);
let listdata: MsgChat = {
time: new Date(),
content: ret.err.message,
};
this.list.push(listdata);
return;
}
// Success
this.input = "";
},
// Send input message
async send(): Promise<void> {
let ret = await this.client.callApi("Send", {
content: this.input,
});
// Error
if (!ret.isSucc) {
// alert(ret.err.message);
let listdata: MsgChat = {
time: new Date(),
content: ret.err.message,
};
this.list.push(listdata);
return;
}
// Success
this.input = "";
},
},
});
</script>
<style scoped lang="less">
// .div .Chatroom {
// display: inline-block;
// }
.Chatroom {
// display: inline-block;
display: flex;
flex-direction: column;
width: 460px;
height: 480px;
margin: 20px;
background: #f7f7f7;
border: 1px solid #454545;
border-radius: 5px;
overflow: hidden;
>header {
background: #454545;
color: white;
text-align: center;
padding: 10px;
}
>.send {
flex: 0 0 40px;
display: flex;
border-top: 1px solid #454545;
>* {
border: none;
outline: none;
height: 100%;
font-size: 16px;
}
>input {
flex: 1;
background: #fff;
padding: 0 10px;
}
>button {
flex: 0 0 100px;
background: #215fa4;
color: white;
cursor: pointer;
&:hover {
background: #4b80bb;
}
}
}
>.list {
flex: 1;
overflow-y: auto;
list-style: none;
border-radius: 5px;
padding: 10px;
padding-bottom: 20px;
background: #f2f2f2;
>li {
margin-bottom: 10px;
padding: 10px;
background: #fff;
line-height: 1.5em;
border-radius: 5px;
>.content {
font-size: 14px;
text-align: left;
white-space: pre-wrap;
word-wrap: break-word;
}
>.time {
font-size: 12px;
color: #4b80bb;
text-align: right;
}
&:last-child {
border-bottom: none;
margin-bottom: 0;
}
}
}
}
</style>

View File

@ -0,0 +1,100 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body>h1 {
text-align: center;
margin-top: 20px;
}
.app {
display: flex;
justify-content: center;
}
.Chatroom {
display: flex;
flex-direction: column;
width: 460px;
height: 480px;
margin: 20px;
background: #f7f7f7;
border: 1px solid #454545;
border-radius: 5px;
overflow: hidden;
}
.chat-room>header {
background: #454545;
color: white;
text-align: center;
padding: 10px;
}
.send {
flex: 0 0 40px;
display: flex;
border-top: 1px solid #454545;
}
.send>* {
border: none;
outline: none;
height: 100%;
font-size: 16px;
}
.send>input {
flex: 1;
background: #fff;
padding: 0 10px;
}
.send>button {
flex: 0 0 100px;
background: #215fa4;
color: white;
cursor: pointer;
}
.send>button:hover {
background: #4b80bb;
}
.list {
flex: 1;
overflow-y: auto;
list-style: none;
border-radius: 5px;
padding: 10px;
padding-bottom: 20px;
background: #f2f2f2;
}
.list>li {
margin-bottom: 10px;
padding: 10px;
background: #fff;
line-height: 1.5em;
border-radius: 5px;
}
.list>li>.content {
font-size: 14px;
text-align: left;
white-space: pre-wrap;
word-wrap: break-word;
}
.list>li>.time {
font-size: 12px;
color: #4b80bb;
text-align: right;
}
.list>li:last-child {
border-bottom: none;
margin-bottom: 0;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -0,0 +1,52 @@
<script setup lang="ts">
import { ref } from 'vue'
defineProps<{ msg: string }>()
const count = ref(0)
</script>
<template>
<h1>{{ msg }}</h1>
<p>
Recommended IDE setup:
<a href="https://code.visualstudio.com/" target="_blank">VS Code</a>
+
<a href="https://github.com/johnsoncodehk/volar" target="_blank">Volar</a>
</p>
<p>See <code>README.md</code> for more information.</p>
<p>
<a href="https://vitejs.dev/guide/features.html" target="_blank">
Vite Docs
</a>
|
<a href="https://v3.vuejs.org/" target="_blank">Vue 3 Docs</a>
</p>
<button type="button" @click="count++">count is: {{ count }}</button>
<p>
Edit
<code>components/HelloWorld.vue</code> to test hot module replacement.
</p>
</template>
<style scoped>
a {
color: #42b983;
}
label {
margin: 0 0.5em;
font-weight: bold;
}
code {
background-color: #eee;
padding: 2px 4px;
border-radius: 4px;
color: #304455;
}
</style>

16
frontend/src/env.d.ts vendored Normal file
View File

@ -0,0 +1,16 @@
/* eslint-disable */
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
// TSRPC would decode ObjectId as string in frontend.
declare module 'mongodb' {
export type ObjectId = string;
export type ObjectID = string;
}
declare module 'bson' {
export type ObjectId = string;
export type ObjectID = string;
}

11
frontend/src/getClient.ts Normal file
View File

@ -0,0 +1,11 @@
import { WsClient } from "tsrpc-browser";
import { serviceProto, ServiceType } from "./shared/protocols/serviceProto";
export function getClient(): WsClient<ServiceType> {
return new WsClient(serviceProto, {
server: "ws://127.0.0.1:4000",
// Remove this to use binary mode (remove from the server too)
json: true,
logger: console,
})
}

4
frontend/src/main.ts Normal file
View File

@ -0,0 +1,4 @@
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')

1
frontend/src/shared Symbolic link
View File

@ -0,0 +1 @@
D:/Project/Test/Test_NodeJS/TSRPC_Badminton-Scoreboard/backend/src/shared

33
frontend/tsconfig.json Normal file
View File

@ -0,0 +1,33 @@
{
"compilerOptions": {
"target": "esnext",
"useDefineForClassFields": true,
"module": "esnext",
"moduleResolution": "node",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"baseUrl": ".",
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
],
"skipLibCheck": true
},
"include": [
"src/**/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue"
],
"references": [
{
"path": "./tsconfig.node.json"
}
]
}

View File

@ -0,0 +1,8 @@
{
"compilerOptions": {
"composite": true,
"module": "esnext",
"moduleResolution": "node"
},
"include": ["vite.config.ts"]
}

11
frontend/vite.config.ts Normal file
View File

@ -0,0 +1,11 @@
import vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vite'
// https://vitejs.dev/config/
export default defineConfig({
base: "./",
plugins: [vue()],
server: {
port: 5000
}
})

6
vetur.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
// Vue project root
projects: [
'./frontend'
]
}