first commit

main
Aaron 2025-02-03 23:58:33 +08:00
commit c70c79dda8
39 changed files with 7625 additions and 0 deletions

36
builds/Jenkinsfile vendored Normal file
View File

@ -0,0 +1,36 @@
pipeline {
agent any
environment {
APP_NAME="ocr-server-admin"
K8S_FILE="builds/ingress.yaml"
K8S_NAMESPACE="kube-qa"
}
stages {
stage('compile'){
steps {
nodejs('Node21') {
sh "npm install --registry=https://registry.npm.taobao.org && npm run build"
}
sh "mkdir -p /etc/nginx/html/${APP_NAME}/"
sh "rm -rf /etc/nginx/html/${APP_NAME}/*"
sh "mv dist/* /etc/nginx/html/${APP_NAME}/"
}
}
stage('ack deploy') {
agent none
steps {
sh "cat builds/nginx.conf > /etc/nginx/conf.d/${APP_NAME}.conf"
configFileProvider([configFile(fileId: '87b5c827-bd51-40af-99f4-31a800614e92', targetLocation: 'K8S-CONFIG', variable: 'KUBECONFIG')]) {
sh 'export tag=$BUILD_ID && envsubst < $K8S_FILE | kubectl apply -f -'
sh "kubectl rollout restart deployment -n ${K8S_NAMESPACE} nginx"
}
}
}
}
}

24
builds/ingress.yaml Normal file
View File

@ -0,0 +1,24 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ocr-server-admin-ingress
namespace: kube-qa
labels:
name: ocr-server-admin-ingress
annotations:
kubernetes.io/ingress.class: "nginx"
nginx.ingress.kubernetes.io/ssl-redirect: "false"
nginx.ingress.kubernetes.io/proxy-buffer-size: "16k"
nginx.ingress.kubernetes.io/proxy-buffers-number: "8"
spec:
rules:
- host: ocr-server-admin.bskies.cc
http:
paths:
- pathType: Prefix
path: /
backend:
service:
name: nginx
port:
number: 80

13
builds/nginx.conf Normal file
View File

@ -0,0 +1,13 @@
server {
listen 80;
server_name ocr-server-admin.bskies.cc;
client_body_buffer_size 10m;
client_max_body_size 100m;
client_header_buffer_size 1m;
location / {
root /etc/nginx/html/ocr-server-admin/;
index index.html;
}
}

28
.gitignore vendored Normal file
View File

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

39
README.md Normal file
View File

@ -0,0 +1,39 @@
# houtai
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## 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 [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
## Customize configuration
See [Vite Configuration Reference](https://vite.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Type-Check, Compile and Minify for Production
```sh
npm run build
```
### Run Unit Tests with [Vitest](https://vitest.dev/)
```sh
npm run test:unit
```

1
env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

13
index.html Normal file
View File

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

5540
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

37
package.json Normal file
View File

@ -0,0 +1,37 @@
{
"name": "houtai",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "run-p \"build-only {@}\" --",
"preview": "vite preview",
"test:unit": "vitest",
"build-only": "vite build",
"type-check": "vue-tsc --build"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"axios": "^1.7.9",
"element-plus": "^2.9.3",
"pinia": "^2.3.0",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@tsconfig/node22": "^22.0.0",
"@types/jsdom": "^21.1.7",
"@types/node": "^22.10.2",
"@vitejs/plugin-vue": "^5.2.1",
"@vue/test-utils": "^2.4.6",
"@vue/tsconfig": "^0.7.0",
"jsdom": "^25.0.1",
"npm-run-all2": "^7.0.2",
"typescript": "~5.6.3",
"vite": "^6.0.5",
"vite-plugin-vue-devtools": "^7.6.8",
"vitest": "^2.1.8",
"vue-tsc": "^2.1.10"
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

5
src/App.vue Normal file
View File

@ -0,0 +1,5 @@
<script setup lang="ts"></script>
<template>
<router-view></router-view>
</template>

86
src/assets/base.css Normal file
View File

@ -0,0 +1,86 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition:
color 0.5s,
background-color 0.5s;
line-height: 1.6;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

1
src/assets/logo.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 276 B

91
src/assets/main.css Normal file
View File

@ -0,0 +1,91 @@
@import './base.css';
#app {
width: 100vw;
margin: 0 auto;
font-weight: normal;
}
a,
.green {
text-decoration: none;
color: hsla(160, 100%, 37%, 1);
transition: 0.4s;
padding: 3px;
}
.flex {
display: flex;
align-items: center;
}
.flex-a {
display: flex;
justify-content: space-around;
align-items: center;
}
.flex-s {
display: flex;
justify-content: space-between;
align-items: center;
}
.flex-e {
display: flex;
justify-content: flex-end;
align-items: center;
}
.flex-c {
display: flex;
flex-direction: column;
align-items: center;
}
.flex-cc {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%
}
/* 上外边距 */
.mt30{
margin-top: 30px;
}
/* 左外边距 */
.ml10{
margin-left: 10px;
}
.ml20{
margin-left: 20px;
}
.mlr10{
margin: 0 10px;
}
/* 内边距 */
.p5{
padding: 5px;
}
/* 颜色 */
.white{
color: white;
}
/* @media (hover: hover) {
a:hover {
background-color: hsla(160, 100%, 37%, 0.2);
}
}
@media (min-width: 1024px) {
body {
display: flex;
place-items: center;
}
#app {
display: grid;
padding: 0 2rem;
}
} */

View File

@ -0,0 +1,41 @@
<script setup lang="ts">
defineProps<{
msg: string
}>()
</script>
<template>
<div class="greetings">
<h1 class="green">{{ msg }}</h1>
<h3>
Youve successfully created a project with
<a href="https://vite.dev/" target="_blank" rel="noopener">Vite</a> +
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>. What's next?
</h3>
</div>
</template>
<style scoped>
h1 {
font-weight: 500;
font-size: 2.6rem;
position: relative;
top: -10px;
}
h3 {
font-size: 1.2rem;
}
.greetings h1,
.greetings h3 {
text-align: center;
}
@media (min-width: 1024px) {
.greetings h1,
.greetings h3 {
text-align: left;
}
}
</style>

View File

@ -0,0 +1,94 @@
<script setup lang="ts">
import WelcomeItem from './WelcomeItem.vue'
import DocumentationIcon from './icons/IconDocumentation.vue'
import ToolingIcon from './icons/IconTooling.vue'
import EcosystemIcon from './icons/IconEcosystem.vue'
import CommunityIcon from './icons/IconCommunity.vue'
import SupportIcon from './icons/IconSupport.vue'
const openReadmeInEditor = () => fetch('/__open-in-editor?file=README.md')
</script>
<template>
<WelcomeItem>
<template #icon>
<DocumentationIcon />
</template>
<template #heading>Documentation</template>
Vues
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
provides you with all information you need to get started.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<ToolingIcon />
</template>
<template #heading>Tooling</template>
This project is served and bundled with
<a href="https://vite.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
recommended IDE setup is
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a>
+
<a href="https://github.com/johnsoncodehk/volar" target="_blank" rel="noopener">Volar</a>. If
you need to test your components and web pages, check out
<a href="https://vitest.dev/" target="_blank" rel="noopener">Vite</a>
and
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a>
/
<a href="https://playwright.dev/" target="_blank" rel="noopener">Playwright</a>.
<br />
More instructions are available in
<a href="javascript:void(0)" @click="openReadmeInEditor"><code>README.md</code></a
>.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<EcosystemIcon />
</template>
<template #heading>Ecosystem</template>
Get official tools and libraries for your project:
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
you need more resources, we suggest paying
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
a visit.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<CommunityIcon />
</template>
<template #heading>Community</template>
Got stuck? Ask your question on
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>
(our official Discord server), or
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
>StackOverflow</a
>. You should also follow the official
<a href="https://bsky.app/profile/vuejs.org" target="_blank" rel="noopener">@vuejs.org</a>
Bluesky account or the
<a href="https://x.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
X account for latest news in the Vue world.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<SupportIcon />
</template>
<template #heading>Support Vue</template>
As an independent project, Vue relies on community backing for its sustainability. You can help
us by
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
</WelcomeItem>
</template>

View File

@ -0,0 +1,87 @@
<template>
<div class="item">
<i>
<slot name="icon"></slot>
</i>
<div class="details">
<h3>
<slot name="heading"></slot>
</h3>
<slot></slot>
</div>
</div>
</template>
<style scoped>
.item {
margin-top: 2rem;
display: flex;
position: relative;
}
.details {
flex: 1;
margin-left: 1rem;
}
i {
display: flex;
place-items: center;
place-content: center;
width: 32px;
height: 32px;
color: var(--color-text);
}
h3 {
font-size: 1.2rem;
font-weight: 500;
margin-bottom: 0.4rem;
color: var(--color-heading);
}
@media (min-width: 1024px) {
.item {
margin-top: 0;
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
}
i {
top: calc(50% - 25px);
left: -26px;
position: absolute;
border: 1px solid var(--color-border);
background: var(--color-background);
border-radius: 8px;
width: 50px;
height: 50px;
}
.item:before {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
bottom: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:after {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
top: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:first-of-type:before {
display: none;
}
.item:last-of-type:after {
display: none;
}
}
</style>

View File

@ -0,0 +1,11 @@
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import HelloWorld from '../HelloWorld.vue'
describe('HelloWorld', () => {
it('renders properly', () => {
const wrapper = mount(HelloWorld, { props: { msg: 'Hello Vitest' } })
expect(wrapper.text()).toContain('Hello Vitest')
})
})

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
/>
</svg>
</template>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
<path
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
/>
</svg>
</template>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
<path
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
/>
</svg>
</template>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
/>
</svg>
</template>

View File

@ -0,0 +1,19 @@
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
role="img"
class="iconify iconify--mdi"
width="24"
height="24"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 24 24"
>
<path
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
fill="currentColor"
></path>
</svg>
</template>

36
src/main.ts Normal file
View File

@ -0,0 +1,36 @@
import './assets/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
import router from './router'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
const app = createApp(App)
app.use(ElementPlus)
app.use(createPinia())
app.use(router)
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.config.globalProperties.$messageS = function msgError(content: any) {
ElMessage({ message: content, type: 'success', })
};
app.config.globalProperties.$messageW = function msgError(content: any) {
ElMessage({ message: content, type: 'warning', })
};
app.config.globalProperties.$messageI = function msgError(content: any) {
ElMessage({ message: content, type: 'info', })
};
app.config.globalProperties.$messageE = function msgError(content: any) {
ElMessage({ message: content, type: 'error', })
};
app.mount('#app')

58
src/router/index.ts Normal file
View File

@ -0,0 +1,58 @@
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/login',
name: 'login',
component: () => import('../views/login.vue'),
},
// 主页
{
path: '/',
redirect: '/index',
component: () => import('../views/home.vue'),
meta: {
comp: 'Home'
},
children: [
// 租户管理
{
path: '/index',
component: () => import('../views/index.vue'),
meta: {
comp: 'index',
name: '租户管理'
}
},
{
path: '/about',
component: () => import('../views/about.vue'),
meta: {
comp: 'about',
name: '产品'
}
},
{
path: '/ceshi',
component: () => import('../views/ceshi.vue'),
meta: {
comp: 'ceshi',
name: '测试'
}
},
]
}
],
})
// 全局路由导航
router.beforeEach((to, from, next) => {
if (to.meta.name) {
document.title = to.meta.name
} else {
document.title = '管理系统'
}
return next()
})
export default router

12
src/stores/counter.ts Normal file
View File

@ -0,0 +1,12 @@
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})

106
src/utils/api.ts Normal file
View File

@ -0,0 +1,106 @@
import { GET, POST } from '@/utils/request';
/**
* @title
* @param params accountpassword
* @returns
*/
export const userLogin = (params?: Object) => {
return POST(
{
url: '/api/user/login',
params
}
)
}
/**
* @title
*/
export const getProduct = async () => {
return POST(
{
url: "/api/product/list",
}
)
}
/**
* @title
*/
export const createProduct = async (params: Object) => {
return POST(
{
url: "/api/product/create",
params
}
)
}
/**
* @title
*/
export const updateProduct = async (params: Object) => {
return POST(
{
url: "/api/product/update",
params
}
)
}
/**
* @title
*/
export const tenantRegister = async (params: Object)=>{
return POST(
{
url: "/api/tenant/register",
params
}
)
}
/**
* @title
*/
export const tenantUpdate = async (params: Object)=>{
return POST(
{
url: "/api/tenant/update",
params
}
)
}
/**
* @title
*/
export const tenantList = async (params: Object)=>{
return POST(
{
url: "/api/tenant/page",
params
}
)
}
/**
* @title
*/
export const create_payorder = async (params: Object)=>{
return POST(
{
url: "/open/api/create_payorder",
params
}
)
}
/**
* @title
*/
export const id_card = async (params: Object)=>{
return POST(
{
url: "/open/api/ocr/id_card",
params
}
)
}

145
src/utils/request.ts Normal file
View File

@ -0,0 +1,145 @@
import axios from "axios";
import { ElMessage } from 'element-plus'
interface requestType {
url: string
params?: any
}
const handleCode = async (code: number, msg: string) => {
switch (code) {
case 401:
ElMessage.error(msg || '登录失效')
setTimeout(() => {
console.log('登录失效')
location.href="/login"
}, 1500)
// 跳转登录
break
default:
ElMessage.error(msg || `后端接口${code}异常`)
break
}
}
//创建axsio 赋给常量service
const service = axios.create({
baseURL: "/api",
timeout: 30000,
headers: {
'Content-Type': 'application/json;charset=UTF-8',
"authorization": localStorage.getItem("token"),
}
});
// 添加请求拦截器
service.interceptors.request.use(
(config: any) => {
console.log(config, 'config')
return config;
},
(error: any) => {
// 对请求错误做些什么
console.log(error, 'error')
return Promise.reject(error);
}
);
// 添加响应拦截器
service.interceptors.response.use(
(response) => {
//response参数是响应对象
// 对响应数据做点什么
const { data, config } = response
return data;
},
(error: any) => {
const { response } = error
if (error.response && error.response.data) {
const { status, data } = response
handleCode(status, data.msg)
// 对响应错误做点什么
return Promise.reject(error);
} else {
let { message } = error
if (message === 'Network Error') {
message = '后端接口连接异常'
}
if (message.includes('timeout')) {
message = '后端接口请求超时'
}
if (message.includes('Request failed with status code')) {
const code = message.substr(message.length - 3)
message = '后端接口' + code + '异常'
}
ElMessage.error(message || `后端接口未知异常`)
return Promise.reject(error);
}
}
);
/**
* @description GET
*/
const GET = ({ url, params }: requestType) => {
return service({
url,
method: "GET",
params
})
}
/**
* @description POST
*/
const POST = ({ url, params }: requestType) => {
return service({
url,
method: "POST",
data: params
})
}
/**
* @description PUT
*/
const PUT = ({ url, params }: requestType) => {
return service({
url,
method: "PUT",
data: params
})
}
/**
* @description DELETE
*/
const DELETE = ({ url, params }: requestType) => {
return service({
url,
method: 'delete',
data: params
})
}
/**
* @description PATCH
*/
const PATCH = ({ url, params }: requestType) => {
return new Promise((resolve, reject) => {
service
.put(url, params)
.then((res: any) => {
if (res && res.status == 200) {
resolve(res)
}
})
.catch((error: any) => {
reject(error)
})
})
}
export { GET, POST, PUT, DELETE, PATCH }

172
src/views/about.vue Normal file
View File

@ -0,0 +1,172 @@
<template>
<el-form>
<el-form-item>
<el-button
@click="
dialogTableVisible = true;
createFrom.resetFields();
"
type="primary"
>新增</el-button
>
</el-form-item>
</el-form>
<el-table :data="tableData" border style="width: 100%">
<el-table-column prop="name" label="名称" width="180" />
<el-table-column prop="price" label="价格" width="180" />
<el-table-column prop="api_usage_limit" label="次数" />
<el-table-column prop="created_at" label="开通时间" />
<el-table-column prop="description" label="备注" />
<el-table-column label="操作">
<template #default="scope">
<div>
<el-button @click="editProduct(scope.row)" type="primary"
>编辑</el-button
>
<el-button type="danger">删除</el-button>
</div>
</template>
</el-table-column>
</el-table>
<!-- 新增或修改弹框 start -->
<el-dialog
v-model="dialogTableVisible"
:title="id ? '修改产品' : '新增产品'"
width="800"
>
<el-form ref="createFrom" :model="createFormData" :rules="createFromRules">
<el-form-item label="名称" prop="name">
<el-input v-model="createFormData.name" placeholder="请输入名称" />
</el-form-item>
<el-form-item label="价格" prop="price">
<el-input v-model="createFormData.price" placeholder="请输入价格" />
</el-form-item>
<el-form-item label="图片"> </el-form-item>
<el-form-item label="次数" prop="api_usage_limit">
<el-input
type="number"
v-model="createFormData.api_usage_limit"
placeholder="请输入可使用次数"
/>
</el-form-item>
<el-form-item label="备注" prop="description">
<el-input
v-model="createFormData.description"
type="textarea"
placeholder="请输入可使用次数"
/>
</el-form-item>
</el-form>
<template #footer>
<div>
<el-button @click="dialogTableVisible = false">取消</el-button>
<el-button @click="submitForm(createFrom)" type="primary"
>提交</el-button
>
</div>
</template>
</el-dialog>
<!-- 新增或修改弹框 end -->
</template>
<script lang="ts" setup>
import { ref, reactive, onMounted } from "vue";
import type { ComponentSize, FormInstance, FormRules } from "element-plus";
import { ElMessage } from "element-plus";
import { getProduct, createProduct, updateProduct } from "@/utils/api.ts";
onMounted(() => {
getList();
});
interface RuleForm {
id?: String;
name: String; //
price: Number; //
description: String; //
image_url: String; //
api_usage_limit: String; //API使
}
const id = ref("");
const createFrom = ref(); //DOM
const dialogTableVisible = ref(false); //&
const createFormData = reactive({
id: "",
name: "", //
price: "", //
description: "", //
image_url: "", //
api_usage_limit: "", //API使
});
const createFromRules = reactive<FormRules<RuleForm>>({
name: [{ required: true, message: "请输入产品名称", trigger: "blur" }],
price: [{ required: true, message: "请输入产品价格", trigger: "blur" }],
api_usage_limit: [
{ required: true, message: "请输入可使用次数", trigger: "blur" },
],
});
//
const tableData = ref([]);
/**
* @titel 获取产品列表
*/
const getList = async () => {
const { code, body } = await getProduct();
if (code == 200) {
tableData.value = body;
}
};
/**
* @title 修改产品
*/
const editProduct = (row) => {
createFormData.id = row.id;
createFormData.api_usage_limit = row.api_usage_limit;
createFormData.description = row.description;
createFormData.name = row.name;
createFormData.price = row.price;
createFormData.image_url = row.image_url;
dialogTableVisible.value = true;
};
/**
* @title 创建产品
*/
const submitForm = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
await formEl.validate(async (valid, fields) => {
if (valid) {
createFormData.price = Number(createFormData.price);
createFormData.api_usage_limit = Number(createFormData.api_usage_limit);
//
if (createFormData.id) {
const { code } = await updateProduct(createFormData);
if (code == 200) {
ElMessage.success("成功!");
dialogTableVisible.value = false;
getList();
}
}
//
else {
const { code } = await createProduct(createFormData);
if (code == 200) {
ElMessage.success("成功!");
dialogTableVisible.value = false;
getList();
}
}
}
});
};
</script>
<style scoped>
::v-deep().el-form-item__content {
justify-content: flex-end !important;
}
</style>

128
src/views/ceshi.vue Normal file
View File

@ -0,0 +1,128 @@
<template>
<div class="tooltip-base-box">
<div class="row center">
<el-tooltip
class="box-item"
effect="dark"
content="Top Left prompts info"
placement="top-start"
>
<el-button>top-start</el-button>
</el-tooltip>
<el-tooltip
class="box-item"
effect="dark"
content="Top Center prompts info"
placement="top"
>
<el-button>top</el-button>
</el-tooltip>
<el-tooltip
class="box-item"
effect="dark"
content="Top Right prompts info"
placement="top-end"
>
<el-button>top-end</el-button>
</el-tooltip>
</div>
<div class="row">
<el-tooltip
class="box-item"
effect="dark"
content="Left Top prompts info"
placement="left-start"
>
<el-button>left-start</el-button>
</el-tooltip>
<el-tooltip
class="box-item"
effect="dark"
content="Right Top prompts info"
placement="right-start"
>
<el-button>right-start</el-button>
</el-tooltip>
</div>
<div class="row">
<el-tooltip
class="box-item"
effect="dark"
content="Left Center prompts info"
placement="left"
>
<el-button class="mt-3 mb-3">left</el-button>
</el-tooltip>
<el-tooltip
class="box-item"
effect="dark"
content="Right Center prompts info"
placement="right"
>
<el-button>right</el-button>
</el-tooltip>
</div>
<div class="row">
<el-tooltip
class="box-item"
effect="dark"
content="Left Bottom prompts info"
placement="left-end"
>
<el-button>left-end</el-button>
</el-tooltip>
<el-tooltip
class="box-item"
effect="dark"
content="Right Bottom prompts info"
placement="right-end"
>
<el-button>right-end</el-button>
</el-tooltip>
</div>
<div class="row center">
<el-tooltip
class="box-item"
effect="dark"
content="Bottom Left prompts info"
placement="bottom-start"
>
<el-button>bottom-start</el-button>
</el-tooltip>
<el-tooltip
class="box-item"
effect="dark"
content="Bottom Center prompts info"
placement="bottom"
>
<el-button>bottom</el-button>
</el-tooltip>
<el-tooltip
class="box-item"
effect="dark"
content="Bottom Right prompts info"
placement="bottom-end"
>
<el-button>bottom-end</el-button>
</el-tooltip>
</div>
</div>
</template>
<style>
.tooltip-base-box {
width: 100%;
}
.tooltip-base-box .row {
display: flex;
align-items: center;
justify-content: space-between;
}
.tooltip-base-box .center {
justify-content: center;
}
.tooltip-base-box .box-item {
width: 110px;
margin-top: 10px;
}
</style>

468
src/views/home.vue Normal file
View File

@ -0,0 +1,468 @@
<template>
<div class="common-layout">
<el-container>
<el-aside
:width="isCollapse ? '63px' : '200px'"
style="background: #1d3043"
>
<div
class="collapse white"
:style="{
fontSize: isCollapse ? '35px' : '20px',
background: '#18222c',
}"
>
{{ isCollapse ? "管" : "管理系统" }}
</div>
<div class="flex-a p5">
<div class="person_img2" style="display: flex; align-items: center">
<el-avatar
:style="{
width: isCollapse ? '40px' : '80px',
height: isCollapse ? '40px' : '80px',
}"
src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png"
/>
</div>
<div class="f14 white" v-if="!isCollapse">
<div>管理员</div>
<div>(总经理)</div>
<div>17584958214</div>
</div>
</div>
<el-menu
:default-active="state.activePath"
class="el-menu-vertical-demo"
:collapse="isCollapse"
router
background-color="#1d3043"
active-color="red"
active-text-color="#ffd04b"
>
<div v-for="item in state.menuList" :key="item.id">
<el-menu-item
:index="'/' + item.browserUrl"
v-if="item.children.length == 0"
>
<el-icon><Link /></el-icon>
<template #title>{{ item.name }} </template>
</el-menu-item>
<el-sub-menu v-else>
<template #title>
<el-icon><Link /></el-icon>
<span class="ml15">{{ item.name }}</span>
</template>
<div v-for="item2 in item.children" :key="item2.id">
<el-menu-item-group>
<el-menu-item :index="'/' + item2.browserUrl">
<el-icon><Link /></el-icon>{{ item2.name }}</el-menu-item
>
</el-menu-item-group>
</div>
</el-sub-menu>
</div>
</el-menu>
</el-aside>
<el-container
:style="{
height: '100vh',
}"
>
<el-header
:style="{
height: '50px',
background: '#1d3043',
}"
class="flex-s white"
>
<div class="flex">
<el-radio-group v-model="isCollapse">
<el-radio-button :value="false" v-if="isCollapse"
><el-icon :size="25"><Fold /></el-icon
></el-radio-button>
<el-radio-button :value="true" v-if="!isCollapse"
><el-icon :size="25"><Fold /></el-icon
></el-radio-button>
</el-radio-group>
<div @click="reload" class="flex mlr10">
<el-icon :size="20"><RefreshRight /></el-icon>
</div>
<div @click="toggleFullScreen" class="flex ml20">
<el-icon :size="19" class=""><FullScreen /></el-icon>
</div>
</div>
<div class="flex">
<div class="header_info">
<div class="flex" @click="state.userVisible = true">
<el-icon size="20"><EditPen /></el-icon>
</div>
<div class="flex ml20" @click="loginOut">
<el-icon size="20"><SwitchButton /></el-icon>退
</div>
</div>
</div>
</el-header>
<el-main class="p5">
<el-tabs
@tab-click="tabClick"
@tab-remove="tabRemove"
v-model="state.activeTab"
type="border-card"
>
<el-tab-pane
v-for="item in state.tabsItem"
:label="item.title"
:name="item.name"
:ref="item.ref"
:closable="item.closable"
>
<keepAlive>
<component
v-if="isRouterAlive"
:is="dom[state.tabsItem.content]"
:key="state.activeTab"
/>
</keepAlive>
</el-tab-pane>
</el-tabs>
</el-main>
<!-- <el-footer
:style="{
height: '30px',
lineHeight: '30px',
}"
><div>
<strong>
Copyright © 2016-2020
<a href="javascript:;">管理系统</a>
</strong>
. All rights reserved.
</div></el-footer
> -->
</el-container>
</el-container>
<el-dialog
title="修改密码"
v-model="state.userVisible"
@close="userDialog"
width="25%"
>
<el-form
label-width="100px"
ref="userRef"
:model="alterUserInfo"
size="mini"
:rules="rules"
>
<div>
<el-form-item
style="width: 89%"
prop="currentPassword"
label="旧密码"
>
<el-input
show-password
v-model="alterUserInfo.currentPassword"
></el-input>
</el-form-item>
<el-form-item style="width: 89%" prop="password" label="新密码">
<el-input show-password v-model="alterUserInfo.password"></el-input>
</el-form-item>
<el-form-item
style="width: 89%"
prop="confirmPassword"
label="再次确定"
>
<el-input
show-password
v-model="alterUserInfo.confirmPassword"
></el-input>
</el-form-item>
</div>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="state.userVisible = false">取消</el-button>
<el-button type="primary" @click="alterPassword(userRef)">
保存
</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import {
ref,
watch,
shallowRef,
onMounted,
reactive,
nextTick,
getCurrentInstance,
} from "vue";
import { useRouter } from "vue-router";
import type { FormInstance, FormRules } from "element-plus";
import index from "./index.vue";
import about from "./about.vue";
import ceshi from "./ceshi.vue";
const { appContext } = getCurrentInstance();
const dom = shallowRef({
ceshi,
about,
index,
});
const proxy = appContext.config.globalProperties;
const router = useRouter();
const state = reactive({
userVisible: false,
activePath: "/index",
activeTab: "index",
tabsItem: [
{
title: "租户管理",
name: "index",
//
content: "index",
ref: "tabs",
closable: false,
query: "",
path: "/index",
},
],
menuList: [
{
action: "index",
appIcon: null,
browserUrl: "index",
children: [],
icon: "fa fa-link",
name: "租户管理",
},
{
action: "about",
appIcon: null,
browserUrl: "about",
children: [],
icon: "fa fa-link",
name: "关于",
},
{
action: "ceshi",
appIcon: null,
browserUrl: "ceshi",
children: [],
icon: "fa fa-link",
name: "测试",
},
] as any,
});
const validatePass = (rule: any, value: any, callback: any) => {
console.log(value);
if (value.length < 6) {
callback(new Error("密码长度不能小于6位"));
} else {
callback();
}
};
const alterUserInfo = reactive({
currentPassword: "",
password: "",
confirmPassword: "",
});
const userRef = ref<FormInstance>();
const rules = {
currentPassword: [
{ required: true, message: "请输入密码", trigger: "blur" },
{ validator: validatePass, trigger: "blur" },
],
password: [
{ required: true, message: "请输入密码", trigger: "blur" },
{ validator: validatePass, trigger: "blur" },
],
confirmPassword: [
{ required: true, message: "请确认密码", trigger: "blur" },
{
validator: (rule: any, value: any, callback: any) => {
if (value !== alterUserInfo.password) {
callback(new Error("两次输入的密码不一致"));
} else {
callback();
}
},
trigger: "blur",
},
],
};
const isRouterAlive = ref(true);
//
let isCollapse = ref<Boolean>(false);
onMounted(async () => {
console.log(8888);
});
//
const reload = () => {
// router.go(0);
isRouterAlive.value = false;
nextTick(() => {
isRouterAlive.value = true;
});
};
// 退
const loginOut = () => {
router.push("/login");
};
// dialog
const userDialog = () => {
for (let i in alterUserInfo) {
alterUserInfo[i] = "";
}
};
//
const alterPassword = (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.validate(async (valid: any, fields: any) => {
if (valid) {
state.userVisible = false;
router.push("/login");
} else {
return proxy.$messageE("请完善信息");
}
});
};
//
const isFullScreen = ref(false);
function toggleFullScreen() {
if (!document.fullscreenElement && !isFullScreen.value) {
//
document.documentElement.requestFullscreen().then(() => {
isFullScreen.value = true;
});
} else {
// 退
if (document.exitFullscreen) {
document.exitFullscreen().then(() => {
isFullScreen.value = false;
});
}
}
}
//
document.addEventListener("fullscreenchange", () => {
isFullScreen.value = !!document.fullscreenElement;
});
// tab
const tabClick = (tab: any) => {
let val = state.tabsItem.filter((item) => tab.props.label == item.title);
router.push(val[0].path);
state.activePath = val[0].path;
};
// tab
const tabRemove = (tab: any) => {
state.tabsItem.forEach((val: any, index: Number) => {
if (val.name === tab) {
state.tabsItem.splice(index, 1);
if (tab === state.activeTab) {
router.push(state.tabsItem[index - 1].path);
state.activePath = state.tabsItem[index - 1].path;
state.activeTab = state.tabsItem[index - 1].name;
}
}
});
};
//
watch(
() => router.currentRoute.value,
(val: any) => {
console.log(8888, val);
state.activePath = val.path;
state.tabsItem.title = val.meta.name;
state.tabsItem.name = val.meta.comp;
state.tabsItem.closable = true;
state.tabsItem.ref = "tabs";
state.tabsItem.content = val.meta.comp;
state.tabsItem.query = val.query.id;
state.tabsItem.path = val.path;
console.log(56696, state.tabsItem);
let result = state.tabsItem.some((item) => item.title == val.meta.name);
console.log(result);
state.activeTab = val.meta.comp;
if (!result) {
state.tabsItem.push({
title: val.meta.name,
name: val.meta.comp,
//
content: val.meta.comp,
ref: "tabs",
closable: true,
query: "",
path: "/" + val.meta.comp,
});
} else {
return;
}
},
{ immediate: true }
);
</script>
<style>
.header_info {
display: flex;
justify-content: right;
align-items: center;
width: auto;
}
.collapse {
text-align: center;
cursor: pointer;
align-items: center;
width: 100%;
height: 50px;
line-height: 50px;
}
.person_img2 img {
margin: 0 auto;
border-radius: 50%;
/* background: #ffffff; */
}
.el-tab-pane {
max-height: calc(100vh - 150px);
overflow-y: hidden;
}
.el-radio-button__inner {
background: #1d3043;
color: white;
border-color: #1d3043;
}
.el-radio-button__original-radio:checked + .el-radio-button__inner {
background: #1d3043;
border-color: #1d3043;
}
.el-radio-button:first-child .el-radio-button__inner {
border-left: #1d3043;
}
.el-menu {
border-right: none;
}
.el-menu-item-group__title {
padding: 0 !important;
}
.el-menu-item {
color: white;
}
.el-main {
padding: 0;
}
</style>

120
src/views/index.vue Normal file
View File

@ -0,0 +1,120 @@
<template>
<el-form>
<el-form-item>
<el-button type="primary">新增</el-button>
</el-form-item>
</el-form>
<el-table :data="tableData" border style="width: 100%">
<el-table-column align="center" fixed prop="tenant_name" label="租户昵称" />
<el-table-column align="center" prop="account" label="账号" width="220" />
<el-table-column align="center" prop="enable" label="是否开启" width="120" />
<el-table-column align="center" prop="created_at" label="创建时间" width="220" />
<el-table-column align="center" fixed="right" label="操作" width="120">
<template #default="scope">
<el-button @click="update(scope.row)" link type="primary" size="small">编辑</el-button>
</template>
</el-table-column>
</el-table>
<div class="flex-c-c mt20">
<el-pagination
v-model:current-page="page"
v-model:page-size="pageSize"
:page-sizes="[100, 200, 300, 400]"
layout=" prev, pager, next"
:total="count"
background
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</template>
<script lang="ts" setup>
import { ElMessage } from "element-plus";
import { tenantList, tenantUpdate, tenantRegister } from "@/utils/api.ts"
const handleClick = () => {
console.log("click");
};
import { ref, onMounted, reactive } from "vue";
//
const formData = reactive({
account: "",
password: "",
tenant_name: "",
})
onMounted(async () => {
getList()
});
const page = ref(1)
const pageSize = ref(20)
const handleSizeChange = (val: number) => {
console.log(`${val} items per page`)
}
const handleCurrentChange = (val: number) => {
page.value=val
getList()
}
//
const tableData = ref([]);
//
const count = ref(0)
/**
* @title 获取租户列表
*/
const getList = async () => {
const params = {
tenant_name: "",
page: page.value,
size: pageSize.value,
}
const { code, body:{records,total} } = await tenantList(params)
if (code == 200) {
tableData.value = records
count.value = total
}
}
/**
* @title 注册租户
*/
const regesiter = async (params: Object) => {
const { code } = await tenantRegister(formData)
if(code==200){
ElMessage.success("成功!")
}
}
/**
* @title 修改租户
*/
const update = async ()=>{
console.log(formData);
return
const {code} = await tenantUpdate(formData)
if(code==200){
ElMessage.success("修改成功!")
}
}
</script>
<style scoped>
.flex-c-c{
display: flex;
align-items: center;
justify-content: center;
}
.mt20{
margin-top: 20px;
}
</style>

90
src/views/login.vue Normal file
View File

@ -0,0 +1,90 @@
<template>
<div class="contain">
<el-card class="box-card" shadow="always">
<template #header>
<div class="flex-c">欢迎登录</div>
</template>
<el-form ref="ruleFormRef" :rules="rules" :model="ruleForm">
<el-form-item label="账号" prop="account">
<el-input :prefix-icon="User" v-model="ruleForm.account" />
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input :prefix-icon="Lock" v-model="ruleForm.password" />
</el-form-item>
</el-form>
<div class="flex-s">
<div>
<el-checkbox v-model="state.isRemember"></el-checkbox>
</div>
<div style="color: #606266; font-size: 14px">忘记密码</div>
</div>
<div class="flex-c mt30">
<el-button
type="primary"
@click="submitForm(ruleFormRef)"
:loading="state.loading"
style="width: 100%"
>登录</el-button
>
</div>
</el-card>
</div>
</template>
<script lang="ts" setup>
import { reactive, getCurrentInstance, ref } from "vue";
import { User, Lock } from "@element-plus/icons-vue";
import { useRouter } from "vue-router";
import { ElMessage } from "element-plus";
import { userLogin } from "@/utils/api.ts";
const { appContext } = getCurrentInstance();
const router = useRouter();
const proxy = appContext.config.globalProperties;
const ruleFormRef = ref();
const ruleForm = reactive({
account: "",
password: "",
});
const state = reactive({
loading: false,
isRemember: false,
});
const rules = reactive<FormRules<ruleForm>>({
account: [{ required: true, message: "请输入账号", trigger: "blur" }],
password: [{ required: true, message: "请输入密码", trigger: "blur" }],
});
const submitForm = async (formEl) => {
if (!formEl) return;
await formEl.validate(async (valid, fields) => {
if (valid) {
const {
code,
body: { token },
} = await userLogin(ruleForm);
if (code == 200) {
localStorage.setItem("token", token);
}
ElMessage.success("登录成功")
router.push("/");
} else {
ElMessage.error("请完善信息")
}
});
};
</script>
<style>
.contain {
width: 100%;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
.box-card {
width: 400px;
}
</style>

12
tsconfig.app.json Normal file
View File

@ -0,0 +1,12 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"paths": {
"@/*": ["./src/*"]
}
}
}

14
tsconfig.json Normal file
View File

@ -0,0 +1,14 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.vitest.json"
}
]
}

18
tsconfig.node.json Normal file
View File

@ -0,0 +1,18 @@
{
"extends": "@tsconfig/node22/tsconfig.json",
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"nightwatch.conf.*",
"playwright.config.*"
],
"compilerOptions": {
"noEmit": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["node"]
}
}

11
tsconfig.vitest.json Normal file
View File

@ -0,0 +1,11 @@
{
"extends": "./tsconfig.app.json",
"include": ["src/**/__tests__/*", "env.d.ts"],
"exclude": [],
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.vitest.tsbuildinfo",
"lib": [],
"types": ["node", "jsdom"]
}
}

27
vite.config.ts Normal file
View File

@ -0,0 +1,27 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueDevTools(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
})

14
vitest.config.ts Normal file
View File

@ -0,0 +1,14 @@
import { fileURLToPath } from 'node:url'
import { mergeConfig, defineConfig, configDefaults } from 'vitest/config'
import viteConfig from './vite.config'
export default mergeConfig(
viteConfig,
defineConfig({
test: {
environment: 'jsdom',
exclude: [...configDefaults.exclude, 'e2e/**'],
root: fileURLToPath(new URL('./', import.meta.url)),
},
}),
)