no message

Mix
鹿和sa0ChunLuyu 3 years ago
parent 3578ef735d
commit 2d330ce829

@ -5,69 +5,69 @@ import {ConfigGetAction, $response, AdminStatusAction} from "~/api"
import {reGetAdmin} from "~/tool/info"
import {$favicon} from "~/tool/favicon"
import {
useStore, useSaveTokenType, useSessionToken, useToken, useRouterActive
useStore, useSaveTokenType, useSessionToken, useToken, useRouterActive
} from '~/store'
const no_login_list = ['login', '404'];
const $router_active = useRouterActive()
const router = createRouter({
history: createWebHashHistory(import.meta.env.BASE_URL), routes: setupLayouts(generatedRoutes)
history: createWebHashHistory(import.meta.env.BASE_URL), routes: setupLayouts(generatedRoutes)
})
const updateRouterActive = (matched) => {
matched.shift()
const last = matched[matched.length - 1]
if (no_login_list.indexOf(last.name) !== -1) return
setTimeout(() => {
$router_active.value = matched.map((item) => {
return {
title: 'title' in item.meta ? item.meta.title : item.name,
key: item.name
}
})
matched.shift()
const last = matched[matched.length - 1]
if (no_login_list.indexOf(last.name) !== -1) return
setTimeout(() => {
$router_active.value = matched.map((item) => {
return {
title: 'title' in item.meta ? item.meta.title : item.name,
key: item.name
}
})
})
}
router.beforeEach(async (to, from, next) => {
window.$loading.start()
const $store = useStore()
if (!$store.config) {
const response = await ConfigGetAction(['Logo', 'Favicon', 'Login欢迎图片', '网站名称'])
$response(response, () => {
$store.config = response.data
})
$favicon($store.config['Favicon'])
window.$loading.start()
const $store = useStore()
if (!$store.config) {
const response = await ConfigGetAction(['Logo', 'Favicon', 'Login欢迎图片', '网站名称'])
$response(response, () => {
$store.config = response.data
})
$favicon($store.config['Favicon'])
}
document.title = ('title' in to.meta && to.meta.title !== '首页') ? `${to.meta.title} ${$store.config['网站名称']}` : $store.config['网站名称']
if (no_login_list.indexOf(to.name) === -1) {
const $save_token_type = useSaveTokenType()
let $token;
if ($save_token_type.value === 'local') {
$token = useToken()
} else {
$token = useSessionToken()
}
document.title = ('title' in to.meta && to.meta.title !== '首页') ? `${to.meta.title} ${$store.config['网站名称']}` : $store.config['网站名称']
if (no_login_list.indexOf(to.name) === -1) {
const $save_token_type = useSaveTokenType()
let $token;
if ($save_token_type.value === 'local') {
$token = useToken()
} else {
$token = useSessionToken()
}
if ($token.value === '') {
next('/login?f=' + encodeURIComponent(to.fullPath))
setTimeout(() => {
window.$loading.finish()
})
} else {
const response = await AdminStatusAction()
$response(response, () => {
if (!$store.admin_info) reGetAdmin()
updateRouterActive(to.matched.map(item => item))
next()
}, next)
setTimeout(() => {
window.$loading.finish()
})
}
if ($token.value === '') {
next('/login?f=' + encodeURIComponent(to.fullPath))
setTimeout(() => {
window.$loading.finish()
})
} else {
const response = await AdminStatusAction()
$response(response, () => {
if (!$store.admin_info) reGetAdmin()
updateRouterActive(to.matched.map(item => item))
next()
setTimeout(() => {
window.$loading.finish()
})
}, next)
setTimeout(() => {
window.$loading.finish()
})
}
} else {
next()
setTimeout(() => {
window.$loading.finish()
})
}
})
export default router

24
normal/.gitignore vendored

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

@ -0,0 +1,14 @@
# Vue 3 + Vite
```bash
pnpm i
# If you don't have pnpm installed, run: npm install -g pnpm
pnpm dev
pnpm build
```
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
## Recommended IDE Setup
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar)

@ -0,0 +1,56 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>鹿和后台</title>
<link rel="shortcut icon" href="favicon.png"/>
<script>
// PUBLIC CONFIG
(function () {
const config_data = {
token_key: 'TOKEN',
api: {
url: 'http://mix.sa0.online',
error_message: '网络请求发生错误',
login: [100001, 100003],
success: 200
},
layout: {
logo: 40,
background: '#f5f7f9',
header: 64,
footer: 30,
sider: {
inverted: false,
open: 240,
close: 64,
background: '#ffffff',
},
},
title: document.title,
app_theme: '#1c8eee',
version: {
version: '13.0.1 [Mix]',
date: '2022年12月25日 14:58:11',
desc: '鹿和后台管理系统 北有神鹿 其名鹿和'
}
}
localStorage.setItem('APP_CONFIG', JSON.stringify(config_data))
})()
// PUBLIC CONFIG END
</script>
</head>
<body>
<div id="app"></div>
<script>
(function () {
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
const setting = localStorage.getItem('color-schema') || 'auto'
if (setting === 'dark' || (prefersDark && setting !== 'light'))
document.documentElement.classList.toggle('dark', true)
})()
</script>
<script type="module" src="/src/main.js"></script>
</body>
</html>

@ -0,0 +1,31 @@
{
"private": true,
"name": "lu-code-vue-user",
"packageManager": "pnpm@7.0.0",
"version": "0.0.0",
"scripts": {
"dev": "vite --port 3333 --open",
"build": "vite build"
},
"dependencies": {
"@icon-park/vue-next": "^1.4.2",
"@vueuse/core": "^8.4.2",
"animate.css": "^4.1.1",
"axios": "^0.26.1",
"naive-ui": "^2.34.3",
"normalize.css": "^8.0.1",
"pinia": "^2.0.14",
"vue": "^3.2.33",
"vue-router": "^4.0.15"
},
"devDependencies": {
"@vitejs/plugin-vue": "^2.3.3",
"pnpm": "^7.0.1",
"unocss": "^0.33.2",
"unplugin-auto-import": "^0.7.1",
"unplugin-vue-components": "^0.19.5",
"vite": "^2.9.9",
"vite-plugin-pages": "^0.23.0",
"vite-plugin-vue-layouts": "^0.6.0"
}
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

@ -0,0 +1,53 @@
<script setup>
import {zhCN, dateZhCN, darkTheme} from 'naive-ui'
import color from '~/tool/color'
import {isDark} from '~/composables'
import {useStore, useConfig} from '~/store'
const $store = useStore()
const $config = useConfig()
const get_theme_overrides = $computed(() => {
const app_theme = $config.value.app_theme;
const lighten_str = color.lighten($config.value.app_theme, 6);
return {
common: {
primaryColor: app_theme,
primaryColorHover: lighten_str,
primaryColorSuppl: lighten_str,
primaryColorPressed: lighten_str,
},
LoadingBar: {
colorLoading: app_theme,
},
};
});
</script>
<template>
<n-config-provider :theme-overrides="get_theme_overrides" :theme="isDark?darkTheme:{}" :locale="zhCN"
:date-locale="dateZhCN">
<n-spin class="spin_wrapper" :show="$store.api_loading > 0">
<n-notification-provider>
<n-loading-bar-provider>
<n-message-provider>
<n-dialog-provider>
<Mounting/>
<main font-sans>
<router-view/>
</main>
</n-dialog-provider>
</n-message-provider>
</n-loading-bar-provider>
</n-notification-provider>
</n-spin>
</n-config-provider>
</template>
<style>
.form_tag_wrapper {
width: 80px;
text-align: center;
}
.form_input_wrapper {
width: 200px !important;
}
</style>

@ -0,0 +1,53 @@
import {$post} from '~/tool/axios'
import {useConfig, useSessionToken, useSaveTokenType, useToken} from "~/store";
import $router from "~/router";
const $save_token_type = useSaveTokenType()
const $session_token = useSessionToken()
const $token = useToken()
const $config = useConfig()
const admin_api = 'Admin'
export const yo = async (data) => await $post({url: `${$config.value.api.url}/api/yo`, data}, true)
export const ConfigGetAction = async (label_arr) => await $post({
url: `${$config.value.api.url}/api/${admin_api}/Config/get`, data: {label_arr}
}, true)
export const $headers = () => {
let $token
if ($save_token_type.value === 'local') {
$token = useToken()
} else {
$token = useSessionToken()
}
return {
'Authorization': 'Bearer ' + $token.value
}
}
export const $image = (path) => {
const path_ret = ['http://', 'https://', ';base64,']
for (let i = 0; i < path_ret.length; i++) {
if (path.indexOf(path_ret[i]) !== -1) {
return path
}
}
return `${$config.value.api.url}${path}`
}
export const $base64 = (file) => {
let reader = new FileReader()
reader.readAsDataURL(file)
return new Promise(resolve => (reader.onloadend = () => resolve(reader.result)))
}
export const $response = (res, then, next = false) => {
if (res) {
if ($config.value.api.login.indexOf(res.code) !== -1) {
$session_token.value = null
$token.value = null
if (!!next) {
next('/login')
} else {
$router.push('/login')
}
}
if (res.code !== $config.value.api.success) return window.$message().error(res.message)
then()
}
}

@ -0,0 +1,30 @@
<script setup>
/**
* name
* usersa0ChunLuyu
* date2022-05-24 11:23:13
*/
import {useStore, useRouterActive} from '~/store'
const $store = useStore()
const $router_active = useRouterActive()
const breadcrumb_list = computed(() => {
let list = []
for (let i in $router_active.value) {
if ($router_active.value[i].key !== 'index') {
list.push($router_active.value[i])
}
}
return list
})
</script>
<template>
<n-breadcrumb ml-5>
<n-breadcrumb-item v-if="breadcrumb_list.length !== 0" href="#">
{{ $store.config['网站名称'] }}
</n-breadcrumb-item>
<n-breadcrumb-item v-for="(bi,bk) in breadcrumb_list" :key="bk" :disabled="bk !== breadcrumb_list.length - 1">
{{ bi.title }}
</n-breadcrumb-item>
</n-breadcrumb>
</template>

@ -0,0 +1,29 @@
<script setup>
/**
* name
* usersa0ChunLuyu
* date2022年12月25日 19:57:16
*/
import {IconPark} from '@icon-park/vue-next/es/all';
const $props = defineProps({
type: {
type: String,
default: 'AddText'
},
size: {
type: Number,
default: 20
},
theme: {
type: String,
default: 'outline'
}
})
</script>
<template>
<div>
<IconPark :type="$props.type" :size="$props.size" :theme="$props.theme"></IconPark>
</div>
</template>

@ -0,0 +1,94 @@
<script setup>
/**
* name
* usersa0ChunLuyu
* date2022-05-24 10:19:45
*/
import {useConfig, useStore} from "~/store";
import {isDark} from '~/composables'
import $router from "~/router";
import {$image} from '~/api'
const $store = useStore()
const $config = useConfig()
const layout_header_wrapper_height = computed(() => {
return `${$config.value.layout.header}px`
})
const layout_header_wrapper_blank_height = computed(() => {
return `${$config.value.layout.header - 25}px`
})
const logo_size = computed(() => {
return `${$config.value.layout.logo}px`
})
const logo_menu_wrapper_left = ref(`${($config.value.layout.sider.close - $config.value.layout.logo) / 2}px`)
</script>
<template>
<div class="logo_wrapper">
<div @click="$router.push('/')" class="logo_cover_wrapper"></div>
<div class="logo_container_wrapper">
<img class="logo_image_wrapper" :class="($config.layout.sider.inverted || isDark) ?'logo_image_filter':''"
:src="$image($store.config['Logo'])" alt="">
<n-menu class="logo_menu_wrapper" :indent="12" :inverted="$config.layout.sider.inverted || isDark"
:icon-size="$config.layout.logo" :collapsed-width="$config.layout.sider.close"
:value="($config.layout.sider.inverted || isDark)?'logo':''"
:options="[{label: $store.config['网站名称'],key: 'logo'}]"/>
</div>
</div>
<n-divider dashed></n-divider>
</template>
<style>
.logo_wrapper .logo_menu_wrapper {
--n-item-color-active: #00000000 !important;
}
</style>
<style scoped>
.logo_wrapper .logo_image_filter {
background: #ffffff;
border-radius: 6px;
}
.logo_wrapper .logo_image_wrapper {
width: v-bind(logo_size);
height: v-bind(logo_size);
position: absolute;
top: 50%;
transform: translateY(-50%);
left: v-bind(logo_menu_wrapper_left);
z-index: 999;
}
.logo_container_wrapper {
position: absolute;
left: 0;
right: 0;
top: 0;
height: v-bind(layout_header_wrapper_height);
}
.logo_menu_wrapper {
position: absolute;
top: 50%;
left: 0;
right: 0;
text-align: center;
transform: translateY(-50%);
}
.logo_cover_wrapper {
position: absolute;
cursor: pointer;
top: 0;
height: v-bind(layout_header_wrapper_height);
left: 0;
right: 0;
z-index: 2;
}
.logo_wrapper {
position: relative;
height: v-bind(layout_header_wrapper_blank_height);
}
</style>

@ -0,0 +1,100 @@
<script setup>
/**
* name
* usersa0ChunLuyu
* date2022-05-24 10:19:45
*/
import {isDark} from '~/composables'
import {h} from 'vue'
import {useRouterActive, useConfig} from '~/store'
import $router from '~/router'
import Icon from "~/components/Icon.vue";
const menuListItem = (item) => {
let data = {
title: item.title,
name: item.name,
status: item.status,
icon: item.icon
}
if ('children' in item) {
data.children = item.children.map((i) => {
return menuListItem(i)
})
}
return data
}
const getMenu = async () => {
menu_list.value.unshift({
title: '首页',
name: 'index',
status: 1,
icon: 'home'
})
}
onMounted(() => {
getMenu()
})
function renderIcon(icon) {
return () => h(Icon, {
size: 20,
type: !!icon ? icon : 'round'
}, {default: () => h(icon)})
}
const menu_list = ref([])
const menuItem = (list) => {
return list.map((item) => {
let item_info = {label: item.title, key: item.name, disabled: item.status === 2}
if ('children' in item) {
if (item.children.length === 1) {
item_info = {
label: item.children[0].title,
key: item.children[0].name,
icon: renderIcon(item.children[0].icon)
}
} else {
if (item.children.length !== 0) {
item_info = {label: item.title, key: item.name, children: menuItem(item.children)}
}
}
}
if ('query' in item && item.query !== '{}') item_info.query = JSON.parse(item.query)
if ('params' in item && item.params !== '{}') item_info.params = JSON.parse(item.params)
if ('icon' in item && item.icon !== '') {
if (!('icon' in item_info)) item_info.icon = renderIcon(item.icon)
}
return item_info
})
}
const menu_options = computed(() => {
return menuItem(menu_list.value)
})
const menuUpdate = (_, item) => {
let push = {name: item.key}
if ('query' in item) push.query = item.query
if ('params' in item) push.params = item.params
try {
$router.push(push)
} catch (e) {
$router.push({
name: 'index'
})
}
}
const $router_active = useRouterActive()
const expanded_keys = computed(() => {
return $router_active.value.map((item) => {
return item.key
})
})
const $config = useConfig()
</script>
<template>
<n-menu class="menu_wrapper" :indent="12" :value="expanded_keys[expanded_keys.length - 1]" :options="menu_options"
:watch-props="['defaultExpandedKeys']" :default-expanded-keys="expanded_keys" @update:value="menuUpdate"
:inverted="$config.layout.sider.inverted || isDark" :collapsed-width="64" :icon-size="20"/>
</template>

@ -0,0 +1,13 @@
<template></template>
<script setup>
import {useMessage, useLoadingBar, useDialog, useNotification} from 'naive-ui'
const $message = useMessage()
window.$message = () => {
$message.destroyAll()
return $message
}
window.$dialog = useDialog();
window.$loading = useLoadingBar();
window.$notification = useNotification();
</script>

@ -0,0 +1,63 @@
<script setup>
/**
* name
* usersa0ChunLuyu
* date2022-05-24 14:48:31
*/
import {useStore, useConfig} from "~/store";
import {isDark, toggleDark, isFull, toggleFull} from '~/composables'
import $router from "~/router";
import {$image} from '~/api'
const $store = useStore()
const $config = useConfig()
const layout_header_wrapper_height = computed(() => {
return `${$config.value.layout.header}px`
})
const options = [{
label: '退出登录',
key: "quit",
props: {
onClick: () => {
$router.push({
name: 'login'
})
}
}
}]
</script>
<template>
<div class="user_wrapper">
<n-button text @click="toggleFull()" mr-5>
<Icon v-if="isFull" type="off-screen"></Icon>
<Icon v-else type="full-screen"></Icon>
</n-button>
<n-button text @click="toggleDark()" mr-5>
<Icon v-if="isDark" type="sun-one"></Icon>
<Icon v-else type="moon"></Icon>
</n-button>
<div mr-5>
<n-dropdown trigger="hover" :options="options">
<n-space align="center">
<n-avatar round :size="40" class="avatar_wrapper" :src="$image($store.config['Logo'])"
:fallback-src="$image($store.config['Logo'])"/>
<span>{{ $store.admin_info ? $store.admin_info.nickname : $store.config['网站名称'] }}</span>
</n-space>
</n-dropdown>
</div>
</div>
</template>
<style scoped>
.avatar_wrapper {
background: #ffffff;
}
.user_wrapper {
position: absolute;
right: 0;
top: 0;
height: v-bind(layout_header_wrapper_height);
display: flex;
align-items: center;
}
</style>

@ -0,0 +1,23 @@
<script setup>
/***
* nameApi 示例
* usersa0ChunLuyu
* date2022-04-13 09:20:12
*/
import {yo} from '~/api'
const response_content = ref('')
const yoClick = async () => {
const response = await yo({yoo: 'foo'})
if (response) response_content.value = JSON.stringify(response)
}
</script>
<template>
<tr>
<td>Api</td>
<td>{{ response_content }}</td>
<td>
<n-button @click="yoClick()" secondary strong>发送</n-button>
</td>
</tr>
</template>

@ -0,0 +1,28 @@
<script setup>
/***
* name图标 示例
* usersa0ChunLuyu
* date2022-04-13 09:20:12
*/
const toIconDoc = () => {
window.open('https://iconpark.oceanengine.com/official')
}
</script>
<template>
<tr>
<td>图标</td>
<td>
<n-space>
<Icon type="off-screen"></Icon>
<Icon type="full-screen"></Icon>
<Icon type="sun-one"></Icon>
<Icon type="moon"></Icon>
</n-space>
</td>
<td>
<n-button secondary strong @click="toIconDoc()">
文档
</n-button>
</td>
</tr>
</template>

@ -0,0 +1,20 @@
<script setup>
/***
* name路由Params参数 示例
* usersa0ChunLuyu
* date2022-04-13 09:20:12
*/
const $props = defineProps({
name: {
type: String,
default: '鹿和'
}
})
</script>
<template>
<tr>
<td>Params & Props</td>
<td>{{ JSON.stringify($props) }}</td>
<td></td>
</tr>
</template>

@ -0,0 +1,19 @@
<script setup>
/***
* name路由跳转 示例
* usersa0ChunLuyu
* date2022-04-13 09:20:12
*/
import $router from '~/router'
</script>
<template>
<tr>
<td>路由跳转</td>
<td></td>
<td>
<n-button secondary strong @click="$router.push({name: 'index'})">
首页
</n-button>
</td>
</tr>
</template>

@ -0,0 +1,34 @@
<script setup>
/***
* name路由Query参数 示例
* usersa0ChunLuyu
* date2022-04-13 09:20:12
*/
import $router from '~/router'
import {onBeforeRouteUpdate} from "vue-router";
const default_page_options = {
name: '鹿和'
}
const page_options = ref(JSON.parse(JSON.stringify(default_page_options)))
onBeforeRouteUpdate((to) => {
routerChange(to.query)
})
const routerChange = (query) => {
page_options.value = {
name: query.name || default_page_options.name
}
}
onMounted(() => {
routerChange($router.currentRoute.value.query)
})
</script>
<template>
<tr>
<td>Query</td>
<td>{{ JSON.stringify(page_options) }}</td>
<td></td>
</tr>
</template>

@ -0,0 +1,32 @@
<script setup>
/**
* name子父组件通信 父组件
* usersa0ChunLuyu
* date2022-04-13 11:14:00
*/
import SonComponent from './components/Son.vue'
const fa_value = ref('fa_value')
const son_ref = ref(false)
const SonRef = (e) => {
son_ref.value = e
}
const faFunc = () => {
console.log('faFunc')
}
</script>
<template>
<tr>
<td rowspan="2">子父组件通信</td>
<td>
<n-input v-model:value="fa_value"></n-input>
<n-input mt-1 v-model:value="son_ref.son_value"></n-input>
</td>
<td>
<n-button secondary strong @click="son_ref.sonFunc()">
子函数
</n-button>
</td>
</tr>
<SonComponent @faFunc="faFunc" :fa_value="fa_value" :ref="SonRef"></SonComponent>
</template>

@ -0,0 +1,35 @@
<script setup>
/**
* name子父组件通信 子组件
* usersa0ChunLuyu
* date2022-04-13 11:14:00
*/
const $emit = defineEmits(['faFunc'])
const $props = defineProps({
fa_value: {
type: String,
default: ''
}
})
const son_value = ref('son_value')
const sonFunc = () => {
console.log('sonFunc')
}
defineExpose({
son_value,
sonFunc
})
</script>
<template>
<tr>
<td>
<n-input disabled v-model:value="$props.fa_value"></n-input>
<n-input mt-1 v-model:value="son_value"></n-input>
</td>
<td>
<n-button secondary strong @click="$emit('faFunc')">
父函数
</n-button>
</td>
</tr>
</template>

@ -0,0 +1,52 @@
<script setup>
/***
* name状态管理 示例
* usersa0ChunLuyu
* date2022-04-13 09:20:12
*/
// Pinia
import {useStore} from "~/store";
const $store = useStore()
// Pinia end
// Session
const session_count = useStorage('session_count', 0, sessionStorage)
// Session end
// Local
const local_count = useStorage('local_count', 0)
// Local end
</script>
<template>
<tr>
<td>Pinia</td>
<td>{{ $store.count }}</td>
<td>
<n-space>
<n-button secondary strong @click="$store.count++"></n-button>
<n-button secondary strong @click="$store.count--"></n-button>
</n-space>
</td>
</tr>
<tr>
<td>Session</td>
<td>{{ session_count }}</td>
<td>
<n-space>
<n-button secondary strong @click="session_count++"></n-button>
<n-button secondary strong @click="session_count--"></n-button>
</n-space>
</td>
</tr>
<tr>
<td>Local</td>
<td>{{ local_count }}</td>
<td>
<n-space>
<n-button secondary strong @click="local_count++"></n-button>
<n-button secondary strong @click="local_count--"></n-button>
</n-space>
</td>
</tr>
</template>

@ -0,0 +1,21 @@
<script setup>/***
* namestyle v-bind 示例
* usersa0ChunLuyu
* date2022-04-13 09:20:12
*/
const opacity = ref(1)
</script>
<template>
<tr>
<td>动态 Style v-bind</td>
<td class="opt">滑动滑块查看效果</td>
<td>
<n-slider v-model:value="opacity" :step="0.01" :min="0" :max="1"/>
</td>
</tr>
</template>
<style scoped>
.opt {
opacity: v-bind(opacity);
}
</style>

@ -0,0 +1,17 @@
<script setup>
/**
* nameUnoCSS 示例
* usersa0ChunLuyu
* date2022-04-13 09:20:12
*/
</script>
<template>
<tr>
<td>UnoCSS</td>
<td>
<div bg-dark-50 text-light-50 p="y-1 x-3">YO</div>
</td>
<td></td>
</tr>
</template>

@ -0,0 +1,21 @@
<script setup>
/***
* nameNaive Message 示例
* usersa0ChunLuyu
* date2022-04-13 09:20:12
*/
const showMessage = (msg) => {
window.$message().success(msg)
}
</script>
<template>
<tr>
<td>Naive Message</td>
<td></td>
<td>
<n-button secondary strong @click="showMessage('Yo')">
弹出
</n-button>
</td>
</tr>
</template>

@ -0,0 +1,2 @@
export const isDark = useDark()
export const toggleDark = useToggle(isDark)

@ -0,0 +1,3 @@
const {isFullscreen, toggle} = useFullscreen()
export const isFull = isFullscreen
export const toggleFull = toggle

@ -0,0 +1,2 @@
export * from './dark'
export * from './fullscreen'

@ -0,0 +1,9 @@
<script setup>
const $router = useRouter()
</script>
<template>
<main p="x4 y10" text="center blue-500 dark:gray-200">
<RouterView/>
</main>
</template>

@ -0,0 +1,116 @@
<script setup>
import {useConfig, useCollapsed} from "~/store"
import {isDark} from '~/composables'
const $config = useConfig()
const layout_content_wrapper_background = ref($config.value.layout.background)
const layout_sider_wrapper_background = ref($config.value.layout.sider.background)
const layout_header_wrapper_height = ref(`${$config.value.layout.header}px`)
const layout_footer_wrapper_height = ref(`${$config.value.layout.footer}px`)
const layout_router_view_margin_bottom = ref('10px');
const layout_router_view_wrapper_min_height = ref(`calc(100vh - ${$config.value.layout.header}px - ${$config.value.layout.footer}px - 1px - ${layout_router_view_margin_bottom.value})`)
const layout_content_wrapper_height = ref(`calc(100vh - ${$config.value.layout.header}px)`)
const layout_content_container_wrapper_height = ref(`calc(100vh - ${$config.value.layout.header}px)`)
const $collapsed = useCollapsed()
const collapsedOnUpdate = (e) => {
$collapsed.value = e
}
</script>
<template>
<div class="layout_wrapper">
<n-layout has-sider class="layout_header_container_wrapper">
<n-layout-sider :collapsed="$collapsed" :class="[isDark?'':'layout_sider_background']"
collapse-mode="width" :collapsed-width="$config.layout.sider.close"
:width="$config.layout.sider.open" :inverted="$config.layout.sider.inverted || isDark"
:native-scrollbar="false" bordered>
<Logo></Logo>
</n-layout-sider>
<n-layout>
<n-layout-header class="layout_header_wrapper" bordered>
<n-button ml-5 text @click="collapsedOnUpdate(!$collapsed)">
<Icon v-if="$collapsed" type="menu-unfold-one"></Icon>
<Icon v-else type="menu-fold-one"></Icon>
</n-button>
<Breadcrumb></Breadcrumb>
<User></User>
</n-layout-header>
</n-layout>
</n-layout>
<n-layout has-sider class="layout_content_container_wrapper">
<n-layout-sider :collapsed="$collapsed" :class="[isDark?'':'layout_sider_background']"
collapse-mode="width" :collapsed-width="$config.layout.sider.close"
:width="$config.layout.sider.open" :inverted="$config.layout.sider.inverted || isDark"
:native-scrollbar="false" bordered>
<Menu></Menu>
</n-layout-sider>
<n-layout :class="[isDark?'':'layout_content_background']">
<n-layout position="absolute" :native-scrollbar="false" class="layout_content_wrapper">
<div class="layout_router_view_wrapper" px-2>
<router-view v-slot="{ Component }">
<component :is="Component"/>
</router-view>
</div>
<n-layout-footer class="layout_footer_wrapper" text="xs center gray-500" bordered>
<span>{{ $config.version.desc }}</span>
<span mx-3>{{ $config.version.version }}</span>
<span>{{ $config.version.date }}</span>
</n-layout-footer>
</n-layout>
</n-layout>
</n-layout>
</div>
</template>
<style scoped>
.layout_wrapper {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
.layout_content_container_wrapper {
height: v-bind(layout_content_container_wrapper_height);
}
.layout_header_container_wrapper {
height: v-bind(layout_header_wrapper_height);
}
.layout_header_wrapper {
position: relative;
height: v-bind(layout_header_wrapper_height);
display: flex;
align-items: center;
}
.layout_sider_background {
background: v-bind(layout_sider_wrapper_background);
}
.layout_content_background {
background: v-bind(layout_content_wrapper_background);
}
.layout_footer_wrapper {
line-height: v-bind(layout_footer_wrapper_height);
height: v-bind(layout_footer_wrapper_height);
}
.layout_content_wrapper {
top: 0.5rem;
background: #00000000;
height: v-bind(layout_content_wrapper_height);
overflow: hidden;
}
.layout_router_view_wrapper {
min-height: v-bind(layout_router_view_wrapper_min_height);
overflow: hidden;
margin-bottom: v-bind(layout_router_view_margin_bottom);
}
</style>

@ -0,0 +1,7 @@
<script setup>
</script>
<template>
<main class="px-4 py-10 text-gray-700 dark:text-gray-200">
<RouterView/>
</main>
</template>

@ -0,0 +1,14 @@
import {createApp} from 'vue'
import App from './App.vue'
import router from '~/router'
import {createPinia} from 'pinia'
import './styles/main.css'
import 'uno.css'
import 'normalize.css'
import 'animate.css';
import '@icon-park/vue-next/styles/index.css';
const app = createApp(App)
app.use(router)
app.use(createPinia())
app.mount('#app')

@ -0,0 +1,26 @@
<script setup>
</script>
<template>
<div>
<div class="i404_wrapper">
<img src="https://iph.href.lu/300x300/?text=404" alt="">
</div>
未找到页面
<div mt-10>
<n-button type="primary" @click="$router.back()"></n-button>
</div>
</div>
</template>
<style scoped>
.i404_wrapper {
width: 300px;
margin: 0 auto;
}
.i404_wrapper img {
width: 300px;
}
</style>
<route>
{"name":"404","meta":{"layout":"404"}}
</route>

@ -0,0 +1,49 @@
<script setup>
import ApiComponent from '~/components/dev/Api.vue'
import StoreComponent from '~/components/dev/Store.vue'
import RouterQueryComponent from '~/components/dev/RouterQuery.vue'
import RouterPushComponent from '~/components/dev/RouterPush.vue'
import RouterParamsComponent from '~/components/dev/RouterParams.vue'
import IconParkComponent from '~/components/dev/IconPark.vue'
import StyleVBindComponent from '~/components/dev/StyleVBind.vue'
import UnoCSSComponent from '~/components/dev/UnoCSS.vue'
import UseMessageComponent from '~/components/dev/UseMessage.vue'
import SonFatherComponent from '~/components/dev/SonFather/Father.vue'
const $props = defineProps({
name: {
type: String,
default: '鹿和'
}
})
const $router = useRouter()
</script>
<template>
<div>
<n-table striped size="small">
<thead>
<tr>
<th>功能</th>
<th>内容</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<ApiComponent></ApiComponent>
<StoreComponent></StoreComponent>
<RouterQueryComponent></RouterQueryComponent>
<RouterParamsComponent :name="$props.name"></RouterParamsComponent>
<RouterPushComponent></RouterPushComponent>
<IconParkComponent></IconParkComponent>
<StyleVBindComponent></StyleVBindComponent>
<UnoCSSComponent></UnoCSSComponent>
<UseMessageComponent></UseMessageComponent>
<SonFatherComponent></SonFatherComponent>
</tbody>
</n-table>
</div>
</template>
<route>
{"meta":{"title":"基础功能示例","icon":"i-ic-round-code"}}
</route>

@ -0,0 +1,24 @@
<script setup>
/**
* name
* usersa0ChunLuyu
* date2022-05-19 09:18:31
*/
import {useStore} from "~/store"
const $store = useStore()
</script>
<template>
<div>
<n-alert :title="`你好 ${$store.admin_info.nickname?$store.admin_info.nickname:''}!`" type="info">
<template #icon>
<n-icon>
<Icon type="tips-one"></Icon>
</n-icon>
</template>
</n-alert>
</div>
</template>
<route>
{"meta":{"title":"首页"}}
</route>

@ -0,0 +1,188 @@
<script setup>
/**
* name
* usersa0ChunLuyu
* date2022-05-19 09:18:31
*/
import $router from '~/router'
import {
useConfig,
useSaveTokenType,
useSessionToken,
useStore,
useToken,
} from '~/store'
const $config = useConfig()
import {$image} from '~/api'
import {onBeforeRouteUpdate} from "vue-router";
import {isDark, toggleDark, isFull, toggleFull} from '~/composables'
const $store = useStore()
const $save_token_type = useSaveTokenType()
const $session_token = useSessionToken()
const $token = useToken()
const account = ref('')
const password = ref('')
const local_token = ref(false)
const localTokenClick = (e) => {
local_token.value = e
}
const default_page_options = {
f: '/'
}
const page_options = ref(default_page_options)
onBeforeRouteUpdate((to) => {
routerChange(to.query)
})
const routerChange = (query) => {
page_options.value = {
f: query.f || default_page_options.f
}
}
onMounted(() => {
routerChange($router.currentRoute.value.query)
})
const loginClick = () => {
const account_ = account.value.replace(/^\s+|\s+$/g, "")
if (account_ === '') return window.$message().error('请输入账号')
if (password.value === '') return window.$message().error('请输入密码')
const token = String(new Date() / 1)
if (local_token.value) {
$save_token_type.value = 'local'
$session_token.value = ''
$token.value = token
} else {
$save_token_type.value = 'session'
$session_token.value = token
$token.value = ''
}
const $store = useStore()
$store.admin_info = {
nickname: account_
}
$router.push(decodeURIComponent(page_options.value.f))
}
const accountEnter = () => {
if (password.value === '') {
password_input.value.focus()
} else {
loginClick()
}
}
const password_input = ref(null)
const passwordInputRef = (e) => {
password_input.value = e
}
</script>
<template>
<div class="login_page_wrapper" :style="{
background:$config.app_theme+'20',
}">
<div class="login_logo_wrapper">
<img :src="$image($store.config['Logo'])" alt="">
</div>
<div class="login_space_wrapper shadow-lg" :style="{
background: isDark?'#333333':'#ffffff'
}">
<div class="login_space_image_wrapper">
<img :src="$image($store.config['Login欢迎图片'])" alt="">
</div>
<div class="login_space_form_wrapper">
<div mt-5>
<n-h1>登录</n-h1>
</div>
<div mt-5>
<n-input v-model:value="account" @keydown.enter="accountEnter()"
placeholder="请输入账号"></n-input>
</div>
<div mt-5>
<n-input :ref="passwordInputRef" type="password" @keydown.enter="loginClick()"
v-model:value="password"
placeholder="请输入密码"></n-input>
</div>
<div mt-10>
<n-button @click="loginClick()" type="primary" w-full>登录</n-button>
</div>
<div mt-5>
<n-checkbox @update:checked="localTokenClick" :default-checked="local_token">自动登录</n-checkbox>
</div>
<div class="login_space_form_divider_wrapper"></div>
<n-divider dashed></n-divider>
<n-space justify="center">
<n-button text @click="toggleFull()" mr-5>
<Icon v-if="isFull" type="off-screen"></Icon>
<Icon v-else type="full-screen"></Icon>
</n-button>
<n-button text @click="toggleDark()" mr-5>
<Icon v-if="isDark" type="sun-one"></Icon>
<Icon v-else type="moon"></Icon>
</n-button>
</n-space>
</div>
</div>
</div>
</template>
<style scoped>
.login_space_form_divider_wrapper {
margin-top: 210px;
}
.login_space_form_wrapper {
width: 500px;
padding: 30px;
}
.login_logo_wrapper img {
width: 80px;
height: 80px;
display: block;
}
.login_logo_wrapper {
position: absolute;
top: 20px;
left: 20px;
width: 80px;
height: 80px;
}
.login_space_image_wrapper img {
width: 400px;
height: 600px;
display: block;
}
.login_space_image_wrapper {
width: 400px;
height: 600px;
}
.login_space_wrapper {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 900px;
height: 600px;
border-radius: 6px;
display: flex;
overflow: hidden;
}
.login_page_wrapper {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
</style>
<route>
{"meta":{"layout":"login","title":"登录"}}
</route>

@ -0,0 +1,68 @@
import {createRouter, createWebHashHistory} from 'vue-router'
import {setupLayouts} from 'virtual:generated-layouts'
import generatedRoutes from 'virtual:generated-pages'
import {ConfigGetAction, $response} from "~/api"
import {$favicon} from "~/tool/favicon"
import {
useStore, useSaveTokenType, useSessionToken, useToken, useRouterActive
} from '~/store'
const no_login_list = ['login', '404'];
const $router_active = useRouterActive()
const router = createRouter({
history: createWebHashHistory(import.meta.env.BASE_URL), routes: setupLayouts(generatedRoutes)
})
const updateRouterActive = (matched) => {
matched.shift()
const last = matched[matched.length - 1]
if (no_login_list.indexOf(last.name) !== -1) return
setTimeout(() => {
$router_active.value = matched.map((item) => {
return {
title: 'title' in item.meta ? item.meta.title : item.name,
key: item.name
}
})
})
}
router.beforeEach(async (to, from, next) => {
window.$loading.start()
const $store = useStore()
if (!$store.config) {
const response = await ConfigGetAction(['Logo', 'Favicon', 'Login欢迎图片', '网站名称'])
$response(response, () => {
$store.config = response.data
})
$favicon($store.config['Favicon'])
}
document.title = ('title' in to.meta && to.meta.title !== '首页') ? `${to.meta.title} ${$store.config['网站名称']}` : $store.config['网站名称']
if (no_login_list.indexOf(to.name) === -1) {
const $save_token_type = useSaveTokenType()
let $token;
if ($save_token_type.value === 'local') {
$token = useToken()
} else {
$token = useSessionToken()
}
if ($token.value === '') {
next('/login?f=' + encodeURIComponent(to.fullPath))
setTimeout(() => {
window.$loading.finish()
})
} else {
updateRouterActive(to.matched.map(item => item))
next()
setTimeout(() => {
window.$loading.finish()
})
}
} else {
next()
setTimeout(() => {
window.$loading.finish()
})
}
})
export default router

@ -0,0 +1,17 @@
import {defineStore} from 'pinia'
export const useStore = defineStore('main', {
state: () => {
return {
config: false,
admin_info: false, api_loading: 0, count: 0
}
}
})
const TOKEN_KEY = JSON.parse(localStorage.getItem('APP_CONFIG') ?? '{}').token_key
export const useConfig = createGlobalState(() => useStorage('APP_CONFIG', JSON.parse(localStorage.getItem('APP_CONFIG') ?? '{}')))
export const useToken = createGlobalState(() => useStorage(TOKEN_KEY, ''))
export const useSessionToken = createGlobalState(() => useStorage(TOKEN_KEY, '', sessionStorage))
export const useSaveTokenType = createGlobalState(() => useStorage('SAVE_TOKEN_TYPE', 'session'))
export const useRouterActive = createGlobalState(() => useStorage('ROUTER_ACTIVE', []))
export const useCollapsed = createGlobalState(() => useStorage('COLLAPSED', false))

@ -0,0 +1,11 @@
html,
body,
#app {
height: 100%;
margin: 0;
padding: 0;
}
html.dark {
background: #333;
}

@ -0,0 +1,30 @@
import axios from 'axios'
import {useToken, useStore, useConfig, useSaveTokenType, useSessionToken} from '~/store'
const $save_token_type = useSaveTokenType()
let $token;
const post = axios.create({
method: 'POST'
})
post.interceptors.request.use(config => {
if ($save_token_type.value === 'local') {
$token = useToken()
} else {
$token = useSessionToken()
}
config.headers['Authorization'] = 'Bearer ' + $token.value
return config
}, error => Promise.reject(error))
post.interceptors.response.use(response => {
return (response.status === 200) ? response : Promise.reject('[ERROR] response.status: ' + response.status)
}, error => Promise.reject(error))
const $config = useConfig()
export const $post = async (request, loading = false) => {
const $store = useStore()
if (loading) $store.api_loading++
const response = await post(request).catch((e) => {
window.$message().error($config.value.api.error_message)
})
if (loading) $store.api_loading--
return !!response ? response.data : false
}

@ -0,0 +1,13 @@
function addLight(color, amount) {
const cc = parseInt(color, 16) + amount;
const c = cc > 255 ? 255 : cc;
return c.toString(16).length > 1 ? c.toString(16) : `0${c.toString(16)}`;
}
function lighten(color, amount) {
color = color.indexOf('#') >= 0 ? color.substring(1, color.length) : color;
amount = Math.trunc((255 * amount) / 100);
return `#${addLight(color.substring(0, 2), amount)}${addLight(color.substring(2, 4), amount)}${addLight(color.substring(4, 6), amount)}`;
}
export default {lighten}

@ -0,0 +1,10 @@
import {$image} from "~/api";
export const $favicon = (path, t = false) => {
const link = document.querySelector("link[rel*='icon']") || document.createElement('link')
link.type = 'image/x-icon'
link.rel = 'shortcut icon'
let href = $image(path)
link.href = t ? [href, 't=' + String(new Date() / 1)].join(href.indexOf('?') === -1 ? '?' : '&') : href
document.getElementsByTagName('head')[0].appendChild(link)
}

@ -0,0 +1,26 @@
import {defineConfig} from 'vite'
import Vue from '@vitejs/plugin-vue'
import path from 'path'
import Pages from 'vite-plugin-pages'
import Layouts from 'vite-plugin-vue-layouts'
import Components from 'unplugin-vue-components/vite'
import AutoImport from 'unplugin-auto-import/vite'
import {NaiveUiResolver} from 'unplugin-vue-components/resolvers'
import Unocss from 'unocss/vite'
import {presetAttributify, presetUno, presetIcons} from 'unocss'
const admin_path = 'normal'
export default defineConfig({
base: `/${admin_path}/`,
build: {
outDir: `../public/${admin_path}`, assetsDir: 'lib'
},
resolve: {alias: {'~/': `${path.resolve(__dirname, 'src')}/`}},
plugins: [Vue({reactivityTransform: true}), Pages(), Layouts(), AutoImport({
imports: ['vue', 'vue/macros', 'vue-router', '@vueuse/core',],
}), Components({resolvers: [NaiveUiResolver()]}), Unocss({
presets: [presetAttributify({}), presetUno(), presetIcons({
warn: true,
})],
})]
})
Loading…
Cancel
Save