commit
fc8cc2a05d
37 changed files with 7483 additions and 0 deletions
@ -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 |
@ -0,0 +1,2 @@ |
|||
VITE_APP_TITLE=HK |
|||
VITE_API_URL=http://127.0.0.1:8090 |
@ -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 |
@ -0,0 +1,7 @@ |
|||
{ |
|||
"$schema": "https://json.schemastore.org/prettierrc", |
|||
"semi": false, |
|||
"singleQuote": true, |
|||
"printWidth": 120, |
|||
"tabWidth": 4 |
|||
} |
@ -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 |
|||
``` |
@ -0,0 +1 @@ |
|||
/// <reference types="vite/client" />
|
@ -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', |
|||
] |
@ -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> |
File diff suppressed because it is too large
@ -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" |
|||
} |
|||
} |
After Width: | Height: | Size: 4.2 KiB |
@ -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> |
@ -0,0 +1,10 @@ |
|||
export interface FormInfo { |
|||
avatar: string |
|||
id: number |
|||
account: string |
|||
nickname: string |
|||
password: string |
|||
roleID: number |
|||
secret: string |
|||
status: number |
|||
} |
@ -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; |
|||
} |
After Width: | Height: | Size: 4.2 KiB |
After Width: | Height: | Size: 207 KiB |
After Width: | Height: | Size: 276 B |
@ -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,0 +1 @@ |
|||
export const pageLimit = 10 |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -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') |
@ -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 |
@ -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 |
@ -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 } |
|||
}) |
@ -0,0 +1,12 @@ |
|||
import { defineStore } from 'pinia' |
|||
|
|||
export const useManagerStore = defineStore('manager', { |
|||
state: () => { |
|||
return { |
|||
name: '', |
|||
token: '', |
|||
menus: [], |
|||
} |
|||
}, |
|||
persist: true, |
|||
}) |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -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 |
|||
} |
|||
} |
@ -0,0 +1,11 @@ |
|||
{ |
|||
"files": [], |
|||
"references": [ |
|||
{ |
|||
"path": "./tsconfig.node.json" |
|||
}, |
|||
{ |
|||
"path": "./tsconfig.app.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"] |
|||
} |
|||
} |
@ -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…
Reference in new issue