feat:初始提交
- 新建项目结构和基础配置 - 添加用户登录和车号查询功能 - 实现消息提示和加载指令 - 配置路由和全局状态管理
This commit is contained in:
commit
921d454640
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
||||
* text=auto eol=lf
|
32
.gitignore
vendored
Normal file
32
.gitignore
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
*.tsbuildinfo
|
||||
|
||||
*.lock*
|
28
.prettierrc.json
Normal file
28
.prettierrc.json
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/prettierrc",
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"singleQuote": true,
|
||||
"quoteProps": "as-needed",
|
||||
"trailingComma": "all",
|
||||
"bracketSpacing": true,
|
||||
"jsxBracketSameLine": true,
|
||||
"arrowParens": "always",
|
||||
"rangeStart": 0,
|
||||
"vueIndentScriptAndStyle": false,
|
||||
"endOfLine": "auto",
|
||||
"semi": true,
|
||||
"usePrettierrc": true,
|
||||
"requirePragma": false,
|
||||
"bracketSameLine": true,
|
||||
"htmlWhitespaceSensitivity": "ignore",
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.json",
|
||||
"options": {
|
||||
"tabWidth": 4
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
6
.vscode/extensions.json
vendored
Normal file
6
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"Vue.volar",
|
||||
"esbenp.prettier-vscode"
|
||||
]
|
||||
}
|
30
index.html
Normal file
30
index.html
Normal file
@ -0,0 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link href="/src/resource/img/icon.png" rel="icon">
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html body main[main] {
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
<title>利达外调服务平台</title>
|
||||
</head>
|
||||
<body>
|
||||
<main main>
|
||||
<div
|
||||
style="display: flex;justify-content: center;align-items: center;height:
|
||||
100vh;width: 100%;">
|
||||
加载中...
|
||||
</div>
|
||||
</main>
|
||||
<script src="/src/main.ts" type="module"></script>
|
||||
</body>
|
||||
</html>
|
48
package.json
Normal file
48
package.json
Normal file
@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "lida-app",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "run-p type-check \"build-only {@}\" --",
|
||||
"preview": "vite preview",
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --build",
|
||||
"format": "prettier --write src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@idux/cdk": "^2.6.3",
|
||||
"@idux/components": "^2.6.3",
|
||||
"@idux/pro": "^2.6.3",
|
||||
"@types/vue-router": "^2.0.0",
|
||||
"@vueuse/core": "^13.6.0",
|
||||
"alova": "^3.3.4",
|
||||
"ant-design-vue": "^4.2.6",
|
||||
"base64-arraybuffer": "^1.0.2",
|
||||
"js-md5": "^0.8.3",
|
||||
"marked": "^16.1.2",
|
||||
"naive-ui": "^2.42.0",
|
||||
"pinia": "^3.0.3",
|
||||
"pinia-plugin-persistedstate": "^4.5.0",
|
||||
"sass-embedded": "^1.90.0",
|
||||
"vue": "^3.5.18",
|
||||
"vue-router": "^4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify/vue": "^5.0.0",
|
||||
"@tsconfig/node22": "^22.0.2",
|
||||
"@types/node": "^22.16.5",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"npm-run-all2": "^8.0.4",
|
||||
"prettier": "3.6.2",
|
||||
"typescript": "~5.8.0",
|
||||
"vite": "^7.0.6",
|
||||
"vite-plugin-vue-devtools": "^8.0.0",
|
||||
"vue-tsc": "^3.0.4"
|
||||
}
|
||||
}
|
16
src/apis/auth/index.ts
Normal file
16
src/apis/auth/index.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import alova, { type AlovaResponse } from '@/plugins/alova';
|
||||
import type { LoginBody, RegisterBody, ResUserInfo } from '@/apis/auth/type.ts';
|
||||
|
||||
export const login = (lb: LoginBody) =>
|
||||
alova.Post<AlovaResponse<string>>('/auth/login', lb, {
|
||||
meta: { notbefore: true },
|
||||
});
|
||||
export const logout = () => alova.Delete<AlovaResponse>('/auth/logout');
|
||||
export const register = (rb: RegisterBody) => {
|
||||
const { passre, ...rbs } = rb;
|
||||
return alova.Post<AlovaResponse<string>>('/auth/register', rbs, { meta: { notbefore: true } });
|
||||
};
|
||||
|
||||
export const info = () => {
|
||||
return alova.Get<AlovaResponse<ResUserInfo>>('/auth/info', { meta: { notbefore: true } });
|
||||
};
|
29
src/apis/auth/type.ts
Normal file
29
src/apis/auth/type.ts
Normal file
@ -0,0 +1,29 @@
|
||||
export interface LoginBody {
|
||||
passWold: string;
|
||||
userName: string;
|
||||
}
|
||||
|
||||
export interface RegisterBody {
|
||||
tel: string;
|
||||
car: string;
|
||||
pass: string;
|
||||
passre: string;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
carNumber: string;
|
||||
telNumber: string;
|
||||
passWold: '***';
|
||||
type: 1;
|
||||
createTime: string;
|
||||
createBy: string;
|
||||
updateTime: string;
|
||||
updateBy: string;
|
||||
userName: string;
|
||||
}
|
||||
|
||||
export interface ResUserInfo {
|
||||
user: User;
|
||||
login: boolean;
|
||||
}
|
4
src/apis/bacth/index.ts
Normal file
4
src/apis/bacth/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import alova, { type AlovaResponse } from '@/plugins/alova';
|
||||
import type { CarDataResType } from '@/apis/bacth/type.ts';
|
||||
|
||||
export const getMyCar = () => alova.Get<AlovaResponse<CarDataResType>>('/batch/mycar');
|
26
src/apis/bacth/type.ts
Normal file
26
src/apis/bacth/type.ts
Normal file
@ -0,0 +1,26 @@
|
||||
export interface CarDataResType {
|
||||
carNumber: string;
|
||||
getOk: boolean;
|
||||
carData: CarData;
|
||||
}
|
||||
|
||||
export interface CarData {
|
||||
id: string;
|
||||
key_info_of_binding_media: string;
|
||||
car_weight: number;
|
||||
remark: string;
|
||||
pick_up_weight: string;
|
||||
bind_obj_id: string;
|
||||
cargo_transport_batch_number: string;
|
||||
company: string;
|
||||
goods_name: string;
|
||||
op_user_id: string;
|
||||
receive_company: string;
|
||||
p_result: number;
|
||||
status: number;
|
||||
create_time: string;
|
||||
declar_time: string;
|
||||
update_time: string;
|
||||
delete_time: 0;
|
||||
receipt_remark: string;
|
||||
}
|
9
src/apis/message/inedex.ts
Normal file
9
src/apis/message/inedex.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import alova, { type AlovaResponse } from '@/plugins/alova';
|
||||
import type { AppMessage } from '@/apis/message/type.ts';
|
||||
|
||||
const BaseUrl = '/system/message';
|
||||
export function getMessage() {
|
||||
return alova.Get<AlovaResponse<AppMessage>>(`${BaseUrl}/newest`, {
|
||||
meta: { notbefore: true },
|
||||
});
|
||||
}
|
8
src/apis/message/type.ts
Normal file
8
src/apis/message/type.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export interface AppMessage {
|
||||
id: string;
|
||||
data: string;
|
||||
createTime: string;
|
||||
createBy: string;
|
||||
updateTime: string;
|
||||
updateBy: string;
|
||||
}
|
9
src/apis/system/index.ts
Normal file
9
src/apis/system/index.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import alova, { type AlovaResponse } from '@/plugins/alova';
|
||||
|
||||
export function getInfo() {
|
||||
return alova.Get<AlovaResponse<{ isLogin: boolean }>>('/system/info', {
|
||||
meta: {
|
||||
notbefore: true,
|
||||
},
|
||||
});
|
||||
}
|
0
src/apis/system/type.ts
Normal file
0
src/apis/system/type.ts
Normal file
32
src/component/Icon.vue
Normal file
32
src/component/Icon.vue
Normal file
@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<div>
|
||||
<Icon :icon="name" class="icon" />
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { Icon } from '@iconify/vue';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
color: {
|
||||
type: String,
|
||||
default: 'inherit',
|
||||
},
|
||||
size: {
|
||||
type: [String, Number],
|
||||
default: '24px',
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const iconSize = computed(() => (props.size.toString().endsWith('px') ? props.size : props.size + 'px'));
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.icon {
|
||||
color: v-bind(color);
|
||||
font-size: v-bind(iconSize);
|
||||
}
|
||||
</style>
|
30
src/layout/App.vue
Normal file
30
src/layout/App.vue
Normal file
@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<n-config-provider :theme="settingStore.theme === 'light' ? lightTheme : darkTheme">
|
||||
<n-loading-bar-provider>
|
||||
<RouterView />
|
||||
<com />
|
||||
<contextHolder />
|
||||
</n-loading-bar-provider>
|
||||
</n-config-provider>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { RouterView } from 'vue-router';
|
||||
import { darkTheme, lightTheme, NConfigProvider, NLoadingBarProvider, useLoadingBar } from 'naive-ui';
|
||||
import { SettingStore } from '@/plugins';
|
||||
import golob from '@/utils/golob.ts';
|
||||
import { defineComponent } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
const settingStore = SettingStore();
|
||||
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
golob.message = messageApi;
|
||||
const com = defineComponent({
|
||||
setup(props, context) {
|
||||
golob.loadingBar = useLoadingBar();
|
||||
return () => context.slots.default;
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
143
src/layout/index.vue
Normal file
143
src/layout/index.vue
Normal file
@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<div>
|
||||
<menu class="menu">
|
||||
<div style="display: flex; justify-content: center; align-items: center; gap: 10px; height: 100%">
|
||||
<img height="40" src="@/resource/img/icon.png" />
|
||||
<NGradientText style="font-size: large" type="info">利达外调服务平台</NGradientText>
|
||||
</div>
|
||||
</menu>
|
||||
<NScrollbar
|
||||
class="main"
|
||||
style="min-height: calc(100vh - 118px); max-height: calc(100vh - 118px); width: 100%; padding: 5px"
|
||||
trigger="none">
|
||||
<main style="width: 100%">
|
||||
<RouterView />
|
||||
</main>
|
||||
</NScrollbar>
|
||||
<menu class="menu" style="display: flex; justify-content: space-between; height: 70px">
|
||||
<div
|
||||
class="maybutton"
|
||||
v-bind:class="{
|
||||
'maybutton-atv': rname === 'index',
|
||||
}"
|
||||
@click="router.push({ name: 'index' })">
|
||||
<div class="icon">
|
||||
<Icon name="line-md:home-md-twotone" />
|
||||
</div>
|
||||
<div class="text">主页</div>
|
||||
</div>
|
||||
<div
|
||||
class="maybutton"
|
||||
v-bind:class="{
|
||||
'maybutton-atv': rname === 'chehaoca',
|
||||
}"
|
||||
@click="router.push({ name: 'chehaoca' })">
|
||||
<div class="icon">
|
||||
<Icon name="line-md:brake-parking-twotone" />
|
||||
</div>
|
||||
<div class="text">车号查询</div>
|
||||
</div>
|
||||
<div
|
||||
class="maybutton"
|
||||
v-bind:class="{
|
||||
'maybutton-atv': rname === 'user',
|
||||
}"
|
||||
@click="router.push({ name: 'user' })">
|
||||
<div class="icon">
|
||||
<Icon name="line-md:person-twotone" />
|
||||
</div>
|
||||
<div class="text">我的</div>
|
||||
</div>
|
||||
</menu>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { RouterView } from 'vue-router';
|
||||
import { computed, onMounted, reactive } from 'vue';
|
||||
import { NGradientText, NScrollbar } from 'naive-ui';
|
||||
import Icon from '@/component/Icon.vue';
|
||||
import { router } from '@/plugins';
|
||||
|
||||
const vivm = reactive({
|
||||
height: '0px',
|
||||
width: '0px',
|
||||
});
|
||||
const resizeObserver = new ResizeObserver((entries: ResizeObserverEntry[], observer: ResizeObserver) => {
|
||||
entries.forEach((a) => {
|
||||
vivm.height = a.target.clientHeight + 'px';
|
||||
vivm.width = a.target.clientWidth + 'px';
|
||||
});
|
||||
});
|
||||
const rname = computed(() => router.currentRoute.value.meta.name);
|
||||
onMounted(() => {
|
||||
const doc = document.querySelector('.main');
|
||||
if (doc) {
|
||||
resizeObserver.observe(doc);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.main {
|
||||
--height: v-bind(vivm.height);
|
||||
--width: v-bind(vivm.width);
|
||||
}
|
||||
|
||||
html[theme='dark'] {
|
||||
.menu {
|
||||
background: #2b2b2b;
|
||||
}
|
||||
|
||||
.maybutton {
|
||||
color: #d4d4d4;
|
||||
|
||||
&:hover {
|
||||
color: #a9c7b0;
|
||||
}
|
||||
|
||||
border-left: 1px solid #919191;
|
||||
|
||||
&:first-child {
|
||||
border-left: none;
|
||||
}
|
||||
}
|
||||
|
||||
.maybutton-atv {
|
||||
color: #74ed91;
|
||||
&:hover {
|
||||
color: #74ed91;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu {
|
||||
height: 48px;
|
||||
background: #f2f2f2;
|
||||
}
|
||||
|
||||
.maybutton {
|
||||
user-select: none;
|
||||
width: 33.3333%;
|
||||
border-left: 1px solid #b5b5b5;
|
||||
padding: 5px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #444444;
|
||||
|
||||
&:hover {
|
||||
color: #5b955b;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-left: none;
|
||||
}
|
||||
}
|
||||
|
||||
.maybutton-atv {
|
||||
color: #0d832a;
|
||||
&:hover {
|
||||
color: #0d832a;
|
||||
}
|
||||
}
|
||||
</style>
|
15
src/main.ts
Normal file
15
src/main.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { createApp } from 'vue';
|
||||
import '@res/css/index.scss';
|
||||
import App from '@/layout/App.vue';
|
||||
import { directive, pinia, router, UserStore } from '@/plugins';
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(router);
|
||||
app.use(pinia);
|
||||
app.use(directive);
|
||||
|
||||
app.mount('main[main]');
|
||||
|
||||
const userStore = UserStore();
|
||||
userStore.getLogin();
|
110
src/page/chehaoca/index.vue
Normal file
110
src/page/chehaoca/index.vue
Normal file
@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<div v-if="!userStore.ishaslogin">
|
||||
<NAlert title="警告" type="error">
|
||||
请先登录后使用
|
||||
<RouterLink to="/user/login">前往登录</RouterLink>
|
||||
</NAlert>
|
||||
</div>
|
||||
<div v-else>
|
||||
<n-alert title="提示" type="info">
|
||||
只有当天的数据才可以上磅
|
||||
<p />
|
||||
请对比下方的时间与当天是否符合
|
||||
</n-alert>
|
||||
<div v-loading="regis_login" class="card" style="margin-top: 20px">
|
||||
<div f-c-c>
|
||||
<NGradientText v-if="data.carData.p_result === 2 && data.carData.status === 2" size="65">可上磅</NGradientText>
|
||||
<NGradientText v-if="data.carData.p_result === 40 && data.carData.status === 1" size="65">已出库</NGradientText>
|
||||
<NGradientText v-if="data.carData.p_result === 2 && data.carData.status === 0" size="65">已入库</NGradientText>
|
||||
<NGradientText v-if="data.carData.p_result === 40 && data.carData.status === 2" size="65">已撤销</NGradientText>
|
||||
<NGradientText v-if="data.carData.p_result === 2 && data.carData.status === 1" size="65">已出库</NGradientText>
|
||||
<NGradientText v-else size="65" type="error">不可上磅</NGradientText>
|
||||
</div>
|
||||
<div class="card-line"></div>
|
||||
<NDescriptions
|
||||
:column="1"
|
||||
bordered
|
||||
label-placement="left"
|
||||
size="small"
|
||||
style="border-radius: 5px; overflow: clip">
|
||||
<NDescriptionsItem label="车牌号">{{ data.carNumber }}</NDescriptionsItem>
|
||||
<NDescriptionsItem label="手机号">{{ userStore.userData.telNumber }}</NDescriptionsItem>
|
||||
<NDescriptionsItem label="发货单位">{{ data.carData.company }}</NDescriptionsItem>
|
||||
<NDescriptionsItem label="收货单位">{{ data.carData.receive_company.split(',')[0] }}</NDescriptionsItem>
|
||||
<NDescriptionsItem label="货物种类">
|
||||
{{ data.carData.receive_company.split(',')[1].replace('(', '').replace(')', '') }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="海关审核信息">
|
||||
<div style="max-width: 170px; word-wrap: break-word">
|
||||
{{ data.carData.receipt_remark }}
|
||||
</div>
|
||||
</NDescriptionsItem>
|
||||
|
||||
<NDescriptionsItem label="导入时间">{{ data.carData.create_time }}</NDescriptionsItem>
|
||||
|
||||
<NDescriptionsItem label="申报时间">{{ data.carData.declar_time }}</NDescriptionsItem>
|
||||
</NDescriptions>
|
||||
<div class="card-line"></div>
|
||||
<div>
|
||||
<NButton secondary strong style="width: calc(100% - 20px); margin: 10px" type="success" @click="getdata">
|
||||
刷新
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { getMyCar } from '@/apis/bacth';
|
||||
|
||||
import { UserStore } from '@/plugins';
|
||||
import {
|
||||
NAlert,
|
||||
NButton,
|
||||
NDescriptions,
|
||||
NDescriptionsItem,
|
||||
NGradientText
|
||||
} from 'naive-ui';
|
||||
import type { CarDataResType } from '@/apis/bacth/type.ts';
|
||||
import UseApiLoading from '@/utils/UseApiLoading.ts';
|
||||
|
||||
const userStore = UserStore();
|
||||
|
||||
const data = ref<CarDataResType>({
|
||||
carData: {
|
||||
bind_obj_id: '',
|
||||
car_weight: 0,
|
||||
cargo_transport_batch_number: '',
|
||||
company: '',
|
||||
create_time: '',
|
||||
declar_time: '',
|
||||
delete_time: 0,
|
||||
goods_name: '',
|
||||
id: '',
|
||||
key_info_of_binding_media: '',
|
||||
op_user_id: '',
|
||||
p_result: 2,
|
||||
pick_up_weight: '',
|
||||
receipt_remark: '',
|
||||
receive_company: ',',
|
||||
remark: '',
|
||||
status: 1,
|
||||
update_time: '',
|
||||
},
|
||||
carNumber: '',
|
||||
getOk: false,
|
||||
});
|
||||
const [[regis_login], getMyCars] = UseApiLoading(getMyCar);
|
||||
function getdata() {
|
||||
getMyCars().then((a) => {
|
||||
console.log(a.json().Data);
|
||||
data.value = a.json().Data;
|
||||
});
|
||||
}
|
||||
onMounted(() => {
|
||||
if (userStore.ishaslogin) {
|
||||
getdata();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
32
src/page/index/index.vue
Normal file
32
src/page/index/index.vue
Normal file
@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<div class="output" v-html="output"></div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { marked } from 'marked';
|
||||
import { getMessage } from '@/apis/message/inedex.ts';
|
||||
|
||||
const output = ref('');
|
||||
|
||||
function getData() {
|
||||
getMessage().then((res) => {
|
||||
output.value = marked(res.json().Data.data);
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(getData);
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.output {
|
||||
blockquote {
|
||||
padding-left: 10px;
|
||||
border-left: 4px solid rgba(4, 255, 0, 0.27);
|
||||
}
|
||||
|
||||
li {
|
||||
&::before {
|
||||
content: '• ';
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
15
src/page/user/index.vue
Normal file
15
src/page/user/index.vue
Normal file
@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="!userStore.ishaslogin" style="display: flex; justify-content: center; align-items: center">
|
||||
<NText style="font-size: larger">请登录或注册</NText>
|
||||
</div>
|
||||
<RouterView />
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { UserStore } from '@/plugins';
|
||||
import { NText } from 'naive-ui';
|
||||
|
||||
const userStore = UserStore();
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
94
src/page/user/login.vue
Normal file
94
src/page/user/login.vue
Normal file
@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-loading="loading_login" class="card" style="margin-top: 10px">
|
||||
<NGradientText f-c-c size="18" style="padding: 10px" type="warning">登录账号</NGradientText>
|
||||
<div class="card-line"></div>
|
||||
<NForm
|
||||
ref="formRef"
|
||||
:model="fromdata"
|
||||
:rules="rules"
|
||||
label-align="right"
|
||||
label-placement="left"
|
||||
label-width="auto"
|
||||
style="padding: 24px 10px 10px">
|
||||
<NFormItem label="账号" path="userName">
|
||||
<NInput v-model:value="fromdata.userName" placeholder="请输入手机号/车牌号" />
|
||||
</NFormItem>
|
||||
<NFormItem label="密码" path="passWold">
|
||||
<NInput v-model:value="fromdata.passWold" placeholder="请输入密码" type="password" />
|
||||
</NFormItem>
|
||||
<NFormItem label=" ">
|
||||
<NCheckbox v-model:checked="userStore.userDataRemberData.renb">记住我</NCheckbox>
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
<div f-c-c>
|
||||
<NText style="padding-right: 5px">我还没有账号</NText>
|
||||
<RouterLink to="/user/register">前往注册</RouterLink>
|
||||
</div>
|
||||
<div class="card-line" style="margin-top: 24px"></div>
|
||||
<div style="padding: 10px">
|
||||
<NButton secondary strong style="width: 100%" type="info" @click="regis">登录</NButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { type FormInst, NButton, NCheckbox, NForm, NFormItem, NGradientText, NInput, NText } from 'naive-ui';
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import message from '@/utils/message.ts';
|
||||
import { login } from '@/apis/auth';
|
||||
import { UserStore } from '@/plugins';
|
||||
import UseApiLoading from '@/utils/UseApiLoading.ts';
|
||||
|
||||
const formRef = ref<FormInst | null>(null);
|
||||
|
||||
const userStore = UserStore();
|
||||
|
||||
const fromdata = reactive({
|
||||
passWold: '',
|
||||
userName: '',
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (userStore.userDataRemberData.renb) {
|
||||
fromdata.userName = userStore.userDataRemberData.user;
|
||||
fromdata.passWold = userStore.userDataRemberData.pass;
|
||||
}
|
||||
});
|
||||
|
||||
const [[loading_login], logins] = UseApiLoading(login);
|
||||
|
||||
const regis = () => {
|
||||
formRef.value?.validate((errors: any) => {
|
||||
if (!errors) {
|
||||
logins(fromdata).then((a) => {
|
||||
userStore.ishaslogin = true;
|
||||
userStore.token = a.json().Data;
|
||||
if (userStore.userDataRemberData.renb) {
|
||||
userStore.userDataRemberData.user = fromdata.userName;
|
||||
userStore.userDataRemberData.pass = fromdata.passWold;
|
||||
}
|
||||
window.location.replace('/');
|
||||
});
|
||||
} else {
|
||||
message.error('请确认 填写信息是否正确');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const rules = {
|
||||
userName: {
|
||||
key: 'userName',
|
||||
required: true,
|
||||
message: '请输入用户名',
|
||||
trigger: ['input'],
|
||||
},
|
||||
passWold: {
|
||||
key: 'passWold',
|
||||
required: true,
|
||||
message: '请输入密码',
|
||||
trigger: ['input'],
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
120
src/page/user/register.vue
Normal file
120
src/page/user/register.vue
Normal file
@ -0,0 +1,120 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-loading="regis_login" class="card" style="margin-top: 10px">
|
||||
<NGradientText f-c-c size="18" style="padding: 10px" type="warning">注册账号</NGradientText>
|
||||
<div class="card-line"></div>
|
||||
<NForm
|
||||
ref="formRef"
|
||||
:model="fromdata"
|
||||
:rules="rules"
|
||||
label-align="right"
|
||||
label-placement="left"
|
||||
label-width="auto"
|
||||
style="padding: 24px 10px 10px">
|
||||
<NFormItem label="手机号" path="tel">
|
||||
<NInput v-model:value="fromdata.tel" placeholder="请输入手机号" />
|
||||
</NFormItem>
|
||||
<NFormItem label="车牌号" path="car">
|
||||
<NInput v-model:value="fromdata.car" placeholder="请输入车牌号" />
|
||||
</NFormItem>
|
||||
<NFormItem label="密码" path="pass">
|
||||
<NInput v-model:value="fromdata.pass" placeholder="请输入密码" type="password" />
|
||||
</NFormItem>
|
||||
<NFormItem label="确认密码" path="passre">
|
||||
<NInput v-model:value="fromdata.passre" placeholder="请输入确认密码" type="password" />
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
<div f-c-c>
|
||||
<NText style="padding-right: 5px">我已已经有账号了</NText>
|
||||
<RouterLink to="/user/login">前往登录</RouterLink>
|
||||
</div>
|
||||
<div class="card-line" style="margin-top: 24px"></div>
|
||||
<div style="padding: 10px">
|
||||
<NButton secondary strong style="width: 100%" type="info" @click="regis">注册</NButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
type FormInst,
|
||||
type FormItemRule,
|
||||
NButton,
|
||||
NForm,
|
||||
NFormItem,
|
||||
NGradientText,
|
||||
NInput,
|
||||
NText
|
||||
} from 'naive-ui';
|
||||
import { reactive, ref } from 'vue';
|
||||
import message from '@/utils/message.ts';
|
||||
import { register } from '@/apis/auth';
|
||||
import { router, UserStore } from '@/plugins';
|
||||
import UseApiLoading from '@/utils/UseApiLoading.ts';
|
||||
|
||||
const formRef = ref<FormInst | null>(null);
|
||||
|
||||
const userStore = UserStore();
|
||||
|
||||
const fromdata = reactive({ tel: '', car: '', pass: '', passre: '' });
|
||||
const [[regis_login], regise] = UseApiLoading(register);
|
||||
const regis = () => {
|
||||
formRef.value?.validate((errors: any) => {
|
||||
if (!errors) {
|
||||
regise(fromdata).then((a) => {
|
||||
userStore.ishaslogin = true;
|
||||
userStore.token = a.json().Data;
|
||||
userStore.getLogin();
|
||||
router.push({ name: 'index' });
|
||||
});
|
||||
} else {
|
||||
message.error('请确认 填写信息是否正确');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const rules = {
|
||||
tel: {
|
||||
key: 'tel',
|
||||
required: true,
|
||||
message: '手机号不正确',
|
||||
trigger: ['input'],
|
||||
validator(rule: FormItemRule, value: string) {
|
||||
return RegExp('^1[3-9]\\d{9}$').test(value);
|
||||
},
|
||||
},
|
||||
car: {
|
||||
key: 'car',
|
||||
required: true,
|
||||
message: '车牌号不正确',
|
||||
trigger: ['input'],
|
||||
validator(rule: FormItemRule, value: string) {
|
||||
return RegExp(
|
||||
'^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼][A-HJ-NP-Za-hj-np-z][A-HJ-NP-Za-hj-np-z0-9]{4}[A-HJ-NP-Za-hj-np-z0-9]([A-HJ-NP-Za-hj-np-z])?$',
|
||||
).test(value);
|
||||
},
|
||||
},
|
||||
pass: {
|
||||
key: 'pass',
|
||||
required: true,
|
||||
message:
|
||||
'密码强度低\n至少包含字母、数字、特殊字符,最少9位,并且不能连续出现3个大小连续或相同的数字(如:456、654、888)',
|
||||
trigger: ['input'],
|
||||
validator(rule: FormItemRule, value: string) {
|
||||
return RegExp(
|
||||
'^(?=.*\\d)(?!.*(\\d)\\1{2})(?!.*(012|123|234|345|456|567|678|789|987|876|765|654|543|432|321|210))(?=.*[a-zA-Z])(?=.*[^\\da-zA-Z\\s]).{1,9}$',
|
||||
).test(value);
|
||||
},
|
||||
},
|
||||
passre: {
|
||||
key: 'passre',
|
||||
required: true,
|
||||
message: '密码不匹配',
|
||||
trigger: ['input'],
|
||||
validator(rule: FormItemRule, value: string) {
|
||||
return fromdata.pass === value;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
39
src/page/user/user.vue
Normal file
39
src/page/user/user.vue
Normal file
@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div class="card">
|
||||
<NDescriptions
|
||||
:column="1"
|
||||
:label-style="{
|
||||
width: 'auto',
|
||||
}"
|
||||
bordered
|
||||
label-placement="left"
|
||||
size="small"
|
||||
style="border-radius: 5px; overflow: clip">
|
||||
<NDescriptionsItem label="车牌号">{{ userStore.userData.carNumber }}</NDescriptionsItem>
|
||||
<NDescriptionsItem label="手机号">{{ userStore.userData.telNumber }}</NDescriptionsItem>
|
||||
</NDescriptions>
|
||||
<div class="card-line" style="margin-bottom: 10px"></div>
|
||||
<NButton
|
||||
secondary
|
||||
strong
|
||||
style="margin-bottom: 10px; margin-left: 10px; margin-right: 10px; width: calc(100% - 20px)"
|
||||
type="error"
|
||||
@click="userStore.Logout">
|
||||
登出
|
||||
</NButton>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { router, UserStore } from '@/plugins';
|
||||
import { onMounted } from 'vue';
|
||||
import { NButton, NDescriptions, NDescriptionsItem } from 'naive-ui';
|
||||
|
||||
const userStore = UserStore();
|
||||
|
||||
onMounted(() => {
|
||||
if (!userStore.ishaslogin) {
|
||||
router.push({ name: 'login' });
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
145
src/plugins/alova/index.ts
Normal file
145
src/plugins/alova/index.ts
Normal file
@ -0,0 +1,145 @@
|
||||
import { createAlova, Method } from 'alova';
|
||||
import adapterFetch from 'alova/fetch';
|
||||
import { router, UserStore } from '@/plugins';
|
||||
import message from '@/utils/message';
|
||||
import AesUtils from '@/utils/AesUtils';
|
||||
import golob from '@/utils/golob.ts';
|
||||
|
||||
export enum ResponseCodes {
|
||||
ERROR_INTERNAL_SERVER = 'E00500',
|
||||
ERROR_PAYMENT_REQUIRED = 'E00402',
|
||||
ERROR_NOT_FOUND = 'E00404',
|
||||
ERROR_METHOD_NOT_ALLOWED = 'E00405',
|
||||
ERROR_UNAUTHORIZED = 'E00401',
|
||||
ERROR_FORBIDDEN = 'E00403',
|
||||
OK = 'S00200',
|
||||
}
|
||||
|
||||
export interface HaikewulianResponseType<T = any, K = any> {
|
||||
msg: string;
|
||||
code: 200 | 401 | 500;
|
||||
total: string;
|
||||
data: T;
|
||||
rows: K[];
|
||||
}
|
||||
|
||||
export interface AlovaResponseType<T = any> {
|
||||
headers: Headers;
|
||||
ok: boolean;
|
||||
redirected: boolean;
|
||||
status: number;
|
||||
statusText: string;
|
||||
type: ResponseType;
|
||||
url: string;
|
||||
body: ReadableStream<Uint8Array> | null;
|
||||
bodyUsed: boolean;
|
||||
method: Method;
|
||||
|
||||
clone(): AlovaResponseType<T>;
|
||||
|
||||
arrayBuffer(): Promise<ArrayBuffer>;
|
||||
|
||||
blob(): Promise<Blob>;
|
||||
|
||||
formData(): Promise<FormData>;
|
||||
|
||||
json(): T;
|
||||
|
||||
text(): string;
|
||||
}
|
||||
|
||||
export type defResponse<T, K> = {
|
||||
Message: string;
|
||||
Total: string;
|
||||
Data: T;
|
||||
Rows: K[];
|
||||
Code: ResponseCodes;
|
||||
Page: PageType<K>;
|
||||
};
|
||||
|
||||
export interface PageType<K> {
|
||||
rows: K[];
|
||||
total: number;
|
||||
pages: number;
|
||||
pageIndex: number;
|
||||
pagesize: number;
|
||||
}
|
||||
|
||||
export type AlovaResponse<T = any, K = any> = AlovaResponseType<defResponse<T, K>>;
|
||||
|
||||
const loginisundifind = () => {
|
||||
const login_out = () => {
|
||||
localStorage.setItem('Login_redirect', router.currentRoute.value.fullPath);
|
||||
setTimeout(() => router.push('/user/login'), 10);
|
||||
};
|
||||
const useauthStore = UserStore();
|
||||
if (useauthStore.ishaslogin) {
|
||||
useauthStore.ClearLogin();
|
||||
golob.showLogingOut.open();
|
||||
} else {
|
||||
useauthStore.ClearLogin();
|
||||
login_out();
|
||||
}
|
||||
};
|
||||
|
||||
const alovaInstance = createAlova({
|
||||
baseURL: '/api/v3/app',
|
||||
cacheFor: null,
|
||||
requestAdapter: adapterFetch(),
|
||||
timeout: 120000,
|
||||
beforeRequest(method) {
|
||||
const useauthStore = UserStore();
|
||||
if (useauthStore.ishaslogin) {
|
||||
method.config.headers['Admin-Token'] = useauthStore.GetToken;
|
||||
} else {
|
||||
try {
|
||||
if (method.meta.notbefore) return Promise.resolve();
|
||||
} catch (e) {
|
||||
if (window.location.href.replace(window.location.origin, '') !== '/login') {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
return Promise.reject('Not logged in');
|
||||
}
|
||||
}
|
||||
},
|
||||
responded: {
|
||||
onSuccess: async (response: AlovaResponseType & Response, method) => {
|
||||
if (response.status !== 200) {
|
||||
message.error(method.url + ' 请求失败 ' + response.status + ' ' + response.statusText);
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
const data = response;
|
||||
const text = await response.text();
|
||||
let json: defResponse<any, any> | null = null;
|
||||
json = JSON.parse(text);
|
||||
if (json?.Data) {
|
||||
json.Data = JSON.parse(await AesUtils.decrypt(json.Data, response.headers.get('server-time')));
|
||||
}
|
||||
if (json?.Rows) {
|
||||
json.Rows = JSON.parse(await AesUtils.decrypt(json.Rows, response.headers.get('server-time')));
|
||||
}
|
||||
if (json?.Page) {
|
||||
json.Page = JSON.parse(await AesUtils.decrypt(json.Page, response.headers.get('server-time')));
|
||||
}
|
||||
if (json?.Code !== ResponseCodes.OK) {
|
||||
if (json?.Code === ResponseCodes.ERROR_UNAUTHORIZED) {
|
||||
loginisundifind();
|
||||
} else {
|
||||
message.error(json.Message);
|
||||
}
|
||||
throw new Error(json.Message);
|
||||
}
|
||||
data.json = () => json;
|
||||
data.text = () => text;
|
||||
data.method = method;
|
||||
return data;
|
||||
},
|
||||
onError: (err, method) => {
|
||||
message.error(err.message);
|
||||
const useauthStore = UserStore();
|
||||
if (!useauthStore.ishaslogin) router.push('/login');
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default alovaInstance;
|
10
src/plugins/directive/index.ts
Normal file
10
src/plugins/directive/index.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import VEnter from './vEnter';
|
||||
import VLoad from './vLoad';
|
||||
import type { App } from 'vue';
|
||||
|
||||
export default {
|
||||
install(app: App) {
|
||||
app.directive('loading', VLoad);
|
||||
app.directive('enter', VEnter);
|
||||
},
|
||||
};
|
42
src/plugins/directive/vEnter/index.ts
Normal file
42
src/plugins/directive/vEnter/index.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import type { Directive } from 'vue';
|
||||
|
||||
type objfunc = {
|
||||
key: HTMLElement;
|
||||
funs: (e: KeyboardEvent) => void;
|
||||
time: Date;
|
||||
};
|
||||
const data: objfunc[] = [];
|
||||
|
||||
function deldata(el: HTMLElement) {
|
||||
return data.findIndex((data) => data.key === el);
|
||||
}
|
||||
|
||||
const VEnter: Directive = {
|
||||
mounted(el: HTMLElement, binding, vnode) {
|
||||
const { value } = binding;
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (data.sort((a, b) => (a.time < b.time ? 1 : -1))[0].key === el) {
|
||||
value(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
data.push({
|
||||
key: el,
|
||||
funs: handler,
|
||||
time: new Date(),
|
||||
});
|
||||
document.body.addEventListener('keydown', handler);
|
||||
},
|
||||
beforeUnmount(el: HTMLElement, binding, vnode) {
|
||||
const index = deldata(el);
|
||||
const datas = data[index];
|
||||
if (index !== -1) {
|
||||
document.body.removeEventListener('keydown', datas.funs);
|
||||
data.splice(index, 1);
|
||||
}
|
||||
},
|
||||
};
|
||||
export default VEnter;
|
60
src/plugins/directive/vLoad/index.ts
Normal file
60
src/plugins/directive/vLoad/index.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { createVNode, render, type VNode } from 'vue';
|
||||
import type { Directive, DirectiveBinding } from 'vue';
|
||||
import LoadComponent from './index.vue';
|
||||
import './load.scss';
|
||||
|
||||
type datamapitem = {
|
||||
timeout?: any;
|
||||
resizeObserver?: ResizeObserver;
|
||||
Vnode: VNode;
|
||||
};
|
||||
const datamap = new Map<HTMLElement, datamapitem>();
|
||||
const lodaon = (el: HTMLElement, binding: DirectiveBinding) => {
|
||||
const item: HTMLElement | null = el.querySelector('#loading');
|
||||
if (item) {
|
||||
const datas = datamap.get(el);
|
||||
if (datas) {
|
||||
clearTimeout(datas?.timeout);
|
||||
datas.timeout = null;
|
||||
datamap.set(el, datas);
|
||||
item.style.opacity = '1';
|
||||
}
|
||||
return;
|
||||
}
|
||||
const Mynode = createVNode(LoadComponent, {});
|
||||
datamap.set(el, {
|
||||
Vnode: Mynode,
|
||||
});
|
||||
render(Mynode, el);
|
||||
};
|
||||
const lodaoff = (el: HTMLElement, binding: DirectiveBinding) => {
|
||||
const item: HTMLElement | null = el.querySelector('#loading');
|
||||
if (!item) return;
|
||||
item.style.opacity = '0';
|
||||
const tmout = setTimeout(() => {
|
||||
render(null, el);
|
||||
datamap.delete(el);
|
||||
}, 300);
|
||||
const datas = datamap.get(el);
|
||||
if (datas) {
|
||||
datas.timeout = tmout;
|
||||
datamap.set(el, datas);
|
||||
}
|
||||
};
|
||||
const load = (el: HTMLElement, binding: DirectiveBinding) => {
|
||||
if (binding.value) {
|
||||
lodaon(el, binding);
|
||||
} else {
|
||||
lodaoff(el, binding);
|
||||
}
|
||||
};
|
||||
const VLoad: Directive = {
|
||||
mounted: (el, binding) => load(el, binding),
|
||||
updated: (el, binding) => load(el, binding),
|
||||
beforeUnmount: (el, binding) => {
|
||||
render(null, el);
|
||||
datamap.delete(el);
|
||||
},
|
||||
};
|
||||
|
||||
export default VLoad;
|
72
src/plugins/directive/vLoad/index.vue
Normal file
72
src/plugins/directive/vLoad/index.vue
Normal file
@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<IxSpin id="loading" ref="dataref" :style="styref" :tip="tip_str" size="md"></IxSpin>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { IxSpin } from '@idux/components';
|
||||
import { onBeforeUnmount, onMounted, reactive, ref } from 'vue';
|
||||
|
||||
import '@idux/components/spin/style/index.css';
|
||||
|
||||
let nubs: any;
|
||||
const tip_str = ref('正在努力加载中');
|
||||
const dataref = ref<InstanceType<typeof IxSpin>>();
|
||||
const styref = reactive({
|
||||
height: '0',
|
||||
width: '0',
|
||||
marginTop: '0',
|
||||
marginRight: '0',
|
||||
marginBottom: '0',
|
||||
marginLeft: '0',
|
||||
});
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
for (let entry of entries) {
|
||||
dataref.value?.$el.parentNode.classList.add('loginfather');
|
||||
const computedStyle = window.getComputedStyle(dataref.value?.$el.parentNode);
|
||||
const width = parseFloat(computedStyle.width);
|
||||
const height = parseFloat(computedStyle.height);
|
||||
styref.height = `${height + 1}px`;
|
||||
styref.width = `${width + 1}px`;
|
||||
}
|
||||
});
|
||||
onMounted(() => {
|
||||
resizeObserver.observe(dataref.value?.$el.parentNode);
|
||||
nubs = setInterval(() => {
|
||||
switch (tip_str.value) {
|
||||
case '正在努力加载中':
|
||||
tip_str.value = '正在努力加载中.';
|
||||
break;
|
||||
case '正在努力加载中.':
|
||||
tip_str.value = '正在努力加载中..';
|
||||
break;
|
||||
case '正在努力加载中..':
|
||||
tip_str.value = '正在努力加载中...';
|
||||
break;
|
||||
case '正在努力加载中...':
|
||||
tip_str.value = '正在努力加载中';
|
||||
break;
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
clearInterval(nubs);
|
||||
resizeObserver.unobserve(dataref.value?.$el.parentNode);
|
||||
resizeObserver.disconnect();
|
||||
dataref.value?.$el.parentNode.classList.remove('loginfather');
|
||||
return false;
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@use './load';
|
||||
|
||||
#loading {
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
box-shadow: var(--el-box-shadow-light);
|
||||
transition: opacity 0.3s ease;
|
||||
top: -0.5px;
|
||||
left: -0.5px;
|
||||
border-radius: inherit;
|
||||
overflow: hidden;
|
||||
background-color: var(--load-background-color);
|
||||
}
|
||||
</style>
|
40
src/plugins/directive/vLoad/load.scss
Normal file
40
src/plugins/directive/vLoad/load.scss
Normal file
@ -0,0 +1,40 @@
|
||||
.loginfather {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.loading-full {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
|
||||
.el-loading-spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
color: #e70303;
|
||||
|
||||
.circular {
|
||||
color: #e70303;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.Loading_out {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
html[theme="dark"] {
|
||||
.ix-spin-spinner {
|
||||
--ix-spin-mask-bg-color: rgba(235, 235, 235, 0.2) !important;
|
||||
}
|
||||
}
|
||||
|
||||
html .ix-spin-spinner {
|
||||
--ix-spin-mask-bg-color: rgba(20, 20, 20, 0.3) !important;
|
||||
}
|
||||
|
||||
.ix-spin-spinner {
|
||||
background-color: var(--ix-spin-mask-bg-color) !important;
|
||||
backdrop-filter: blur(1px);
|
||||
}
|
5
src/plugins/index.ts
Normal file
5
src/plugins/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export * from './router';
|
||||
export * from './pinia';
|
||||
import directive from './directive';
|
||||
|
||||
export { directive };
|
11
src/plugins/pinia/index.ts
Normal file
11
src/plugins/pinia/index.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { createPinia } from 'pinia';
|
||||
|
||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
|
||||
|
||||
const pinia = createPinia();
|
||||
|
||||
pinia.use(piniaPluginPersistedstate);
|
||||
|
||||
export { pinia };
|
||||
|
||||
export * from './store';
|
17
src/plugins/pinia/store/SettingStore.ts
Normal file
17
src/plugins/pinia/store/SettingStore.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
import { useColorMode } from '@vueuse/core';
|
||||
|
||||
const SettingStore = defineStore('Setting', {
|
||||
state: () => {
|
||||
const theme = useColorMode({
|
||||
attribute: 'theme',
|
||||
});
|
||||
return { theme: theme.system };
|
||||
},
|
||||
getters: {},
|
||||
actions: {},
|
||||
persist: true,
|
||||
});
|
||||
|
||||
export { SettingStore };
|
50
src/plugins/pinia/store/UserStore.ts
Normal file
50
src/plugins/pinia/store/UserStore.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { info, logout } from '@/apis/auth';
|
||||
import { ref } from 'vue';
|
||||
import type { User } from '@/apis/auth/type.ts';
|
||||
|
||||
const UserStore = defineStore('User', {
|
||||
state: () => {
|
||||
const userData = ref<User>({
|
||||
carNumber: '',
|
||||
createBy: '',
|
||||
createTime: '',
|
||||
id: '',
|
||||
passWold: '***',
|
||||
telNumber: '',
|
||||
type: 1,
|
||||
updateBy: '',
|
||||
updateTime: '',
|
||||
userName: '',
|
||||
});
|
||||
const userDataRemberData = ref({ user: '', pass: '', renb: false });
|
||||
return { ishaslogin: false, token: '', userData, userDataRemberData };
|
||||
},
|
||||
actions: {
|
||||
getLogin() {
|
||||
info().then((a) => {
|
||||
console.log(a.json());
|
||||
this.ishaslogin = a.json().Data.login;
|
||||
Object.assign(this.userData, a.json().Data.user);
|
||||
});
|
||||
},
|
||||
ClearLogin() {
|
||||
this.ishaslogin = false;
|
||||
this.token = '';
|
||||
},
|
||||
Logout() {
|
||||
logout().finally(() => {
|
||||
this.ClearLogin();
|
||||
window.location.replace('/user/login');
|
||||
});
|
||||
},
|
||||
},
|
||||
getters: {
|
||||
GetToken(): string {
|
||||
return 'Bearer ' + this.token;
|
||||
},
|
||||
},
|
||||
persist: true,
|
||||
});
|
||||
|
||||
export { UserStore };
|
2
src/plugins/pinia/store/index.ts
Normal file
2
src/plugins/pinia/store/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './SettingStore';
|
||||
export * from './UserStore';
|
56
src/plugins/router/index.ts
Normal file
56
src/plugins/router/index.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
name: 'layout',
|
||||
path: '/',
|
||||
component: () => import('@/layout/index.vue'),
|
||||
children: [
|
||||
{
|
||||
name: 'index',
|
||||
path: '/',
|
||||
component: () => import('@/page/index/index.vue'),
|
||||
meta: { name: 'index' },
|
||||
},
|
||||
{
|
||||
name: 'chehaoca',
|
||||
path: '/chehaoca',
|
||||
component: () => import('@/page/chehaoca/index.vue'),
|
||||
meta: { name: 'chehaoca' },
|
||||
},
|
||||
{
|
||||
name: 'users',
|
||||
path: '/user',
|
||||
component: () => import('@/page/user/index.vue'),
|
||||
meta: { name: 'user' },
|
||||
children: [
|
||||
{
|
||||
name: 'login',
|
||||
path: '/user/login',
|
||||
component: () => import('@/page/user/login.vue'),
|
||||
meta: { name: 'user' },
|
||||
},
|
||||
{
|
||||
name: 'register',
|
||||
path: '/user/register',
|
||||
component: () => import('@/page/user/register.vue'),
|
||||
meta: { name: 'user' },
|
||||
},
|
||||
{
|
||||
name: 'user',
|
||||
path: '/user',
|
||||
component: () => import('@/page/user/user.vue'),
|
||||
meta: { name: 'user' },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: routes,
|
||||
});
|
||||
|
||||
export { router };
|
17
src/resource/css/card.scss
Normal file
17
src/resource/css/card.scss
Normal file
@ -0,0 +1,17 @@
|
||||
html[theme="dark"] {
|
||||
--card-border-color: #404040;
|
||||
}
|
||||
|
||||
html {
|
||||
--card-border-color: #cdcdcd;
|
||||
}
|
||||
|
||||
|
||||
.card {
|
||||
border-radius: 5px;
|
||||
border: 1px solid var(--card-border-color);
|
||||
}
|
||||
|
||||
.card-line {
|
||||
border-top: 1px solid var(--card-border-color);
|
||||
}
|
17
src/resource/css/index.scss
Normal file
17
src/resource/css/index.scss
Normal file
@ -0,0 +1,17 @@
|
||||
@use "card";
|
||||
|
||||
html[theme="dark"] {
|
||||
background-color: #202020;
|
||||
color: #d8d8d8;
|
||||
}
|
||||
|
||||
*[f-c-c] {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #1bbb44;
|
||||
text-decoration: none;
|
||||
}
|
BIN
src/resource/img/icon.png
Normal file
BIN
src/resource/img/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.3 KiB |
8
src/types/RouteMeta.d.ts
vendored
Normal file
8
src/types/RouteMeta.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
import 'vue-router';
|
||||
export {};
|
||||
|
||||
declare module 'vue-router' {
|
||||
interface RouteMeta {
|
||||
name: string;
|
||||
}
|
||||
}
|
88
src/utils/AesUtils.ts
Normal file
88
src/utils/AesUtils.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import { decode as base64Decode, encode as base64Encode } from 'base64-arraybuffer';
|
||||
import { md5 } from 'js-md5';
|
||||
|
||||
export default class AesUtils {
|
||||
private static readonly ALGORITHM = 'AES-GCM';
|
||||
private static readonly GCM_IV_LENGTH = 12;
|
||||
private static readonly GCM_TAG_LENGTH = 16;
|
||||
|
||||
public static async encrypt(sSrc: string, sKey: string): Promise<string | null> {
|
||||
if (!sKey) return null;
|
||||
|
||||
try {
|
||||
// 生成密钥
|
||||
const key = await this.createSecretKey(sKey);
|
||||
|
||||
// 生成随机IV (12字节)
|
||||
const iv = crypto.getRandomValues(new Uint8Array(this.GCM_IV_LENGTH));
|
||||
|
||||
// 执行加密
|
||||
const encrypted = await crypto.subtle.encrypt(
|
||||
{
|
||||
name: this.ALGORITHM,
|
||||
iv: iv,
|
||||
tagLength: this.GCM_TAG_LENGTH * 8,
|
||||
},
|
||||
key,
|
||||
new TextEncoder().encode(sSrc),
|
||||
);
|
||||
|
||||
// 将IV和加密数据转换为Base64
|
||||
const ivBase64 = base64Encode(iv.buffer);
|
||||
const encryptedBase64 = base64Encode(encrypted);
|
||||
|
||||
// 组合并再次Base64编码(与Java实现匹配)
|
||||
const combined = `${ivBase64}:${encryptedBase64}`;
|
||||
const combinedBuffer = new TextEncoder().encode(combined);
|
||||
|
||||
// 确保传递的是纯ArrayBuffer
|
||||
return base64Encode(combinedBuffer.buffer);
|
||||
} catch (e) {
|
||||
console.error('加密错误:', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static async decrypt(sSrc: string, sKey: string): Promise<string | null> {
|
||||
if (!sKey) return null;
|
||||
|
||||
try {
|
||||
// 解码外层Base64
|
||||
const decodedBuffer = base64Decode(sSrc);
|
||||
const decoded = new TextDecoder().decode(decodedBuffer);
|
||||
|
||||
// 分割IV和加密数据
|
||||
const parts = decoded.split(':', 2);
|
||||
if (parts.length !== 2) return null;
|
||||
|
||||
const iv = base64Decode(parts[0]);
|
||||
const encryptedData = base64Decode(parts[1]);
|
||||
|
||||
if (iv.byteLength !== this.GCM_IV_LENGTH) return null;
|
||||
|
||||
const key = await this.createSecretKey(sKey);
|
||||
|
||||
// 执行解密
|
||||
const decrypted = await crypto.subtle.decrypt(
|
||||
{
|
||||
name: this.ALGORITHM,
|
||||
iv: new Uint8Array(iv),
|
||||
tagLength: this.GCM_TAG_LENGTH * 8,
|
||||
},
|
||||
key,
|
||||
encryptedData,
|
||||
);
|
||||
|
||||
return new TextDecoder().decode(decrypted);
|
||||
} catch (e) {
|
||||
console.error('解密错误:', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static async createSecretKey(sKey: string): Promise<CryptoKey> {
|
||||
// 使用js-md5库生成与Java一致的MD5哈希
|
||||
const md5Hash = md5.arrayBuffer(sKey);
|
||||
return crypto.subtle.importKey('raw', md5Hash, { name: this.ALGORITHM }, false, ['encrypt', 'decrypt']);
|
||||
}
|
||||
}
|
23
src/utils/UseApiLoading.ts
Normal file
23
src/utils/UseApiLoading.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import UseRefBool, { type UseRefBoolType } from '@/utils/UseRefBool.ts';
|
||||
|
||||
/**
|
||||
* 执行一个异步函数并返回一个包含状态控制和结果的元组
|
||||
* @param t 要执行的异步函数,该函数不接受参数并返回Promise<D>
|
||||
* @returns 返回一个只读元组,包含:
|
||||
* - UseRefBoolType: 用于控制和监视异步操作状态的引用对象
|
||||
* - Promise<D>: 包装后的Promise,其结果类型与传入函数的返回值类型相同
|
||||
*/
|
||||
export default function <D, T extends (...args: any[]) => Promise<D>>(t: T): readonly [UseRefBoolType, T] {
|
||||
const us = UseRefBool();
|
||||
return [
|
||||
us,
|
||||
(...args: P[]) =>
|
||||
new Promise<D>((resolve, reject) => {
|
||||
us[1].open();
|
||||
t(...args)
|
||||
.then(resolve)
|
||||
.catch((...err) => reject(...err)) // 修复点:显式调用 reject
|
||||
.finally(() => us[1].close());
|
||||
}) as T,
|
||||
] as const;
|
||||
}
|
26
src/utils/UseRefBool.ts
Normal file
26
src/utils/UseRefBool.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { type Ref, ref } from 'vue';
|
||||
|
||||
function UseRefBool(
|
||||
v = false,
|
||||
): readonly [Ref<boolean>, { open: () => void; close: () => void; reset: () => void; requter: () => void }] {
|
||||
const s = ref(v);
|
||||
return [
|
||||
s,
|
||||
{
|
||||
open() {
|
||||
s.value = true;
|
||||
},
|
||||
close() {
|
||||
s.value = false;
|
||||
},
|
||||
reset() {
|
||||
s.value = v;
|
||||
},
|
||||
requter() {
|
||||
s.value = !s.value;
|
||||
},
|
||||
},
|
||||
] as const;
|
||||
}
|
||||
export default UseRefBool;
|
||||
export type UseRefBoolType = ReturnType<typeof UseRefBool>;
|
18
src/utils/golob.ts
Normal file
18
src/utils/golob.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import type { LoadingBarApiInjection } from 'naive-ui/es/loading-bar/src/LoadingBarProvider';
|
||||
import UseRefBool from '@/utils/UseRefBool';
|
||||
import type { MessageInstance } from 'ant-design-vue/es/message/interface';
|
||||
|
||||
class Golob {
|
||||
public message: MessageInstance | undefined;
|
||||
public loadingBar: LoadingBarApiInjection | undefined;
|
||||
public showLogingOut: ReturnType<typeof UseRefBool> = UseRefBool();
|
||||
}
|
||||
|
||||
const golob = new Golob();
|
||||
export default golob;
|
||||
window.golob = golob;
|
||||
declare global {
|
||||
interface Window {
|
||||
golob: typeof golob;
|
||||
}
|
||||
}
|
31
src/utils/message.ts
Normal file
31
src/utils/message.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import type { MessageInstance } from 'ant-design-vue/es/message/interface';
|
||||
import golob from '@/utils/golob';
|
||||
|
||||
export default {
|
||||
successOK() {
|
||||
golob.message?.success('操作成功');
|
||||
},
|
||||
info(...data) {
|
||||
golob.message?.info(...data);
|
||||
},
|
||||
success(...data) {
|
||||
golob.message?.success(...data);
|
||||
},
|
||||
error(...data) {
|
||||
golob.message?.error(...data);
|
||||
},
|
||||
warning(...data) {
|
||||
golob.message?.warning(...data);
|
||||
},
|
||||
loading(...data) {
|
||||
golob.message?.loading(...data);
|
||||
},
|
||||
open(...data) {
|
||||
golob.message?.open(...data);
|
||||
},
|
||||
destroy(...data) {
|
||||
golob.message?.destroy(...data);
|
||||
},
|
||||
} as MessageInstance & {
|
||||
successOK: () => void;
|
||||
};
|
26
tsconfig.app.json
Normal file
26
tsconfig.app.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": [
|
||||
"env.d.ts",
|
||||
"src/**/*",
|
||||
"src/**/*.vue"
|
||||
],
|
||||
"exclude": [
|
||||
"src/**/__tests__/*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
],
|
||||
"@com/*": [
|
||||
"src/components/*"
|
||||
],
|
||||
"@res/*": [
|
||||
"src/resource/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
11
tsconfig.json
Normal file
11
tsconfig.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
}
|
||||
]
|
||||
}
|
19
tsconfig.node.json
Normal file
19
tsconfig.node.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"extends": "@tsconfig/node22/tsconfig.json",
|
||||
"include": [
|
||||
"vite.config.*",
|
||||
"vitest.config.*",
|
||||
"cypress.config.*",
|
||||
"nightwatch.conf.*",
|
||||
"playwright.config.*",
|
||||
"eslint.config.*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"noEmit": true,
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"types": ["node"]
|
||||
}
|
||||
}
|
23
vite.config.ts
Normal file
23
vite.config.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { fileURLToPath, URL } from 'node:url';
|
||||
|
||||
import { defineConfig } from 'vite';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import vueDevTools from 'vite-plugin-vue-devtools';
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
proxy: {
|
||||
'/api': 'http://localhost:7546',
|
||||
},
|
||||
},
|
||||
plugins: [vue(), vueDevTools()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
'@res': fileURLToPath(new URL('./src/resource', import.meta.url)),
|
||||
'@com': fileURLToPath(new URL('./src/component', import.meta.url)),
|
||||
},
|
||||
},
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user