添加必要组件;优化项目架构

This commit is contained in:
LittleBoy 2024-01-08 22:56:24 +08:00
parent 37e4e0d4a0
commit a258aa05cf
19 changed files with 447 additions and 119 deletions

View File

@ -1,13 +1,34 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<head>
<meta charset="UTF-8"/>
<link rel="icon" type="image/svg+xml" href="/logo.png"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>营养与健康数据管理处理平台</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
<style>
.app-init-loading{font-size:14px;line-height:1.5;background-color:#fff;color:#2d8cf0;align-items:center;justify-content:center;text-align:center;display:flex;height:100vh;position:fixed;inset:0}
@keyframes app-init-rotate{100%{transform:rotate(360deg)}
}
@keyframes app-init-dash{0%{stroke-dasharray:1,200;stroke-dashoffset:0}
50%{stroke-dasharray:89,200;stroke-dashoffset:-35px}
100%{stroke-dasharray:89,200;stroke-dashoffset:-124px}
}
.app-init-loading .circular{width:50px;height:50px;animation:app-init-rotate 2s linear infinite;margin:auto}
.app-init-loading .path{stroke-dasharray:1,200;stroke-dashoffset:0;stroke-width:2px;stroke:#2d8cf0;animation:app-init-dash 1.5s ease-in-out infinite}
</style>
</head>
<body>
<div id="app">
<div class="app-init-loading">
<div class="app-init-inner">
<svg class="circular" viewbox="25 25 50 50">
<circle class="path" cx="50" cy="50" r="20" fill="none"/>
</svg>
<div class="description">初始化中...</div>
</div>
</div>
</div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@ -9,9 +9,11 @@
"preview": "vite preview"
},
"dependencies": {
"@handsontable/vue3": "^14.0.0",
"dayjs": "^1.11.10",
"pinia": "^2.1.7",
"view-ui-plus": "^1.3.15",
"vue": "^3.4.0"
"vue": "^3.4.0",
"vue-router": "4"
},
"devDependencies": {
"@types/node": "^20.10.5",

View File

@ -1,69 +1,32 @@
<script setup lang="ts">
import {onMounted, ref} from "vue";
import PageLoading from "./components/page-loading/index.vue";
import {useUserStore} from "./service/user-store.ts";
import Login from "./components/login/index.vue";
import DataFields from "./components/data-fields/index.vue";
import Product from "./components/product/index.vue";
import Field from "./components/field/index.vue";
import Result from "./components/result/index.vue";
import {MenuItem, Menu, Icon} from "view-ui-plus";
import {useHash} from "./service/router.ts";
const page_data = ref({
showLogin: false,
})
//
const LOGIN_SESSION_KEY = 'x-yy-js-data-user';
const onLoginSuccess = () => {
page_data.value.showLogin = false
localStorage.setItem(LOGIN_SESSION_KEY, Date.now().toString(16));
}
onMounted(() => {
const loginSession = localStorage.getItem(LOGIN_SESSION_KEY);
console.log('loginSession', loginSession)
if (!loginSession || (Date.now() - parseInt(loginSession, 16) > 3600 * 24 * 1000)) {
page_data.value.showLogin = true
}
})
const MENU_LIST = [
{title: '药品管理', name: 'product', icon: 'ios-cube'},
{title: '数据项管理', name: 'field', icon: 'ios-apps'},
{title: '数据管理', name: 'product_value', icon: 'ios-paper'},
{title: '计算结果', name: 'result', icon: 'ios-calculator'},
]
const hash = useHash()
const activeMenu = ref((hash.value || MENU_LIST[0].name))
const onMenuSelect = (name: string) => {
activeMenu.value = name
window.location.hash = name
}
const store = useUserStore()
//
// const MENU_LIST = [
// {title: '', name: 'product', icon: 'ios-cube'},
// {title: '', name: 'field', icon: 'ios-apps'},
// {title: '', name: 'product_value', icon: 'ios-paper'},
// {title: '', name: 'result', icon: 'ios-calculator'},
// ]
// const activeMenu = ref(MENU_LIST[0].name)
</script>
<template>
<div class="app-container">
<Login v-if="page_data.showLogin" @login-success="onLoginSuccess"/>
<!-- <Layout />-->
<div class="app-header">
<div class="app-logo">
<img class="logo" src="./assets/images/logo.png" alt=""/>
</div>
<div class="app-menu">
<Menu mode="horizontal" :active-name="activeMenu" @on-select="onMenuSelect">
<MenuItem v-for="m in MENU_LIST" :key="m.name" :name="m.name">
<Icon :type="m.icon" size="18"/>
<span>{{ m.title }}</span>
</MenuItem>
</Menu>
</div>
</div>
<div class="app-content">
<DataFields v-if="activeMenu == 'product_value'"/>
<Field v-else-if="activeMenu == 'field'"/>
<Product v-else-if="activeMenu == 'product'"/>
<Result v-else/>
</div>
<PageLoading :class="{ hideLoading: store.userInit }"/>
<Login v-if="store.userInit && (!store.userInfo || store.userInfo.uid < 1) " />
<template v-if="store.userInit">
<router-view />
</template>
</div>
</template>
@ -74,6 +37,12 @@ const onMenuSelect = (name: string) => {
box-sizing: border-box;
}
.hideLoading {
background: rgba(255, 255, 255, 0);
pointer-events: none;
opacity: 0;
}
.app-header {
display: flex;
align-items: center;
@ -114,5 +83,5 @@ body {
will-change: filter;
transition: filter 300ms;
}
</style>
./service/api/user.ts

View File

@ -77,9 +77,12 @@ const onInputPaste = (e: ClipboardEvent, pIndex: number, fIndex: number) => {
//border-bottom: solid 1px #eee;
&:hover {
.item:not(.item-header) {
background: #fafafa;
// .item:not(.item-header) {
// background: #fafafa;
// }
}
&:focus-within{
background-color: #fcf2e0;
}
}
@ -112,9 +115,12 @@ const onInputPaste = (e: ClipboardEvent, pIndex: number, fIndex: number) => {
//transform: translateX(-1px) translateY(-1px);
&:focus {
box-shadow: 0 0 5px rgba(87, 116, 189, 0.5) inset;
// box-shadow: 0 0 5px rgba(87, 116, 189, 0.5) inset;
//box-shadow: 0 0 1px rgb(0, 0, 0) inset;
//outline: solid 1px rgb(87, 116, 189);
outline: solid 2px rgb(87, 116, 189);
position: relative;
left:0px;
background-color: #fff;
//border-color: #ccc;
//background: rgba(87,87,189,0.15);
}

View File

@ -1,22 +1,13 @@
<script lang="ts" setup>
import {Login, UserName, Password, Submit, Message} from 'view-ui-plus';
import {useUserStore} from "../../service/user-store.ts";
import {BizError} from "../../core/errors.ts";
type LoginModel = {
username: string;
password: string;
}
//const emits = defineEmits(['loginSuccess'])
const emits = defineEmits<{
(e: 'loginSuccess'): void
}>()
const handleSubmit = (...e:any) => {
const [valid,params] = e as [valid: true, params: LoginModel];
const handleSubmit = (...e: any) => {
const [valid, params] = e as [valid: true, params: any];
const store = useUserStore();
if (valid) {
if (params.username == 'admin' && params.password == 'admin') {
emits('loginSuccess')
} else {
Message.error('登录信息不正确')
}
store.login(params).catch((e: BizError) => Message.info(e.message))
}
}
@ -30,7 +21,7 @@ const handleSubmit = (...e:any) => {
</div>
<p class="desc">营养与健康数据管理处理平台</p>
<Login @on-submit="handleSubmit">
<UserName name="username" enter-to-submit/>
<UserName name="account" enter-to-submit/>
<Password name="password" enter-to-submit/>
<div class="submit">
<Submit/>
@ -48,7 +39,7 @@ const handleSubmit = (...e:any) => {
align-items: center;
text-align: center;
justify-content: center;
background: rgba(0, 0, 0, 0.1);
background: rgba(0, 0, 0, 0.3);
z-index: 101;
}

View File

@ -0,0 +1,110 @@
<script setup lang="ts">
import { Icon, Spin } from 'view-ui-plus'
</script>
<template>
<div class="page-loading">
<Spin fix>
<!-- <div class="loader">
<svg class="circular" viewBox="25 25 50 50">
<circle class="path" cx="50" cy="50" r="20" fill="none" stroke-width="5" stroke-miterlimit="10">
</circle>
</svg>
</div> -->
<Icon type="ios-loading" size="40" class="demo-spin-icon-load"></Icon>
<div class="description">初始化中...</div>
</Spin>
</div>
<!-- <div class="app-init-loading">-->
<!-- <div class="app-init-inner">-->
<!-- <svg class="circular" viewbox="25 25 50 50">-->
<!-- <circle class="path" cx="50" cy="50" r="20" fill="none"/>-->
<!-- </svg>-->
<!-- <div class="description">初始化中...</div>-->
<!-- </div>-->
<!-- </div>-->
</template>
<style scoped lang="scss">
.page-loading {
height: 100vh;
background:#fff;
position:fixed;
inset:0;
z-index:9999;
transition: all 0.5s;
opacity: 1;
}
.description {
margin-top: 10px;
}
.demo-spin-col .circular {
width: 25px;
height: 25px;
}
.demo-spin-icon-load {
animation: ani-demo-spin 1s linear infinite;
}
@keyframes rotate {
to {
-webkit-transform: rotate(1turn);
transform: rotate(1turn)
}
}
@keyframes dash {
0% {
stroke-dasharray: 1, 200;
stroke-dashoffset: 0
}
50% {
stroke-dasharray: 89, 200;
stroke-dashoffset: -35
}
to {
stroke-dasharray: 89, 200;
stroke-dashoffset: -124
}
}
.loader {
color: #000;
}
.circular {
animation: rotate 2s linear infinite;
.path {
stroke-dasharray: 1, 200;
stroke-dashoffset: 0;
animation: dash 1.5s ease-in-out infinite, color 6s ease-in-out infinite;
stroke-linecap: round;
}
}
@keyframes ani-demo-spin {
from {
transform: rotate(0deg);
}
50% {
transform: rotate(180deg);
}
to {
transform: rotate(360deg);
}
}
.demo-spin-col {
height: 100px;
position: relative;
border: 1px solid #eee;
}
</style>

8
src/core/errors.ts Normal file
View File

@ -0,0 +1,8 @@
export class BizError extends Error {
code: number;
constructor(code: number, message: string='') {
super(message);
this.code = code;
}
}

5
src/core/sleep.ts Normal file
View File

@ -0,0 +1,5 @@
export function sleep(time: number) {
return new Promise((resolve) => {
setTimeout(resolve, time);
})
}

View File

@ -1,9 +1,11 @@
import { createApp } from 'vue'
// import ViewUIPlus from 'view-ui-plus'
import 'view-ui-plus/dist/styles/viewuiplus.css'
import './style.css'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import './style.css'
createApp(App)
// .use(ViewUIPlus)
.use(createPinia())
.use(router)
.mount('#app')

26
src/pages/Layout.vue Normal file
View File

@ -0,0 +1,26 @@
<script lang="ts" setup>
import {routes} from "../router.ts";
import {useUserStore} from "../service/user-store.ts";
import {computed} from "vue";
const store = useUserStore()
const currentMenus = computed(()=>{
if(!store.userInfo) return []
return routes.filter((s)=>{
return !s.meta || !s.meta['role'] || store.userInfo?.role == s.meta.role;
})
})
</script>
<template>
<div class="layout">
<h1>12312</h1>
<div class="menu">
<div v-for="r in currentMenus">
<router-link :to="(r.path||'/')">{{r.meta?.title}}</router-link>
</div>
</div>
<div class="content">
<router-view />
</div>
</div>
</template>

3
src/pages/datas.vue Normal file
View File

@ -0,0 +1,3 @@
<template>
<h1>result</h1>
</template>

3
src/pages/result.vue Normal file
View File

@ -0,0 +1,3 @@
<template>
<h1>输出计算</h1>
</template>

3
src/pages/user.vue Normal file
View File

@ -0,0 +1,3 @@
<template>
<h1>user</h1>
</template>

69
src/router.ts Normal file
View File

@ -0,0 +1,69 @@
import {onMounted, onUnmounted, ref} from "vue";
import {createRouter, createWebHashHistory, RouteRecordRaw} from "vue-router";
function getHash() {
const hash = window.location.hash
return hash.length > 0 ? hash.slice(1) : '';
}
export const useHash = () => {
const hash = ref<string>(getHash())
const onHashChange = () => {
hash.value = getHash()
}
onMounted(() => {
window.addEventListener('hashchange', onHashChange, false)
})
onUnmounted(() => {
window.removeEventListener('hashchange', onHashChange, false)
})
return hash
}
export const routes:RouteRecordRaw[] = [
{
path: '',
name: 'home',
meta: {
title: '输出计算'
},
component: () => import('./pages/result.vue')
},
{
path: 'data',
name: 'data',
meta: {
title: '数据管理'
},
component: () => import('./pages/datas.vue')
},
{
path: 'user',
name: 'user',
component: () => import('./pages/user.vue'),
meta: {
role: 'root',
title: '用户管理'
}
}
]
export const router = createRouter({
routes: [
// 路由配置
{
path: '/',
component: () => import('./pages/Layout.vue'),
children: routes
},
// {
// path: '/login',
// name: 'login',
// component: () => import('./pages/login.vue')
// }
],
history: createWebHashHistory()
})
export default router

29
src/service/api/user.ts Normal file
View File

@ -0,0 +1,29 @@
import {BizError} from "../../core/errors.ts";
export type LoginModel = {
account: string;
password: string;
}
const fakeUser: UserInfo = {
account: "admin",
nickname: "管理员",
password: "",
role: 'root',
uid: 1
}
export async function LoginService(params: LoginModel) {
if (params.account == 'admin' && params.password == 'admin') {
const loginInfo = Date.now().toString(16);
return {
userinfo: fakeUser,
token: loginInfo
};
}
throw new BizError(1001, '用户名或密码错误');
}
export async function GetLoginInfo() {
return fakeUser;
}

View File

@ -1,22 +0,0 @@
import {onMounted, onUnmounted, ref} from "vue";
function getHash() {
const hash = window.location.hash
return hash.length > 0 ? hash.slice(1) : '';
}
export const useHash = () => {
const hash = ref<string>(getHash())
const onHashChange = () => {
hash.value = getHash()
}
onMounted(() => {
window.addEventListener('hashchange', onHashChange, false)
})
onUnmounted(() => {
window.removeEventListener('hashchange', onHashChange, false)
})
return hash
}

63
src/service/user-store.ts Normal file
View File

@ -0,0 +1,63 @@
import {defineStore} from "pinia"
import {onMounted, ref} from "vue"
import {GetLoginInfo, LoginService} from "./api/user";
import {BizError} from "../core/errors.ts";
import router from "../router.ts";
import {sleep} from "../core/sleep.ts";
type LoginParam = {
account: string;
password: string;
}
export const LOGIN_SESSION_KEY = 'x-yy-js-data-user';
export const useUserStore = defineStore('counter', () => {
const userInfo = ref<UserInfo>()
const userInit = ref(false)
// 登录
const login = async (data: LoginParam) => {
await sleep(1000);
const info = await LoginService(data);
userInfo.value = info.userinfo;
localStorage.setItem(LOGIN_SESSION_KEY, info.token)
}
// 登出
const logout = async () => {
localStorage.removeItem(LOGIN_SESSION_KEY)
userInfo.value = undefined;
}
// 更新用户信息
const updateUserInfo = async (info: UserInfo) => {
userInfo.value = info
}
// 获取用户信息
const getUserInfo = async () => {
const token = localStorage.getItem(LOGIN_SESSION_KEY);
if (!token) {
throw new BizError(401)
}
userInfo.value = await GetLoginInfo();
}
onMounted(() => {
getUserInfo().catch((e: BizError) => {
if (e.code == 401) {
router.replace(`/login?redirect=${router.currentRoute.value.path}`).then(() => console.log('401 show login'))
}
}).finally(() => {
console.log('onMounted inited')
userInit.value = true
})
})
return {
userInfo,
userInit,
login,
logout,
updateUserInfo,
getUserInfo
}
})

15
src/vite-env.d.ts vendored
View File

@ -1 +1,16 @@
/// <reference types="vite/client" />
type int = number;
type double = number;
type bool = boolean;
type AccountRole = 'root' | 'admin' | 'user';
type UserInfo = {
uid: number;
nickname: string;
account: string;
password: string;
avatar?: string;
lastUpdate?:string;
createTime?: string;
role: AccountRole;
}

View File

@ -117,11 +117,6 @@
resolved "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz#786c5f41f043b07afb1af37683d7c33668858f6d"
integrity sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==
"@handsontable/vue3@^14.0.0":
version "14.0.0"
resolved "https://registry.npmmirror.com/@handsontable/vue3/-/vue3-14.0.0.tgz#7c62091cc3393cb556771d9678cced4622095f1c"
integrity sha512-4/XuCxXw+8d3fHB3/v0zbb9PYgyjJfU6Tol+dsxRHk0L8FGUFCM8WIDFn3DCb5PHtE2peQBIUl/HvnsCpPpEMg==
"@jridgewell/sourcemap-codec@^1.4.15":
version "1.4.15"
resolved "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz"
@ -203,6 +198,11 @@
"@vue/compiler-dom" "3.4.0"
"@vue/shared" "3.4.0"
"@vue/devtools-api@^6.5.0":
version "6.5.1"
resolved "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.5.1.tgz#7f71f31e40973eeee65b9a64382b13593fdbd697"
integrity sha512-+KpckaAQyfbvshdDW5xQylLni1asvNSGme1JFs8I1+/H5pHEhqUKMEQD/qn3Nx5+/nycBq11qAEi8lk+LXI2dA==
"@vue/language-core@1.8.27":
version "1.8.27"
resolved "https://registry.npmmirror.com/@vue/language-core/-/language-core-1.8.27.tgz"
@ -332,6 +332,11 @@ dayjs@^1.11.0:
resolved "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.10.tgz"
integrity sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==
dayjs@^1.11.10:
version "1.11.10"
resolved "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.10.tgz#68acea85317a6e164457d6d6947564029a6a16a0"
integrity sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==
de-indent@^1.0.2:
version "1.0.2"
resolved "https://registry.npmmirror.com/de-indent/-/de-indent-1.0.2.tgz"
@ -511,6 +516,14 @@ picomatch@^2.0.4, picomatch@^2.2.1:
resolved "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
pinia@^2.1.7:
version "2.1.7"
resolved "https://registry.npmmirror.com/pinia/-/pinia-2.1.7.tgz#4cf5420d9324ca00b7b4984d3fbf693222115bbc"
integrity sha512-+C2AHFtcFqjPih0zpYuvof37SFxMQ7OEG2zV9jRI12i9BOy3YQVAHwdKtyyc8pDcDyIc33WCIsZaCFWU7WWxGQ==
dependencies:
"@vue/devtools-api" "^6.5.0"
vue-demi ">=0.14.5"
popper.js@^1.14.6:
version "1.16.1"
resolved "https://registry.npmmirror.com/popper.js/-/popper.js-1.16.1.tgz"
@ -622,6 +635,18 @@ vite@^4.3.2:
optionalDependencies:
fsevents "~2.3.2"
vue-demi@>=0.14.5:
version "0.14.6"
resolved "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.6.tgz#dc706582851dc1cdc17a0054f4fec2eb6df74c92"
integrity sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==
vue-router@4:
version "4.2.5"
resolved "https://registry.npmmirror.com/vue-router/-/vue-router-4.2.5.tgz#b9e3e08f1bd9ea363fdd173032620bc50cf0e98a"
integrity sha512-DIUpKcyg4+PTQKfFPX88UWhlagBEBEfJ5A8XDXRJLUnZOvcpMF8o/dnL90vpVkGaPbjvXazV/rC1qBKrZlFugw==
dependencies:
"@vue/devtools-api" "^6.5.0"
vue-template-compiler@^2.7.14:
version "2.7.16"
resolved "https://registry.npmmirror.com/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz"