master
parent
3d60e8d047
commit
e996b958ec
@ -1,24 +0,0 @@
|
||||
# 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?
|
||||
@ -1,3 +0,0 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
|
||||
}
|
||||
@ -1,18 +0,0 @@
|
||||
# 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) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
|
||||
|
||||
## Type Support For `.vue` Imports in TS
|
||||
|
||||
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
|
||||
|
||||
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
|
||||
|
||||
1. Disable the built-in TypeScript Extension
|
||||
1. Run `Extensions: Show Built-in Extensions` from VSCode's command palette
|
||||
2. Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
|
||||
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
|
||||
@ -1,9 +0,0 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
// Generated by unplugin-auto-import
|
||||
export {}
|
||||
declare global {
|
||||
|
||||
}
|
||||
@ -1,24 +0,0 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
export {}
|
||||
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
Brand: typeof import('./src/components/brand/brand.vue')['default']
|
||||
CommonButton: typeof import('./src/components/button/commonButton.vue')['default']
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
||||
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
||||
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
|
||||
ElTable: typeof import('element-plus/es')['ElTable']
|
||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||
Header: typeof import('./src/components/header/header.vue')['default']
|
||||
IconInput: typeof import('./src/components/input/iconInput.vue')['default']
|
||||
LeftMenu: typeof import('./src/components/menu/leftMenu.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
}
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + Vue + TS</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,17 +0,0 @@
|
||||
interface IResponseData<T> {
|
||||
code: number
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
interface ILoginRequestData {
|
||||
input_username: string
|
||||
input_password: string
|
||||
verfiyCode: string
|
||||
}
|
||||
|
||||
interface ILoginResponseData {
|
||||
username: string
|
||||
token: string
|
||||
token_expire: number
|
||||
}
|
||||
@ -1,92 +0,0 @@
|
||||
import { MockMethod } from 'vite-plugin-mock'
|
||||
import { Random } from 'mockjs'
|
||||
// code 0 success
|
||||
// code 505 login error
|
||||
|
||||
interface IOrder {
|
||||
id: string;
|
||||
createAt: string;
|
||||
updateAt: string;
|
||||
college: string;
|
||||
pm_name: string;
|
||||
pm_tel: number;
|
||||
order_time: string;
|
||||
size: number;
|
||||
need: boolean;
|
||||
status: number;
|
||||
verify_url: string;
|
||||
sub_pm_type: string;
|
||||
}
|
||||
|
||||
|
||||
const order_list: IOrder[] = [];
|
||||
// x 100
|
||||
for (let i = 0; i < 100; i++) {
|
||||
order_list.push({
|
||||
college: Random.csentence(5, 10),
|
||||
createAt: Random.datetime(),
|
||||
id: Random.guid().slice(0, 10),
|
||||
pm_name: Random.cname(),
|
||||
pm_tel: Random.integer(10000000000, 19999999999),
|
||||
size: Random.integer(1, 100),
|
||||
need: Random.boolean(),
|
||||
order_time: Random.datetime(),
|
||||
status: Random.integer(0, 3),
|
||||
updateAt: Random.datetime(),
|
||||
verify_url: Random.url(),
|
||||
// IMG or PDF
|
||||
sub_pm_type: Random.pick(['IMG', 'PDF'])
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default [
|
||||
// user login
|
||||
{
|
||||
url: '/api/user/login',
|
||||
method: 'post',
|
||||
// res
|
||||
response: (data) => {
|
||||
const { input_username, input_password, verfiyCode } = data.body;
|
||||
if (input_username === 'admin' &&
|
||||
input_password === 'admin123' &&
|
||||
verfiyCode === '1234') {
|
||||
return {
|
||||
code: 0,
|
||||
message: 'success',
|
||||
data: {
|
||||
token: 'Token',
|
||||
username: 'user_1',
|
||||
token_expire: 3600 * 24,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// test error
|
||||
return {
|
||||
code: 505,
|
||||
message: '用户名或密码错误',
|
||||
data: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 请求订单假数据
|
||||
,
|
||||
{
|
||||
url: '/api/order/list',
|
||||
method: 'get',
|
||||
// res
|
||||
response: (data) => {
|
||||
console.log(data.query);
|
||||
const { page, limit } = data.query;
|
||||
// data 传入分页数据
|
||||
const query_order_list: IOrder[] = [];
|
||||
// 根据page limit 从order_list截取 返回数据
|
||||
const start = (page - 1) * limit;
|
||||
const end = page * limit;
|
||||
query_order_list.push(...order_list.slice(start, end));
|
||||
return query_order_list;
|
||||
}
|
||||
}
|
||||
] as MockMethod[]
|
||||
@ -1,34 +0,0 @@
|
||||
{
|
||||
"name": "cauc_admin",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/mockjs": "^1.0.7",
|
||||
"axios": "^1.5.0",
|
||||
"element-plus": "^2.3.14",
|
||||
"fast-glob": "^3.3.1",
|
||||
"mitt": "^3.0.1",
|
||||
"pinia": "^2.1.6",
|
||||
"remixicon": "^3.5.0",
|
||||
"sass": "^1.68.0",
|
||||
"vite-plugin-svg-icons": "^2.0.1",
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "^4.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-image": "^3.0.2",
|
||||
"@vitejs/plugin-vue": "^4.2.3",
|
||||
"mockjs": "^1.1.0",
|
||||
"typescript": "^5.0.2",
|
||||
"unplugin-auto-import": "^0.16.6",
|
||||
"unplugin-vue-components": "^0.25.2",
|
||||
"vite": "^4.4.5",
|
||||
"vite-plugin-mock": "^2.9.8",
|
||||
"vue-tsc": "^1.8.5"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
Before Width: | Height: | Size: 1.5 KiB |
@ -1,9 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<RouterView />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 496 B |
@ -1,53 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="container"
|
||||
:style="{
|
||||
width: _bg_size + 'vh',
|
||||
height: _bg_size + 'vh',
|
||||
backgroundSize: `${_size}vh,${_size}vh`,
|
||||
}"
|
||||
></div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { PropType, onMounted } from "vue";
|
||||
import { ref } from "vue";
|
||||
|
||||
const enum _Size {
|
||||
small = 0.4,
|
||||
normal = 1,
|
||||
large = 1.5,
|
||||
}
|
||||
|
||||
const normal_bg_size = 11;
|
||||
const normal_size = 9;
|
||||
|
||||
const props = defineProps({
|
||||
size: {
|
||||
type: String as PropType<"small" | "normal" | "large">,
|
||||
},
|
||||
});
|
||||
|
||||
const _scale = ref(1);
|
||||
const _size = ref();
|
||||
const _bg_size = ref();
|
||||
onMounted(() => {
|
||||
_scale.value =
|
||||
props.size === "large"
|
||||
? _Size.large
|
||||
: props.size === "small"
|
||||
? _Size.small
|
||||
: _Size.normal;
|
||||
_size.value = normal_size * _scale.value;
|
||||
_bg_size.value = normal_bg_size * _scale.value;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container {
|
||||
background-image: url("../../assets/images/icon/brand/leaf_top.png"),
|
||||
url("../../assets/images/icon/brand/leaf_bot.png");
|
||||
background-repeat: no-repeat;
|
||||
background-blend-mode: luminosity;
|
||||
background-position: 0 0, right bottom;
|
||||
}
|
||||
</style>
|
||||
@ -1,29 +0,0 @@
|
||||
<template>
|
||||
<button type="button" class="common-button" @click="click">{{ text }}</button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
text: {
|
||||
type: String,
|
||||
default: "button",
|
||||
},
|
||||
click: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.common-button {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
border-radius: var(--common-radius);
|
||||
background-color: #409eff;
|
||||
color: #fff;
|
||||
border: none;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
@ -1,27 +0,0 @@
|
||||
<template>
|
||||
<div class="header-area">
|
||||
<i class="ri-list-unordered" @click="toggle_menu"></i>
|
||||
<div class="header-group"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getCurrentInstance } from "vue";
|
||||
const ctx = getCurrentInstance();
|
||||
const bus = ctx?.appContext.config.globalProperties.$bus;
|
||||
|
||||
const toggle_menu = () => {
|
||||
bus?.emit("toggle_menu");
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.header-area {
|
||||
background-color: var(--header-bg-color);
|
||||
height: var(--menu-brand-height);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 var(--common-padding);
|
||||
background-color: var(--content-bg-color);
|
||||
}
|
||||
</style>
|
||||
@ -1,142 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="icon-input-content"
|
||||
:class="{
|
||||
focus: input_focus,
|
||||
}"
|
||||
:style="{
|
||||
width: props.width,
|
||||
}"
|
||||
>
|
||||
<i :class="left_icon" class="icon-1"></i>
|
||||
<input
|
||||
class="input"
|
||||
:placeholder="placeholder"
|
||||
v-model="val"
|
||||
:type="type === 'password' ? (show_pwd ? 'text' : 'password') : type"
|
||||
@focus="on_focus"
|
||||
@blur="on_blur"
|
||||
@input="on_input"
|
||||
:name="type === 'password' ? 'leaf-admin-password' : 'leaf-admin-text'"
|
||||
autocomplete="leaf-admin-password"
|
||||
/>
|
||||
<Transition name="fade">
|
||||
<i
|
||||
v-show="val"
|
||||
class="ri-close-circle-line"
|
||||
:class="{
|
||||
'icon-2': type === 'password',
|
||||
'icon-3': type !== 'password',
|
||||
}"
|
||||
@click="clear"
|
||||
></i>
|
||||
</Transition>
|
||||
<i
|
||||
v-show="type === 'password'"
|
||||
class="ri-eye-line icon-3"
|
||||
:class="{
|
||||
'ri-eye-fill': show_pwd,
|
||||
'ri-eye-off-line': !show_pwd,
|
||||
}"
|
||||
@click="toggle_show_pwd"
|
||||
></i>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
type: {
|
||||
type: String,
|
||||
default: "text",
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: "100%",
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
},
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
left_icon: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
const input_focus = ref(false);
|
||||
|
||||
const val = ref(props.modelValue);
|
||||
// pwd
|
||||
const show_pwd = ref(false);
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
const on_input = () => {
|
||||
emit("update:modelValue", val.value);
|
||||
};
|
||||
|
||||
const on_focus = () => {
|
||||
input_focus.value = true;
|
||||
};
|
||||
const on_blur = () => {
|
||||
console.log("blur");
|
||||
input_focus.value = false;
|
||||
};
|
||||
|
||||
const clear = () => {
|
||||
val.value = "";
|
||||
};
|
||||
|
||||
const toggle_show_pwd = () => {
|
||||
show_pwd.value = !show_pwd.value;
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.fade-enter,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.icon-input-content {
|
||||
border: 1.5px solid #e5e5e5;
|
||||
border-radius: var(--common-radius);
|
||||
padding-block: 1.2vh;
|
||||
display: grid;
|
||||
grid-template-columns: 40px 1fr 20px 40px;
|
||||
grid-template-areas: "icon-1 input icon-2 icon-3";
|
||||
justify-items: center;
|
||||
align-items: center;
|
||||
transition: all 0.3s;
|
||||
height: max-content;
|
||||
|
||||
&.focus {
|
||||
border-color: var(--common-input-focus-color);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
input {
|
||||
border: none;
|
||||
inset: none;
|
||||
width: 100%;
|
||||
height: max-content;
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
grid-area: "input";
|
||||
}
|
||||
.icon-1 {
|
||||
grid-area: icon-1;
|
||||
}
|
||||
.icon-2 {
|
||||
grid-area: icon-2;
|
||||
}
|
||||
.icon-3 {
|
||||
grid-area: icon-3;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,142 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="menu-area"
|
||||
:class="{
|
||||
active: only_show_icon,
|
||||
}"
|
||||
v-if="show_menu"
|
||||
>
|
||||
<div class="brand">
|
||||
<div class="icon">
|
||||
<Brand size="small"></Brand>
|
||||
</div>
|
||||
<span>LeafAdmin</span>
|
||||
</div>
|
||||
<div
|
||||
class="menu-item"
|
||||
@click="linkTo(menuItem.link_name)"
|
||||
v-for="(menuItem, idx) in menu_list"
|
||||
>
|
||||
<i class="icon" :class="menuItem.icon"></i>
|
||||
<div class="menu-item-name">{{ menuItem.name }}</div>
|
||||
</div>
|
||||
<div class="menu-footer menu-item">
|
||||
<i class="ri-skip-back-fill icon" @click="toggle_menu"></i>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Brand from "@/components/brand/brand.vue";
|
||||
import { ref, getCurrentInstance, onMounted } from "vue";
|
||||
import { router } from "@/router";
|
||||
|
||||
const bus = getCurrentInstance()?.appContext.config.globalProperties.$bus;
|
||||
|
||||
const show_menu = ref(true);
|
||||
const only_show_icon = ref(false);
|
||||
|
||||
const menu_list = [
|
||||
{
|
||||
name: "首页",
|
||||
icon: "ri-dashboard-fill",
|
||||
path: "/dashboard",
|
||||
link_name: "Home",
|
||||
},
|
||||
{
|
||||
name: "订单管理",
|
||||
icon: "ri-bookmark-3-line",
|
||||
path: "/order",
|
||||
link_name: "Order",
|
||||
},
|
||||
{
|
||||
name: "用户管理",
|
||||
icon: "ri-folder-user-line",
|
||||
path: "/setting",
|
||||
link_name: "User",
|
||||
},
|
||||
{
|
||||
name: "其他",
|
||||
icon: "ri-psychotherapy-line",
|
||||
path: "/other",
|
||||
link_name: "Other",
|
||||
},
|
||||
];
|
||||
|
||||
onMounted(() => {
|
||||
bus?.on("toggle_menu", () => {
|
||||
show_menu.value = !show_menu.value;
|
||||
});
|
||||
});
|
||||
|
||||
const toggle_menu = () => {
|
||||
only_show_icon.value = !only_show_icon.value;
|
||||
};
|
||||
|
||||
const linkTo = (name: string) => {
|
||||
router.push({
|
||||
name: name,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.menu-area {
|
||||
width: 200px;
|
||||
height: 100%;
|
||||
background-color: var(--menu-bg-color);
|
||||
position: relative;
|
||||
transition: var(--common-transition);
|
||||
overflow-x: hidden;
|
||||
&.active {
|
||||
width: var(--menu-icon-size);
|
||||
transition: var(--common-transition);
|
||||
}
|
||||
.brand {
|
||||
background-color: var(--menu-brand-bg-color) !important;
|
||||
color: var(--menu-brand-color);
|
||||
font-weight: 600;
|
||||
font-size: 20px !important;
|
||||
height: var(--menu-brand-height);
|
||||
display: grid;
|
||||
grid-template-columns: var(--menu-icon-size) 1fr;
|
||||
gap: 1vw;
|
||||
align-items: center;
|
||||
color: var(--menu-brand-color);
|
||||
font-size: 15px;
|
||||
transition: var(--common-transition);
|
||||
.icon {
|
||||
justify-self: center;
|
||||
}
|
||||
}
|
||||
.menu-item {
|
||||
// border: 1px solid blue;
|
||||
height: var(--menu-item-height);
|
||||
display: grid;
|
||||
grid-template-columns: var(--menu-icon-size) 1fr;
|
||||
gap: 1vw;
|
||||
align-items: center;
|
||||
color: var(--menu-item-color);
|
||||
font-size: 15px;
|
||||
transition: var(--common-transition);
|
||||
white-space: nowrap;
|
||||
.icon {
|
||||
justify-self: center;
|
||||
}
|
||||
&:hover {
|
||||
filter: brightness(1.5);
|
||||
transition: var(--common-transition);
|
||||
}
|
||||
.menu-item-name {
|
||||
justify-self: start;
|
||||
}
|
||||
}
|
||||
.menu-footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: var(--menu-item-height);
|
||||
background-color: var(--menu-footer-bg-color);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,38 +0,0 @@
|
||||
<template>
|
||||
<div class="common-layout-area">
|
||||
<div class="menu-area">
|
||||
<LeftMenu></LeftMenu>
|
||||
</div>
|
||||
<div class="content">
|
||||
<Header></Header>
|
||||
<div class="main-content">
|
||||
<RouterView></RouterView>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import LeftMenu from "@/components/menu/leftMenu.vue";
|
||||
import Header from "@/components/header/header.vue";
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.common-layout-area {
|
||||
display: grid;
|
||||
grid-template-columns: max-content 1fr;
|
||||
height: 100vh;
|
||||
max-height: 100vh;
|
||||
.content {
|
||||
background-color: var(--content-bg-bt-color);
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template-rows: max-content 1fr;
|
||||
// border: 1px solid red;
|
||||
.main-content {
|
||||
padding: var(--common-padding);
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,23 +0,0 @@
|
||||
import { createApp } from 'vue'
|
||||
import '@/style.css'
|
||||
import 'element-plus/dist/index.css'
|
||||
import 'remixicon/fonts/remixicon.css'
|
||||
|
||||
import App from './App.vue'
|
||||
import setupRouter from './router'
|
||||
import pinia from '@/store'
|
||||
import mitt from 'mitt'
|
||||
|
||||
|
||||
const bootstrap = () => {
|
||||
const app = createApp(App)
|
||||
setupRouter(app)
|
||||
app.use(pinia)
|
||||
const bus = mitt()
|
||||
app.config.globalProperties.$bus = bus
|
||||
|
||||
|
||||
app.mount('#app')
|
||||
}
|
||||
|
||||
bootstrap()
|
||||
@ -1,17 +0,0 @@
|
||||
import { createRouter, createWebHistory, Router } from 'vue-router'
|
||||
import { constantRoutes as routes } from '@/router/modules'
|
||||
import { App } from 'vue';
|
||||
import guard from '@/router/modules/gurad'
|
||||
|
||||
export const router: Router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: routes,
|
||||
});
|
||||
|
||||
const setupRouter = (app: App) => {
|
||||
guard(router)
|
||||
app.use(router)
|
||||
}
|
||||
|
||||
|
||||
export default setupRouter;
|
||||
@ -1,68 +0,0 @@
|
||||
import { Router, RouteLocationNormalized } from 'vue-router'
|
||||
import { useUserStoreForSetup } from '@/store/user'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { store, load, remove } from '@/utils/json'
|
||||
|
||||
const enum LogintorageKey {
|
||||
USER_TOKEN_KEY = 'user_token_key'
|
||||
}
|
||||
|
||||
class Gurad {
|
||||
constructor(
|
||||
private router: Router,
|
||||
private gurad_token_expire: number = 0
|
||||
) { }
|
||||
public run() {
|
||||
this.router.beforeEach((to, from) => this.beforeEach(to, from))
|
||||
}
|
||||
private beforeEach(to: RouteLocationNormalized, from: RouteLocationNormalized) {
|
||||
if (to.meta.auth && !this.authToken()) {
|
||||
return 'login'
|
||||
}
|
||||
}
|
||||
|
||||
private authToken() {
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
if (!isNaN(this.gurad_token_expire) && this.gurad_token_expire > now) {
|
||||
console.log(this.gurad_token_expire);
|
||||
return true;
|
||||
}
|
||||
// 检查浏览器中是否有缓存
|
||||
const storage: IStoreData<ILoginStorage> | null = load<ILoginStorage>(LogintorageKey.USER_TOKEN_KEY)
|
||||
if (storage) {
|
||||
// 检查是否过期
|
||||
if (storage.data.token_expire > now) {
|
||||
// 未过期
|
||||
this.gurad_token_expire = storage.data.token_expire
|
||||
return true
|
||||
}
|
||||
remove(LogintorageKey.USER_TOKEN_KEY)
|
||||
}
|
||||
|
||||
|
||||
this.gurad_token_expire = 0;
|
||||
|
||||
const userStore = useUserStoreForSetup()
|
||||
const { token, token_expire } = storeToRefs(userStore)
|
||||
|
||||
// 获取当前时间戳
|
||||
|
||||
if (!token_expire.value || token_expire.value < now) {
|
||||
return false
|
||||
}
|
||||
this.gurad_token_expire = token_expire.value
|
||||
store<ILoginStorage>(LogintorageKey.USER_TOKEN_KEY, {
|
||||
data: {
|
||||
token: token.value,
|
||||
token_expire: token_expire.value
|
||||
}
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
export default (router: Router) => {
|
||||
new Gurad(router).run()
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
export * from '@/router/modules/routes'
|
||||
@ -1,64 +0,0 @@
|
||||
import { RouteRecordRaw } from 'vue-router'
|
||||
|
||||
export const constantRoutes: Array<RouteRecordRaw> = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
meta: {
|
||||
auth: true
|
||||
},
|
||||
component: () => import('@/views/home/home.vue'),
|
||||
children: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Dashboard',
|
||||
meta: {
|
||||
auth: true
|
||||
},
|
||||
component: () => import('@/views/dashboard/dashboard.vue')
|
||||
},
|
||||
{
|
||||
path: '/order',
|
||||
name: 'Order',
|
||||
meta: {
|
||||
auth: true
|
||||
},
|
||||
component: () => import('@/views/order/order.vue')
|
||||
},
|
||||
{
|
||||
path: '/user',
|
||||
name: 'User',
|
||||
meta: {
|
||||
auth: true
|
||||
},
|
||||
component: () => import('@/views/user/user.vue')
|
||||
},
|
||||
{
|
||||
path: '/other',
|
||||
name: 'Other',
|
||||
meta: {
|
||||
auth: true
|
||||
},
|
||||
component: () => import('@/views/other/other.vue')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
meta: {
|
||||
auth: false
|
||||
},
|
||||
component: () => import('@/views/login/login.vue')
|
||||
},
|
||||
|
||||
{
|
||||
path: '/test',
|
||||
name: 'Test',
|
||||
meta: {
|
||||
auth: false
|
||||
},
|
||||
component: () => import('@/views/test/test.vue')
|
||||
}
|
||||
|
||||
]
|
||||
@ -1,18 +0,0 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
|
||||
export const useCountStoreForSetup = defineStore('count', {
|
||||
state: () => ({
|
||||
count: 0
|
||||
}),
|
||||
actions: {
|
||||
increment() {
|
||||
this.count++
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
doubleCount(): number {
|
||||
return this.count * 2
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -1,5 +0,0 @@
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
const pinia = createPinia()
|
||||
|
||||
export default pinia
|
||||
@ -1,60 +0,0 @@
|
||||
import axios from 'axios'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref, h } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
export const useUserStoreForSetup = defineStore('user', () => {
|
||||
|
||||
//state
|
||||
// 用户名
|
||||
const username = ref('')
|
||||
// 登陆token
|
||||
const token = ref('')
|
||||
// token过期时间
|
||||
const token_expire = ref(0)
|
||||
|
||||
|
||||
//getters
|
||||
|
||||
// function
|
||||
const login = async (userData: ILoginRequestData) => {
|
||||
// axios
|
||||
const res: IResponseData<ILoginResponseData> = await axios.post('/api/user/login', userData
|
||||
).then(res => res.data)
|
||||
if (res.code === 0) {
|
||||
username.value = res.data.username
|
||||
token.value = res.data.token
|
||||
// 获得当前时间戳
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
token_expire.value = now + res.data.token_expire
|
||||
ElMessage({
|
||||
message: '登陆成功',
|
||||
type: 'success',
|
||||
duration: 2000
|
||||
})
|
||||
return true
|
||||
} else {
|
||||
ElMessage({
|
||||
message: res.message,
|
||||
type: 'error',
|
||||
duration: 2000
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
username.value = '';
|
||||
token.value = ''
|
||||
}
|
||||
|
||||
return {
|
||||
username,
|
||||
token,
|
||||
token_expire,
|
||||
login,
|
||||
logout
|
||||
}
|
||||
|
||||
})
|
||||
@ -1,45 +0,0 @@
|
||||
:root {
|
||||
/* color */
|
||||
--common-color: #386bf3;
|
||||
--bg-primary-color: #fefefe;
|
||||
--bg-left-container-color: #386bf3;
|
||||
--common-icon-color: #a1a1a1;
|
||||
--common-input-focus-color: #386bf3;
|
||||
--menu-bg-color: #001529;
|
||||
--menu-brand-bg-color: #002140;
|
||||
--menu-brand-color: #fefefe;
|
||||
--menu-item-color: #aaa;
|
||||
--menu-footer-bg-color: #002140;
|
||||
--content-bg-color: #ffffff;
|
||||
--content-bg-bt-color: #f0f2f5;
|
||||
|
||||
/* radius */
|
||||
--common-radius: 4px;
|
||||
/* transition */
|
||||
--common-transition: all 0.3s;
|
||||
/* h */
|
||||
--menu-item-height: 6vh;
|
||||
--menu-brand-height: 7vh;
|
||||
/* width */
|
||||
--menu-icon-size: 60px;
|
||||
/* --padding */
|
||||
--common-padding: 1vw;
|
||||
--common-padding-2: 2vw;
|
||||
|
||||
|
||||
/* button */
|
||||
--button-primary-bg-color: #386bf3;
|
||||
--button-primary-color: var(--bg-primary-color);
|
||||
--button-warning-bg-color: #ff7300;
|
||||
--button-warning-color: var(--bg-primary-color);
|
||||
}
|
||||
|
||||
* {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
input[type="password"]::-ms-reveal {
|
||||
display: none
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
interface IResponseData<T> {
|
||||
code: number
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
interface ILoginRequestData {
|
||||
input_username: string
|
||||
input_password: string
|
||||
verfiyCode: string
|
||||
}
|
||||
|
||||
interface ILoginResponseData {
|
||||
username: string
|
||||
token: string
|
||||
token_expire: number
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
declare module "*.vue" {
|
||||
import { DefineComponent } from "vue"
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
interface IStoreData<T> {
|
||||
data: T
|
||||
}
|
||||
|
||||
|
||||
interface ILoginStorage {
|
||||
token: string
|
||||
token_expire: number
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
interface IOrder {
|
||||
id: string;
|
||||
createAt: string;
|
||||
updateAt: string;
|
||||
college: string;
|
||||
pm_name: string;
|
||||
pm_tel: number;
|
||||
size: number;
|
||||
order_time:string;
|
||||
need:boolean;
|
||||
status: number;
|
||||
verify_url: string;
|
||||
sub_pm_type: string;
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
import 'vue-router'
|
||||
|
||||
declare module 'vue-router' {
|
||||
interface RouteMeta {
|
||||
auth?: boolean;
|
||||
}
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
export const enum _Size_type {
|
||||
small = "small",
|
||||
normal = "normal",
|
||||
large = "large",
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
// 序列化
|
||||
const store = <T>(key: string, item: IStoreData<T>) => {
|
||||
const data_json = JSON.stringify(item)
|
||||
// 存储在浏览器中
|
||||
localStorage.setItem(key, data_json)
|
||||
}
|
||||
|
||||
// 反序列化
|
||||
const load = <T>(name: string): IStoreData<T> | null => {
|
||||
const data_json = localStorage.getItem(name)
|
||||
if (data_json) {
|
||||
return JSON.parse(data_json)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
//删除
|
||||
const remove = (name: string) => {
|
||||
localStorage.removeItem(name)
|
||||
}
|
||||
|
||||
export { store, load, remove }
|
||||
@ -1,3 +0,0 @@
|
||||
export const getAssetsImage = (name: string) => {
|
||||
return new URL(`/src/assets/${name}`, import.meta.url).href
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
darshboard
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
|
||||
</style>
|
||||
@ -1,9 +0,0 @@
|
||||
<template>
|
||||
<CommonLayout></CommonLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import CommonLayout from "@/layouts/common/commonLayout.vue";
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@ -1,202 +0,0 @@
|
||||
<template>
|
||||
<div class="order-area">
|
||||
<el-table :data="tableData" stripe style="width: 100%" class="center-table">
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
width="80"
|
||||
fixed="left"
|
||||
:align="'center'"
|
||||
>
|
||||
<template v-slot="{ $index }">
|
||||
<span>{{ $index + 1 }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="id" label="订单ID" width="120" :align="'center'" />
|
||||
|
||||
<el-table-column
|
||||
prop="college"
|
||||
label="所属学院"
|
||||
width="180"
|
||||
:align="'center'"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="pm_name"
|
||||
label="授权人名称"
|
||||
width="100"
|
||||
:align="'center'"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="pm_tel"
|
||||
label="授权人电话"
|
||||
width="120"
|
||||
:align="'center'"
|
||||
/>
|
||||
<el-table-column
|
||||
sortable
|
||||
prop="status"
|
||||
label="订单状态"
|
||||
:align="'center'"
|
||||
>
|
||||
<template v-slot="{ row }">
|
||||
<div>
|
||||
{{
|
||||
row.status === 0
|
||||
? "未审核"
|
||||
: row.status === 1
|
||||
? "已审核"
|
||||
: "已取消"
|
||||
}}
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="verify_url"
|
||||
label="授权书链接"
|
||||
width="120"
|
||||
show-overflow-tooltip
|
||||
:align="'center'"
|
||||
>
|
||||
<template v-slot="{ row }">
|
||||
<div>
|
||||
<button @click="() => getPermission(row.verify_url)">
|
||||
查看授权书
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
sortable
|
||||
prop="order_time"
|
||||
label="订单创建时间"
|
||||
width="180"
|
||||
:align="'center'"
|
||||
/>
|
||||
<el-table-column
|
||||
sortable
|
||||
prop="size"
|
||||
label="预定参观人数"
|
||||
width="120"
|
||||
:align="'center'"
|
||||
/>
|
||||
<el-table-column prop="need" label="指导员需求"
|
||||
width="120"
|
||||
:align="'center'" />
|
||||
<el-table-column
|
||||
prop="sub_pm_type"
|
||||
label="授权书格式"
|
||||
:align="'center'"
|
||||
width="120"
|
||||
/>
|
||||
|
||||
<el-table-column
|
||||
sortable
|
||||
prop="createAt"
|
||||
label="创建时间"
|
||||
width="180"
|
||||
:align="'center'"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="updateAt"
|
||||
label="更新时间"
|
||||
width="180"
|
||||
:align="'center'"
|
||||
/>
|
||||
<!-- ops groups -->
|
||||
<el-table-column
|
||||
label="操作"
|
||||
fixed="right"
|
||||
width="150"
|
||||
show-overflow-tooltip
|
||||
:align="'center'"
|
||||
>
|
||||
<template v-slot="{ row }">
|
||||
<div class="ops-group">
|
||||
<i class="ri-edit-box-line op-icon primary"></i>
|
||||
<i class="ri-delete-bin-2-line op-icon warning"></i>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import axios from "axios";
|
||||
import { onMounted, ref } from "vue";
|
||||
|
||||
const row_height = 50;
|
||||
const limit_row = 10;
|
||||
|
||||
const tableData = ref([]);
|
||||
|
||||
onMounted(() => {
|
||||
axios
|
||||
.get("/api/order/list", {
|
||||
params: {
|
||||
limit: limit_row,
|
||||
page: 2,
|
||||
},
|
||||
})
|
||||
.then((res) => {
|
||||
tableData.value = res.data;
|
||||
});
|
||||
|
||||
axios.get("http://localhost:3000/api/order/all").then((res) => {
|
||||
console.log(res.data);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
const getPermission = (url: string) => {
|
||||
console.log(url);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ops-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
.op-icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
&.primary {
|
||||
border: 1.5px solid var(--button-primary-bg-color);
|
||||
color: var(--button-primary-bg-color);
|
||||
transition: var(--common-transition);
|
||||
&:hover {
|
||||
background-color: var(--button-primary-bg-color);
|
||||
color: var(--button-primary-color);
|
||||
transition: var(--common-transition);
|
||||
}
|
||||
}
|
||||
&.warning {
|
||||
border: 1.5px solid var(--button-warning-bg-color);
|
||||
color: var(--button-warning-bg-color);
|
||||
transition: var(--common-transition);
|
||||
&:hover {
|
||||
background-color: var(--button-warning-bg-color);
|
||||
color: var(--button-warning-color);
|
||||
transition: var(--common-transition);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.order-area {
|
||||
height: 100%;
|
||||
background-color: var(--content-bg-color);
|
||||
padding: var(--common-padding);
|
||||
.test {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,13 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
other
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang='ts'>
|
||||
|
||||
</script>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
|
||||
</style>
|
||||
@ -1,77 +0,0 @@
|
||||
<template>
|
||||
<el-table
|
||||
:data="tableData"
|
||||
stripe
|
||||
style="width: 100%"
|
||||
:height="250"
|
||||
:row-style="{
|
||||
height: `${row_height}px`,
|
||||
}"
|
||||
>
|
||||
<el-table-column type="index" label="序号" width="80">
|
||||
<template v-slot="{ $index }">
|
||||
<span>{{ $index + 1 }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="id" label="订单ID" width="180" />
|
||||
<el-table-column sortable prop="createAt" label="创建时间" width="180" />
|
||||
<el-table-column prop="updateAt" label="更新时间" width="180" />
|
||||
<el-table-column prop="college" label="所属学院" width="180" />
|
||||
<el-table-column prop="pm_name" label="授权人名称" width="180" />
|
||||
<el-table-column prop="pm_tel" label="授权人电话" width="180" />
|
||||
<el-table-column
|
||||
sortable
|
||||
prop="order_time"
|
||||
label="订单创建时间"
|
||||
width="180"
|
||||
/>
|
||||
<el-table-column sortable prop="size" label="预定参观人数" />
|
||||
<el-table-column prop="need" label="指导员需求" />
|
||||
<el-table-column sortable prop="status" label="订单状态" />
|
||||
<el-table-column prop="sub_pm_type" label="授权书格式" />
|
||||
<el-table-column
|
||||
prop="verify_url"
|
||||
label="授权书链接"
|
||||
fixed="right"
|
||||
width="180"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
</el-table>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import axios from "axios";
|
||||
import { onMounted, ref } from "vue";
|
||||
|
||||
const row_height = 50;
|
||||
const limit_row = 5;
|
||||
|
||||
const tableData = ref([]);
|
||||
|
||||
onMounted(() => {
|
||||
axios
|
||||
.get("/api/order/list", {
|
||||
params: {
|
||||
limit: limit_row,
|
||||
page: 2,
|
||||
},
|
||||
})
|
||||
.then((res) => {
|
||||
tableData.value = res.data;
|
||||
});
|
||||
});
|
||||
|
||||
const getRowHeight = (rowCount: number) => {
|
||||
return rowCount * rowCount;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.main-content {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@ -1,13 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
user
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang='ts'>
|
||||
|
||||
</script>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
|
||||
</style>
|
||||
@ -1 +0,0 @@
|
||||
/// <reference types="vite/client" />
|
||||
@ -1,42 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": [
|
||||
"ES2020",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"src/*"
|
||||
]
|
||||
},
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "preserve",
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.d.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.vue"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@ -1,31 +0,0 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
|
||||
import { viteMockServe } from 'vite-plugin-mock'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig((config) => {
|
||||
const { command } = config
|
||||
return {
|
||||
plugins: [
|
||||
AutoImport({
|
||||
resolvers: [ElementPlusResolver()]
|
||||
}),
|
||||
Components({
|
||||
resolvers: [ElementPlusResolver()]
|
||||
}),
|
||||
vue(),
|
||||
viteMockServe({
|
||||
mockPath: 'mock',
|
||||
enable: true
|
||||
})
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': '/src'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -1,48 +0,0 @@
|
||||
# Environment variables declared in this file are automatically made available to Prisma.
|
||||
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
|
||||
|
||||
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
|
||||
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
|
||||
|
||||
DATABASE_URL="mysql://root:yinhan123YH@101.42.154.98:3306/laoyou?schema=public"
|
||||
|
||||
|
||||
# WX APP
|
||||
WX_APP_ID="wx4c765f9dbc6e4ce8"
|
||||
WX_APP_SECRET="dafe13d02bd6be50d8aa50d7f71ada72"
|
||||
WX_LOGIN_URL="https://api.weixin.qq.com/sns/jscode2session"
|
||||
|
||||
# JWT
|
||||
JWT_SECRET_KEY = "LAOYOU_JWT_SECRET"
|
||||
JWT_EXPIRES_IN = "7d"
|
||||
|
||||
# COS
|
||||
COS_IMG_BUCKET="laoyou-img-1300131488"
|
||||
COS_FACE_BUCKET="laoyou-face-1300131488"
|
||||
COS_REGION="ap-beijing"
|
||||
COS_SECRET_ID="AKIDiN1RrOGD5qa7PYbQ6ZsESSDoHbigeOtE"
|
||||
COS_SECRET_KEY="QjtzsX4SUK0V2Vq5Vkro8KMFyhT7dyBF"
|
||||
|
||||
|
||||
# REDIS
|
||||
REDIS_HOST= "localhost"
|
||||
REDIS_PORT= 6379
|
||||
|
||||
# 高德
|
||||
GAODE_KEY = "0f2c68a72e09ff9faf121364dab92c05"
|
||||
WEATHER_URL = "https://restapi.amap.com/v3/weather/weatherInfo"
|
||||
|
||||
|
||||
#REDIS_CONFIG
|
||||
REDIS_KEY_EXPIRE= 604800 # 7days
|
||||
|
||||
#BAIDU
|
||||
|
||||
# NLP
|
||||
BAIDU_NLP_API_KEY = "i4uY0g3UGAzQWK0HfTcEUCI8"
|
||||
BAIDU_NLP_SECRET_KEY = "edRjTDyzgFxtzrg54ofaiTAckBn6XX2Z"
|
||||
|
||||
# FACE
|
||||
BAIDU_FACE_API_KEY = "tLf2zfWOnxYbAYEF6rq561Yc"
|
||||
BAIDU_FACE_SECRET_KEY = "0P4s7wc2o4h4kcADMcjMYezd670onr1n"
|
||||
BAIDU_FACE_GROUP_ID = "common_user"
|
||||
@ -1,25 +0,0 @@
|
||||
module.exports = {
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
project: 'tsconfig.json',
|
||||
tsconfigRootDir: __dirname,
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: ['@typescript-eslint/eslint-plugin'],
|
||||
extends: [
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
],
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
jest: true,
|
||||
},
|
||||
ignorePatterns: ['.eslintrc.js'],
|
||||
rules: {
|
||||
'@typescript-eslint/interface-name-prefix': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
},
|
||||
};
|
||||
@ -1,35 +0,0 @@
|
||||
# compiled output
|
||||
/dist
|
||||
/node_modules
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
pnpm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
|
||||
# Tests
|
||||
/coverage
|
||||
/.nyc_output
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
@ -1,4 +0,0 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
@ -1,73 +0,0 @@
|
||||
<p align="center">
|
||||
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="200" alt="Nest Logo" /></a>
|
||||
</p>
|
||||
|
||||
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
|
||||
[circleci-url]: https://circleci.com/gh/nestjs/nest
|
||||
|
||||
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
|
||||
<p align="center">
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
|
||||
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
|
||||
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
|
||||
<a href="https://coveralls.io/github/nestjs/nest?branch=master" target="_blank"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#9" alt="Coverage" /></a>
|
||||
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
|
||||
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
|
||||
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
|
||||
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg"/></a>
|
||||
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
|
||||
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow"></a>
|
||||
</p>
|
||||
<!--[](https://opencollective.com/nest#backer)
|
||||
[](https://opencollective.com/nest#sponsor)-->
|
||||
|
||||
## Description
|
||||
|
||||
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
$ pnpm install
|
||||
```
|
||||
|
||||
## Running the app
|
||||
|
||||
```bash
|
||||
# development
|
||||
$ pnpm run start
|
||||
|
||||
# watch mode
|
||||
$ pnpm run start:dev
|
||||
|
||||
# production mode
|
||||
$ pnpm run start:prod
|
||||
```
|
||||
|
||||
## Test
|
||||
|
||||
```bash
|
||||
# unit tests
|
||||
$ pnpm run test
|
||||
|
||||
# e2e tests
|
||||
$ pnpm run test:e2e
|
||||
|
||||
# test coverage
|
||||
$ pnpm run test:cov
|
||||
```
|
||||
|
||||
## Support
|
||||
|
||||
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
|
||||
|
||||
## Stay in touch
|
||||
|
||||
- Author - [Kamil Myśliwiec](https://kamilmysliwiec.com)
|
||||
- Website - [https://nestjs.com](https://nestjs.com/)
|
||||
- Twitter - [@nestframework](https://twitter.com/nestframework)
|
||||
|
||||
## License
|
||||
|
||||
Nest is [MIT licensed](LICENSE).
|
||||
@ -1,8 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true
|
||||
}
|
||||
}
|
||||
@ -1,88 +0,0 @@
|
||||
{
|
||||
"name": "laoyou_backend",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
"license": "UNLICENSED",
|
||||
"scripts": {
|
||||
"dev": "nest start --watch",
|
||||
"build": "nest build",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"start": "nest start",
|
||||
"start:dev": "nest start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "node dist/main",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs-modules/ioredis": "^1.0.1",
|
||||
"@nestjs/cache-manager": "^2.0.0",
|
||||
"@nestjs/common": "^9.0.0",
|
||||
"@nestjs/config": "^3.1.1",
|
||||
"@nestjs/core": "^9.0.0",
|
||||
"@nestjs/jwt": "^10.1.1",
|
||||
"@nestjs/passport": "^10.0.2",
|
||||
"@nestjs/platform-express": "^9.0.0",
|
||||
"@nestjs/schedule": "^3.0.4",
|
||||
"@nestjs/serve-static": "^4.0.0",
|
||||
"@prisma/client": "^5.3.1",
|
||||
"@types/passport-jwt": "^3.0.10",
|
||||
"axios": "^1.5.1",
|
||||
"cache-manager": "^4.1.0",
|
||||
"cos-nodejs-sdk-v5": "^2.12.4",
|
||||
"dotenv": "^16.3.1",
|
||||
"ioredis": "^5.3.2",
|
||||
"multer": "1.4.5-lts.1",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"prisma": "^5.3.1",
|
||||
"redis": "^4.6.10",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rxjs": "^7.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^9.0.0",
|
||||
"@nestjs/schematics": "^9.0.0",
|
||||
"@nestjs/testing": "^9.0.0",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/jest": "29.5.0",
|
||||
"@types/node": "18.15.11",
|
||||
"@types/supertest": "^2.0.11",
|
||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||
"@typescript-eslint/parser": "^5.0.0",
|
||||
"eslint": "^8.0.1",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"jest": "29.5.0",
|
||||
"prettier": "^2.3.2",
|
||||
"source-map-support": "^0.5.20",
|
||||
"supertest": "^6.1.3",
|
||||
"ts-jest": "29.0.5",
|
||||
"ts-loader": "^9.2.3",
|
||||
"ts-node": "^10.0.0",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"typescript": "^4.7.4"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"ts"
|
||||
],
|
||||
"rootDir": "src",
|
||||
"testRegex": ".*\\.spec\\.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"collectCoverageFrom": [
|
||||
"**/*.(t|j)s"
|
||||
],
|
||||
"coverageDirectory": "../coverage",
|
||||
"testEnvironment": "node"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,108 +0,0 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE `User` (
|
||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
`openid` VARCHAR(191) NOT NULL,
|
||||
`nickname` VARCHAR(191) NOT NULL DEFAULT '微信用户',
|
||||
`avatar` VARCHAR(191) NOT NULL DEFAULT 'https://laoyou-static-1300131488.cos.ap-beijing.myqcloud.com/user.png',
|
||||
`sex` INTEGER NOT NULL,
|
||||
`city` VARCHAR(191) NOT NULL,
|
||||
`birthday` VARCHAR(191) NOT NULL,
|
||||
`face_auth_status` BOOLEAN NOT NULL DEFAULT false,
|
||||
`face_url` VARCHAR(191) NULL,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updatedAt` DATETIME(3) NOT NULL,
|
||||
`commonShareCount` INTEGER NOT NULL DEFAULT 0,
|
||||
`videoShareCount` INTEGER NOT NULL DEFAULT 0,
|
||||
`commentCount` INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
UNIQUE INDEX `User_openid_key`(`openid`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `CommonShare` (
|
||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
`title` VARCHAR(191) NOT NULL,
|
||||
`content` TEXT NOT NULL,
|
||||
`images` TEXT NOT NULL,
|
||||
`cover` VARCHAR(191) NOT NULL,
|
||||
`like` INTEGER NOT NULL,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updatedAt` DATETIME(3) NOT NULL,
|
||||
`openid` VARCHAR(191) NOT NULL,
|
||||
`key_words` VARCHAR(191) NULL,
|
||||
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `VideoShare` (
|
||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
`title` VARCHAR(191) NOT NULL,
|
||||
`video` VARCHAR(191) NOT NULL,
|
||||
`like` INTEGER NOT NULL,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updatedAt` DATETIME(3) NOT NULL,
|
||||
`openid` VARCHAR(191) NOT NULL,
|
||||
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `CommonShareComment` (
|
||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
`content` VARCHAR(191) NOT NULL,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updatedAt` DATETIME(3) NOT NULL,
|
||||
`openid` VARCHAR(191) NOT NULL,
|
||||
`like` INTEGER NOT NULL DEFAULT 0,
|
||||
`share_id` INTEGER NULL,
|
||||
`fa_comment_id` INTEGER NULL,
|
||||
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `CommonShareLike` (
|
||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updatedAt` DATETIME(3) NOT NULL,
|
||||
`openid` VARCHAR(191) NOT NULL,
|
||||
`share_id_list` VARCHAR(191) NOT NULL,
|
||||
|
||||
UNIQUE INDEX `CommonShareLike_openid_key`(`openid`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE `CommonShareCommentLike` (
|
||||
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updatedAt` DATETIME(3) NOT NULL,
|
||||
`openid` VARCHAR(191) NOT NULL,
|
||||
`comment_id_list` VARCHAR(191) NOT NULL,
|
||||
|
||||
UNIQUE INDEX `CommonShareCommentLike_openid_key`(`openid`),
|
||||
PRIMARY KEY (`id`)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `CommonShare` ADD CONSTRAINT `CommonShare_openid_fkey` FOREIGN KEY (`openid`) REFERENCES `User`(`openid`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `VideoShare` ADD CONSTRAINT `VideoShare_openid_fkey` FOREIGN KEY (`openid`) REFERENCES `User`(`openid`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `CommonShareComment` ADD CONSTRAINT `CommonShareComment_openid_fkey` FOREIGN KEY (`openid`) REFERENCES `User`(`openid`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `CommonShareComment` ADD CONSTRAINT `CommonShareComment_share_id_fkey` FOREIGN KEY (`share_id`) REFERENCES `CommonShare`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `CommonShareComment` ADD CONSTRAINT `CommonShareComment_fa_comment_id_fkey` FOREIGN KEY (`fa_comment_id`) REFERENCES `CommonShareComment`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `CommonShareLike` ADD CONSTRAINT `CommonShareLike_openid_fkey` FOREIGN KEY (`openid`) REFERENCES `User`(`openid`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE `CommonShareCommentLike` ADD CONSTRAINT `CommonShareCommentLike_openid_fkey` FOREIGN KEY (`openid`) REFERENCES `User`(`openid`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@ -1,3 +0,0 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "mysql"
|
||||
@ -1,99 +0,0 @@
|
||||
// This is your Prisma schema file,
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "mysql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
openid String @unique
|
||||
nickname String @default("微信用户")
|
||||
avatar String @default("https://laoyou-static-1300131488.cos.ap-beijing.myqcloud.com/user.png")
|
||||
sex Int
|
||||
city String
|
||||
birthday String
|
||||
face_auth_status Boolean @default(false)
|
||||
face_url String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
// commonShare count
|
||||
commonShareCount Int @default(0)
|
||||
// videoShare count
|
||||
videoShareCount Int @default(0)
|
||||
// comment count
|
||||
commentCount Int @default(0)
|
||||
// 建立 User 与 Share 的关联关系
|
||||
common_shares CommonShare[] @relation("commonShare")
|
||||
video_shares VideoShare[] @relation("videoShare")
|
||||
// VideoShare VideoShare[] @relation("videoShare")
|
||||
Comment CommonShareComment[] @relation("user_comment")
|
||||
CommonShareLike CommonShareLike[]
|
||||
CommonShareCommentLike CommonShareCommentLike[]
|
||||
}
|
||||
|
||||
model CommonShare {
|
||||
id Int @id @default(autoincrement())
|
||||
title String
|
||||
content String @db.Text
|
||||
images String @db.Text
|
||||
cover String
|
||||
like Int
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
// 使用 openid 字段与 User 表建立关联
|
||||
openid String
|
||||
user User @relation("commonShare", fields: [openid], references: [openid])
|
||||
Comment CommonShareComment[] @relation("commshare_comment")
|
||||
key_words String?
|
||||
}
|
||||
|
||||
model VideoShare {
|
||||
id Int @id @default(autoincrement())
|
||||
title String
|
||||
video String
|
||||
like Int
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
// 使用 openid 字段与 User 表建立关联
|
||||
openid String
|
||||
user User @relation("videoShare", fields: [openid], references: [openid])
|
||||
}
|
||||
|
||||
model CommonShareComment {
|
||||
id Int @id @default(autoincrement())
|
||||
content String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
openid String
|
||||
like Int @default(0)
|
||||
user User @relation("user_comment", fields: [openid], references: [openid])
|
||||
share_id Int?
|
||||
commonshare CommonShare? @relation("commshare_comment", fields: [share_id], references: [id])
|
||||
sonComment CommonShareComment[] @relation("sonComment")
|
||||
fa_comment_id Int?
|
||||
fa_comment CommonShareComment? @relation("sonComment", fields: [fa_comment_id], references: [id])
|
||||
}
|
||||
|
||||
model CommonShareLike {
|
||||
id Int @id @default(autoincrement())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
openid String @unique
|
||||
share_id_list String
|
||||
user User @relation(fields: [openid], references: [openid])
|
||||
}
|
||||
|
||||
model CommonShareCommentLike {
|
||||
id Int @id @default(autoincrement())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
openid String @unique
|
||||
comment_id_list String
|
||||
user User @relation(fields: [openid], references: [openid])
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { AppService } from './app.service';
|
||||
import { HttpService } from './modules/http/http.service';
|
||||
|
||||
@Controller()
|
||||
export class AppController {
|
||||
constructor(private readonly appService: AppService,
|
||||
private readonly httpService: HttpService
|
||||
) { }
|
||||
|
||||
@Get('hello')
|
||||
getHello(): string {
|
||||
return this.appService.getHello();
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,44 +0,0 @@
|
||||
import { CacheModule, Module } from '@nestjs/common';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { AuthModule } from './modules/auth/auth.module';
|
||||
import { CommentModule } from './modules/comment/comment.module';
|
||||
import { FileUploadModule } from './modules/file-upload/file-upload.module';
|
||||
import { HttpModule } from './modules/http/http.module';
|
||||
import { PrismaModule } from './modules/prisma/prisma.module';
|
||||
import { UserModule } from './modules/user/user.module';
|
||||
import { CommonShareModule } from './modules/commonShare/common-share.module'
|
||||
import { LikeModule } from './modules/like/like.module';
|
||||
import { ScheduleModule } from './modules/schedule/schedule.module';
|
||||
import { ApiModule } from './modules/api/api.module';
|
||||
import { TestModule } from './test/test.module';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { FaceModule } from './modules/face/face.module';
|
||||
import { NewsModule } from './modules/news/news.module';
|
||||
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
UserModule,
|
||||
PrismaModule,
|
||||
FileUploadModule,
|
||||
AuthModule,
|
||||
HttpModule,
|
||||
CommentModule,
|
||||
CommonShareModule,
|
||||
LikeModule,
|
||||
ScheduleModule,
|
||||
ApiModule,
|
||||
TestModule,
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
envFilePath: '.env'
|
||||
}),
|
||||
FaceModule,
|
||||
CacheModule.register(),
|
||||
NewsModule
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
})
|
||||
export class AppModule { }
|
||||
@ -1,8 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class AppService {
|
||||
getHello(): string {
|
||||
return 'Hello World!';
|
||||
}
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
import { UseGuards, applyDecorators } from "@nestjs/common";
|
||||
import { AuthGuard } from "@nestjs/passport";
|
||||
|
||||
export function Auth() {
|
||||
return applyDecorators(UseGuards(AuthGuard('jwt')))
|
||||
}
|
||||
@ -1,24 +0,0 @@
|
||||
import { UseInterceptors, applyDecorators } from "@nestjs/common";
|
||||
import { FileInterceptor } from "@nestjs/platform-express";
|
||||
import { FileTooLargeException, InvalidFileTypeException } from "../error/fileError";
|
||||
|
||||
export function ImgUploadInterceptors() {
|
||||
return applyDecorators(UseInterceptors(
|
||||
FileInterceptor('img', {
|
||||
fileFilter: (req, file, callback) => {
|
||||
// 限制文件大小最大为 2mb
|
||||
if (file.size > 2 * 1024 * 1024) {
|
||||
callback(new FileTooLargeException(), false); // 拒绝上传
|
||||
}
|
||||
|
||||
//
|
||||
const allowedMimes = ['image/jpeg', 'image/png', 'image/webp']; // 允许的图片上传类型
|
||||
if (allowedMimes.includes(file.mimetype)) {
|
||||
callback(null, true); // 允许上传
|
||||
} else {
|
||||
callback(new InvalidFileTypeException('图片'), false); // 拒绝上传
|
||||
}
|
||||
},
|
||||
})
|
||||
))
|
||||
}
|
||||
@ -1,2 +0,0 @@
|
||||
export * from './auth.decorator'
|
||||
export * from './imgUpload.decorator'
|
||||
@ -1,8 +0,0 @@
|
||||
class CommonShareDto {
|
||||
readonly id?: number
|
||||
readonly title: string
|
||||
readonly content: string
|
||||
readonly images: string
|
||||
// 使用 openid 字段与 User 表建立关联
|
||||
readonly openid: string
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
class VideoShareDto {
|
||||
readonly id: number
|
||||
readonly title: String
|
||||
readonly video: String
|
||||
// 使用 openid 字段与 User 表建立关联
|
||||
readonly openid: String
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
export class UserDto {
|
||||
id?: number;
|
||||
openid?: string
|
||||
nickname?: string;
|
||||
avatar?: string;
|
||||
birthday?: string;
|
||||
local?: string;
|
||||
sex?: number;
|
||||
city?: string;
|
||||
}
|
||||
@ -1,4 +0,0 @@
|
||||
export const enum API_TYPE {
|
||||
NLP = "nlp",
|
||||
FACE = "face",
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
export const enum HttpCode {
|
||||
SUCCESS = 201,
|
||||
LOGIN_VERIFY_ERROR = 400,
|
||||
LOGIN_ERROR = 401
|
||||
}
|
||||
@ -1,32 +0,0 @@
|
||||
import { HttpException, HttpStatus } from '@nestjs/common';
|
||||
|
||||
// 图片格式错误
|
||||
export class InvalidFileTypeException extends HttpException {
|
||||
constructor(filetype: String) {
|
||||
super(`上传${filetype}格式错误`, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 文件大小超过限制
|
||||
export class FileTooLargeException extends HttpException {
|
||||
constructor() {
|
||||
super('文件大小超过限制', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
//空文件异常
|
||||
export class EmptyFileException extends HttpException {
|
||||
constructor() {
|
||||
super('上传文件为空', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
// 文件上传异常
|
||||
export class FileUploadException extends HttpException {
|
||||
constructor() {
|
||||
super('文件上传失败', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,16 +0,0 @@
|
||||
import { HttpException, HttpStatus } from '@nestjs/common';
|
||||
import { HttpCode } from '../enum/httpCode'
|
||||
|
||||
// 图片格式错误
|
||||
export class LoginVerifyException extends HttpException {
|
||||
constructor() {
|
||||
super('验证码错误', HttpCode.LOGIN_VERIFY_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
// 登录错误
|
||||
export class LoginErrorException extends HttpException {
|
||||
constructor() {
|
||||
super('登录错误', HttpCode.LOGIN_ERROR);
|
||||
}
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
import { HttpException, HttpStatus } from "@nestjs/common";
|
||||
|
||||
export class ShareCreateException extends HttpException {
|
||||
constructor() {
|
||||
super(`分享上传失败`, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
// 分享查询失败
|
||||
export class ShareQueryException extends HttpException {
|
||||
constructor() {
|
||||
super(`分享查询失败`, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
import { HttpException, HttpStatus } from "@nestjs/common";
|
||||
|
||||
export class UserUpdateException extends HttpException {
|
||||
constructor(message: string) {
|
||||
super(message, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
@ -1,20 +0,0 @@
|
||||
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
|
||||
import { Observable } from 'rxjs';
|
||||
import { LoginVerifyException } from 'src/error/login';
|
||||
import { getSessionKey, hasUser } from 'src/store';
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class AuthInterceptor implements NestInterceptor {
|
||||
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
||||
const req = context.switchToHttp().getRequest();
|
||||
const openId = req.headers['openid'];
|
||||
const seesionKey = req.headers['sessionkey'];
|
||||
|
||||
if (hasUser(openId) && getSessionKey(openId) === seesionKey) {
|
||||
return next.handle();
|
||||
} else {
|
||||
throw new LoginVerifyException()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from './app.module';
|
||||
import { global_prefix, port } from './utils/config/index'
|
||||
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
app.setGlobalPrefix(global_prefix);
|
||||
await app.listen(port);
|
||||
}
|
||||
|
||||
bootstrap();
|
||||
@ -1,35 +0,0 @@
|
||||
import { Body, Controller, Post, Req } from '@nestjs/common';
|
||||
import { Auth } from 'src/decorator';
|
||||
import { Request } from 'express';
|
||||
import { ApiService } from './api.service';
|
||||
|
||||
@Controller('api')
|
||||
export class ApiController {
|
||||
|
||||
constructor(
|
||||
private readonly apiService: ApiService
|
||||
) { }
|
||||
|
||||
|
||||
@Post('wxrun')
|
||||
@Auth()
|
||||
wxrun(@Req() req: Request, @Body() body: any) {
|
||||
const user = req.user as any;
|
||||
const session_key = user.session_key
|
||||
return this.apiService.wxrun(session_key, body.encryptedData, body.iv)
|
||||
}
|
||||
|
||||
@Post('weather')
|
||||
@Auth()
|
||||
async getWeather() {
|
||||
return this.apiService.getWeather();
|
||||
}
|
||||
|
||||
|
||||
@Post('getKeywords')
|
||||
@Auth()
|
||||
async getKeyWords(@Body('content') content: string, @Body('title') title: string) {
|
||||
return (await this.apiService.getKeywords(title, content)).data;
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { ApiController } from './api.controller';
|
||||
import { ApiService } from './api.service';
|
||||
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
controllers: [ApiController],
|
||||
providers: [ApiService],
|
||||
exports: [ApiService]
|
||||
})
|
||||
export class ApiModule { }
|
||||
@ -1,95 +0,0 @@
|
||||
import { InjectRedis, Redis } from '@nestjs-modules/ioredis';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { API_TYPE } from 'src/enum/baidu';
|
||||
import { BAIDU_ACCESS_TOKEN_KEY } from 'src/store/redis/keys';
|
||||
import { WXBizDataCrypt } from '../../utils/fn/crypt';
|
||||
import { HttpService } from '../http/http.service';
|
||||
|
||||
@Injectable()
|
||||
export class ApiService {
|
||||
private readonly weatherUrl: string;
|
||||
private readonly appid: string;
|
||||
private readonly gaodeKey: string;
|
||||
// nlp
|
||||
private readonly baidu_nlp_api_key: string;
|
||||
private readonly baidu_nlp_secret_key: string;
|
||||
// face
|
||||
private readonly baidu_face_api_key: string;
|
||||
private readonly baidu_face_secret_key: string;
|
||||
|
||||
constructor(
|
||||
private configService: ConfigService,
|
||||
@InjectRedis() private readonly redis: Redis,
|
||||
private readonly httpService: HttpService,
|
||||
) {
|
||||
this.appid = configService.get<string>('WX_APP_ID');
|
||||
this.gaodeKey = configService.get<string>('GAODE_KEY');
|
||||
this.weatherUrl = configService.get<string>('WEATHER_URL');
|
||||
this.baidu_nlp_api_key = configService.get<string>('BAIDU_NLP_API_KEY');
|
||||
this.baidu_nlp_secret_key = configService.get<string>('BAIDU_NLP_SECRET_KEY');
|
||||
this.baidu_face_api_key = configService.get<string>('BAIDU_FACE_API_KEY');
|
||||
this.baidu_face_secret_key = configService.get<string>('BAIDU_FACE_SECRET_KEY');
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
wxrun(session_key: string, encryptedData: string, iv: string) {
|
||||
const pc = new WXBizDataCrypt(process.env.WX_APP_ID, session_key)
|
||||
const data = pc.decryptData(encryptedData, iv)
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
// 天津市 120000 022
|
||||
async getWeather() {
|
||||
return (await this.httpService.get(this.weatherUrl, {
|
||||
params: {
|
||||
key: this.gaodeKey,
|
||||
city: '120000',
|
||||
}
|
||||
}) as any).data;
|
||||
}
|
||||
|
||||
// 获得百度 accessToken
|
||||
async getBaiduOauthAccessToken(type: string, ak: string, sk: string) {
|
||||
const access_token_from_redis = await this.redis.get(BAIDU_ACCESS_TOKEN_KEY + ':' + type);
|
||||
if (access_token_from_redis) {
|
||||
return access_token_from_redis;
|
||||
}
|
||||
const res = await this.httpService.get('https://aip.baidubce.com/oauth/2.0/token', {
|
||||
params: {
|
||||
grant_type: 'client_credentials',
|
||||
client_id: ak,
|
||||
client_secret: sk
|
||||
}
|
||||
})
|
||||
const expires_in = (res.data as any).expires_in;
|
||||
const access_token = (res.data as any).access_token;
|
||||
|
||||
// 存入redis
|
||||
this.redis.set(BAIDU_ACCESS_TOKEN_KEY + ':' + type, access_token, 'EX', expires_in);
|
||||
return access_token;
|
||||
}
|
||||
|
||||
// 根据文章标题和内容提取关键词
|
||||
async getKeywords(title: string, content: string) {
|
||||
return await this.httpService.post<TopicData>('https://aip.baidubce.com/rpc/2.0/nlp/v1/topic',
|
||||
{
|
||||
title,
|
||||
content
|
||||
},
|
||||
{
|
||||
params: {
|
||||
access_token: await this.getBaiduOauthAccessToken(API_TYPE.NLP, this.baidu_nlp_api_key, this.baidu_nlp_secret_key),
|
||||
charset: 'UTF-8'
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,51 +0,0 @@
|
||||
import { Body, Controller, Get, Post, Res } from '@nestjs/common';
|
||||
import { Response } from 'express';
|
||||
import { Auth } from 'src/decorator/auth.decorator';
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
constructor(
|
||||
private readonly authService: AuthService
|
||||
) { }
|
||||
|
||||
//登陆
|
||||
@Post('login')
|
||||
async login(@Body('code') code: string, @Res() res: Response) {
|
||||
// 在这里进行登录逻辑,验证用户信息等
|
||||
try {
|
||||
const login_info = (await this.authService.login(code));
|
||||
// 设置 Access-Control-Expose-Headers : 'Key'
|
||||
// 用于前端获取自定义的响应头
|
||||
res.header('Access-Control-Expose-Headers', 'authorization,token-max-age,username');
|
||||
res.header('authorization', `Bearer ${login_info.token}`);
|
||||
res.header('token-max-age', process.env.JWT_EXPIRES_IN);
|
||||
return res.status(201).json({
|
||||
message: 'Login successful',
|
||||
user: login_info.user
|
||||
});
|
||||
} catch (error) {
|
||||
return res.status(401).json({ message: 'Unauthorized' });
|
||||
}
|
||||
}
|
||||
|
||||
@Post('faceLogin')
|
||||
async faceLogin(@Body('face_url') face_url: string, @Res() res: Response) {
|
||||
try {
|
||||
const login_info = (await this.authService.faceLogin(face_url));
|
||||
// 设置 Access-Control-Expose-Headers : 'Key'
|
||||
// 用于前端获取自定义的响应头
|
||||
res.header('Access-Control-Expose-Headers', 'authorization,token-max-age,username');
|
||||
res.header('authorization', `Bearer ${login_info.token}`);
|
||||
res.header('token-max-age', process.env.JWT_EXPIRES_IN);
|
||||
return res.status(201).json({
|
||||
message: 'Login successful',
|
||||
user: login_info.user
|
||||
});
|
||||
} catch (error) {
|
||||
return res.status(401).json({ message: 'Unauthorized' });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,28 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthService } from './auth.service';
|
||||
import { AuthController } from './auth.controller';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { ConfigModule } from '@nestjs/config'
|
||||
import { UserService } from '../user/user.service';
|
||||
import { JwtStrategy } from 'src/strategy/jwt.strategy';
|
||||
import COS from 'cos-nodejs-sdk-v5'
|
||||
import { FaceService } from '../face/face.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
JwtModule.registerAsync({
|
||||
imports: [ConfigModule],
|
||||
useFactory: async () => {
|
||||
return {
|
||||
secret: process.env.JWT_SECRET_KEY,
|
||||
signOptions: {
|
||||
expiresIn: process.env.JWT_EXPIRES_IN
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
],
|
||||
controllers: [AuthController],
|
||||
providers: [AuthService, UserService, JwtStrategy, FaceService]
|
||||
})
|
||||
export class AuthModule { }
|
||||
@ -1,90 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { LoginErrorException } from 'src/error/login';
|
||||
import { HttpService } from '../http/http.service';
|
||||
import { UserService } from '../user/user.service';
|
||||
import { ApiService } from '../api/api.service';
|
||||
import { filterObject } from '../../utils/fn/format'
|
||||
import { FaceService } from '../face/face.service';
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
private readonly wx_login_url = process.env.WX_LOGIN_URL;
|
||||
|
||||
constructor(
|
||||
private readonly jwtService: JwtService,
|
||||
private readonly httpService: HttpService,
|
||||
private readonly userService: UserService,
|
||||
private readonly faceService: FaceService
|
||||
) { }
|
||||
|
||||
async login(code: string) {
|
||||
try {
|
||||
const res = await this.httpService.get<WXLoginResponse>(this.wx_login_url, {
|
||||
params: {
|
||||
appid: process.env.WX_APP_ID,
|
||||
secret: process.env.WX_APP_SECRET,
|
||||
js_code: code,
|
||||
grant_type: 'authorization_code'
|
||||
}
|
||||
})
|
||||
const login_info = res.data;
|
||||
const openid = login_info.openid;
|
||||
let user = await this.userService.findUserByOpenId(openid);
|
||||
if (user === null) {
|
||||
// 创建用户
|
||||
user = await this.userService.createUser(openid);
|
||||
}
|
||||
return {
|
||||
user,
|
||||
token: (await this.token(login_info)).token
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw new LoginErrorException();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// {
|
||||
// session_key: 'iEaVjg1JmLWrUI2dHfuYpg==',
|
||||
// openid: 'oyBXy5ZPz7etmnwy34TICz2QT0O4'
|
||||
// }
|
||||
|
||||
async faceLogin(face_url: string) {
|
||||
|
||||
try {
|
||||
const auth_res = await this.faceService.faceMatch(face_url);
|
||||
// 完全不匹配
|
||||
if (auth_res.result.user_list.length === 0) {
|
||||
throw new LoginErrorException();
|
||||
}
|
||||
// 匹配度低于80
|
||||
if (auth_res.result.user_list[0].score < 80) {
|
||||
throw new LoginErrorException();
|
||||
}
|
||||
const openid = auth_res.result.user_list[0].user_id;
|
||||
// 查询用户
|
||||
let user = await this.userService.findUserByOpenId(openid);
|
||||
// 用户不存在
|
||||
if (user === null) {
|
||||
throw new LoginErrorException();
|
||||
}
|
||||
return {
|
||||
user,
|
||||
token: (await this.token({ openid })).token
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('face login:', error);
|
||||
throw new LoginErrorException();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async token(payload: any) {
|
||||
return {
|
||||
token: await this.jwtService.signAsync(payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,44 +0,0 @@
|
||||
import { Body, Controller, Get, ParseIntPipe, Post, Query, Req } from '@nestjs/common';
|
||||
import { Auth } from '../../decorator';
|
||||
import { Request } from 'express';
|
||||
import { CommentService } from './comment.service';
|
||||
|
||||
@Controller('comment')
|
||||
export class CommentController {
|
||||
|
||||
constructor(
|
||||
private readonly commentService: CommentService,
|
||||
) { }
|
||||
|
||||
@Post('add')
|
||||
@Auth()
|
||||
createPost(@Req() req: Request, @Body() comment) {
|
||||
|
||||
const user = req.user as any;
|
||||
const openid = user.openid;
|
||||
comment.openid = openid;
|
||||
return this.commentService.createComment(comment);
|
||||
}
|
||||
|
||||
|
||||
@Get('getCommonShareComment')
|
||||
@Auth()
|
||||
getCommonShareComment(@Query('share_id', ParseIntPipe) common_share_id: number) {
|
||||
return this.commentService.getCommentByShareId(common_share_id);
|
||||
}
|
||||
|
||||
@Get('getUserSelfCommentCnt')
|
||||
@Auth()
|
||||
getSelfCommentCount(@Req() req: Request) {
|
||||
const user = req.user as any;
|
||||
const openid = user.openid;
|
||||
return this.commentService.getSelfCommentCount(openid);
|
||||
}
|
||||
|
||||
@Get('list')
|
||||
@Auth()
|
||||
getCommentList(@Query('page', ParseIntPipe) page: number, @Query('size', ParseIntPipe) size: number, @Query('openid') openid?: string) {
|
||||
return this.commentService.getCommentList(page, size, openid);
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { CommentController } from './comment.controller';
|
||||
import { CommentService } from './comment.service';
|
||||
import { UserService } from 'src/modules/user/user.service';
|
||||
|
||||
@Module({
|
||||
controllers: [CommentController],
|
||||
providers: [CommentService,UserService]
|
||||
})
|
||||
export class CommentModule {}
|
||||
@ -1,171 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { UserService } from '../user/user.service';
|
||||
import { Prisma } from '@prisma/client';
|
||||
|
||||
@Injectable()
|
||||
export class CommentService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly userService: UserService
|
||||
) { }
|
||||
|
||||
|
||||
// {
|
||||
// content: '跟评1',
|
||||
// share_type: 0,
|
||||
// common_share_id: 11,
|
||||
// fa_comment_id: 5,
|
||||
// openid: 'oyBXy5ZPz7etmnwy34TICz2QT0O4'
|
||||
// }
|
||||
async createComment(data) {
|
||||
try {
|
||||
// have fa_comment_id
|
||||
let create_comment_transaction;
|
||||
if (data.fa_comment_id) {
|
||||
create_comment_transaction = this.prisma.commonShareComment.create({
|
||||
data: {
|
||||
content: data.content,
|
||||
user: {
|
||||
connect: {
|
||||
openid: data.openid
|
||||
}
|
||||
},
|
||||
commonshare: {
|
||||
connect: {
|
||||
id: data.common_share_id
|
||||
}
|
||||
},
|
||||
fa_comment: {
|
||||
connect: {
|
||||
id: data.fa_comment_id
|
||||
}
|
||||
}
|
||||
},
|
||||
include: {
|
||||
user: true
|
||||
}
|
||||
})
|
||||
} else {
|
||||
create_comment_transaction = this.prisma.commonShareComment.create(
|
||||
{
|
||||
data: {
|
||||
content: data.content,
|
||||
user: {
|
||||
connect: {
|
||||
openid: data.openid
|
||||
}
|
||||
},
|
||||
commonshare: {
|
||||
connect: {
|
||||
id: data.common_share_id
|
||||
}
|
||||
},
|
||||
},
|
||||
include: {
|
||||
user: true
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// 建立事务
|
||||
const transaction = await this.prisma.$transaction([
|
||||
// 创建评论
|
||||
create_comment_transaction,
|
||||
// 用户评论数+1
|
||||
this.prisma.user.update({
|
||||
where: {
|
||||
openid: data.openid,
|
||||
},
|
||||
data: {
|
||||
commentCount: {
|
||||
increment: 1
|
||||
}
|
||||
}
|
||||
})
|
||||
]);
|
||||
return transaction[0];
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw new Error('评论失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 根据share id获取评论
|
||||
async getCommentByShareId(id) {
|
||||
try {
|
||||
return await this.prisma.commonShareComment.findMany({
|
||||
where: {
|
||||
commonshare: {
|
||||
id: id
|
||||
}
|
||||
},
|
||||
include: {
|
||||
user: true
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw new Error('获取评论失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 根据openid 获取用户评论总数
|
||||
async getSelfCommentCount(openid: string) {
|
||||
try {
|
||||
return await this.prisma.commonShareComment.count({
|
||||
where: {
|
||||
user: {
|
||||
openid
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
throw new Error('获取评论总数失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 分页获取评论列表
|
||||
async getCommentList(page: number, size: number, openid?: string) {
|
||||
try {
|
||||
const where_condition: Prisma.CommonShareCommentWhereInput = {};
|
||||
// 如果有openid 则查询该用户的评论
|
||||
if (openid) {
|
||||
where_condition.user = {
|
||||
openid: openid
|
||||
}
|
||||
}
|
||||
const comment_list = this.prisma.commonShareComment.findMany({
|
||||
where: where_condition,
|
||||
skip: (page - 1) * size,
|
||||
take: size,
|
||||
orderBy: {
|
||||
createdAt: 'desc'
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
nickname: true,
|
||||
avatar: true,
|
||||
openid: true,
|
||||
},
|
||||
},
|
||||
commonshare: true,
|
||||
}
|
||||
});
|
||||
const comment_total_count = this.prisma.commonShareComment.count({
|
||||
where: where_condition
|
||||
});
|
||||
const res = await Promise.all([comment_list, comment_total_count]);
|
||||
return {
|
||||
comment_list: res[0],
|
||||
total_count: res[1]
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error('获取评论列表失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,38 +0,0 @@
|
||||
import { Body, Controller, Get, ParseIntPipe, Post, Query, Req } from '@nestjs/common';
|
||||
import { Request } from 'express';
|
||||
import { Auth } from 'src/decorator/auth.decorator';
|
||||
import { CommonShareService } from './common-share.service';
|
||||
|
||||
@Controller('commonshare')
|
||||
export class CommonShareController {
|
||||
|
||||
|
||||
constructor(
|
||||
private readonly commonShareService: CommonShareService,
|
||||
) { }
|
||||
|
||||
@Post('add/commonshare')
|
||||
@Auth()
|
||||
async commitShare(@Req() req: Request, @Body() body) {
|
||||
return this.commonShareService.createCommonShare(req, body);
|
||||
}
|
||||
|
||||
|
||||
@Get('get/commonshare')
|
||||
@Auth()
|
||||
async getCommonShare(@Query('page', ParseIntPipe) page, @Query('size', ParseIntPipe) size, @Query('openid') openid) {
|
||||
openid = openid === 'undefined' ? undefined : openid;
|
||||
return await this.commonShareService.findCommonShare(page, size, openid)
|
||||
}
|
||||
|
||||
@Get('self/commonshare/cnt')
|
||||
@Auth()
|
||||
async getSelfCommonShareCount(@Req() req: Request) {
|
||||
const user = req.user as any;
|
||||
const openid = user.openid;
|
||||
return await this.commonShareService.getSelfCommonShareCount(openid);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { CommonShareController } from './common-share.controller';
|
||||
import { CommonShareService } from './common-share.service';
|
||||
import { UserService } from 'src/modules/user/user.service';
|
||||
import { LikeModule } from '../like/like.module';
|
||||
import { LikeService } from '../like/like.service';
|
||||
import { ApiService } from '../api/api.service';
|
||||
import { ApiModule } from '../api/api.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
LikeModule
|
||||
],
|
||||
controllers: [CommonShareController],
|
||||
providers: [CommonShareService, UserService, LikeService]
|
||||
})
|
||||
export class CommonShareModule { }
|
||||
@ -1,176 +0,0 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { Request } from 'express';
|
||||
import { ShareCreateException, ShareQueryException } from 'src/error/post';
|
||||
import { PrismaService } from 'src/modules/prisma/prisma.service';
|
||||
import { LikeService } from '../like/like.service';
|
||||
import { ApiService } from '../api/api.service';
|
||||
|
||||
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class CommonShareService {
|
||||
private readonly like_commonshare_shareid_key = process.env.LIKE_COMMONSHARE_SHAREID_KEY;
|
||||
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly likeService: LikeService,
|
||||
private apiService: ApiService
|
||||
) { }
|
||||
|
||||
|
||||
|
||||
|
||||
// {
|
||||
// pic_list: [
|
||||
// 'https://laoyou-img-1300131488.cos.ap-beijing.myqcloud.com/183493377dd45d1a34c84228c48fa8a9.jpg'
|
||||
// ],
|
||||
// common_share_title: '这是一个文章标题',
|
||||
// common_share_content: '这是正文部分',
|
||||
// nickname: '微信用户',
|
||||
// avatar: 'https://laoyou-static-1300131488.cos.ap-beijing.myqcloud.com/user.png'
|
||||
// }
|
||||
|
||||
// 创建commonShare
|
||||
async createCommonShare(req: Request, body: any) {
|
||||
const user = req.user as any;
|
||||
const openid = user.openid;
|
||||
const imagesPath = body.pic_list.join(',');
|
||||
|
||||
const content = body.common_share_content;
|
||||
const title = body.common_share_title;
|
||||
let key_words: TopicData;
|
||||
const item_list = [];
|
||||
try {
|
||||
key_words = (await this.apiService.getKeywords(title, content)).data;
|
||||
const article_topic = this.getArticleTopic(key_words);
|
||||
|
||||
|
||||
const commonShare: Prisma.CommonShareCreateInput = {
|
||||
title: body.common_share_title,
|
||||
content: body.common_share_content,
|
||||
images: imagesPath,
|
||||
cover: body.pic_list[0],
|
||||
like: 0,
|
||||
key_words: article_topic,
|
||||
user: {
|
||||
connect: {
|
||||
openid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const transaction = await this.prisma.$transaction([
|
||||
this.prisma.commonShare.create({
|
||||
data: commonShare
|
||||
}),
|
||||
this.prisma.user.update({
|
||||
where: {
|
||||
openid
|
||||
},
|
||||
data: {
|
||||
commonShareCount: {
|
||||
increment: 1
|
||||
}
|
||||
}
|
||||
})
|
||||
]);
|
||||
return transaction[0];
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw new ShareCreateException();
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
getArticleTopic(topicData: TopicData) {
|
||||
const lv1_tag_list = topicData.item.lv1_tag_list;
|
||||
const lv2_tag_list = topicData.item.lv2_tag_list;
|
||||
const tag_list = lv1_tag_list.concat(lv2_tag_list);
|
||||
const tag_list_str = tag_list.map(item => {
|
||||
return item.tag;
|
||||
}).join(',');
|
||||
return tag_list_str;
|
||||
}
|
||||
|
||||
|
||||
// 分页查询commonShare
|
||||
async findCommonShare(page: number, pageSize: number, openid?: string | undefined) {
|
||||
// 查询post 并附带user信息
|
||||
try {
|
||||
const where_condition = openid ? {
|
||||
user: {
|
||||
openid: openid
|
||||
}
|
||||
} : undefined;
|
||||
|
||||
const commonshare_list = await this.prisma.commonShare.findMany({
|
||||
where: where_condition,
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
orderBy: {
|
||||
createdAt: 'desc'
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
city: true,
|
||||
nickname: true,
|
||||
avatar: true,
|
||||
openid: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
const promises_commonshare_list = Promise.all(commonshare_list.map(async item => {
|
||||
// 从缓存中获取like数量
|
||||
// 如果缓存中没有,就从数据库中获取
|
||||
const share_like_cnt_in_redis = await this.likeService.get_commonshare_like_cnt(item.id)
|
||||
if (share_like_cnt_in_redis === null) {
|
||||
// 将db中的like数量存入redis
|
||||
this.likeService.change_commonshare_like_cnt(item.id, item.like);
|
||||
} else {
|
||||
item.like = parseInt(share_like_cnt_in_redis);
|
||||
}
|
||||
return item;
|
||||
}))
|
||||
|
||||
const total_count = this.prisma.commonShare.count({
|
||||
where: where_condition
|
||||
});
|
||||
const promises = await Promise.all([promises_commonshare_list, total_count]);
|
||||
|
||||
return {
|
||||
commonshare_list: promises[0],
|
||||
total_count: promises[1]
|
||||
}
|
||||
} catch (error) {
|
||||
throw new ShareQueryException();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// 根据openid查询commonShare数量
|
||||
async getSelfCommonShareCount(openid: string) {
|
||||
try {
|
||||
return await this.prisma.commonShare.count({
|
||||
where: {
|
||||
user: {
|
||||
openid
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
throw new ShareQueryException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,24 +0,0 @@
|
||||
import { Body, Controller, Post, Req } from '@nestjs/common';
|
||||
import { Request } from 'express';
|
||||
import { FaceService } from './face.service';
|
||||
import { Auth } from 'src/decorator';
|
||||
|
||||
@Controller('face')
|
||||
export class FaceController {
|
||||
|
||||
constructor(
|
||||
private readonly faceService: FaceService
|
||||
) { }
|
||||
|
||||
@Post('register')
|
||||
@Auth()
|
||||
async register(@Body('face_url') face_url: string, @Req() req: Request) {
|
||||
return this.faceService.faceRegister(face_url, req.user);
|
||||
}
|
||||
|
||||
@Post('match')
|
||||
async match(@Body('face_url') face_url: string) {
|
||||
return this.faceService.faceMatch(face_url);
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { FaceController } from './face.controller';
|
||||
import { FaceService } from './face.service';
|
||||
|
||||
@Module({
|
||||
controllers: [FaceController],
|
||||
providers: [FaceService]
|
||||
})
|
||||
export class FaceModule { }
|
||||
@ -1,72 +0,0 @@
|
||||
import { InjectRedis, Redis } from '@nestjs-modules/ioredis';
|
||||
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { API_TYPE } from 'src/enum/baidu';
|
||||
import { BAIDU_ACCESS_TOKEN_KEY } from 'src/store/redis/keys';
|
||||
import { WXBizDataCrypt } from '../../utils/fn/crypt';
|
||||
import { HttpService } from '../http/http.service';
|
||||
import { ApiService } from '../api/api.service';
|
||||
import { UserService } from '../user/user.service';
|
||||
|
||||
@Injectable()
|
||||
export class FaceService {
|
||||
private readonly baidu_face_api_key: string;
|
||||
private readonly baidu_face_secret_key: string;
|
||||
private readonly baidu_face_group_id: string;
|
||||
|
||||
constructor(
|
||||
private configService: ConfigService,
|
||||
private readonly httpService: HttpService,
|
||||
private readonly userService: UserService,
|
||||
private readonly apiService: ApiService,
|
||||
@InjectRedis() private readonly redis: Redis,
|
||||
|
||||
) {
|
||||
this.baidu_face_api_key = configService.get<string>('BAIDU_FACE_API_KEY');
|
||||
this.baidu_face_secret_key = configService.get<string>('BAIDU_FACE_SECRET_KEY');
|
||||
this.baidu_face_group_id = configService.get<string>('BAIDU_FACE_GROUP_ID');
|
||||
}
|
||||
|
||||
// 创建用户组
|
||||
|
||||
// 人脸注册
|
||||
async faceRegister(face_url: string, user: any) {
|
||||
try {
|
||||
const access_token = await this.apiService.getBaiduOauthAccessToken(API_TYPE.FACE, this.baidu_face_api_key, this.baidu_face_secret_key);
|
||||
const openid = user.openid;
|
||||
const res = await this.httpService.post('https://aip.baidubce.com/rest/2.0/face/v3/faceset/user/add?access_token=' + access_token, {
|
||||
group_id: this.baidu_face_group_id,
|
||||
image: face_url,
|
||||
image_type: "URL",
|
||||
user_id: openid,
|
||||
});
|
||||
this.userService.updateUser({
|
||||
openid: openid,
|
||||
face_auth_status: true
|
||||
});
|
||||
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw new HttpException('人脸注册失败', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
// 人脸匹配
|
||||
async faceMatch(face_url: string): Promise<FaceAuthData> {
|
||||
try {
|
||||
const access_token = await this.apiService.getBaiduOauthAccessToken(API_TYPE.FACE, this.baidu_face_api_key, this.baidu_face_secret_key);
|
||||
const res = await this.httpService.post('https://aip.baidubce.com/rest/2.0/face/v3/search?access_token=' + access_token, {
|
||||
image: face_url,
|
||||
image_type: "URL",
|
||||
group_id_list: this.baidu_face_group_id,
|
||||
})
|
||||
return res.data as any;
|
||||
} catch (error) {
|
||||
throw new HttpException('人脸匹配失败', HttpStatus.BAD_REQUEST)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
import { Controller, Post, UploadedFile } from '@nestjs/common';
|
||||
import { ImgUploadInterceptors } from '../../decorator';
|
||||
import { FileUploadService } from './file-upload.service';
|
||||
|
||||
|
||||
@Controller('upload')
|
||||
export class FileUploadController {
|
||||
|
||||
constructor(
|
||||
private readonly fileUploadService: FileUploadService
|
||||
) { }
|
||||
|
||||
@Post('img')
|
||||
@ImgUploadInterceptors()
|
||||
async uploadImg(@UploadedFile() file) {
|
||||
return this.fileUploadService.uploadImg(file, "img");
|
||||
}
|
||||
|
||||
|
||||
@Post('face')
|
||||
@ImgUploadInterceptors()
|
||||
async uploadFace(@UploadedFile() file) {
|
||||
// TODO 进行节流判断
|
||||
return this.fileUploadService.uploadImg(file, "face");
|
||||
}
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { MulterModule } from '@nestjs/platform-express/multer';
|
||||
import { FileUploadController } from './file-upload.controller';
|
||||
import { FileUploadService } from './file-upload.service';
|
||||
import * as COS from 'cos-nodejs-sdk-v5'
|
||||
|
||||
@Module({
|
||||
imports: [MulterModule],
|
||||
controllers: [FileUploadController],
|
||||
providers: [FileUploadService,
|
||||
{
|
||||
provide: 'COS',
|
||||
useFactory: () => {
|
||||
return new COS({
|
||||
SecretId: process.env.COS_SECRET_ID,
|
||||
SecretKey: process.env.COS_SECRET_KEY
|
||||
})
|
||||
}
|
||||
},
|
||||
],
|
||||
})
|
||||
export class FileUploadModule { }
|
||||
@ -1,71 +0,0 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as COS from 'cos-nodejs-sdk-v5';
|
||||
import { extname } from 'path';
|
||||
import { EmptyFileException, FileUploadException } from '../../error/fileError';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
type FILE_TYPE = 'img' | 'face';
|
||||
|
||||
@Injectable()
|
||||
export class FileUploadService {
|
||||
private readonly bucketImg: string;
|
||||
private readonly bucket_face: string;
|
||||
private readonly region: string;
|
||||
|
||||
constructor(
|
||||
private readonly config: ConfigService,
|
||||
@Inject('COS') private readonly cos: COS,
|
||||
) {
|
||||
this.bucketImg = config.get<string>('COS_IMG_BUCKET');
|
||||
this.bucket_face = config.get<string>('COS_FACE_BUCKET');
|
||||
this.region = config.get<string>('COS_REGION');
|
||||
}
|
||||
|
||||
async uploadImg(file: any, file_type: FILE_TYPE): Promise<string> {
|
||||
// 处理上传的文件
|
||||
if (!file) {
|
||||
throw new EmptyFileException();
|
||||
}
|
||||
|
||||
let buket_name;
|
||||
switch (file_type) {
|
||||
case 'img':
|
||||
buket_name = this.bucketImg;
|
||||
break;
|
||||
case 'face':
|
||||
buket_name = this.bucket_face;
|
||||
break;
|
||||
default:
|
||||
throw new FileUploadException();
|
||||
}
|
||||
|
||||
const randomName = this.generateRandomName();
|
||||
const params = {
|
||||
Bucket: buket_name,
|
||||
Region: this.region,
|
||||
Key: randomName + extname(file.originalname),
|
||||
Body: file.buffer
|
||||
} as COS.PutObjectParams;
|
||||
try {
|
||||
await this.cos.putObject(params, (err, data) => {
|
||||
if (err) {
|
||||
console.log(err)
|
||||
throw new FileUploadException();
|
||||
}
|
||||
})
|
||||
return `https://${buket_name}.cos.${this.region}.myqcloud.com/${randomName}${extname(file.originalname)}`;
|
||||
} catch (err) {
|
||||
throw new FileUploadException();
|
||||
}
|
||||
}
|
||||
|
||||
generateRandomName(): string {
|
||||
return Array(32)
|
||||
.fill(null)
|
||||
.map(() => Math.floor(Math.random() * 16).toString(16))
|
||||
.join('');
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { HttpService } from './http.service';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [HttpService],
|
||||
exports: [HttpService]
|
||||
})
|
||||
export class HttpModule { }
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue