first commit

This commit is contained in:
lizhiqiang 2025-04-24 11:10:30 +08:00
commit 8444433e5b
134 changed files with 13484 additions and 0 deletions

7
pc-b/.editorconfig Normal file
View File

@ -0,0 +1,7 @@
[*.{js,jsx,ts,tsx,vue}]
indent_style = space
indent_size = 2
end_of_line = crlf
trim_trailing_whitespace = true
insert_final_newline = true
max_line_length = 120

5
pc-b/.env Normal file
View File

@ -0,0 +1,5 @@
# 本地运行端口号
VITE_PORT = 8686
# API接口域名配置
API_BASE_URL = https://api.example.com/

8
pc-b/.env.development Normal file
View File

@ -0,0 +1,8 @@
# 本地环境
VITE_USER_NODE_ENV = development
# 公共基础路径
VITE_PUBLIC_PATH = /
# proxy
VITE_PROXY = [["/api","http://localhost:8080"]]

5
pc-b/.env.production Normal file
View File

@ -0,0 +1,5 @@
# 线上环境
VITE_USER_NODE_ENV = production
# 公共基础路径
VITE_PUBLIC_PATH = /

7
pc-b/.eslintignore Normal file
View File

@ -0,0 +1,7 @@
node_modules
dist
public
*.md
*.txt
.vscode
index.html

37
pc-b/.eslintrc.cjs Normal file
View File

@ -0,0 +1,37 @@
module.exports = {
env: {
browser: true,
es2021: true,
node: true
},
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:vue/vue3-essential',
'plugin:prettier/recommended' // 后续兼容prettier
],
overrides: [
{
env: {
node: true
},
files: ['.eslintrc.{js,cjs}'],
parserOptions: {
sourceType: 'script'
}
}
],
parserOptions: {
ecmaVersion: 'latest',
parser: '@typescript-eslint/parser',
sourceType: 'module'
},
plugins: ['@typescript-eslint', 'vue'],
rules: {
// Switch语句 https://zh-hans.eslint.org/docs/latest/rules/indent#switchcase
indent: ['error', 2, { SwitchCase: 1 }],
'linebreak-style': ['error', 'windows'],
quotes: ['error', 'single'],
semi: ['error', 'never']
}
}

24
pc-b/.gitignore vendored Normal file
View File

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

7
pc-b/.prettierignore Normal file
View File

@ -0,0 +1,7 @@
node_modules
dist
public
*.md
*.txt
.vscode
index.html

10
pc-b/.prettierrc Normal file
View File

@ -0,0 +1,10 @@
{
"useTabs": false,
"tabWidth": 2,
"printWidth": 120,
"singleQuote": true,
"trailingComma": "none",
"bracketSpacing": true,
"semi": false,
"endOfLine": "crlf"
}

3
pc-b/.vscode/extensions.json vendored Normal file
View File

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

1
pc-b/PATH.txt Normal file
View File

@ -0,0 +1 @@
PATH=D:\Software\Python36\Scripts\;D:\Software\Python36;D:\Software\VMware\VMware Workstation\bin\;C:\Program Files (x86)\Common Files\Oracle\Java\javapath;C:\Users\Administrator\AppData\Local\Microsoft\WindowsApps;C:\Windows;C:\Windows\System32\OpenSSH\;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Windows\system32;D:\Cache\Android\android-sdk\platform-tools;D:\Software\Git\cmd;D:\Software\JetBrains\IntelliJ IDEA 2023.3.5\bin;D:\Software\JetBrains\PhpStorm 2023.3.2\bin;D:\Software\JetBrains\PyCharm 2023.3.4\bin;c:\Users\Administrator\AppData\Local\Programs\cursor\resources\app\bin;C:\ProgramData\ComposerSetup\bin;D:\Software\Tencent\΢ÐÅweb¿ª·¢Õß¹¤¾ß\dll;C:\Program Files\python;C:\Program Files\python\Scripts;D:\BtSoft\panel\script;C:\Program Files (x86)\NVIDIA Corporation\PhysX\Common;C:\Program Files\NVIDIA Corporation\NVIDIA NvDLISR;C:\WINDOWS\system32;C:\WINDOWS;C:\WINDOWS\System32\Wbem;C:\WINDOWS\System32\WindowsPowerShell\v1.0\;C:\WINDOWS\System32\OpenSSH\;D:\Software\TortoiseGit\bin;D:\Software\nvm;D:\Software\nvm4w\nodejs;D:\Software\phpstudy_pro\Extensions\php\php7.4.3nts;C:\Program Files\PuTTY\;d:\Software\Trae CN\bin;D:\Software\Python36\;D:\Software\Python39\Scripts\;D:\Software\Python39\;D:\Software\Python310\Scripts\;D:\Software\Python310\;C:\Users\Administrator\AppData\Local\JetBrains\Toolbox\scripts;D:\phpEnv\php\php-8.0;D:\phpEnv\server\mysql\mysql-5.7\bin;D:\phpEnv\tools\Composer;C:\Users\Administrator\AppData\Roaming\Composer\vendor\bin;D:\Software\Microsoft VS Code\bin;C:\Users\Administrator\AppData\Local\Programs\Ollama

46
pc-b/README.md Normal file
View File

@ -0,0 +1,46 @@
# Vue 3 + TypeScript + Vite
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).
# 目录结构
```
| .env
| .env.development
| .env.production
| .gitignore
| index.html
| package-lock.json
| package.json
| README.md
| tree.txt
| tsconfig.json
| tsconfig.node.json
| vite.config.ts
|
+---.vscode
| extensions.json
|
+---build
| index.ts
|
+---node_modules
+---public
| vite.svg
|
+---src
| | App.vue
| | main.ts
| | style.css
| | vite-env.d.ts
| |
| +---assets
| | vue.svg
| |
| \---components
| HelloWorld.vue
|
\---types
index.d.ts
```

17
pc-b/build/index.ts Normal file
View File

@ -0,0 +1,17 @@
// 环境变量处理方法
export function wrapperEnv(envConf: Recordable): ViteEnv {
const ret: Record<string, string | number | boolean> = {};
for (const envName of Object.keys(envConf)) {
let realName = envConf[envName].replace(/\\n/g, "\n");
realName = realName === "true" ? true : realName === "false" ? false : realName;
if (envName === "VITE_PORT") realName = Number(realName);
ret[envName] = realName;
if (typeof realName === "string") {
process.env[envName] = realName;
} else if (typeof realName === "object") {
process.env[envName] = JSON.stringify(realName);
}
}
return ret;
}

View File

@ -0,0 +1,16 @@
import vue from '@vitejs/plugin-vue'
/**
* * setup插件script标签中使用name属性
* usage: <script setup name="MyComp"></script>
*/
import VueSetupExtend from 'vite-plugin-vue-setup-extend'
export function createVitePlugins(viteEnv: Record<string, string>, isBuild: boolean): any[] {
const plugins = [
vue(),
VueSetupExtend(),
]
return plugins
}

14
pc-b/eslint.config.js Normal file
View File

@ -0,0 +1,14 @@
import js from "@eslint/js";
import globals from "globals";
import tseslint from "typescript-eslint";
import pluginVue from "eslint-plugin-vue";
import { defineConfig } from "eslint/config";
export default defineConfig([
{ files: ["**/*.{js,mjs,cjs,ts,vue}"], plugins: { js }, extends: ["js/recommended"] },
{ files: ["**/*.{js,mjs,cjs,ts,vue}"], languageOptions: { globals: {...globals.browser, ...globals.node} } },
tseslint.configs.recommended,
pluginVue.configs["flat/essential"],
{ files: ["**/*.vue"], languageOptions: { parserOptions: { parser: tseslint.parser } } },
]);

13
pc-b/index.html Normal file
View File

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

5996
pc-b/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

50
pc-b/package.json Normal file
View File

@ -0,0 +1,50 @@
{
"name": "vue3project",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"type-check": "vue-tsc --noEmit",
"preview": "vite preview",
"lint": "eslint --fix --ext .ts,.tsx,.vue,.js,.jsx --max-warnings 0"
},
"dependencies": {
"@types/js-cookie": "^3.0.6",
"@vueuse/core": "^13.1.0",
"axios": "^1.8.4",
"echarts": "^5.6.0",
"element-plus": "^2.9.8",
"js-cookie": "^3.0.5",
"pinia": "^3.0.2",
"pinia-plugin-persistedstate": "^4.2.0",
"vue": "^3.5.13",
"vue-i18n": "^11.1.3",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@eslint/js": "^9.25.1",
"@types/node": "^22.14.1",
"@typescript-eslint/eslint-plugin": "^8.31.0",
"@typescript-eslint/parser": "^8.31.0",
"@vitejs/plugin-vue": "^5.2.2",
"@vue/tsconfig": "^0.7.0",
"eslint": "^9.25.1",
"eslint-config-prettier": "^10.1.2",
"eslint-plugin-prettier": "^5.2.6",
"eslint-plugin-vue": "^10.0.0",
"globals": "^16.0.0",
"naive-ui": "^2.41.0",
"prettier": "^3.5.3",
"sass": "^1.87.0",
"sass-embedded": "^1.87.0",
"typescript": "~5.7.2",
"typescript-eslint": "^8.31.0",
"unplugin-vue-components": "^28.5.0",
"unplugin-vue-router": "^0.12.0",
"vite": "^6.3.1",
"vite-plugin-vue-setup-extend": "^0.4.0",
"vue-tsc": "^2.2.8"
}
}

1
pc-b/public/vite.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

4
pc-b/settings.txt Normal file
View File

@ -0,0 +1,4 @@
root: pc
path: C:\Program Files\nodejs
arch: 64
proxy: none

48
pc-b/src/App.vue Normal file
View File

@ -0,0 +1,48 @@
<script setup lang="ts">
import { onMounted, ref ,provide} from 'vue'
import * as echarts from "echarts";
import { useI18n } from 'vue-i18n'
//provideecharts
provide("echarts", echarts);
const I18n = useI18n()
const { locale } = useI18n()
// locale.valueindex.jsmessage
const translate = (lang) => {
locale.value = lang
localStorage.setItem('lang', lang)
}
const type = ref('light')
const onChange = (e) => {
document.documentElement.setAttribute('theme-mode', type.value)
}
</script>
<template>
<div>
<div>
<p>{{ $t('welcome') }}</p>
</div>
<button @click="translate('zh-cn')">切换为中文</button>
<button @click="translate('en-us')">切换为英文</button>
</div>
<div>
<el-select style="width: 80px;margin: 10px;" v-model="type" @change="onChange">
<el-option label="light" value="light" />
<el-option label="dark" value="dark" />
<el-option label="red" value="red" />
</el-select>
</div>
<router-link to="/"> 去首页 </router-link> <router-link to="/login"> 去登录 </router-link> <router-link to="/demo"> 查看demo </router-link>
<router-view />
</template>
<style lang="scss">
#app {
background-color: $primaryColor;
}
</style>

6
pc-b/src/api/login.ts Normal file
View File

@ -0,0 +1,6 @@
import { defRequest } from '../utils/request'
export const loginApi = (params: Record<string, unknown>) => {
// 设置 showLoadingtimeout 会覆盖index.ts里的默认值
return defRequest.post<Record<string, unknown>>('/login', params, { showLoading: false, timeout: 1000 })
}

View File

@ -0,0 +1,14 @@
:root[theme-mode='light'] {
--bg-color: #fff;
--text-color: #000
}
:root[theme-mode='dark'] {
--bg-color: #2c2c2c;
--text-color: #fff
}
:root[theme-mode='red'] {
--bg-color: rgb(0, 128, 255);
--text-color: red;
}

View File

@ -0,0 +1,4 @@
:root {
color: var(--text-color);
background-color: var(--bg-color);
}

View File

@ -0,0 +1 @@
$primaryColor: #316c72;

1
pc-b/src/assets/vue.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

View File

@ -0,0 +1,41 @@
<script setup lang="ts">
import { ref } from 'vue'
defineProps<{ msg: string }>()
const count = ref(0)
</script>
<template>
<h1>{{ msg }}</h1>
<div class="card">
<button type="button" @click="count++">count is {{ count }}</button>
<p>
Edit
<code>components/HelloWorld.vue</code> to test HMR
</p>
</div>
<p>
Check out
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
>create-vue</a
>, the official Vue + Vite starter
</p>
<p>
Learn more about IDE Support for Vue in the
<a
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
target="_blank"
>Vue Docs Scaling up Guide</a
>.
</p>
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
</template>
<style scoped>
.read-the-docs {
color: #888;
}
</style>

View File

@ -0,0 +1,6 @@
export default {
welcome: 'Welcome',
login: 'Login',
register: 'Register',
// 可以根据实际需求添加更多翻译项
};

View File

@ -0,0 +1,20 @@
import {createI18n} from 'vue-i18n'
// 从语言包文件中导入语言包对象
import zh from '@language/zh-CN'
import en from '@language/en-US'
const messages = {
'zh-cn': zh,
'en-us': en
}
const language = (navigator.language || 'en').toLocaleLowerCase() // 这是获取浏览器的语言
// 获取浏览器当前使用的语言,并进行处理
const i18n = createI18n({
legacy: false,
locale: localStorage.getItem('lang') || language.split('-')[0] || 'en', // 首先从缓存里拿,没有的话就用浏览器语言,
fallbackLocale: 'zh-cn', // 设置备用语言
messages,
})
export default i18n

View File

@ -0,0 +1,6 @@
export default {
welcome: '欢迎',
login: '登录',
register: '注册',
// 可以根据实际需求添加更多翻译项
};

27
pc-b/src/main.ts Normal file
View File

@ -0,0 +1,27 @@
import { createApp } from 'vue'
import pinia from '@/store'
import './style.css'
import '@css/index.scss'
import App from './App.vue'
import * as echarts from 'echarts'
import router from '@/router/index'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import i18n from './language'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
// 初始化pinia插件
pinia.use(piniaPluginPersistedstate)
// 创建并配置Vue应用
const app = createApp(App)
.use(router)
.use(pinia)
.use(i18n)
.use(ElementPlus)
// 全局挂载echarts
app.config.globalProperties.$echarts = echarts
// 挂载应用
app.mount('#app')

58
pc-b/src/router/index.ts Normal file
View File

@ -0,0 +1,58 @@
import { Router, createRouter, createWebHistory } from 'vue-router'
/** src/router/modules
* import.meta.glob使用说明https://cn.vitejs.dev/guide/features#glob-import
*/
const modules: Record<string, unknown> = import.meta.glob(['./modules/**/*.ts'], {
eager: true
})
/** 初始路由 **/
const routes: unknown[] = []
Object.keys(modules).forEach((key) => {
const module = (modules[key] as { default: unknown }).default;
if (Array.isArray(module)) {
for (const item of module) {
routes.push(item)
}
} else {
routes.push(module)
}
})
/**
*
* createRouter选项有https://router.vuejs.org/zh/api/interfaces/RouterOptions.html
* hash模式使用createWebHashHistory(): https://router.vuejs.org/zh/api/#Functions-createWebHashHistory
*/
export const router: Router = createRouter({
history: createWebHistory(),
routes,
strict: true,
scrollBehavior(_to, from, savedPosition) {
return new Promise((resolve) => {
if (savedPosition) {
return savedPosition
} else {
if (from.meta.saveSrollTop) {
const top: number = document.documentElement.scrollTop || document.body.scrollTop
resolve({ left: 0, top })
}
}
})
}
})
/**
*
* https://router.vuejs.org/zh/guide/advanced/navigation-guards.html
*/
router.beforeEach((to, _from, next) => {
// isAuthenticated 代表你的鉴权
const isAuthenticated = true
if (to.name !== 'Login' && !isAuthenticated) next({ name: 'Login' })
else next()
})
export default router

View File

@ -0,0 +1,12 @@
const routes = [
{
path: '/',
component: () => import('@/views/default/home.vue')
},
{
path: '/login',
component: () => import('@/views/default/login.vue') //路由懒加载
}
]
export default routes

7
pc-b/src/store/index.ts Normal file
View File

@ -0,0 +1,7 @@
import { createPinia } from 'pinia'
import persist from 'pinia-plugin-persistedstate';
const pinia = createPinia();
pinia.use(persist);
export default pinia

View File

@ -0,0 +1,31 @@
import { defineStore } from 'pinia'
import { UserState } from 'types/store'
import { getToken, setToken } from '@/utils/auth'
// 第一个参数是id唯一
export const useUserStore = defineStore('user', {
state: () => {
return {
token: getToken() || 'YUNZER68205747',
userInfo: { name: 'yunzer', phone: '19895983967' }
}
},
getters: {
namePic: (state) => state.userInfo.name.substring(0, 1)
},
actions: {
setToken(token: string) {
this.token = token
},
setUserInfo(userInfo: UserState['userInfo']) {
this.userInfo = { ...this.userInfo, ...userInfo }
}
},
setToken(token: string) {
this.token = token
setToken({
token,
expires: 30
})
}
})

View File

@ -0,0 +1,19 @@
import {defineStore} from 'pinia'
import {getToken,setToken} from "@/utils/storage.ts";
export const useSettingsStore = defineStore('settings', {
id: 'settings', // id必填且需要唯一
state: () => {
return {
menuCollapse: false,//// 是否水平折叠收起菜单
// 布局方式 Classic 经典布局 Streamline 单行布局
layoutMode: getToken('layoutMode')?getToken('layoutMode'):'Classic'
}
},
actions: {
changeSetting({ key, value }) {
//改变全局变量的方法
this[key] = value
setToken(key, value)
},
}
})

25
pc-b/src/store/user.ts Normal file
View File

@ -0,0 +1,25 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
import type { User } from '@/types/user';
export const usersStore = defineStore('users', () => {
const userInfo = ref<User>({
name:'abc',
avatar: '123', // 头像
mobile: '13221091091', // 手机号
account: 'lita', // 用户名
id: 1
});
const setUserInfo = (u:User) =>{
userInfo.value = u;
}
const clearUserInfo = () =>{
// void 是用来创建 undefined不管它后面跟个啥得到的都是 undefined
userInfo.value = void 0;
// 上面的代码代表 userinfo.value = undefined;
}
return { userInfo ,setUserInfo, clearUserInfo }
},{persist: true})

79
pc-b/src/style.css Normal file
View File

@ -0,0 +1,79 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
.card {
padding: 2em;
}
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

22
pc-b/src/utils/auth.ts Normal file
View File

@ -0,0 +1,22 @@
import Cookies from 'js-cookie'
export const TokenKey = 'yunzer-token'
type ExpiresData = Date | number
export interface TokenInfo {
token: string
expires: ExpiresData
}
export function getToken() {
return Cookies.get(TokenKey)
}
export function setToken(data: TokenInfo) {
const { token, expires } = data
return expires ? Cookies.set(TokenKey, token, { expires: expires }) : Cookies.set(TokenKey, token)
}
export function removeToken() {
return Cookies.remove(TokenKey)
}

View File

@ -0,0 +1,35 @@
/**
*
*/
import Request from './request'
import { getToken } from '@/utils/auth'
const defRequest = new Request({
// 这里用 Easy Mock 模拟了真实接口
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 5000,
showLoading: true,
interceptorHooks: {
requestInterceptor: (config) => {
const token = getToken()
if (token) {
config.headers.Authorization = token
}
return config
},
requestInterceptorCatch: (err) => {
return err
},
responseInterceptor: (res) => {
return res.data
},
responseInterceptorCatch: (err) => {
return Promise.reject(err)
}
}
})
// 创建其他示例,然后导出
// const otherRequest = new Request({...})
export { defRequest }

View File

@ -0,0 +1,123 @@
/**
* axios
* axios AxiosInstance AxiosRequestConfig AxiosResponseInternalAxiosRequestConfig AxiosRequestConfig
*/
import axios, { AxiosInstance, AxiosRequestConfig, InternalAxiosRequestConfig, AxiosResponse } from 'axios'
import { ErrMessage } from './status'
// 自定义请求返回数据的类型
interface Data<T> {
data: T
code: string
success: boolean
}
// 扩展 InternalAxiosRequestConfig让每个请求都可以控制是否要loading
interface RequestInternalAxiosRequestConfig extends InternalAxiosRequestConfig {
showLoading?: boolean
}
// 拦截器
interface InterceptorHooks {
requestInterceptor?: (config: RequestInternalAxiosRequestConfig) => RequestInternalAxiosRequestConfig
requestInterceptorCatch?: (error: unknown) => unknown
responseInterceptor?: (response: AxiosResponse) => AxiosResponse
responseInterceptorCatch?: (error: unknown) => unknown
}
// 扩展 AxiosRequestConfigshowLoading 给实例默认增加loadinginterceptorHooks 拦截
interface RequestConfig extends AxiosRequestConfig {
showLoading?: boolean
interceptorHooks?: InterceptorHooks
}
class Request {
config: RequestConfig
instance: AxiosInstance
loading?: boolean // 用loading指代加载动画状态
constructor(options: RequestConfig) {
this.config = options
this.instance = axios.create(options)
this.setupInterceptor()
}
// 类型参数的作用T决定AxiosResponse实例中data的类型
request<T>(config: RequestConfig): Promise<T> {
return new Promise((resolve, reject) => {
this.instance
.request<Data<T>, Data<T>>(config)
.then((res) => {
resolve(res.data)
})
.catch((err) => {
reject(err)
})
})
}
// 封装常用方法
// 移除了默认的 any 类型,要求调用者显式指定类型
get<T>(url: string, params?: object, _object = {}): Promise<T> {
return this.request({ url, params, ..._object, method: 'GET' })
}
post<T>(url: string, params?: object, _object = {}): Promise<T> {
return this.request({ url, params, ..._object, method: 'POST' })
}
delete<T>(url: string, params?: object, _object = {}): Promise<T> {
return this.request({ url, params, ..._object, method: 'DELETE' })
}
patch<T>(url: string, params?: object, _object = {}): Promise<T> {
return this.request({ url, params, ..._object, method: 'PATCH' })
}
put<T>(url: string, params?: object, _object = {}): Promise<T> {
return this.request({ url, params, ..._object, method: 'PUT' })
}
// 自定义拦截器 https://axios-http.com/zh/docs/interceptors
setupInterceptor(): void {
/**
*
*/
this.instance.interceptors.request.use((config: RequestInternalAxiosRequestConfig) => {
if (config.showLoading) {
// 加载loading动画
this.loading = true
}
return config
})
// 响应后关闭loading
this.instance.interceptors.response.use(
(res) => {
if (this.loading) this.loading = false
return res
},
(err) => {
const { response, message } = err
if (this.loading) this.loading = false
// 根据不同状态码,返回不同信息
const messageStr = response ? ErrMessage(response.status) : message || '请求失败,请重试'
window.alert(messageStr)
return Promise.reject(err)
}
)
/**
* 使
*/
// 请求拦截
this.instance.interceptors.request.use(
this.config?.interceptorHooks?.requestInterceptor,
this.config?.interceptorHooks?.requestInterceptorCatch
)
// 响应拦截
this.instance.interceptors.response.use(
this.config?.interceptorHooks?.responseInterceptor,
this.config?.interceptorHooks?.responseInterceptorCatch
)
}
}
export default Request

View File

@ -0,0 +1,41 @@
export const ErrMessage = (status: number | string): string => {
let message: string = ''
switch (status) {
case 400:
message = '请求错误!请您稍后重试'
break
case 401:
message = '未授权!请您重新登录'
break
case 403:
message = '当前账号无访问权限!'
break
case 404:
message = '访问的资源不存在!请您稍后重试'
break
case 405:
message = '请求方式错误!请您稍后重试'
break
case 408:
message = '请求超时!请您稍后重试'
break
case 500:
message = '服务异常!请您稍后重试'
break
case 501:
message = '不支持此请求!请您稍后重试'
break
case 502:
message = '网关错误!请您稍后重试'
break
case 503:
message = '服务不可用!请您稍后重试'
break
case 504:
message = '网关超时!请您稍后重试'
break
default:
message = '请求失败!请您稍后重试'
}
return message
}

View File

@ -0,0 +1,32 @@
<script setup lang="ts">
import { onMounted, ref ,provide} from 'vue'
import * as echarts from "echarts";
import { useI18n } from 'vue-i18n'
//provideecharts
provide("echarts", echarts);
const I18n = useI18n()
const { locale } = useI18n()
// locale.valueindex.jsmessage
const translate = (lang) => {
locale.value = lang
localStorage.setItem('lang', lang)
}
</script>
<template>
<div>
<div>
<p>{{ $t('welcome') }}</p>
</div>
<button @click="translate('zh-cn')">切换为中文</button>
<button @click="translate('en-us')">切换为英文</button>
</div>
<router-link to="/"> 去首页 </router-link> <router-link to="/login"> 去登录 </router-link> <router-link to="/demo"> 查看demo </router-link>
<router-view />
</template>
<style scoped></style>

View File

@ -0,0 +1,55 @@
<template>
<div class="right-content">
<div ref="Chart" style="width: 800px; height: 500px"></div>
</div>
</template>
<script setup lang="ts">
import { onMounted, getCurrentInstance, ref } from 'vue'
let internalInstance = getCurrentInstance(); //
let echarts = internalInstance.appContext.config.globalProperties.$echarts; //echarts
//refhtml
const Chart = ref();
const init = () => {
// echarts
var infoEl = Chart.value;
// light dark
var myChart = echarts.init(infoEl, "light"); //echarts
//
var option = {
title: {
text: 'ECharts 入门示例'
},
tooltip: {},
xAxis: {
data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']
},
yAxis: {},
series: [
{
name: '销量',
type: 'bar',
data: [5, 20, 36, 10, 10, 20]
}
]
}
// 使
myChart.setOption(option);
window.onresize = function () {
myChart.resize()
}
}
onMounted(() => {
init()
});
</script>
<style scope lang="scss"></style>

View File

@ -0,0 +1,22 @@
<script setup lang="ts">
import { computed } from 'vue'
import { storeToRefs } from 'pinia'
import { useUserStore } from '@/store/modules/user'
defineOptions({
name: 'V-home'
})
const userStore = useUserStore()
// state使computed使storeToRefs使
const username = computed(() => userStore.userInfo.name)
// getter使storeToRefs使 userStore.namePic
const { token } = storeToRefs(userStore)
const namePic = computed(() => userStore.userInfo.name + '的头像') // store使userInfo.name
</script>
<template>
<div>Hello: {{ namePic }}, your name is {{ username }}, your token is {{ token }}</div>
</template>
<style scoped></style>

View File

@ -0,0 +1,51 @@
<script setup lang="ts">
import { ref } from 'vue'
import { storeToRefs } from 'pinia'
import { useUserStore } from '@store/modules/user'
import { loginApi } from '@/api/login'
defineOptions({
name: 'V-login'
})
const userStore = useUserStore()
const { userInfo, token } = storeToRefs(userStore)
let userName = ref(userInfo.value.name)
let userToken = ref(token)
const updateUserName = () => {
userStore.setUserInfo({
name: userName.value
})
}
const updateUserToken = () => {
userStore.setToken(userToken.value)
}
const login = () => {
loginApi({
name: userName.value
})
.then((res) => {
userName.value = res.name
userToken.value = res.token
updateUserToken()
})
.catch((err) => {
console.log(err)
})
}
</script>
<template>
<div>login page</div>
name:
<input type="text" v-model="userName" @input="updateUserName" />
<br />
token:
<input type="text" v-model="userToken" />
<hr />
<button @click="login">login</button>
</template>
<style scoped></style>

1
pc-b/src/vite-env.d.ts vendored Normal file
View File

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

14
pc-b/tsconfig.app.json Normal file
View File

@ -0,0 +1,14 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
}

54
pc-b/tsconfig.json Normal file
View File

@ -0,0 +1,54 @@
{
"compilerOptions": {
"target": "ESNext", // JS
"useDefineForClassFields": true,
"module": "ESNext", // 使 ES Module
"lib": ["ESNext", "DOM", "DOM.Iterable"], // ES DOM
"skipLibCheck": true, // .d.ts
"esModuleInterop": true, // 使 import 使 export =
"sourceMap": true, // .map
"allowJs": false, // 使js
"baseUrl": ".", //
"paths": {
// ,使
"@": ["src"],
"@/*": ["src/*"],
"@build/*": ["build/*"],
"@language/*": ["src/language/*"],
"@store/*": ["src/store/modules/*"],
"#/*": ["types/*"]
},
/* Bundler mode */
"moduleResolution": "node", // 使 Node/bundler
"allowImportingTsExtensions": true,
"resolveJsonModule": true, // JSON
"isolatedModules": true, // ES Module
"noEmit": true, // ,js
"jsx": "preserve", // JSX
/* Linting */
"strict": true, //
"noUnusedLocals": true, // 使
"noUnusedParameters": true, // 使
"noFallthroughCasesInSwitch": true // switch
},
"include": [
//
"src/**/*.ts",
"build/*.ts",
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue",
"mock/*.ts",
"types/*.d.ts",
"vite.config.ts"
],
"exclude": [
//
"dist",
"**/*.js",
"node_modules"
],
"references": [{ "path": "./tsconfig.node.json" }] //
}

10
pc-b/tsconfig.node.json Normal file
View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true, //
"skipLibCheck": true, // .d.ts
"module": "ESNext", // 使 ES Module
"moduleResolution": "Node", // 使 Node/bundler
"allowSyntheticDefaultImports": true // 使 import 使 export =
},
"include": ["build/*.ts", "types/*.d.ts", "vite.config.ts"]
}

26
pc-b/typed-router.d.ts vendored Normal file
View File

@ -0,0 +1,26 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-vue-router. ‼️ DO NOT MODIFY THIS FILE ‼️
// It's recommended to commit this file.
// Make sure to add this file to your tsconfig.json file as an "includes" or "files" entry.
declare module 'vue-router/auto-routes' {
import type {
RouteRecordInfo,
ParamValue,
ParamValueOneOrMore,
ParamValueZeroOrMore,
ParamValueZeroOrOne,
} from 'vue-router'
/**
* Route name map generated by unplugin-vue-router
*/
export interface RouteNamedMap {
'/default/demo': RouteRecordInfo<'/default/demo', '/default/demo', Record<never, never>, Record<never, never>>,
'/default/demo-i18n': RouteRecordInfo<'/default/demo-i18n', '/default/demo-i18n', Record<never, never>, Record<never, never>>,
'/default/home': RouteRecordInfo<'/default/home', '/default/home', Record<never, never>, Record<never, never>>,
'/default/login': RouteRecordInfo<'/default/login', '/default/login', Record<never, never>, Record<never, never>>,
}
}

22
pc-b/types/index.d.ts vendored Normal file
View File

@ -0,0 +1,22 @@
type TargetContext = "_self" | "_blank";
type EmitType = (event: string, ...args: any[]) => void;
type AnyFunction<T> = (...args: any[]) => T;
type PropType<T> = VuePropType<T>;
type Writable<T> = {
-readonly [P in keyof T]: T[P];
};
type Nullable<T> = T | null;
type NonNullable<T> = T extends null | undefined ? never : T;
type Recordable<T = any> = Record<string, T>;
interface Fn<T = any, R = T> {
(...arg: T[]): R;
}
interface PromiseFn<T = any, R = T> {
(...arg: T[]): Promise<R>;
}
interface ViteEnv {
VITE_USER_NODE_ENV: "development" | "production";
VITE_PUBLIC_PATH: string;
VITE_PORT: number;
}

4
pc-b/types/store.ts Normal file
View File

@ -0,0 +1,4 @@
export interface UserState {
token: string
userInfo: { name?: string; phone?: string }
}

7
pc-b/types/user.d.ts vendored Normal file
View File

@ -0,0 +1,7 @@
interface User {
token: string;
avatar: string; // 头像
mobile:string; // 手机号
account:string; // 用户名
id:number; // 用户id
}

63
pc-b/vite.config.ts Normal file
View File

@ -0,0 +1,63 @@
import { defineConfig, loadEnv, ConfigEnv, UserConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { resolve } from "path";
import { wrapperEnv } from "./build";
import VueRouter from 'unplugin-vue-router/vite';
// 路径查找
const pathResolve = (dir: string): string => {
return resolve(__dirname, ".", dir);
};
// 设置别名,还可以添加其他路径
const alias: Record<string, string> = {
"@": pathResolve("src"),
"@views": pathResolve("src/views"),
"@store": pathResolve("src/store"),
"@language": pathResolve("src/language"),
"@css": pathResolve("src/assets/css"),
};
// https://vitejs.dev/config/
export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
const root = process.cwd();
const env = loadEnv(mode, root);
const viteEnv = wrapperEnv(env);
return {
base: viteEnv.VITE_PUBLIC_PATH,
plugins: [
VueRouter({
routesFolder: 'src/views', // 指定路由文件所在的目录
exclude: ['**/components/*.vue'],
extensions: ['.vue'], // 指定路由文件的后缀名
}),
vue(),
],
resolve: {
alias, // 设置别名
},
css: {
preprocessorOptions: {
scss: {
additionalData: `@import "@css/variables.scss";`, // 引入全局变量
},
},
},
server: {
host: "0.0.0.0", // 设置服务器主机名
port: viteEnv.VITE_PORT, // 设置服务启动端口号
https: undefined, // 是否开启 https
open: true, // 是否自动打开浏览器
cors: true, // 允许跨域
// 本地跨域代理 https://cn.vitejs.dev/config/server-options.html#server-proxy
proxy: {
"^/api": {
target: "http://127.0.0.1:8686", // 代理的目标地址
changeOrigin: true, // 开发模式,默认的 origin 是真实的 origin:localhost:3000
rewrite: (path) => path.replace(/^\/api/, ""), // 把 /api 替换成 target 中的地址
},
},
},
};
});

5
pc/.env Normal file
View File

@ -0,0 +1,5 @@
# 本地运行端口号
VITE_PORT = 8686
# API接口域名配置
VITE_BASE_URL = http://localhost/

8
pc/.env.development Normal file
View File

@ -0,0 +1,8 @@
# 开发环境
NODE_ENV = development
# 后台请求前缀这是mock地址
VITE_BASE_URL = '/apis'
VITE_PERMISSION_MODE = 'CONSTANT'
# VITE_PERMISSION_MODE = 'FRONT'
# VITE_PERMISSION_MODE = 'BACK'

8
pc/.env.production Normal file
View File

@ -0,0 +1,8 @@
# 生产环境
NODE_ENV = prod
# 后台请求前缀这是mock地址
VITE_BASE_URL = 'https://mock.apifox.cn/m1/3365861-0-default'
VITE_PERMISSION_MODE = 'CONSTANT'
# VITE_PERMISSION_MODE = 'FRONT'
# VITE_PERMISSION_MODE = 'BACK'

25
pc/.gitignore vendored Normal file
View File

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

127
pc/LICENSE Normal file
View File

@ -0,0 +1,127 @@
木兰宽松许可证, 第2版
木兰宽松许可证, 第2版
2020年1月 http://license.coscl.org.cn/MulanPSL2
您对“软件”的复制、使用、修改及分发受木兰宽松许可证第2版“本许可证”的如下条款的约束
0. 定义
“软件”是指由“贡献”构成的许可在“本许可证”下的程序和相关文档的集合。
“贡献”是指由任一“贡献者”许可在“本许可证”下的受版权法保护的作品。
“贡献者”是指将受版权法保护的作品许可在“本许可证”下的自然人或“法人实体”。
“法人实体”是指提交贡献的机构及其“关联实体”。
“关联实体”是指对“本许可证”下的行为方而言控制、受控制或与其共同受控制的机构此处的控制是指有受控方或共同受控方至少50%直接或间接的投票权、资金或其他有价证券。
1. 授予版权许可
每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的版权许可,您可以复制、使用、修改、分发其“贡献”,不论修改与否。
2. 授予专利许可
每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的(根据本条规定撤销除外)专利许可,供您制造、委托制造、使用、许诺销售、销售、进口其“贡献”或以其他方式转移其“贡献”。前述专利许可仅限于“贡献者”现在或将来拥有或控制的其“贡献”本身或其“贡献”与许可“贡献”时的“软件”结合而将必然会侵犯的专利权利要求,不包括对“贡献”的修改或包含“贡献”的其他结合。如果您或您的“关联实体”直接或间接地,就“软件”或其中的“贡献”对任何人发起专利侵权诉讼(包括反诉或交叉诉讼)或其他专利维权行动,指控其侵犯专利权,则“本许可证”授予您对“软件”的专利许可自您提起诉讼或发起维权行动之日终止。
3. 无商标许可
“本许可证”不提供对“贡献者”的商品名称、商标、服务标志或产品名称的商标许可但您为满足第4条规定的声明义务而必须使用除外。
4. 分发限制
您可以在任何媒介中将“软件”以源程序形式或可执行形式重新分发,不论修改与否,但您必须向接收者提供“本许可证”的副本,并保留“软件”中的版权、商标、专利及免责声明。
5. 免责声明与责任限制
“软件”及其中的“贡献”在提供时不带任何明示或默示的担保。在任何情况下,“贡献者”或版权所有者不对任何人因使用“软件”或其中的“贡献”而引发的任何直接或间接损失承担责任,不论因何种原因导致或者基于何种法律理论,即使其曾被建议有此种损失的可能性。
6. 语言
“本许可证”以中英文双语表述,中英文版本具有同等法律效力。如果中英文版本存在任何冲突不一致,以中文版为准。
条款结束
如何将木兰宽松许可证第2版应用到您的软件
如果您希望将木兰宽松许可证第2版应用到您的新软件为了方便接收者查阅建议您完成如下三步
1 请您补充如下声明中的空白,包括软件名、软件的首次发表年份以及您作为版权人的名字;
2 请您在软件包的一级目录下创建以“LICENSE”为名的文件将整个许可证文本放入该文件中
3 请将如下声明文本放入每个源文件的头部注释中。
Copyright (c) [Year] [name of copyright holder]
[Software Name] is licensed under Mulan PSL v2.
You can use this software according to the terms and conditions of the Mulan PSL v2.
You may obtain a copy of Mulan PSL v2 at:
http://license.coscl.org.cn/MulanPSL2
THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
See the Mulan PSL v2 for more details.
Mulan Permissive Software LicenseVersion 2
Mulan Permissive Software LicenseVersion 2 (Mulan PSL v2)
January 2020 http://license.coscl.org.cn/MulanPSL2
Your reproduction, use, modification and distribution of the Software shall be subject to Mulan PSL v2 (this License) with the following terms and conditions:
0. Definition
Software means the program and related documents which are licensed under this License and comprise all Contribution(s).
Contribution means the copyrightable work licensed by a particular Contributor under this License.
Contributor means the Individual or Legal Entity who licenses its copyrightable work under this License.
Legal Entity means the entity making a Contribution and all its Affiliates.
Affiliates means entities that control, are controlled by, or are under common control with the acting entity under this License, control means direct or indirect ownership of at least fifty percent (50%) of the voting power, capital or other securities of controlled or commonly controlled entity.
1. Grant of Copyright License
Subject to the terms and conditions of this License, each Contributor hereby grants to you a perpetual, worldwide, royalty-free, non-exclusive, irrevocable copyright license to reproduce, use, modify, or distribute its Contribution, with modification or not.
2. Grant of Patent License
Subject to the terms and conditions of this License, each Contributor hereby grants to you a perpetual, worldwide, royalty-free, non-exclusive, irrevocable (except for revocation under this Section) patent license to make, have made, use, offer for sale, sell, import or otherwise transfer its Contribution, where such patent license is only limited to the patent claims owned or controlled by such Contributor now or in future which will be necessarily infringed by its Contribution alone, or by combination of the Contribution with the Software to which the Contribution was contributed. The patent license shall not apply to any modification of the Contribution, and any other combination which includes the Contribution. If you or your Affiliates directly or indirectly institute patent litigation (including a cross claim or counterclaim in a litigation) or other patent enforcement activities against any individual or entity by alleging that the Software or any Contribution in it infringes patents, then any patent license granted to you under this License for the Software shall terminate as of the date such litigation or activity is filed or taken.
3. No Trademark License
No trademark license is granted to use the trade names, trademarks, service marks, or product names of Contributor, except as required to fulfill notice requirements in Section 4.
4. Distribution Restriction
You may distribute the Software in any medium with or without modification, whether in source or executable forms, provided that you provide recipients with a copy of this License and retain copyright, patent, trademark and disclaimer statements in the Software.
5. Disclaimer of Warranty and Limitation of Liability
THE SOFTWARE AND CONTRIBUTION IN IT ARE PROVIDED WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED. IN NO EVENT SHALL ANY CONTRIBUTOR OR COPYRIGHT HOLDER BE LIABLE TO YOU FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO ANY DIRECT, OR INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES ARISING FROM YOUR USE OR INABILITY TO USE THE SOFTWARE OR THE CONTRIBUTION IN IT, NO MATTER HOW ITS CAUSED OR BASED ON WHICH LEGAL THEORY, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
6. Language
THIS LICENSE IS WRITTEN IN BOTH CHINESE AND ENGLISH, AND THE CHINESE VERSION AND ENGLISH VERSION SHALL HAVE THE SAME LEGAL EFFECT. IN THE CASE OF DIVERGENCE BETWEEN THE CHINESE AND ENGLISH VERSIONS, THE CHINESE VERSION SHALL PREVAIL.
END OF THE TERMS AND CONDITIONS
How to Apply the Mulan Permissive Software LicenseVersion 2 (Mulan PSL v2) to Your Software
To apply the Mulan PSL v2 to your work, for easy identification by recipients, you are suggested to complete following three steps:
i Fill in the blanks in following statement, including insert your software name, the year of the first publication of your software, and your name identified as the copyright owner;
ii Create a file named “LICENSE” which contains the whole context of this License in the first directory of your software package;
iii Attach the statement to the appropriate annotated syntax at the beginning of each source file.
Copyright (c) [Year] [name of copyright holder]
[Software Name] is licensed under Mulan PSL v2.
You can use this software according to the terms and conditions of the Mulan PSL v2.
You may obtain a copy of Mulan PSL v2 at:
http://license.coscl.org.cn/MulanPSL2
THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
See the Mulan PSL v2 for more details.

53
pc/README.md Normal file
View File

@ -0,0 +1,53 @@
### 介绍
**Yunzer-Admin** 基于 Vue3.3、TypeScript、Vite3、Pinia、Element-Plus 专注于表格,表单的企业级后台管理框架,取名 **Yunzer**源于NBA球队圣安东尼奥马刺队San Antonio Yunzer作为一支专业篮球队马刺的卓越不止在于技术水平和战术运筹的精妙更在于他们小石匠精神一直激励着大家。
马刺队更衣室里的一句话:
“当一切看起来无可挽回之时,我跑去看石匠重复捶击他面前的岩石一百次,而那块石头连
一个裂缝都没有露出来。接下来的第一百零一次捶击之时,此石一分为二。不是因为这
一次捶击,而是因为你的始终如一。”
共勉......
[代码gitee地址](https://gitee.com/3439/Yunzer-Admin)
[在线预览](http://jdvip.suipin.net)
### 技术栈+版本
本项目技术栈基于`npm^6.14.7+node^14.8.1+Vue3.3.4 + TypeScript + Vite4.4.5 + vue-router4.2.4 + pinia + axios`
### 运行
```javascript
克隆项目
git clone https://gitee.com/3439/Yunzer-Admin.git
进入项目目录
cd Yunzer-Admin
安装依赖
npm install
本地开发 启动项目
npm run dev
```
### 系列文章
[从零开始vue3+vite+ts+pinia+router4后台管理(1)](https://juejin.cn/post/7286112965609357347)
[从零开始vue3+vite+ts+pinia+router4后台管理(2)-页面布局](https://juejin.cn/post/7286508785104322594)
[从零开始vue3+vite+ts+pinia+router4后台管理(3)-动态路由](https://juejin.cn/post/7286679458131312674)
[从零开始vue3+vite+ts+pinia+router4后台管理(4)-导航标签栏和keep-alive缓存](https://juejin.cn/post/7287053284787028003)
[从零开始vue3+vite+ts+pinia+router4后台管理(5)-二次封装表格1.0](https://juejin.cn/post/7288963909581635618)
[从零开始vue3+vite+ts+pinia+router4后台管理(6)-全局自定义指令实现节流与防抖](https://juejin.cn/post/7290470513116856320)
[什么才是完美的表格二次封装elementPlus表格-从零开始vue3+vite+ts+pinia+router4后台管理(7)](https://juejin.cn/post/7301903019222155264)

6
pc/env.d.ts vendored Normal file
View File

@ -0,0 +1,6 @@
//解决import vue文件红色波浪线问题
declare module '*.vue' {
import { DefineComponent } from 'vue';
const component: DefineComponent<{}, {}, any>;
export default component;
}

13
pc/index.html Normal file
View File

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

2391
pc/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

34
pc/package.json Normal file
View File

@ -0,0 +1,34 @@
{
"name": "Yunzer-Admin",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"@element-plus/icons-vue": "^2.1.0",
"@imengyu/vue3-context-menu": "^1.3.3",
"axios": "^1.5.0",
"element-plus": "^2.4.2",
"element-plus-table-dragable": "^1.0.0",
"js-cookie": "^3.0.5",
"nprogress": "^0.2.0",
"pinia": "^2.1.7",
"vue": "^3.3.4",
"vue-router": "^4.2.4"
},
"devDependencies": {
"@types/node": "^20.9.0",
"@types/nprogress": "^0.2.3",
"@vitejs/plugin-vue": "^4.2.3",
"@vitejs/plugin-vue-jsx": "^3.0.2",
"sass": "^1.69.5",
"typescript": "^5.0.2",
"vite": "^4.4.5",
"vite-plugin-vue-setup-extend": "^0.4.0",
"vue-tsc": "^1.8.5"
}
}

1
pc/public/vite.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

12
pc/src/App.vue Normal file
View File

@ -0,0 +1,12 @@
<template>
<el-config-provider :locale="zh">
<router-view></router-view>
</el-config-provider>
</template>
<script setup lang="ts">
import { ElConfigProvider } from 'element-plus'
import zh from 'element-plus/es/locale/lang/zh-cn'
</script>
<style scoped>
</style>

View File

@ -0,0 +1,12 @@
import request from '@/utils/request'
export default {
//获取菜单管理列表
getMenuList(data) {
return request({
url: '/system/getMenuList',
method: 'post',
params: data
})
},
}

12
pc/src/api/table/index.ts Normal file
View File

@ -0,0 +1,12 @@
import request from '@/utils/request'
export default {
//获取表格数据
packTableList(data:any) {
return request({
url: '/table/packTable',
method: 'post',
params: data
})
},
}

14
pc/src/api/user/index.ts Normal file
View File

@ -0,0 +1,14 @@
import request from '@/utils/request'
export function login(data:object) {
return request({
url: '/system/login',
method: 'post',
data: data
})
}
export function getIndex() {
return request({
url: '/system/index',
method: 'post'
})
}

View File

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="1361px" height="609px" viewBox="0 0 1361 609" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 46.2 (44496) - http://www.bohemiancoding.com/sketch -->
<title>Group 21</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Ant-Design-Pro-3.0" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="账户密码登录-校验" transform="translate(-79.000000, -82.000000)">
<g id="Group-21" transform="translate(77.000000, 73.000000)">
<g id="Group-18" opacity="0.8" transform="translate(74.901416, 569.699158) rotate(-7.000000) translate(-74.901416, -569.699158) translate(4.901416, 525.199158)">
<ellipse id="Oval-11" fill="#CFDAE6" opacity="0.25" cx="63.5748792" cy="32.468367" rx="21.7830479" ry="21.766008"></ellipse>
<ellipse id="Oval-3" fill="#CFDAE6" opacity="0.599999964" cx="5.98746479" cy="13.8668601" rx="5.2173913" ry="5.21330997"></ellipse>
<path d="M38.1354514,88.3520215 C43.8984227,88.3520215 48.570234,83.6838647 48.570234,77.9254015 C48.570234,72.1669383 43.8984227,67.4987816 38.1354514,67.4987816 C32.3724801,67.4987816 27.7006688,72.1669383 27.7006688,77.9254015 C27.7006688,83.6838647 32.3724801,88.3520215 38.1354514,88.3520215 Z" id="Oval-3-Copy" fill="#CFDAE6" opacity="0.45"></path>
<path d="M64.2775582,33.1704963 L119.185836,16.5654915" id="Path-12" stroke="#CFDAE6" stroke-width="1.73913043" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M42.1431708,26.5002681 L7.71190162,14.5640702" id="Path-16" stroke="#E0B4B7" stroke-width="0.702678964" opacity="0.7" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="1.405357899873153,2.108036953469981"></path>
<path d="M63.9262187,33.521561 L43.6721326,69.3250951" id="Path-15" stroke="#BACAD9" stroke-width="0.702678964" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="1.405357899873153,2.108036953469981"></path>
<g id="Group-17" transform="translate(126.850922, 13.543654) rotate(30.000000) translate(-126.850922, -13.543654) translate(117.285705, 4.381889)" fill="#CFDAE6">
<ellipse id="Oval-4" opacity="0.45" cx="9.13482653" cy="9.12768076" rx="9.13482653" ry="9.12768076"></ellipse>
<path d="M18.2696531,18.2553615 C18.2696531,13.2142826 14.1798519,9.12768076 9.13482653,9.12768076 C4.08980114,9.12768076 0,13.2142826 0,18.2553615 L18.2696531,18.2553615 Z" id="Oval-4" transform="translate(9.134827, 13.691521) scale(-1, -1) translate(-9.134827, -13.691521) "></path>
</g>
</g>
<g id="Group-14" transform="translate(216.294700, 123.725600) rotate(-5.000000) translate(-216.294700, -123.725600) translate(106.294700, 35.225600)">
<ellipse id="Oval-2" fill="#CFDAE6" opacity="0.25" cx="29.1176471" cy="29.1402439" rx="29.1176471" ry="29.1402439"></ellipse>
<ellipse id="Oval-2" fill="#CFDAE6" opacity="0.3" cx="29.1176471" cy="29.1402439" rx="21.5686275" ry="21.5853659"></ellipse>
<ellipse id="Oval-2-Copy" stroke="#CFDAE6" opacity="0.4" cx="179.019608" cy="138.146341" rx="23.7254902" ry="23.7439024"></ellipse>
<ellipse id="Oval-2" fill="#BACAD9" opacity="0.5" cx="29.1176471" cy="29.1402439" rx="10.7843137" ry="10.7926829"></ellipse>
<path d="M29.1176471,39.9329268 L29.1176471,18.347561 C23.1616351,18.347561 18.3333333,23.1796097 18.3333333,29.1402439 C18.3333333,35.1008781 23.1616351,39.9329268 29.1176471,39.9329268 Z" id="Oval-2" fill="#BACAD9"></path>
<g id="Group-9" opacity="0.45" transform="translate(172.000000, 131.000000)" fill="#E6A1A6">
<ellipse id="Oval-2-Copy-2" cx="7.01960784" cy="7.14634146" rx="6.47058824" ry="6.47560976"></ellipse>
<path d="M0.549019608,13.6219512 C4.12262681,13.6219512 7.01960784,10.722722 7.01960784,7.14634146 C7.01960784,3.56996095 4.12262681,0.670731707 0.549019608,0.670731707 L0.549019608,13.6219512 Z" id="Oval-2-Copy-2" transform="translate(3.784314, 7.146341) scale(-1, 1) translate(-3.784314, -7.146341) "></path>
</g>
<ellipse id="Oval-10" fill="#CFDAE6" cx="218.382353" cy="138.685976" rx="1.61764706" ry="1.61890244"></ellipse>
<ellipse id="Oval-10-Copy-2" fill="#E0B4B7" opacity="0.35" cx="179.558824" cy="175.381098" rx="1.61764706" ry="1.61890244"></ellipse>
<ellipse id="Oval-10-Copy" fill="#E0B4B7" opacity="0.35" cx="180.098039" cy="102.530488" rx="2.15686275" ry="2.15853659"></ellipse>
<path d="M28.9985381,29.9671598 L171.151018,132.876024" id="Path-11" stroke="#CFDAE6" opacity="0.8"></path>
</g>
<g id="Group-10" opacity="0.799999952" transform="translate(1054.100635, 36.659317) rotate(-11.000000) translate(-1054.100635, -36.659317) translate(1026.600635, 4.659317)">
<ellipse id="Oval-7" stroke="#CFDAE6" stroke-width="0.941176471" cx="43.8135593" cy="32" rx="11.1864407" ry="11.2941176"></ellipse>
<g id="Group-12" transform="translate(34.596774, 23.111111)" fill="#BACAD9">
<ellipse id="Oval-7" opacity="0.45" cx="9.18534718" cy="8.88888889" rx="8.47457627" ry="8.55614973"></ellipse>
<path d="M9.18534718,17.4450386 C13.8657264,17.4450386 17.6599235,13.6143199 17.6599235,8.88888889 C17.6599235,4.16345787 13.8657264,0.332739156 9.18534718,0.332739156 L9.18534718,17.4450386 Z" id="Oval-7"></path>
</g>
<path d="M34.6597385,24.809694 L5.71666084,4.76878945" id="Path-2" stroke="#CFDAE6" stroke-width="0.941176471"></path>
<ellipse id="Oval" stroke="#CFDAE6" stroke-width="0.941176471" cx="3.26271186" cy="3.29411765" rx="3.26271186" ry="3.29411765"></ellipse>
<ellipse id="Oval-Copy" fill="#F7E1AD" cx="2.79661017" cy="61.1764706" rx="2.79661017" ry="2.82352941"></ellipse>
<path d="M34.6312443,39.2922712 L5.06366663,59.785082" id="Path-10" stroke="#CFDAE6" stroke-width="0.941176471"></path>
</g>
<g id="Group-19" opacity="0.33" transform="translate(1282.537219, 446.502867) rotate(-10.000000) translate(-1282.537219, -446.502867) translate(1142.537219, 327.502867)">
<g id="Group-17" transform="translate(141.333539, 104.502742) rotate(275.000000) translate(-141.333539, -104.502742) translate(129.333539, 92.502742)" fill="#BACAD9">
<circle id="Oval-4" opacity="0.45" cx="11.6666667" cy="11.6666667" r="11.6666667"></circle>
<path d="M23.3333333,23.3333333 C23.3333333,16.8900113 18.1099887,11.6666667 11.6666667,11.6666667 C5.22334459,11.6666667 0,16.8900113 0,23.3333333 L23.3333333,23.3333333 Z" id="Oval-4" transform="translate(11.666667, 17.500000) scale(-1, -1) translate(-11.666667, -17.500000) "></path>
</g>
<circle id="Oval-5-Copy-6" fill="#CFDAE6" cx="201.833333" cy="87.5" r="5.83333333"></circle>
<path d="M143.5,88.8126685 L155.070501,17.6038544" id="Path-17" stroke="#BACAD9" stroke-width="1.16666667"></path>
<path d="M17.5,37.3333333 L127.466252,97.6449735" id="Path-18" stroke="#BACAD9" stroke-width="1.16666667"></path>
<polyline id="Path-19" stroke="#CFDAE6" stroke-width="1.16666667" points="143.902597 120.302281 174.935455 231.571342 38.5 147.510847 126.366941 110.833333"></polyline>
<path d="M159.833333,99.7453842 L195.416667,89.25" id="Path-20" stroke="#E0B4B7" stroke-width="1.16666667" opacity="0.6"></path>
<path d="M205.333333,82.1372105 L238.719406,36.1666667" id="Path-24" stroke="#BACAD9" stroke-width="1.16666667"></path>
<path d="M266.723424,132.231988 L207.083333,90.4166667" id="Path-25" stroke="#CFDAE6" stroke-width="1.16666667"></path>
<circle id="Oval-5" fill="#C1D1E0" cx="156.916667" cy="8.75" r="8.75"></circle>
<circle id="Oval-5-Copy-3" fill="#C1D1E0" cx="39.0833333" cy="148.75" r="5.25"></circle>
<circle id="Oval-5-Copy-2" fill-opacity="0.6" fill="#D1DEED" cx="8.75" cy="33.25" r="8.75"></circle>
<circle id="Oval-5-Copy-4" fill-opacity="0.6" fill="#D1DEED" cx="243.833333" cy="30.3333333" r="5.83333333"></circle>
<circle id="Oval-5-Copy-5" fill="#E0B4B7" cx="175.583333" cy="232.75" r="5.25"></circle>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.7 KiB

BIN
pc/src/assets/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
pc/src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -0,0 +1,99 @@
/*自己定义主题的css样式开始*/
$--colors: (
'primary': (
'base': #16baaa
),
'success': (
'base': #16b777
),
'warning': (
'base': #FFB800
),
'danger': (
'base': #FF5722
),
'error': (
'base': #f56c6c
),
'info': (
'base': #909399
),
);
@forward "element-plus/theme-chalk/src/common/var.scss" with
(
$colors: $--colors
);
@use "element-plus/theme-chalk/src/index.scss" as *;
/*自己定义主题的css样式结束*/
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
* {
padding: 0;
margin: 0;
outline: none !important;
box-sizing: border-box;
}
body {
margin: 0;
min-width: 320px;
min-height: 100vh;
}
a {
text-decoration: none
}
html,
body,
#app {
padding: 0;
margin: 0;
background: #fff;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}
.app-container {
box-sizing: border-box;
}
.table-con {
margin-top: 10px;
}
.green-button {
background: #16baaa;
border: none;
color: #fff;
}
.table-con{
width: 100%;
overflow-x: auto;
}

1
pc/src/assets/vue.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

View File

@ -0,0 +1,55 @@
<!-- 分页el-pagination 二次封装 -->
<template>
<div v-show="total>0" class="pagination-con">
<el-pagination
v-model:current-page="curPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 30, 40, 50]"
background
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</template>
<script setup name="Pagination" lang="ts">
import { computed} from 'vue'
const props = defineProps({
page: { type: Number, default: 1 },
size: { type: Number, default: 10 },
total: { type: Number, default: 0 }
})
// 1defineEmits
// 2使defineEmits使emit()
// 3emits
const emit = defineEmits(['update:size', 'update:page', 'pagination'])
// console.vlog(emit)
const pageSize = computed({
get: () => props.size,
set: (val) => {
emit('update:size', val)
}
})
const curPage = computed({
get: () => props.page,
set: (val) => {
emit('update:page', val)
}
})
function handleSizeChange() {
emit('pagination')
}
function handleCurrentChange() {
emit('pagination')
}
</script>
<style scoped>
.pagination-con{
margin-top: 10px;
margin-bottom: 10px;
}
</style>

View File

@ -0,0 +1,45 @@
<template>
<div class="spurs-dialog">
<el-dialog
v-bind="$attrs"
v-model="dialogVisible"
:append-to-body="true"
:close-on-click-modal="false"
:close-on-press-escape="false"
draggable
>
<slot name="body"></slot>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible=false">取消</el-button>
<el-button v-if="dialogType!='readonlyDialog'" type="primary" @click="saveSubmit">
</el-button>
<slot name="footer"></slot>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import {ref} from "vue";
let dialogVisible = ref(false)
defineExpose({
dialogVisible
})
const props = defineProps({
dialogType: {type:String, default:""}//readonlyDialogdialog
});
const close = () =>{
dialogVisible.value = false
}
const emit = defineEmits(['saveSubmit'])
const saveSubmit = () => {
emit('saveSubmit');
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,31 @@
type FormType =
|'input'
| 'password'
| 'select'
| 'datepicker'
| 'timepicker'
| 'switch'
| 'radio'
| 'textarea'
interface ItemOption {
label: string
value: string | number
}
export interface FormItem {
field: string //字段名
type?: FormType //输入框类型
label: string //输入框标题
colSpan?: number// 栅格占据的列数默认24
disabled?:boolean//表单是否可修改 默认false
placeholder?: any //输入框默认显示内容
prop?: string //表单校验
options?: ItemOption[] //选择器的可选子选项 select
otherOptions?: any//特殊情况
isHidden?: boolean
slotName?: string//处理一些自定义内容
}
export interface FormOption {
formItems: FormItem[]
labelWidth?: string//标签的长度
}

View File

@ -0,0 +1,88 @@
<template>
<div class="header">
<slot name="header"> </slot>
</div>
<el-form
ref="ruleFormRef"
:label-width="labelWidth"
status-icon
:model="modelValue"
v-bind="$attrs"
>
<el-row>
<template v-for="item in formItems" :key="item.label">
<el-col :span="item.colSpan??24">
<el-form-item
v-if="!item.isHidden"
:label="item.label"
:prop="item.field"
>
<template v-if="item.type === 'input' || item.type === 'password'">
<el-input
:disabled="item.disabled??false"
:placeholder="item.placeholder"
:show-password="item.type === 'password'"
v-model="modelValue[`${item.field}`]"
clearable
/>
</template>
<template v-else-if="item.type === 'select'">
<el-select
:placeholder="item.placeholder"
v-model="modelValue[`${item.field}`]"
style="width: 100%"
clearable
>
<el-option
v-for="option in item.options"
:key="option.value"
:value="option.value"
:label="option.label"
>
</el-option>
</el-select>
</template>
<template v-else-if="item.type === 'datepicker'">
<el-date-picker
unlink-panels
value-format="YYYY-MM-DD"
v-bind="item.otherOptions"
v-model="modelValue[`${item.field}`]"
></el-date-picker>
</template>
<template v-if="item.slotName!=undefined">
<slot :name="item.slotName"></slot>
</template>
</el-form-item>
</el-col>
</template>
</el-row>
<el-form-item>
<slot name="footer"></slot>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
import {FormItem} from "@/components/YunzerForm/formType.ts";
import { ref } from 'vue'
import type { FormInstance } from 'element-plus'
//
interface Props {
formItems: FormItem[] //
labelWidth?: string //
modelValue: object //
}
const props = withDefaults(defineProps<Props>(), {
formItems: () => [],
})
const ruleFormRef = ref<FormInstance>()
defineExpose({
ruleFormRef
})
</script>
<style scoped lang="scss">
</style>

View File

@ -0,0 +1,78 @@
<template>
<el-table
ref="tableRef"
style="width: 100%"
v-loading="loading"
:data="tableData"
border
v-bind="$attrs"
>
<!-- 1.传入showSelectColumn时展示的全选列 -->
<template v-if="showSelectColumn">
<el-table-column type="selection" />
</template>
<!-- 2.传入showIndexColumn时展示的序号列 -->
<template v-if="showIndexColumn">
<el-table-column type="index" label="#" />
</template>
<!-- 3.propList里面的所有列 -->
<template v-for="item in propList" :key="item.prop">
<el-table-column v-bind="item" show-overflow-tooltip>
<!-- 传有slotName时展示的插槽列 -->
<template #default="scope" v-if="item.filter">
{{item.filter(scope.row[`${item.prop}`])}}
</template>
<template #default="scope" v-if="item.slotName">
<slot :name="item.slotName" :row="scope.row"></slot>
</template>
</el-table-column>
</template>
</el-table>
<Pagination
v-if="showPagination"
v-model:page="pagination.pageNum"
v-model:size="pagination.pageSize"
:total="total"
@pagination="getTableData"
/>
</template>
<script setup lang="ts">
import {withDefaults} from 'vue'
import {ColumnOption} from "@/components/YunzerTable/tableType.ts";
import {useTable} from '@/hooks/useTable.ts'
interface Props {
requestApi: Function // apiaxios ==>
queryForm?:any
propList: ColumnOption[] //el-table-column
showIndexColumn?: boolean //
showSelectColumn?: boolean //
showPagination?: boolean //
childrenProps?: object //
}
const props = withDefaults(defineProps<Props>(), {
showIndexColumn: false,
showSelectColumn: false,
showPagination: false,
childrenProps: () => ({})
})
const {
tableData,
pagination,
total,
loading,
getTableData,
handleSearch,//
refreshTableInfo//
} = useTable(props.requestApi,props.queryForm)
defineExpose({
tableData,
handleSearch,
refreshTableInfo
})
// console.log(props);
</script>
<style scoped></style>

View File

@ -0,0 +1,17 @@
//表格行el-table-column配置项
export interface ColumnOption {
prop?: string
label: string
minWidth?: string
slotName?: string
align?: string
filter?: Function | undefined
}
//表格配置项
export interface TableOption {
propList: ColumnOption[]
showIndexColumn?: boolean
showSelectColumn?: boolean
showPagination?: boolean
childrenProps?: object
}

View File

@ -0,0 +1,23 @@
// 输入框节流
import { App, DirectiveBinding } from "vue";
// 输入框防抖
export default (app: App) => {
app.directive("debounce", {
mounted(el: HTMLElement | any, binding: DirectiveBinding) {
const func = binding.value?binding.value.func : null//binding.value.func 这是输入框传过来的方法
el.timer = null
el.addEventListener('input', () => {
console.log(binding);
if (el.timer !== null) {
clearTimeout(el.timer)
el.timer = null
}
el.timer = setTimeout(() => {
func && func()
}, 1000)
})
}
})
}

53
pc/src/directive/index.ts Normal file
View File

@ -0,0 +1,53 @@
//bindingDirectiveBinding一个包含指令信息的对象。例如包含指令的名称、值、参数等等
import {App, DirectiveBinding} from "vue";
export default (app: App) => {
// 节流 防止按钮多次点击,多次请求
app.directive("throttle", {
mounted(el: HTMLElement | any, binding: DirectiveBinding) {
const time = binding.value ? binding.value : 2000
el.timer = null
el.addEventListener('click', () => {
//console.log(binding);
el.disabled = true
if (el.timer !== null) {
clearTimeout(el.timer)
el.timer = null
el.disabled = true
}
el.timer = setTimeout(() => {
el.disabled = false
}, time)
})
}
})
// 输入框防抖
app.directive("debounce", {
mounted(el: HTMLElement | any, binding: DirectiveBinding) {
const time = binding.value.time ? binding.value.time : 1000//binding.value.time这是输入框传过来的时间
const func = binding.value ? binding.value.func : null//binding.value.func 这是输入框传过来的方法
el.timer = null
el.addEventListener('input', () => {
//console.log(binding);
if (el.timer !== null) {
clearTimeout(el.timer)
el.timer = null
}
el.timer = setTimeout(() => {
func && func()
}, time)
})
}
})
//表格可视区域内滚动
app.directive("allheight", {
mounted(el: HTMLElement | any, binding: DirectiveBinding) {
const top:number = el.offsetTop//el-table距离窗口顶部偏移量
const bottom:number = binding?.value?.bottom ? binding.value.bottom : 65//底部预留的高度 默认高度52
const pageHeight:number = window.innerHeight//页面的高度
el.style.height = pageHeight - top - bottom-90 + 'px'
}
})
}

0
pc/src/hooks/useForm.ts Normal file
View File

61
pc/src/hooks/useTable.ts Normal file
View File

@ -0,0 +1,61 @@
import {reactive, onMounted, ref, nextTick} from 'vue'
import { useRoute, useRouter } from 'vue-router'
export function useTable(loadDataFunc: Function,queryForm: {}) {
let loading = ref(true)
let tableData = ref(new Array<any>())
let total = ref(0)
let tableHeight = ref("0px")
const router = useRouter()
const route = useRoute()
const pagination = reactive({
pageNum: 1,
pageSize: 10
})
const getTableData = async () => {
loading.value = true;
getTableHeight(160,45)//获取表格的自适应高度
const res = await loadDataFunc({...queryForm,...pagination})
tableData.value = res.data.list;
total.value = res.data.total
loading.value = false;
}
const getTableHeight = (tabletop:number,tablebottom:number):void =>{
const top:number = tabletop//el-table距离窗口顶部偏移量
const bottom:number = tablebottom//
const pageHeight:number = window.innerHeight//页面的高度
if(document.getElementsByClassName("el-pagination").length>0){
//判断页面有木有分页,来控制表格的高度自适应
tableHeight.value = pageHeight - top - bottom + 'px'
}else {
tableHeight.value = pageHeight - top + 'px'
}
}
onMounted(() => {
getTableData()
})
// 搜索
const handleSearch = () => {
pagination.pageNum = 1
getTableData()
}
const refreshTableInfo = () => {
//刷新当前路由
const { fullPath } = route
nextTick(() => {
router.replace({
path: '/redirect' + fullPath
})
})
}
return {
loading,
tableData,
total,
pagination,
getTableData,
handleSearch,
tableHeight,
refreshTableInfo
}
}

View File

@ -0,0 +1,33 @@
<template>
<div class="common-layout">
<el-container>
<el-aside :width="menuCollapse?'64px':'200px'">
<v-aside></v-aside>
</el-aside>
<el-container style="overflow-y: auto">
<el-header style="padding: 0px;height: 90px">
<v-header></v-header>
</el-header>
<el-main style="padding: 0px;">
<el-scrollbar>
<v-main></v-main>
</el-scrollbar>
</el-main>
</el-container>
</el-container>
</div>
</template>
<script setup lang="ts">
import VAside from "@/layout/components/vAside/vAside.vue";
import VMain from "@/layout/components/vMain/vMain.vue";
import VHeader from "@/layout/components/vHeader/vHeader.vue";
import {useSettingsStore} from '@/store/settings'
const settingsStore = useSettingsStore()
import {computed} from 'vue'
const menuCollapse = computed(() => settingsStore.menuCollapse)
</script>
<style scoped>
</style>

View File

@ -0,0 +1,59 @@
<template>
<div class="sidebar-logo-container" :class="{ menuCollapse: menuCollapse }">
<transition name="sidebarLogoFade">
<router-link
class="sidebar-logo-link"
to="/"
>
<img :src="LogoImg" class="sidebar-logo" />
<span class="sidebar-title">{{title}}</span>
</router-link>
</transition>
</div>
</template>
<script setup lang="ts">
import { reactive, toRefs } from 'vue'
import LogoImg from '@/assets/logo.png'
const state = reactive({
title: 'Yunzer-Admin'
})
const { title, logo } = toRefs(state)
const props = defineProps({
menuCollapse: {
type: Boolean,
required: true
}
})
// console.log(props.menuCollapse);
</script>
<style scoped>
.sidebar-logo{
width: 40px;
height: 40px;
vertical-align: middle;
margin-right: 12px;
padding-bottom: 4px;
}
.sidebar-title{
line-height: 50px;
font-weight: bold;
display: inline-block;
vertical-align: middle;
color: #333;
font-size: 18px;
}
.sidebar-logo-container{
height: 50px;
line-height: 50px;
border-bottom: 1px solid #ddd;
text-align: center;
box-sizing: border-box;
overflow: hidden;
}
.sidebar-logo-link{
width: 100%;
height: 100%;
}
</style>

View File

@ -0,0 +1,48 @@
<template>
<el-sub-menu
:index="item.path"
v-if="item && !item.hidden && item.children && item.name!='Dashboard'"
>
<template #title>
<el-icon>
<component :is="item.meta.icon"></component>
</el-icon>
<span>{{item.meta.title}}</span>
</template>
<el-menu-item
style="background: #f6f6f6"
v-for="route in item.children"
:key="route.path"
:index="route.path"
>
<span>{{route.meta.title}}</span>
</el-menu-item>
</el-sub-menu>
<el-menu-item style="background: #f6f6f6" :index="item.children[0].path" v-else-if="item && !item.hidden && item.name=='Dashboard'">
<el-icon>
<component :is="item.meta.icon"></component>
</el-icon>
<template #title>{{item.meta.title}}</template>
</el-menu-item>
<el-menu-item style="background: #f6f6f6" :index="item.path" v-else-if="item && !item.hidden">
<el-icon>
<component :is="item.meta.icon"></component>
</el-icon>
<template #title>{{item.meta.title}}</template>
</el-menu-item>
</template>
<script setup lang="ts" name="SidebarItem">
const props = defineProps({
item: { type: Object, default: () => {} }
})
</script>
<style scoped>
</style>

View File

@ -0,0 +1,55 @@
<template>
<div class="aside-con">
<logo :menuCollapse=menuCollapse />
<el-scrollbar class="vertical-menus-scrollbar">
<el-menu
:collapse-transition="false"
:collapse="menuCollapse"
:default-active="activeMenu"
class="el-menu-vertical-demo"
router
>
<sidebar-item
v-for="route in routes"
:key="route.path"
:item="route"
>
</sidebar-item>
</el-menu>
</el-scrollbar>
</div>
</template>
<script setup lang="ts">
import { usePermissionStore } from '@/store/permission'
import { useSettingsStore } from '@/store/settings'
import { computed } from 'vue'
import SidebarItem from '@/layout/components/vAside/components/index.vue'
import Logo from '@/layout/components/vAside/components/Logo.vue'
const permissionStore = usePermissionStore()
const settingsStore = useSettingsStore()
const routes = computed(() => permissionStore.routes)
const menuCollapse = computed(() => settingsStore.menuCollapse)
import { useRoute } from 'vue-router'
const route = useRoute()
const activeMenu = computed(() => {
const {path } = route
return path
})
// npm install vue-awesome-console --save-dev
// console.vlog(routes)
</script>
<style scoped>
.aside-con{
height: 100vh;
overflow: hidden;
background-color: #f6f6f6;
}
.el-menu-vertical-demo{
background: #f6f6f6;
border-right: none;
}
</style>

View File

@ -0,0 +1,120 @@
<template>
<div class="drawer-wrapper">
<el-drawer
v-model="showSettings"
title="系统配置"
direction="rtl"
size="310px"
>
<el-divider>布局方式</el-divider>
<div class="layout-wrapper">
<el-row :gutter="20">
<el-col @click="handleLayoutMode('Classic')" :span="12">
<div
class="classic"
:class="{'active-layout': layoutMode === 'Classic' ? true : false}"
>
<div class="sidebar"></div>
<div class="main-wrapper">
<div class="main-header"></div>
<div class="main"></div>
</div>
<div class="introduce">经典</div>
</div>
</el-col>
<el-col @click="handleLayoutMode('Streamline')" :span="12">
<div
class="streamline"
:class="{'active-layout': layoutMode === 'Streamline' ? true : false}"
>
<div class="main-wrapper">
<div class="main-header"></div>
<div class="main"></div>
</div>
<div class="introduce">单栏</div>
</div>
</el-col>
</el-row>
</div>
</el-drawer>
</div>
</template>
<script setup lang="ts">
import { reactive, toRefs,computed } from 'vue'
import { useSettingsStore } from '@/store/settings'
const settingsStore = useSettingsStore()
const state = reactive({
showSettings:false,
layoutMode:null
})
const {
showSettings
} = toRefs(state)
const layoutMode = computed(() => settingsStore.layoutMode)
const handleLayoutMode = (mode:any) => {
state.layoutMode = mode
state.showSettings = false
settingsStore.changeSetting({
key: 'layoutMode',
value: mode
})
}
defineExpose({
showSettings
})
</script>
<style scoped>
.active-layout {
border: 1px solid #409eff;
}
.streamline{
cursor: pointer;
position: relative;
display: flex;
height: 100px;
box-sizing: border-box;
}
.classic{
cursor: pointer;
position: relative;
display: flex;
height: 100px;
box-sizing: border-box;
}
.sidebar {
width: 20%;
background-color: #ebeef5;
}
.main-wrapper {
flex: 1;
display: flex;
flex-direction: column;
margin-left: 5px;
.main-header {
height: 10%;
background-color: #dcdfe6;
}
.main {
flex: 1;
margin-top: 5px;
background-color: #f2f6fc;
}
}
.introduce{
position: absolute;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
color: #a0cfff;
}
</style>

View File

@ -0,0 +1,310 @@
<template>
<div class="tags-view-container">
<el-scrollbar
@scroll="handleScroll"
ref="refScrollbar"
class="tags-view-wrapper"
>
<router-link
to='/'
:class="route.path=='/dashboard' ? 'active' : ''"
class="tags-view-item"
>
<span class="tags-title">首页</span>
</router-link>
<router-link
v-for="tag in visitedViews"
ref="refTag"
:class="isActive(tag) ? 'active' : ''"
class="tags-view-item"
:to="tag.path"
:key="tag.path"
@click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''"
@contextmenu.prevent="openMenu(tag, $event)"
>
<span class="tags-title">{{tag.meta.title}}</span>
<el-icon @click.prevent.stop="closeSelectedTag(tag)" class="tags-icon">
<Close />
</el-icon>
</router-link>
</el-scrollbar>
<ul
v-show="visible"
:style="{ left: left + 'px', top: top + 'px' }"
class="contextmenu"
>
<li @click="refreshSelectedTag(selectedTag)">刷新</li>
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">
关闭
</li>
<li @click="closeAllTags(selectedTag)">关闭全部</li>
</ul>
</div>
</template>
<script setup lang="ts">
import { computed, nextTick, onMounted, reactive, toRefs, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useTagsViewStore } from '@/store/tagsView'
const tagsViewStore = useTagsViewStore()
import {useSettingsStore} from '@/store/settings'
const settingsStore = useSettingsStore()
const visitedViews = computed(() => tagsViewStore.visitedViews)
const tagAndTagSpacing = 4 // tagAndTagSpacing
const state = reactive({
visible: false,
top: 0,
left: 0,
selectedTag: {},
affixTags: [],
refTag: null,
refScrollbar: null
})
const { visible, top, left, selectedTag, refTag, refScrollbar } = toRefs(state)
const router = useRouter()
const route = useRoute()
// console.log(router);
// console.log(route);
const isActive = (tag) => {
return tag.path === route.path
}
const isAffix = (tag) => {
return tag.meta && tag.meta.affix
}
const addTags = () => {
const { name } = route
if (name && name!='Dashboard') {
tagsViewStore.addView(route)
}
return false
}
const moveToTarget = (currentTag) => {
const $container = refScrollbar.value.wrapRef
const $containerWidth = $container.offsetWidth
const $scrollWidth = $container.scrollWidth
const tagList = refTag.value
let firstTag = null
let lastTag = null
// find first tag and last tag
if (tagList.length > 0) {
firstTag = tagList[0]
lastTag = tagList[tagList.length - 1]
}
if (firstTag === currentTag) {
$container.scrollLeft = 0
} else if (lastTag === currentTag) {
$container.scrollLeft = $scrollWidth - $containerWidth
} else {
// find preTag and nextTag
const currentIndex = tagList.findIndex((item) => item === currentTag)
const prevTag = tagList[currentIndex - 1]
const nextTag = tagList[currentIndex + 1]
// the tag's offsetLeft after of nextTag
const afterNextTagOffsetLeft =
nextTag.$el.offsetLeft + nextTag.$el.offsetWidth + tagAndTagSpacing
// the tag's offsetLeft before of prevTag
const beforePrevTagOffsetLeft = prevTag.$el.offsetLeft - tagAndTagSpacing
if (afterNextTagOffsetLeft > $container.scrollLeft + $containerWidth) {
$container.scrollLeft = afterNextTagOffsetLeft - $containerWidth
} else if (beforePrevTagOffsetLeft < $container.scrollLeft) {
$container.scrollLeft = beforePrevTagOffsetLeft
}
}
}
const moveToCurrentTag = () => {
nextTick(() => {
for (const tag of refTag.value) {
if (tag.to.path === route.path) {
moveToTarget(tag)
// when query is different then update
if (tag.to.fullPath !== route.fullPath) {
tagsViewStore.updateVisitedView(route)
}
break
}
}
})
}
const refreshSelectedTag = (view) => {
// console.vlog(view);
tagsViewStore.delCachedView(view).then(() => {
const { fullPath } = view
nextTick(() => {
console.log(fullPath);
router.replace({
path: '/redirect' + fullPath
})
})
})
}
const closeSelectedTag = (view) => {
tagsViewStore.delView(view).then(({ visitedViews }) => {
if (isActive(view)) {
toLastView(visitedViews, view)
}
})
}
const closeOthersTags = () => {
router.push(selectedTag)
tagsViewStore.delOthersViews(selectedTag).then(() => {
moveToCurrentTag()
})
}
const closeAllTags = (view) => {
tagsViewStore.delAllViews().then(({ visitedViews }) => {
if (state.affixTags.some((tag) => tag.path === view.path)) {
return
}
toLastView(visitedViews, view)
})
}
const toLastView = (visitedViews, view) => {
const latestView = visitedViews.slice(-1)[0]
if (latestView) {
router.push(latestView.fullPath)
} else {
// now the default is to redirect to the home page if there is no tags-view,
// you can adjust it according to your needs.
if (view.name === 'Dashboard') {
// to reload home page
router.replace({ path: '/redirect' + view.fullPath })
} else {
router.push('/')
}
}
}
const openMenu = (tag, e) => {
const menuMinWidth = 105
let offsetLeft:number = 0;
if(!settingsStore.menuCollapse && settingsStore.layoutMode!='Streamline'){
offsetLeft = refScrollbar.value.wrapRef.getBoundingClientRect().left-200 // container margin left
}else {
offsetLeft = refScrollbar.value.wrapRef.getBoundingClientRect().left // container margin left
}
const offsetWidth = refScrollbar.value.offsetWidth // container width
const maxLeft = offsetWidth - menuMinWidth // left boundary
const currentLeft = e.clientX - offsetLeft + 15 // 15: margin right
if (currentLeft > maxLeft) {
state.left = maxLeft
} else {
state.left = currentLeft
}
state.top = e.clientY
state.visible = true
state.selectedTag = tag
}
const closeMenu = () => {
state.visible = false
}
const handleScroll = () => {
closeMenu()
}
onMounted(() => {
addTags()
})
watch(
() => route.path,
() => {
addTags()//
}
)
watch(
() => state.visible,
(newValue) => {
if (newValue) {
document.body.addEventListener('click', closeMenu)
} else {
document.body.removeEventListener('click', closeMenu)
}
}
)
// console.vlog(router);
// console.vlog(route);
</script>
<style scoped>
.tags-view-wrapper{
display: flex;
align-items: center;
height: 40px;
}
.tags-view-item{
display: inline-block;
position: relative;
height: 26px;
line-height: 26px;
border: 1px solid #d8dce5;
padding: 0 10px;
margin-top: 7px;
color: #495060;
font-size: 12px;
margin-right: 10px;
}
.tags-title{
}
.tags-icon{
margin-top: -2px;
vertical-align:middle;
margin-left: 4px;
border-radius: 50%;
}
.tags-icon:hover{
background-color: #b4bccc;
color: #fff;
}
.active{
color: #000c;
margin-right: 10px;
}
.active .tags-title:before{
content: "";
background: #16baaa;
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
position: relative;
margin-right: 5px;
}
.contextmenu {
margin: 0;
background: #fff;
z-index: 3000;
position: absolute;
list-style-type: none;
padding: 5px 0;
border-radius: 4px;
font-size: 12px;
font-weight: 400;
color: #333;
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.3);
}
.contextmenu li{
margin: 0;
padding: 7px 16px;
cursor: pointer;
}
.contextmenu li:hover{
background: #eee;
}
</style>

View File

@ -0,0 +1,249 @@
<template>
<div class="fixed-header">
<div class="navbar" v-if="layoutMode=='Classic'">
<div class="left-menu">
<div @click="toggleClick" class="hamburger-container">
<el-icon v-if="menuCollapse" color="#3c3c3c" :size="20">
<Expand/>
</el-icon>
<el-icon v-else="!menuCollapse" color="#3c3c3c" :size="20">
<Fold/>
</el-icon>
</div>
<div class="breadcrumb-container">
<el-breadcrumb separator="/">
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
<el-breadcrumb-item
v-for="(item,index) in levelList"
:key="index"
>
<a :href="item.path">{{ item.meta.title }}</a>
</el-breadcrumb-item>
</el-breadcrumb>
</div>
</div>
<div class="right-menu">
<div class="user-container">
<el-icon @click="handleDrawerOpen" style="margin-right: 5px" color="#333" size="20px">
<Setting />
</el-icon>
<el-dropdown trigger="click">
<div class="user-wrapper">
<span class="user-icon">
<!-- <el-avatar size="small" shape="square"> {{userInfo.userName.charAt(0)}} </el-avatar> -->
</span>
<!-- <span class="user-name">&nbsp;{{userInfo.userName}}</span> -->
<el-icon class="el-icon--right">
<arrow-down />
</el-icon>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</div>
<div class="navbar-wrapper" v-if="layoutMode=='Streamline'">
<div class="navbar-logo">
<logo :menuCollapse="false" />
</div>
<div class="navbar-left">
<el-scrollbar class="vertical-menus-scrollbar">
<el-menu
mode="horizontal"
:collapse-transition="false"
:collapse="menuCollapse"
:default-active="activeMenu"
class="el-menu-vertical-demo"
router
>
<sidebar-item
v-for="route in routes"
:key="route.path"
:item="route"
>
</sidebar-item>
</el-menu>
</el-scrollbar>
</div>
<div class="navbar-right">
<div class="user-container">
<el-icon @click="handleDrawerOpen" style="margin-right: 5px" color="#333" size="20px">
<Setting />
</el-icon>
<el-dropdown trigger="click">
<div class="user-wrapper">
<span class="user-icon">
<!-- <el-avatar size="small" shape="square"> {{userInfo.userName.charAt(0)}} </el-avatar> -->
</span>
<!-- <span class="user-name">&nbsp;{{userInfo.userName}}</span> -->
<el-icon class="el-icon--right">
<arrow-down />
</el-icon>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</div>
<div class="tags-view">
<tags-view></tags-view>
</div>
<setting-view ref="settingView"></setting-view>
</div>
</template>
<script setup lang="ts">
import TagsView from "@/layout/components/vHeader/components/TagsView.vue";
import SettingView from "@/layout/components/vHeader/components/SettingView.vue";
import Logo from '@/layout/components/vAside/components/Logo.vue'
const routes = computed(() => permissionStore.routes)
import {computed, reactive, toRefs, watch,ref} from 'vue'
import { useUserStore } from '@/store/user'
import {useSettingsStore} from '@/store/settings'
import { usePermissionStore } from '@/store/permission'
const permissionStore = usePermissionStore()
const userStore = useUserStore()
const userInfo = computed(() => userStore.userInfo)
const settingsStore = useSettingsStore()
const menuCollapse = computed(() => settingsStore.menuCollapse)
const layoutMode = computed(() => settingsStore.layoutMode)
import {useRoute,useRouter} from 'vue-router'
import SidebarItem from "@/layout/components/vAside/components/index.vue";
const settingView:any = ref(null)
const state:any = reactive({
levelList: null
})
const {levelList} = toRefs(state)
const route = useRoute()
const router = useRouter()
const activeMenu = computed(() => {
const {path } = route
return path
})
const toggleClick = () => {
settingsStore.menuCollapse = !settingsStore.menuCollapse
}
const handleDrawerOpen = () => {
settingView.value.showSettings = true;
}
const getBreadcrumb = () => {
state.levelList = route.matched.filter(x => x.name != "Dashboard")
}
getBreadcrumb()
const logout = async () => {
await userStore.logout()
router.push('/login')
}
// watch
watch(
() => route.path,
() => {
getBreadcrumb()
}
)
</script>
<style scoped>
.navbar-wrapper{
display: flex;
align-items: center;
justify-content: space-between;
height: 50px;
background-color: #fff;
border-bottom: solid 1px #e6e6e6;
box-sizing: border-box;
}
.navbar-left{
flex: 1;
width: 50%;
height: 100%;
}
.navbar-logo{
display: flex;
align-items: center;
height: 100%;
padding: 0 10px;
}
.navbar-right{
display: flex;
align-items: center;
height: 100%;
padding: 0 10px;
}
.tags-view{
width: 100%;
padding: 0 15px;
height: 40px;
background: #fff;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 0 3px 0 rgba(0, 0, 0, 0.04);
box-sizing: border-box;
}
.user-container{
display: flex;
align-items: center;
height: 100%;
transition: background 0.3s;
cursor: pointer;
}
.navbar {
display: flex;
align-items: center; /*上下的位置*/
justify-content: space-between; /*均匀排列每个元素*/
height: 50px;
background: #fff;
border-bottom: 1px solid #ddd;
box-sizing: border-box;
}
.left-menu {
display: flex;
align-items: center;
height: 100%;
}
.right-menu {
display: flex;
align-items: center;
justify-content: flex-end; /* 从行尾位置开始排列 */
height: 100%;
width: 200px;
}
.hamburger-container {
display: flex;
align-items: center;
height: 100%;
padding: 0 15px;
transition: background 0.3s;
cursor: pointer;
}
.breadcrumb-container {
display: flex;
align-items: center;
height: 100%;
margin-left: 10px;
white-space: nowrap;
}
.user-wrapper{
position: relative;
height: 49px;
line-height: 49px;
padding: 0 10px;
padding-left: 5px;
}
.user-wrapper:hover{
background: #f6f6f6;
}
</style>

View File

@ -0,0 +1,27 @@
<template>
<div class="app-main">
<router-view v-slot="{ Component }">
<transition name="fade-transform" mode="out-in">
<keep-alive :include="cachedViews">
<component :is="Component" />
</keep-alive>
</transition>
</router-view>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useTagsViewStore } from '@/store/tagsView'
const tagsViewStore = useTagsViewStore()
const cachedViews = computed(() => tagsViewStore.cachedViews)
// console.vlog("cachedViews",cachedViews);
</script>
<style scoped>
.app-main{
padding: 10px;
padding-left: 15px;
box-sizing: border-box;
}
</style>

19
pc/src/layout/index.vue Normal file
View File

@ -0,0 +1,19 @@
<template>
<div class="common-layout">
<component :is="layoutMode" />
</div>
</template>
<script setup lang="ts">
import Classic from '@/layout/classic/index.vue'
import Streamline from '@/layout/streamline/index.vue'
import {useSettingsStore} from '@/store/settings'
const settingsStore = useSettingsStore()
import {computed} from 'vue'
const layoutEnum:any = {
Classic: Classic,//
Streamline: Streamline//
}
const layoutMode = computed(() => layoutEnum[settingsStore.layoutMode])
</script>
<style scoped>
</style>

View File

@ -0,0 +1,22 @@
<template>
<div class="common-layout">
<el-container>
<el-header style="padding: 0px;height: 90px">
<v-header></v-header>
</el-header>
<el-main style="padding: 0px">
<v-main></v-main>
</el-main>
</el-container>
</div>
</template>
<script setup lang="ts">
import VMain from "@/layout/components/vMain/vMain.vue";
import VHeader from "@/layout/components/vHeader/vHeader.vue";
</script>
<style scoped>
</style>

40
pc/src/main.ts Normal file
View File

@ -0,0 +1,40 @@
import { createApp } from 'vue' //引入vue
const app = createApp(App)// 创建vue实例
import App from './App.vue'//引入入口组件
import router from './router/index'// 引入路由
import ElementPlus from 'element-plus'//引入element-plus
import 'element-plus/dist/index.css'//引入element-plus样式
import './assets/style/style.scss'//引入全局样式
import "@/permission.ts"//路由钩子权限
import * as ElementPlusIconsVue from '@element-plus/icons-vue'//引入element-plus图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}// 全局导入plus图标
import directive from "@/directive/index";// 引入全局自定义指令
app.use(directive);//注册全局自定义指令
import * as filters from "@/utils/filters.ts";//引入全局过滤器
app.config.globalProperties.$filters =filters//注册全局过滤器
import vlog from '@/utils/vue-awesome-console.ts'//解决console.log打印出对象的问题 自己用的
// 你可以选择将 vlog 方法挂在 console 对象上,然后像使用 console.log 一样使用 console.vlog
// 同时你也可以根据项目中的开发/生产模式,进行不同的使用方式
if (process.env.NODE_ENV === 'development') {
// @ts-ignore
window.console.vlog = vlog
} else {
// @ts-ignore
window.console.vlog = () => {}
}
import Pagination from '@/components/Pagination/index.vue'//引入分页组件
app.component('Pagination', Pagination)//注册分页
import YunzerDialog from '@/components/YunzerDialog/index.vue'//引入dialog组件
app.component('YunzerDialog', YunzerDialog)//注册dialog
import {createPinia} from 'pinia'//(读音:皮尼亚)引入状态管理
const pinia = createPinia()// 创建pinia实例
app.use(router)// 注册路由
app.use(ElementPlus)// 注册element-plus
app.use(pinia)// 注册状态管理
app.mount('#app')// 挂载vue实例

56
pc/src/permission.ts Normal file
View File

@ -0,0 +1,56 @@
import router from './router/index'
import { useUserStore } from '@/store/user'
import { usePermissionStore } from '@/store/permission'
import {getToken} from '@/utils/storage.ts'
import NProgress from 'nprogress'
import'nprogress/nprogress.css'
NProgress.configure({ showSpinner: false }) // NProgress Configuration
const whiteList = ['/login','/test'] // 白名单
router.beforeEach(async (to) => {
NProgress.start();
document.title = `${to.meta.title} | Yunzer-Admin`
const hasToken = getToken('Access-Token')
const userStore = useUserStore()
const permissionStore = usePermissionStore()
// if (hasToken) {//判断token是否存在 存在即为已经登录
// if (to.path !== "/login") {
// if (userStore.init) { // 获取了动态路由 init一定true,就无需再次请求 直接放行
// return true
// }else {
// // init为false,一定没有获取动态路由,就跳转到获取动态路由的方法
// const result:any = await userStore.getInfo() //获取路由
// const accessRoutes:any = await permissionStore.generateRoutes(result.menus) //解析路由,存储路由
// // console.log("accessRoutes",accessRoutes);
// // 动态挂载路由
// accessRoutes.forEach((route:any) => {
// router.addRoute(route)
// })
// userStore.init = true//init改为true,路由初始化完成
// return { ...to, replace: true }// hack方法 确保addRoute已完成
// }
// }else {
// NProgress.done()
// return '/'
// }
// }else {
// // 白名单,直接放行
// if (whiteList.indexOf(to.path) > -1) return true
// // 非白名单,去登录
// else return '/login'
// NProgress.done()
// }
})
router.afterEach(() => {
// finish progress bar
NProgress.done()
})
// 1. 在路由钩子里面判断是否首次进入系统(permission.ts)
// 2. init为true说明已经获取过路由,就直接放行,init为false则向后台请求用户路由
// 3. 获取路由
// 4. 解析路由,存储权限
// 5. 使用router的api,addRouter拼接路由
// 6. 存储路由
// 7. init改为true,路由初始化完成
// 8. 放行路由

69
pc/src/router/index.ts Normal file
View File

@ -0,0 +1,69 @@
import {createRouter, createWebHashHistory } from 'vue-router'
import Layout from '@/layout/index.vue'
//constantRoutes 静态路由 登陆,首页等。。。
export const constantRoutes = [
{
path: '/redirect',//解决刷新问题
component: Layout,
hidden: true,
children: [
{
path: '/redirect/:path(.*)',
component: () => import('@/views/redirect/index.vue')
}
]
},
{
path: "/",
name: 'Dashboard',
redirect: '/dashboard',
meta: {
title: '首页',
icon: "House",
},
component: Layout,
children: [
{
path: '/dashboard',
component: () => import('@/views/dashboard/index.vue'),
name: 'Dashboard',
meta: { title: '首页', icon: 'dashboard', affix: true }
}
]
},
{
path: "/login",
name: "Login",
meta: {
title: '登录'
},
component: () => import('@/views/user/login/index.vue'),
hidden: true
},
// {
// path: "/test",
// name: "Test",
// meta: {
// title: '测试'
// },
// component: () => import('@/views/test/test6.vue'),
// hidden: true
// },
{
path: '/:pathMatch(.*)*',// 此写法解决动态路由页面刷新的 warning 警告
component: () => import('@/views/user/error-page/404.vue'),
hidden: true
},
]
//动态路由 asyncRoutes
export const asyncRoutes = [];
const router = createRouter({
history: createWebHashHistory(),// hash模式,history是createWebHistory(地址栏不带#)hash是createWebHashHistory地址栏带#
routes: constantRoutes
})
export default router
// 1. 定义路由组件.
// 也可以从其他文件导入
// 2. 定义一些路由
// 每个路由都需要映射到一个组件。
// 我们后面再讨论嵌套路由。

View File

@ -0,0 +1,79 @@
import {defineStore} from 'pinia'
import {constantRoutes} from '@/router'
import Layout from '@/layout/index.vue'
const modules = import.meta.glob('@/views/**/**.vue')
interface RoutesItem {
path: string,
name: string,
meta: {
title: string,
icon: string
keepAlive: boolean // 是否使用 keep-alive
},
component: object | null,
children?:Object[]| null
}
// console.log(modules);
export const filterAsyncRoutes = (routerList:any) => {
//进行递归解析
//testData 后端获取的路由
const res:Object[] = []
// console.log(testData);
routerList.forEach((e:any) => {
// console.log(e.component);
let e_new:RoutesItem = {
path: e.url,
name: e.name,
meta: {
title: e.menuName,
icon: e.icon,
keepAlive: true // 是否使用 keep-alive
},
component: null,
}
if (e.menuType === 'M') {
// @ts-ignore
e_new.component = Layout
}else {
// console.log("22222",e.url);
// @ts-ignore
e_new.component = modules[`/src/views${e.url}/index.vue`]
}
// console.log(e_new);
if (e.children && e.children!=null) {
const children:any = filterAsyncRoutes(e.children)
// 保存权限
e_new = { ...e_new, children: children }
}
res.push(e_new)
})
// console.vlog("111",res);
return res
}
export const usePermissionStore = defineStore('permission', {
state: () => {
return {
routes: new Array<any>(),//全部路由
addRoutes: new Array<any>()//后端增加的路由
}
},
actions: {
generateRoutes(routes:any) {
// console.log(routes);
let routerList = JSON.parse(JSON.stringify(routes))
// console.log(routerList);
return new Promise((resolve) => {
const accessedRoutes:any = filterAsyncRoutes(routerList)
// console.log(accessedRoutes);
this.addRoutes = accessedRoutes
// console.log("111",accessedRoutes);
this.routes = constantRoutes.concat(accessedRoutes)
resolve(accessedRoutes)
})
}
}
})

21
pc/src/store/settings.ts Normal file
View File

@ -0,0 +1,21 @@
import {defineStore} from 'pinia'
import {getToken,setToken} from "@/utils/storage.ts";
export const useSettingsStore = defineStore('settings', {
state: () => {
return {
menuCollapse: false,//// 是否水平折叠收起菜单
// 布局方式 Classic 经典布局 Streamline 单行布局
layoutMode: getToken('layoutMode')?getToken('layoutMode'):'Classic'
}
},
actions: {
// @ts-ignore
changeSetting({ key, value }) {
//改变全局变量的方法
// @ts-ignore
this[key] = value
setToken(key, value)
},
}
})

Some files were not shown because too many files have changed in this diff Show More