Browse Source

init

master
sprint 6 months ago
commit
fc8cc2a05d
  1. 6
      .editorconfig
  2. 2
      .env.production
  3. 30
      .gitignore
  4. 7
      .prettierrc.json
  5. 39
      README.md
  6. 1
      env.d.ts
  7. 27
      eslint.config.js
  8. 17
      index.html
  9. 6022
      package-lock.json
  10. 46
      package.json
  11. BIN
      public/favicon.ico
  12. 51
      src/App.vue
  13. 10
      src/api/system.ts
  14. 86
      src/assets/base.css
  15. BIN
      src/assets/favicon.ico
  16. BIN
      src/assets/logo.png
  17. 1
      src/assets/logo.svg
  18. 35
      src/assets/main.css
  19. 0
      src/common/func.ts
  20. 1
      src/common/params.ts
  21. 60
      src/components/ConfirmBox.vue
  22. 75
      src/components/DataTable.vue
  23. 28
      src/components/Snackbar.vue
  24. 39
      src/components/layout/NavbarsBox.vue
  25. 35
      src/main.ts
  26. 37
      src/plugins/axios.ts
  27. 59
      src/router/index.ts
  28. 12
      src/stores/counter.ts
  29. 12
      src/stores/manager.ts
  30. 8
      src/views/NotFound/404.vue
  31. 228
      src/views/login/LoginView.vue
  32. 233
      src/views/system/ManagerList.vue
  33. 202
      src/views/userSearch/index.vue
  34. 15
      tsconfig.app.json
  35. 11
      tsconfig.json
  36. 19
      tsconfig.node.json
  37. 29
      vite.config.ts

6
.editorconfig

@ -0,0 +1,6 @@
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue}]
charset = utf-8
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

2
.env.production

@ -0,0 +1,2 @@
VITE_APP_TITLE=HK
VITE_API_URL=http://127.0.0.1:8090

30
.gitignore

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

7
.prettierrc.json

@ -0,0 +1,7 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"singleQuote": true,
"printWidth": 120,
"tabWidth": 4
}

39
README.md

@ -0,0 +1,39 @@
# backend_web
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
```
### Lint with [ESLint](https://eslint.org/)
```sh
npm run lint
```

1
env.d.ts

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

27
eslint.config.js

@ -0,0 +1,27 @@
import pluginVue from 'eslint-plugin-vue'
import vueTsEslintConfig from '@vue/eslint-config-typescript'
import oxlint from 'eslint-plugin-oxlint'
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
export default [
{
name: 'app/files-to-lint',
files: ['**/*.{ts,mts,tsx,vue}'],
},
{
name: 'app/files-to-ignore',
ignores: ['**/dist/**', '**/dist-ssr/**', '**/coverage/**'],
},
...pluginVue.configs['flat/essential'],
...vueTsEslintConfig(),
oxlint.configs['flat/recommended'],
skipFormatting,
{
rules: {
'max-len': 'off',
},
},
'prettier/prettier',
]

17
index.html

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/src/assets/favicon.ico">
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Telemarketing</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

6022
package-lock.json

File diff suppressed because it is too large

46
package.json

@ -0,0 +1,46 @@
{
"name": "telemarketing_web",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
"build:dev": "run-p type-check \"build-only -- --mode staging\" --",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --build --force",
"lint:oxlint": "oxlint . --fix -D correctness --ignore-path .gitignore",
"lint:eslint": "eslint . --fix",
"lint": "run-s lint:*",
"format": "prettier --write src/"
},
"dependencies": {
"axios": "^1.7.9",
"pinia": "^2.3.0",
"pinia-plugin-persistedstate": "^4.2.0",
"vue": "^3.5.13",
"vue-router": "^4.5.0",
"vuetify": "^3.7.6"
},
"devDependencies": {
"@mdi/font": "^7.4.47",
"@tsconfig/node22": "^22.0.0",
"@types/node": "^22.9.0",
"@vitejs/plugin-vue": "^5.1.4",
"@vue/eslint-config-prettier": "^10.1.0",
"@vue/eslint-config-typescript": "^14.1.3",
"@vue/tsconfig": "^0.5.1",
"eslint": "^9.14.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-oxlint": "^0.11.0",
"eslint-plugin-vue": "^9.30.0",
"npm-run-all2": "^7.0.1",
"oxlint": "^0.11.0",
"prettier": "^3.3.3",
"typescript": "~5.6.3",
"vite": "^5.4.10",
"vite-plugin-vue-devtools": "^7.5.4",
"vue-tsc": "^2.1.10"
}
}

BIN
public/favicon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

51
src/App.vue

@ -0,0 +1,51 @@
<script setup lang="ts">
import Navbars from '@/components/layout/NavbarsBox.vue'
import { RouterView, useRoute, useRouter } from 'vue-router'
import { useManagerStore } from './stores/manager'
const route = useRoute()
const router = useRouter()
const store = useManagerStore()
const title = import.meta.env.VITE_APP_TITLE
//
const viewportHeight = window.innerHeight - 8
//退
const exitManager = () => {
store.$reset()
router.push('/login')
}
</script>
<template>
<v-layout v-if="route.name != undefined && route.name != 'Login' && route.name != 'NotFound'" >
<v-app-bar :title="title" color="primary">
<v-spacer></v-spacer>
<v-divider vertical inset></v-divider>
<v-btn @click="exitManager">退出</v-btn>
</v-app-bar>
<v-navigation-drawer>
<Navbars />
</v-navigation-drawer>
<v-main class="d-flex" :height="viewportHeight">
<v-card width="100%" class="pa-2" style="height: 100%" elevation="0">
<RouterView />
</v-card>
</v-main>
</v-layout>
<RouterView v-else />
</template>
<style>
#app {
height: 100%;
margin: 0;
padding: 0;
}
</style>

10
src/api/system.ts

@ -0,0 +1,10 @@
export interface FormInfo {
avatar: string
id: number
account: string
nickname: string
password: string
roleID: number
secret: string
status: number
}

86
src/assets/base.css

@ -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;
}

BIN
src/assets/favicon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
src/assets/logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

1
src/assets/logo.svg

@ -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

35
src/assets/main.css

@ -0,0 +1,35 @@
@import './base.css';
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
font-weight: normal;
}
a,
.green {
text-decoration: none;
color: hsla(160, 100%, 37%, 1);
transition: 0.4s;
padding: 3px;
}
@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;
grid-template-columns: 1fr 1fr;
padding: 0 2rem;
}
}

0
src/common/func.ts

1
src/common/params.ts

@ -0,0 +1 @@
export const pageLimit = 10

60
src/components/ConfirmBox.vue

@ -0,0 +1,60 @@
<script setup lang="ts">
import { reactive, watch } from 'vue'
const props = defineProps(['show', 'msg'])
const emits = defineEmits(['onSumbit', 'show'])
const data = reactive({
dialog: { show: props.show, msg: props.msg },
overlay: false,
})
const submit = () => {
data.overlay = true
emits('onSumbit', true)
}
watch(
() => [props.show, props.msg],
(v) => {
data.dialog.show = v[0]
data.dialog.msg = v[1]
},
)
const closeOverlay = () => {
emits('show', false)
data.overlay = false
}
defineExpose({ closeOverlay })
</script>
<template>
<v-dialog v-model="data.dialog.show" width="50%">
<v-card>
<v-card-title class="text-end">
<v-btn icon="mdi-close" variant="text" @click="emits('show', false)"></v-btn>
</v-card-title>
<div class="py-12 text-center">
<v-icon class="mb-6" color="pink" icon="mdi-delete-circle-outline" size="88"></v-icon>
<div class="text-h5 font-weight-bold">{{ data.dialog.msg }}</div>
</div>
<v-divider></v-divider>
<div class="pa-4 text-end">
<v-btn variant="text" class="me-2" @click="emits('show', false)">取消</v-btn>
<v-btn color="pink" @click="submit">确定</v-btn>
</div>
</v-card>
</v-dialog>
<v-overlay v-model="data.overlay" class="align-center justify-center">
<span class="text-white mr-2 text-caption">处理中</span>
<v-progress-circular color="primary" indeterminate></v-progress-circular>
</v-overlay>
</template>

75
src/components/DataTable.vue

@ -0,0 +1,75 @@
<script setup lang="ts">
import { reactive, computed, type PropType } from 'vue'
interface Header {
title: string
key: string
align?: 'center' | 'start' | 'end' | undefined
}
const props = defineProps({
headers: Array as PropType<Header[]>,
items: Array,
showSelect: {
type: Boolean,
default: false,
},
page: Number,
size: Number,
})
const data = reactive({
currentPage: props.page,
})
const computedHeaders = computed(() =>
props.headers?.map((header) => ({
...header,
align: header.align ?? 'center',
})),
)
const emits = defineEmits(['getItems'])
</script>
<template>
<v-data-table :headers="computedHeaders" :items="items" :show-select="showSelect" :items-per-page="-1" disable-sort>
<template v-slot:top></template>
<!-- 是否展示select -->
<template v-slot:header.data-table-select="{ allSelected, selectAll, someSelected }">
<v-checkbox-btn
:indeterminate="someSelected && !allSelected"
:model-value="allSelected"
color="primary"
@update:model-value="selectAll(!allSelected)"
></v-checkbox-btn>
</template>
<template v-slot:item.data-table-select="{ internalItem, isSelected, toggleSelect }">
<v-checkbox-btn
:model-value="isSelected(internalItem)"
color="pink"
@update:model-value="toggleSelect(internalItem)"
></v-checkbox-btn>
</template>
<!-- 动态生成列模板 -->
<template v-for="header in headers" #[`item.${header.key}`]="{ item, value }">
<slot :name="`item.${header.key}`" :item="item" :value="value">
{{ value }}
</slot>
</template>
<template v-slot:bottom>
<div class="text-center pt-2">
<v-pagination
v-model="data.currentPage"
:length="size"
:total-visible="7"
@update:model-value="() => emits('getItems', data.currentPage)"
></v-pagination>
</div>
</template>
</v-data-table>
</template>

28
src/components/Snackbar.vue

@ -0,0 +1,28 @@
<script setup lang="ts">
import { reactive, watch } from 'vue'
const props = defineProps(['show', 'msg'])
const emits = defineEmits(['close'])
const data = reactive({
snackbar: { show: props.show, msg: props.msg } as any,
})
watch(
() => [props.show, props.msg],
(v) => {
data.snackbar.show = v[0]
data.snackbar.msg = v[1]
},
)
</script>
<template>
<v-snackbar v-model="data.snackbar.show" vertical>
<p>{{ data.snackbar.msg }}</p>
<template v-slot:actions>
<v-btn color="indigo" variant="text" @click="data.snackbar.show = emits('close', false)"> Close </v-btn>
</template>
</v-snackbar>
</template>

39
src/components/layout/NavbarsBox.vue

@ -0,0 +1,39 @@
<script setup lang="ts">
import router from '@/router'
import { useManagerStore } from '@/stores/manager'
import { onMounted, reactive, watch } from 'vue'
const managerStore = useManagerStore()
const data = reactive({
open: null,
menus: [] as any[],
})
const jump = (path: string) => {
router.push(path)
}
watch(
() => managerStore.$state.menus,
(newMenus) => {
data.menus = newMenus
},
)
onMounted(() => {
data.menus = managerStore.$state.menus
})
</script>
<template>
<v-list v-model="data.open">
<v-list-group v-for="(item, index) in data.menus" :key="index" :value="item.id">
<template v-slot:activator="{ props }">
<v-list-item v-bind="props" :title="item.title"></v-list-item>
</template>
<v-list-item v-for="(v, k) in item.children" :key="k" :title="v.title" :value="v.id" @click="jump(v.path)">
</v-list-item>
</v-list-group>
</v-list>
</template>

35
src/main.ts

@ -0,0 +1,35 @@
//import './assets/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import App from './App.vue'
import router from './router'
// Vuetify
import 'vuetify/styles'
import { createVuetify } from 'vuetify'
import * as components from 'vuetify/components'
import * as directives from 'vuetify/directives'
import '@mdi/font/css/materialdesignicons.css'
import { md3 } from 'vuetify/blueprints'
const vuetify = createVuetify({
components,
directives,
icons: {
defaultSet: 'mdi',
},
blueprint: md3,
})
const app = createApp(App)
app.use(createPinia().use(piniaPluginPersistedstate))
app.use(router)
app.use(vuetify)
app.mount('#app')

37
src/plugins/axios.ts

@ -0,0 +1,37 @@
import router from '@/router'
import { useManagerStore } from '@/stores/manager'
import axios from 'axios'
const instance = axios.create({
baseURL: import.meta.env.VITE_API_URL,
timeout: 30000,
headers: { Authorization: '', Lang: 'zh' },
})
//请求拦截器
instance.interceptors.request.use(
function (config) {
const store = useManagerStore()
config.headers.Authorization = 'Bearer ' + store.$state.token
return config
},
function (error) {
return Promise.reject(error)
},
)
//响应拦截器
instance.interceptors.response.use(
function (response) {
const expireCode = [1000, 1001, 7]
if (expireCode.includes(response.data.code)) router.push("/login")
return response
},
function (error) {
console.log('response ', error)
return Promise.reject(error)
},
)
export default instance

59
src/router/index.ts

@ -0,0 +1,59 @@
import { useManagerStore } from '@/stores/manager'
import { ref } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'Home',
redirect: '/login'
},
{
path: '/login',
name: 'Login',
component: () => import('../views/login/LoginView.vue'),
},
{ path: '/:pathMatch(.*)*', name: 'NotFound', component: () => import('../views/NotFound/404.vue') },
],
})
const components = import.meta.glob('../views/**/*.vue');
//设置导航栏
let initBars = false
export function initNavigation(menus: any) {
initBars = true
for (const item of menus) {
if (item.component != "") router.addRoute({ path: item.path, name: item.path, component: components[`../views${item.component}.vue`] })
if (item.children && item.children.length > 0) initNavigation(item.children)
}
}
router.beforeEach((to) => {
//未登录则跳转至登录
if (useManagerStore().token == "" && to.name != "Login") {
return { name: 'Login' }
}
//设置路由
if (!initBars) {
const store = useManagerStore()
initNavigation(store.menus)
let issetMenu = false
for (const item of router.getRoutes()) {
if (item.path == to.fullPath) {
issetMenu = true
break
}
}
if (!issetMenu) return { name: "NotFound" }
return to.fullPath
}
})
export default router

12
src/stores/counter.ts

@ -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 }
})

12
src/stores/manager.ts

@ -0,0 +1,12 @@
import { defineStore } from 'pinia'
export const useManagerStore = defineStore('manager', {
state: () => {
return {
name: '',
token: '',
menus: [],
}
},
persist: true,
})

8
src/views/NotFound/404.vue

@ -0,0 +1,8 @@
<script lang="ts" setup>
import { RouterLink } from 'vue-router';
</script>
<template>
<div>404</div>
<RouterLink :to="`/system/duck`">aa</RouterLink>
</template>

228
src/views/login/LoginView.vue

@ -0,0 +1,228 @@
<script setup lang="ts">
import instance from '@/plugins/axios'
import { onMounted, reactive } from 'vue'
import Snackbar from '@/components/Snackbar.vue'
import { useManagerStore } from '@/stores/manager'
import { useRouter } from 'vue-router'
const title = import.meta.env.VITE_APP_TITLE
const router = useRouter()
const managerStore = useManagerStore()
const data = reactive({
username: '',
password: '',
snackbar: { show: false, msg: '' },
})
const onSubmit = async () => {
const response = (
await instance.post('/login', { username: data.username, password: data.password, verification: '' })
).data
if (!response.code) {
data.snackbar.show = true
data.snackbar.msg = '用户名或密码错误'
}
managerStore.$patch({
menus: response.data.menus,
token: response.data.token,
})
router.push(response.data.menus[0].children[0].path)
//setNavigation(response.data.menus)
}
</script>
<template>
<div class="login-box">
<div class="svg-top">
<!--?xml version="1.0" encoding="utf-8"?-->
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
height="1337"
width="1337"
>
<defs>
<path
id="path-1"
opacity="1"
fill-rule="evenodd"
d="M1337,668.5 C1337,1037.455193874239 1037.455193874239,1337 668.5,1337 C523.6725684305388,1337 337,1236 370.50000000000006,1094 C434.03835568300906,824.6732385973953 6.906089672974592e-14,892.6277623047779 0,668.5000000000001 C0,299.5448061257611 299.5448061257609,1.1368683772161603e-13 668.4999999999999,0 C1037.455193874239,0 1337,299.544806125761 1337,668.5Z"
></path>
<linearGradient id="linearGradient-2" x1="0.79" y1="0.62" x2="0.21" y2="0.86">
<stop offset="0" stop-color="rgb(88,62,213)" stop-opacity="1"></stop>
<stop offset="1" stop-color="rgb(23,215,250)" stop-opacity="1"></stop>
</linearGradient>
</defs>
<g opacity="1">
<use xlink:href="#path-1" fill="url(#linearGradient-2)" fill-opacity="1"></use>
</g>
</svg>
</div>
<div class="svg-bottom">
<!--?xml version="1.0" encoding="utf-8"?-->
<svg
version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
height="896"
width="967.8852157128662"
>
<defs>
<path
id="path-2"
opacity="1"
fill-rule="evenodd"
d="M896,448 C1142.6325445712241,465.5747656464056 695.2579309733121,896 448,896 C200.74206902668806,896 5.684341886080802e-14,695.2579309733121 0,448.0000000000001 C0,200.74206902668806 200.74206902668791,5.684341886080802e-14 447.99999999999994,0 C695.2579309733121,0 475,418 896,448Z"
></path>
<linearGradient id="linearGradient-3" x1="0.5" y1="0" x2="0.5" y2="1">
<stop offset="0" stop-color="rgb(40,175,240)" stop-opacity="1"></stop>
<stop offset="1" stop-color="rgb(18,15,196)" stop-opacity="1"></stop>
</linearGradient>
</defs>
<g opacity="1">
<use xlink:href="#path-2" fill="url(#linearGradient-3)" fill-opacity="1"></use>
</g>
</svg>
</div>
<section class="container">
<section class="wrapper">
<header>
<div class="logo">
<img width="200" src="@/assets/logo.png" />
</div>
<h1>{{ title }}</h1>
</header>
<section class="main-content">
<input v-model="data.username" type="email" placeholder="账号" />
<div class="line"></div>
<input v-model="data.password" type="password" placeholder="密码" />
<button @click="onSubmit">Login</button>
</section>
</section>
</section>
<Snackbar :show="data.snackbar.show" :msg="data.snackbar.msg" @close="(v) => (data.snackbar.show = v)" />
</div>
</template>
<style>
body {
height: 100vh;
overflow: hidden;
background-color: #dbe0f9;
font-family: 'Roboto', sans-serif;
}
.login-box {
position: relative;
height: 100%;
}
.login-box .svg-top {
position: absolute;
top: -900px;
right: -300px;
}
.login-box .svg-bottom {
position: absolute;
bottom: -500px;
left: -200px;
}
.container {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
.container .wrapper {
padding: 40px;
background-color: #fff;
border-radius: 20px;
width: 430px;
height: 500px;
z-index: 1;
}
.container .wrapper header {
margin-bottom: 40px;
}
.container .wrapper header .logo {
width: 100%;
-height: 70px;
border-radius: 50px;
display: flex;
justify-content: center;
align-items: center;
}
.container .wrapper header .logo span {
font-size: 18px;
color: #fff;
}
.container .wrapper header h1 {
color: #6065d9;
font-size: 35px;
font-weight: 500;
margin-bottom: 0;
margin-top: 40px;
}
.container .wrapper header p {
color: #6065d9;
font-size: 18px;
font-weight: 300;
margin: 5px 0 0 0;
}
.container .wrapper .main-content input {
border: none;
display: block;
width: 100%;
height: 50px;
margin: 15px 0;
font-size: 18px;
color: #999;
}
.container .wrapper .main-content input::placeholder {
color: #999;
font-size: 18px;
font-weight: 300;
}
.container .wrapper .main-content input:focus {
outline: none;
}
.container .wrapper .main-content .line {
width: 100%;
height: 2px;
background-color: #99999955;
}
.container .wrapper .main-content button {
background: linear-gradient(to right, #6065d9, #17d7fa);
border: none;
border-radius: 50px;
font-size: 18px;
font-weight: 300;
color: #fff;
display: block;
width: 100px;
height: 40px;
margin: 0 auto;
outline: none;
cursor: pointer;
margin-top: 20px;
}
</style>

233
src/views/system/ManagerList.vue

@ -0,0 +1,233 @@
<script setup lang="ts">
import { pageLimit } from '@/common/params'
import instance from '@/plugins/axios'
import { onMounted, reactive, ref } from 'vue'
import type { FormInfo } from '@/api/system'
import ConfirmBox from '@/components/ConfirmBox.vue'
import Snackbar from '@/components/Snackbar.vue'
import DataTable from '@/components/DataTable.vue'
const data = reactive({
headers: [
{ title: 'ID', key: 'id', align: 'center' },
{ title: '账号', key: 'account', align: 'center' },
{ title: '角色', key: 'roleName', align: 'center' },
{ title: '创建时间', key: 'createdAt', align: 'center' },
{ title: '状态', key: 'status', align: 'center' },
{ title: '操作', key: 'actions', align: 'center' },
] as any[],
desserts: [],
options: { roles: [] as unknown[] },
form: {} as FormInfo,
rules: {
account: [(v: any) => !!v || '参数不能为空'],
nickname: [(v: any) => !!v || '参数不能为空'],
password: [(v: any) => (v.length >= 6 && v.length <= 20) || '密码字符在6-20之间'],
roles: [(v: any) => v > 0 || '请选择角色'],
},
confirmPassword: '',
dialog: false,
confirm: { show: false, msg: '确定要删除此记录吗' },
request: { method: 0, url: '' },
snackbar: { show: false, msg: '' },
page: { current: 1, total: 10 },
})
const getItems = async (page: number = 1) => {
const params = { page: page, limit: pageLimit }
const response = (await instance.post('/admin/fetch-admin-list', params)).data
if (response.code != 0) {
data.snackbar.show = true
data.snackbar.msg = response.msg
return
}
data.desserts = response.data.list
data.page.total = response.data.totalPage
}
//
const getRoles = async () => {
const params = { page: 1, limit: 100 }
const response = (await instance.post('/role/fetch-role-list', params)).data
if (response.code != 0) {
data.snackbar.show = true
data.snackbar.msg = response.msg
return
}
data.options.roles = [{ title: '请选择', value: 0 }]
for (const item of response.data.list) {
data.options.roles.push({ title: item.name, value: item.id })
}
}
const insertBox = (method: number) => {
data.dialog = true
data.form = { roleID: 0, status: 0 } as FormInfo
data.confirmPassword = ''
data.request.method = method
data.request.url = '/admin/create-admin'
}
const changeBox = (method: number, item: any) => {
data.dialog = true
data.form = item
data.form.password = ''
data.confirmPassword = ''
data.request.method = method
data.request.url = '/admin/update-admin'
}
const destoryBox = (method: number, item: any) => {
data.confirm.show = true
data.form = { id: item.id } as FormInfo
data.request.method = method
data.request.url = '/admin/delete-admin'
}
const form = ref()
const confirms = ref()
const onSubmit = async () => {
let params = {}
if (data.request.method < 2) {
if (!(await form.value.validate()).valid) return
if (data.form.password != data.confirmPassword) {
data.snackbar.show = true
data.snackbar.msg = '两次密码不一致'
return
}
if (data.form.password != '' && (data.form.password.length < 6 || data.form.password.length > 20)) {
data.snackbar.show = true
data.snackbar.msg = '密码字符只能在6-20之间'
return
}
params = {
avatar: '',
id: data.form.id,
account: data.form.account,
nickname: data.form.nickname,
password: data.form.password,
roleID: data.form.roleID,
status: data.form.status,
secret: 'google',
}
} else {
params = { id: data.form.id }
}
const response = (await instance.post(data.request.url, params)).data
if (response.code != 0) {
data.snackbar.show = true
data.snackbar.msg = response.msg
return
}
data.dialog = false
confirms.value.closeOverlay()
getItems()
}
onMounted(() => {
getItems()
getRoles()
})
</script>
<template>
<v-card>
<v-card-title>
<v-btn color="primary" @click="insertBox(0)">添加</v-btn>
</v-card-title>
<v-card-text>
<DataTable
:headers="data.headers"
:items="data.desserts"
:page="data.page.current"
:size="data.page.total"
@get-items="(v) => getItems(v)"
>
<template #item.status="{ value }">
<v-btn
v-if="value === 0"
icon="mdi-close-thick"
color="pink"
variant="tonal"
size="x-small"
></v-btn>
<v-btn
v-if="value === 1"
icon="mdi-check-bold"
color="green"
variant="tonal"
size="x-small"
></v-btn>
</template>
<template #item.actions="{ item }">
<v-btn size="small" class="me-2" @click="changeBox(1, item)">编辑</v-btn>
<v-btn color="pink" size="small" @click="destoryBox(2, item)">删除</v-btn>
</template>
</DataTable>
</v-card-text>
<!-- 编辑 -->
<v-dialog v-model="data.dialog" width="600">
<v-card>
<v-card-title>信息</v-card-title>
<v-card-text>
<v-form ref="form">
<v-text-field
label="账号"
v-model="data.form.account"
:rules="data.rules.account"
></v-text-field>
<v-text-field
label="昵称"
v-model="data.form.nickname"
:rules="data.rules.nickname"
></v-text-field>
<v-text-field label="密码" v-model="data.form.password"></v-text-field>
<v-text-field label="确认密码" v-model="data.confirmPassword"></v-text-field>
<v-select
v-model="data.form.roleID"
:items="data.options.roles"
:rules="data.rules.roles"
></v-select>
<v-switch
v-model="data.form.status"
:true-value="1"
:false-value="0"
inset
color="primary"
></v-switch>
</v-form>
</v-card-text>
<v-card-actions>
<v-btn color="pink" @click="data.dialog = false">取消</v-btn>
<v-btn color="primary" variant="flat" @click="onSubmit">确定</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- 删除 -->
<ConfirmBox
ref="confirms"
:show="data.confirm.show"
:msg="data.confirm.msg"
@on-sumbit="onSubmit"
@show="(v) => (data.confirm.show = v)"
/>
<Snackbar :show="data.snackbar.show" :msg="data.snackbar.msg" />
</v-card>
</template>

202
src/views/userSearch/index.vue

@ -0,0 +1,202 @@
<script setup lang="ts">
import { pageLimit } from '@/common/params'
import instance from '@/plugins/axios'
import { onMounted, reactive, ref } from 'vue'
import Snackbar from '@/components/Snackbar.vue'
import DataTable from '@/components/DataTable.vue';
interface AccountLimit {
user_no: string[]
freeze: number
exchangeLimit: number
giveLimit: number
receiveLimit: number
emailLimit: number
gameLimit: number
}
function column(title: string, key: string, align: 'center' | 'start' | 'end' | undefined = 'center') {
return { title, key, align }
}
const data = reactive({
headers: [
{ title: '昵称', key: 'user_info.nickname', align: 'center' },
// { title: '', key: 'user_info.nickname', align: 'center' },
// { title: '', key: 'user_info.remark', align: 'center' },
// { title: 'VIP', key: 'user_info.vip', align: 'center' },
// { title: '', key: 'user_info.gold', align: 'center' },
// { title: '', key: 'user_info.total_bet', align: 'center' },
// { title: '', key: 'user_info.total_win_lose', align: 'center' },
// { title: '', key: 'user_info.total_deposit', align: 'center' },
// { title: '', key: 'user_info.total_withdraw', align: 'center' },
// { title: '', key: 'user_info.deposit_withdraw_difference', align: 'center' },
// { title: 'ID', key: 'user_info.package_id', align: 'center' },
// { title: 'Link Token', key: 'user_info.ad_token', align: 'center' },
// { title: '广', key: 'user_info.channel_name', align: 'center' },
// { title: '', key: 'user_info.register_time', align: 'center' },
// { title: '', key: 'user_info.last_login_time', align: 'center' },
// { title: '线', key: 'user_info.offline_days', align: 'center' },
// { title: '', key: 'user_info.account_type', align: 'center' },
// { title: '', key: 'user_info.account_status', align: 'center' },
] as any[],
desserts: [],
options: {
searchType: [
{ label: '玩家ID', value: 1 },
{ label: '玩家昵称', value: 2 },
{ label: '玩家手机', value: 3 },
{ label: '玩家备注', value: 4 },
{ label: '玩家IP', value: 5 },
{ label: '玩家设备', value: 6 },
],
accountType: [
{ label: '游客', value: 0 },
{ label: '普通', value: 1 },
{ label: '机器人', value: 2 },
{ label: '系统', value: 3 },
],
},
form: { type: 1, text: '' },
rules: {
account: [(v: any) => !!v || '参数不能为空'],
nickname: [(v: any) => !!v || '参数不能为空'],
password: [(v: any) => (v.length >= 6 && v.length <= 20) || '密码字符在6-20之间'],
roles: [(v: any) => v > 0 || '请选择角色'],
},
selected: [],
dialog: false,
accountAuth: {} as AccountLimit,
request: { method: 0, url: '' },
snackbar: { show: false, msg: '' },
page: { current: 1, total: 10 },
})
const getItems = async (page: number = 1) => {
const params = { page: page, limit: pageLimit, text: data.form.text, type: data.form.type }
const response = (await instance.post('/person/userSearch', params)).data
if (response.code != 0) {
data.snackbar.show = true
data.snackbar.msg = response.msg
return
}
data.desserts = response.data.data
data.page.total = response.data.total
}
const changeBox = () => {
if (data.selected.length == 0) {
data.snackbar.show = true
data.snackbar.msg = '请选择玩家'
return
}
data.dialog = true
}
const changeSelected = (item: any) => {
console.log(item)
}
const onSubmit = async () => {
if (data.selected.length == 0) {
data.snackbar.show = true
data.snackbar.msg = '请选择玩家'
return
}
data.accountAuth.user_no = data.selected
const response = (await instance.post(data.request.url, data.accountAuth)).data
if (response.code != 0) {
data.snackbar.show = true
data.snackbar.msg = response.msg
return
}
data.dialog = false
getItems()
}
onMounted(() => {
getItems()
})
</script>
<template>
<v-card>
<v-card-title>
<v-btn color="primary" variant="flat" @click="changeBox">账号限制</v-btn>
</v-card-title>
<v-card-text>
<DataTable :headers="data.headers" />
</v-card-text>
<!-- 编辑 -->
<v-dialog v-model="data.dialog" width="600">
<v-card>
<v-card-title>信息</v-card-title>
<v-card-text>
<v-form ref="form">
<v-switch
label="账号冻结"
v-model="data.accountAuth.freeze"
:true-value="1"
:false-value="0"
inset
color="primary"
></v-switch>
<v-switch
label="提现冻结"
v-model="data.accountAuth.exchangeLimit"
:true-value="1"
:false-value="0"
inset
color="primary"
></v-switch>
<v-switch
label="转账冻结"
v-model="data.accountAuth.giveLimit"
:true-value="1"
:false-value="0"
inset
color="primary"
></v-switch>
<v-switch
label="游戏冻结"
v-model="data.accountAuth.gameLimit"
:true-value="1"
:false-value="0"
inset
color="primary"
></v-switch>
<v-switch
label="接收冻结"
v-model="data.accountAuth.receiveLimit"
:true-value="1"
:false-value="0"
inset
color="primary"
></v-switch>
<v-switch
label="邮件冻结"
v-model="data.accountAuth.emailLimit"
:true-value="1"
:false-value="0"
inset
color="primary"
></v-switch>
</v-form>
</v-card-text>
<v-card-actions>
<v-btn color="pink" @click="data.dialog = false">取消</v-btn>
<v-btn color="primary" variant="flat" @click="onSubmit">确定</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<Snackbar :show="data.snackbar.show" :msg="data.snackbar.msg" />
</v-card>
</template>

15
tsconfig.app.json

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

11
tsconfig.json

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

19
tsconfig.node.json

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

29
vite.config.ts

@ -0,0 +1,29 @@
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))
},
},
build: {
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
return id.toString().split('node_modules/')[1].split('/')[0];
}
}
}
}
}
})
Loading…
Cancel
Save