初始化

This commit is contained in:
2025-11-20 14:07:55 +08:00
commit b9b1ff5ed4
318 changed files with 42662 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}]
charset = utf-8
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
end_of_line = lf
max_line_length = 100

1
auts_new_mobile.client/.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
* text=auto eol=lf

30
auts_new_mobile.client/.gitignore vendored Normal file
View File

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

View File

@@ -0,0 +1,7 @@
{
"recommendations": [
"Vue.volar",
"dbaeumer.vscode-eslint",
"EditorConfig.EditorConfig"
]
}

View File

@@ -0,0 +1,12 @@
This file explains how Visual Studio created the project.
The following tools were used to generate this project:
- create-vite
The following steps were used to generate this project:
- Create vue project with create-vite: `npm init --yes vue@latest auts_new_mobile.client -- --eslint `.
- Updating `vite.config.js` with port.
- Create project file (`auts_new_mobile.client.esproj`).
- Create `launch.json` to enable debugging.
- Add project to solution.
- Write this file.

View File

@@ -0,0 +1,35 @@
# auts_new_mobile.client
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Customize configuration
See [Vite Configuration Reference](https://vite.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Compile and Minify for Production
```sh
npm run build
```
### Lint with [ESLint](https://eslint.org/)
```sh
npm run lint
```

View File

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.VisualStudio.JavaScript.Sdk/1.0.2191419">
<PropertyGroup>
<StartupCommand>npm run dev</StartupCommand>
<JavaScriptTestRoot>.\</JavaScriptTestRoot>
<JavaScriptTestFramework>Vitest</JavaScriptTestFramework>
<!-- Allows the build (or compile) script located on package.json to run on Build -->
<ShouldRunBuildScript>false</ShouldRunBuildScript>
<!-- Folder where production build objects will be placed -->
<BuildOutputFolder>$(MSBuildProjectDirectory)\dist</BuildOutputFolder>
</PropertyGroup>
<ItemGroup>
<None Remove=".gitignore" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,26 @@
import js from '@eslint/js'
import pluginVue from 'eslint-plugin-vue'
import globals from 'globals'
export default [
{
name: 'app/files-to-lint',
files: ['**/*.{js,mjs,jsx,vue}'],
},
{
name: 'app/files-to-ignore',
ignores: ['**/dist/**', '**/dist-ssr/**', '**/coverage/**'],
},
{
languageOptions: {
globals: {
...globals.browser,
},
},
},
js.configs.recommended,
...pluginVue.configs['flat/essential'],
]

View File

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

View File

@@ -0,0 +1,8 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules", "dist"]
}

5526
auts_new_mobile.client/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,37 @@
{
"name": "auts_new_mobile.client",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint . --fix"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"@vueuse/core": "^13.0.0",
"axios": "^1.8.3",
"dayjs": "^1.11.13",
"element-plus": "^2.9.6",
"qs": "^6.14.0",
"tdesign-icons-vue-next": "^0.3.5",
"tdesign-mobile-vue": "^1.8.2",
"vue": "^3.5.13",
"vue-router": "^4.5.0",
"vue-simple-verify": "^1.1.0",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
"@vitejs/plugin-vue": "^5.2.1",
"eslint": "^9.21.0",
"eslint-plugin-vue": "~10.0.0",
"globals": "^16.0.0",
"unplugin-auto-import": "^19.1.1",
"unplugin-vue-components": "^28.4.1",
"vite": "^6.2.1",
"vite-plugin-vue-devtools": "^7.7.2"
}
}

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1742891293672" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8226" width="128" height="128" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M714.410667 374.528c-3.84-23.509333-15.146667-33.834667-38.997334-35.797333a208.768 208.768 0 0 0-34.261333 0.042666c-76.330667 6.357333-130.517333-89.130667-86.314667-150.357333 6.997333-9.770667 12.842667-20.522667 18.005334-31.402667 4.266667-9.002667 8.064-18.901333 2.517333-28.970666-16.256-29.354667-43.776-43.306667-74.666667-48.938667-27.008-4.949333-35.541333 18.816-46.378666 37.12-5.461333 9.258667-10.666667 18.688-17.024 27.349333-11.52 15.573333-26.496 26.154667-45.568 31.232-53.034667 14.208-93.482667-4.096-121.898667-55.850666-25.856-46.976-43.605333-51.669333-89.514667-23.466667a159.573333 159.573333 0 0 0-15.018666 10.154667c-21.973333 16.981333-25.6 33.578667-12.501334 57.898666 7.637333 14.165333 16.896 27.562667 23.338667 42.24 24.192 55.04-16.341333 125.056-75.989333 131.328-17.28 1.834667-34.858667 0.682667-52.266667 1.493334-16.554667 0.725333-30.805333 6.485333-36.266667 23.594666a123.648 123.648 0 0 0-0.213333 75.306667 31.317333 31.317333 0 0 0 29.738667 23.338667c9.344 0.554667 18.730667 0.085333 28.16 0.085333 32.64-0.725333 62.762667 4.181333 85.930666 30.72 34.218667 39.168 37.418667 77.738667 9.344 125.568-29.952 51.242667-24.661333 68.48 29.312 95.104 6.016 2.816 12.288 5.333333 18.56 7.594667 17.621333 6.485333 32 1.066667 42.368-13.568 8.021333-11.562667 15.232-23.850667 21.504-36.352 32.341333-64.426667 137.258667-64.512 169.770667-0.512 6.357333 12.586667 13.269333 25.088 21.546667 36.352 13.909333 18.901333 28.928 22.229333 53.333333 9.898666 25.088-12.586667 55.637333-22.016 56.234667-58.154666 0.085333-9.813333-6.229333-18.901333-11.434667-27.648-6.4-11.008-13.44-21.76-18.688-33.322667-24.533333-54.229333 12.586667-122.752 71.338667-133.034667 18.688-3.285333 37.504-0.682667 56.234666-1.962666 24.021333-1.664 35.498667-11.178667 39.68-34.858667 3.029333-17.28 2.944-34.944 0.085334-52.224z m-351.658667 166.997333c-79.146667 0.298667-142.677333-62.293333-142.890667-140.8a140.629333 140.629333 0 0 1 141.994667-141.44 140.202667 140.202667 0 0 1 140.714667 140.501334c0.170667 78.08-62.250667 141.397333-139.818667 141.738666z m655.317333 112.554667a92.416 92.416 0 0 0-9.685333-32.768c-7.168-14.421333-17.92-20.650667-34.517333-16.170667-14.165333 3.925333-27.733333 10.325333-41.984 9.685334-45.098667 0.896-76.330667-44.330667-63.744-90.88 9.514667-34.944 3.584-46.165333-28.928-54.698667-32.981333-8.661333-45.184-1.109333-52.565334 32.426667l-1.834666 7.850666c-13.482667 53.12-73.173333 70.869333-113.749334 33.749334-6.4-5.845333-12.672-11.989333-19.157333-17.792-9.173333-8.149333-19.157333-8.32-29.909333-3.029334-38.101333 18.901333-43.648 49.92-13.397334 78.506667 17.493333 16.682667 28.416 34.986667 24.746667 60.416-5.162667 34.602667-25.429333 52.565333-58.24 60.842667-33.066667 8.405333-38.869333 20.352-29.269333 52.821333 9.685333 32.256 21.333333 38.912 53.504 28.330667 27.52-9.002667 51.84-8.661333 74.581333 12.586666 24.064 22.485333 25.685333 48.341333 17.578667 77.482667-3.157333 11.349333-9.344 24.405333 1.066666 34.346667 15.829333 15.146667 36.096 20.181333 57.514667 17.578666 19.072-2.432 20.096-19.072 23.765333-33.237333 1.066667-4.565333 2.176-9.173333 3.498667-13.653333a67.328 67.328 0 0 1 109.994667-31.402667c7.082667 6.186667 13.397333 13.269333 20.565333 19.328 9.002667 7.68 18.901333 9.258667 29.994667 4.010667 38.912-18.688 44.501333-50.517333 13.482666-79.744l-2.901333-2.730667c-39.338667-38.485333-24.32-99.157333 28.416-114.730667 6.4-1.92 12.842667-3.925333 19.242667-5.674666 17.536-4.693333 23.509333-16.597333 21.930666-33.450667z m-237.482666 149.418667c-52.736 0.896-97.493333-43.562667-97.578667-96.981334-0.085333-53.589333 42.837333-96.768 97.237333-97.664 52.437333-0.896 97.237333 43.648 97.664 97.152 0.512 53.248-42.752 96.426667-97.322666 97.493334z" fill="#008B00" p-id="8227"></path></svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@@ -0,0 +1,8 @@
const config = {
// http访问后端接口 | prod | 生产
httpApi: "http://new.uts-data.com:6688/api/",
// http访问后端接口 | dev | 开发环境
//httpApi: "http://localhost:23553/api/",
}
export default config

View File

@@ -0,0 +1,137 @@
<template>
<layout v-if="alreadyLogin" style="z-index: 999999; position: relative;"></layout>
<div v-if="alreadyLogin" style="height:44px; " />
<router-view style="min-height: 85vh;"></router-view>
<t-footer text="Copyright © 2025 AUTS All Rights Reserved --Version0.0.3" />
</template>
<script>
</script>
<script setup>
import layout from '../src/pages/layout/index.vue'
import { ref, onMounted, onBeforeUnmount, watch, provide, watchEffect } from 'vue'
import { Toast } from 'tdesign-mobile-vue'
import { useRouter } from 'vue-router'
const router = useRouter()
setTimeout(() => {
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
webToDark(true);
} else {
webToDark(false);
}
}, 0);
// 切换主题
const isDarkMode = ref(false)
const webToDark = (dark) => {
if (dark) {
document.documentElement.setAttribute('theme-mode', 'dark')
isDarkMode.value = true
} else {
document.documentElement.removeAttribute('theme-mode')
isDarkMode.value = true
}
}
const toggleTheme = () => {
isDarkMode.value = !isDarkMode.value
let root = document.documentElement;
if (!isDarkMode.value) {
// 如果当前是暗色主题,切换到亮色主题
root.style.setProperty('--color-background', '#ffffff');
root.style.setProperty('--color-background-soft', '#f8f8f8');
root.style.setProperty('--color-background-mute', '#f2f2f2');
root.style.setProperty('--color-border', 'rgba(60, 60, 60, 0.12)');
root.style.setProperty('--color-border-hover', 'rgba(60, 60, 60, 0.29)');
root.style.setProperty('--color-heading', '#2c3e50');
root.style.setProperty('--color-text', '#2c3e50');
root.setAttribute('data-theme', 'light');
setTimeout(() => {
root.removeAttribute('theme-mode');
}, 20);
} else {
// 如果当前是亮色主题,切换到暗色主题
root.style.setProperty('--color-background', '#242424');
root.style.setProperty('--color-background-soft', '#222222');
root.style.setProperty('--color-background-mute', '#282828');
root.style.setProperty('--color-border', 'rgba(84, 84, 84, 0.48)');
root.style.setProperty('--color-border-hover', 'rgba(84, 84, 84, 0.65)');
root.style.setProperty('--color-heading', '#ffffff');
root.style.setProperty('--color-text', 'rgba(235, 235, 235, 0.64)');
root.setAttribute('data-theme', 'dark');
setTimeout(() => {
root.setAttribute('theme-mode', 'dark');
}, 20);
}
}
onMounted(() => {
// 初始检查登录状态
checkLoginStatus()
// 主题初始化
const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)')
const handleColorSchemeChange = (e) => {
webToDark(e.matches)
}
// 初始设置
webToDark(darkModeQuery.matches)
// 监听系统主题变化
darkModeQuery.addEventListener('change', handleColorSchemeChange)
// 清理监听器
return () => {
darkModeQuery.removeEventListener('change', handleColorSchemeChange)
}
})
const alreadyLogin = ref(false)
// 初始化检查登录状态
const checkLoginStatus = () => {
alreadyLogin.value = localStorage.getItem('login') == 'true'
setTimeout(() => {
if (alreadyLogin.value) {
//router.push('/home')
} else {
router.push('/login')
}
}, 6);
}
provide('checkLoginStatus', checkLoginStatus)
</script>
<style scoped>
/* 按钮样式优化 */
.t-button {
margin-top: 5px;
height: 40px;
font-weight: 400;
}
.t-checkbox--block {
padding: 0;
}
/* 主题切换按钮 */
.theme-toggle {
position: fixed;
top: 16px;
right: 16px;
padding: 8px 16px;
background: var(--toggle-bg);
color: var(--toggle-text);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
z-index: 100;
}
/* :deep .t-footer__text {
bottom: 2px;
position: relative;
}*/
</style>

View File

@@ -0,0 +1,86 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #242424;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition:
color 0.5s,
background-color 0.5s;
line-height: 1.6;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 276 B

View File

@@ -0,0 +1,34 @@
@import './base.css';
#app {
margin: 0 auto;
padding: 5px;
font-weight: normal;
}
a,
.green {
text-decoration: none;
color: hsla(160, 100%, 37%, 1);
transition: 0.4s;
padding: 3px;
}
@media (hover: hover) {
a:hover {
background-color: hsla(160, 100%, 37%, 0.2);
}
}
/*@media (min-width: 1024px) {
body {*/
/*display: flex;*/
/*place-items: center;
}
#app {
display: grid;*/
/*grid-template-columns: 1fr 1fr;*/
/*padding: 0 5px;
}
}*/

View File

@@ -0,0 +1,129 @@
import axios from 'axios';
import config from '../public/config.js';
import { Toast } from 'tdesign-mobile-vue'
const instance = axios.create({
baseURL: config.httpApi, // 设置基础URL
timeout: 60000
});
// 定义Token刷新状态变量
let isRefreshing = false;
let requestQueue = [];
// 添加请求拦截器
instance.interceptors.request.use(config => {
config.headers['Content-Type'] = 'application/json';
if (config.data && typeof config.data === 'object') {
config.data = JSON.stringify(config.data);
}
// 获取URL最后一段
const url = window.location.href;
const lastSegment = url.substring(url.lastIndexOf('/'));
// 如果不是登录请求
if (config.url !== 'Login/Login') {
const token = localStorage.getItem('token');
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
} else if (lastSegment != '/login') {
handleTokenExpiration();
}
}
return config;
}, error => {
return Promise.reject(error);
});
// 添加响应拦截器
instance.interceptors.response.use(response => {
return response;
}, async error => {
const originalRequest = error.config;
// 检测到Token过期(401错误)
if (error.response && error.response.status === 401 && !originalRequest._retry) {
// 防止重复刷新
if (!isRefreshing) {
isRefreshing = true;
originalRequest._retry = true;
try {
// 尝试刷新Token
const newToken = await refreshToken();
// 用新Token重试原始请求
originalRequest.headers['Authorization'] = `Bearer ${newToken}`;
// 处理队列中等待的请求
requestQueue.forEach(subscriber => subscriber(newToken));
requestQueue = [];
return instance(originalRequest);
} catch (refreshError) {
// 刷新失败处理
handleTokenExpiration();
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
}
// 如果正在刷新Token将请求加入队列
return new Promise((resolve, reject) => {
requestQueue.push((token) => {
originalRequest.headers['Authorization'] = `Bearer ${token}`;
resolve(instance(originalRequest));
});
});
}
return Promise.reject(error);
});
export default instance;
// Token刷新方法 (优化版)
async function refreshToken() {
try {
const username = localStorage.getItem('rememberedUsername');
const password = localStorage.getItem('rememberedPassword');
if (!username || !password) {
throw new Error('登录凭证已过期');
}
const response = await instance.post('Login/Login', {
username,
password
});
if (response.data.isok) {
const newToken = response.data.response.accessToken;
localStorage.setItem('token', newToken);
return newToken;
} else {
throw new Error('Token更新失败');
}
} catch (error) {
console.error('Token刷新失败:', error);
throw error;
}
}
// Token过期处理
function handleTokenExpiration() {
Toast({
theme: 'error',
direction: 'column',
message: '登录已过期,请重新登录',
});
setTimeout(() => {
localStorage.removeItem('token');
window.location.href = '/login';
}, 1000);
}

View File

@@ -0,0 +1,65 @@
<template>
<div class="slider-captcha">
<div class="slider-bg"></div>
<div class="slider-icon" :style="{ left: x + 'px' }" @mousedown="onDragStart"></div>
</div>
</template>
<script>
export default {
data() {
return {
x: 0, // 滑块的x坐标
startX: 0, // 鼠标按下时的x坐标
isDragging: false // 是否正在拖动滑块
}
},
methods: {
onDragStart(e) {
this.startX = e.clientX;
this.isDragging = true;
document.addEventListener('mousemove', this.onDragging);
document.addEventListener('mouseup', this.onDragEnd);
},
onDragging(e) {
if (this.isDragging) {
const offsetX = e.clientX - this.startX;
this.x = Math.max(0, Math.min(200, offsetX));
}
},
onDragEnd() {
if (this.isDragging) {
this.isDragging = false;
document.removeEventListener('mousemove', this.onDragging);
document.removeEventListener('mouseup', this.onDragEnd);
}
}
}
}
</script>
<style scoped>
.slider-captcha {
position: relative;
width: 200px;
height: 50px;
border: 1px solid #ccc;
overflow: hidden;
}
.slider-bg {
width: 200px;
height: 50px;
background-color: #f0f0f0;
}
.slider-icon {
position: absolute;
top: 0;
left: 0;
width: 50px;
height: 50px;
background-color: #3498db;
cursor: move;
}
</style>

View File

@@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
/>
</svg>
</template>

View File

@@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
<path
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
/>
</svg>
</template>

View File

@@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
<path
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
/>
</svg>
</template>

View File

@@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
/>
</svg>
</template>

View File

@@ -0,0 +1,19 @@
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
role="img"
class="iconify iconify--mdi"
width="24"
height="24"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 24 24"
>
<path
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
fill="currentColor"
></path>
</svg>
</template>

View File

@@ -0,0 +1,29 @@
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import config from '../public/config.js';
import axios from './axios' // 导入配置好的实例
import TDesign from 'tdesign-mobile-vue'
import './assets/main.css'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import 'element-plus/theme-chalk/dark/css-vars.css'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
const app = createApp(App)
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
// 挂载到全局属性
app.config.globalProperties.$http = axios
// 提供依赖注入
app.provide('config', config)
app.provide('$http', axios)
app.use(router)
app.use(TDesign)
app.use(ElementPlus, { locale: zhCn })
app.use(ElementPlus)
app.mount('#app')

View File

@@ -0,0 +1,556 @@
<template>
<div style="padding-bottom:5px">
<el-tag type="info">可编辑 / 总字段数</el-tag>
<span>&nbsp;</span>
<el-tag type="success">{{ tableNames.length }} / {{ allTableCount }}</el-tag>
<span>&nbsp;&nbsp;&nbsp;</span>
<el-select v-model="dbradio"
placeholder="选择数据表"
size="small"
@change="dbchange"
style="width: 120px">
<el-option v-for="item in dblist"
:key="item.value"
:label="item.label"
:value="item.value" />
</el-select>
</div>
<div>
<div class="container">
<el-table :data="tableNames" border style="width: 100%; z-index: 0;" empty-text="暂无可变更字段">
<el-table-column label="操作" width="75">
<template #default="scope">
<div style="display: flex; align-items: center">
<t-button variant="text"
shape="square"
size="extra-small"
v-if="!scope.row.del"
@click="openUpdateDialog(scope.row)">
<template #icon>
<t-icon size="20px" name="task-checked-1" v-if="scope.row.edit" />
<t-icon size="20px" name="edit" v-else />
</template>
</t-button>
<span v-if="!scope.row.del" style="font-size:10px">|</span>
<t-button variant="text"
shape="square"
size="extra-small"
v-if="!scope.row.del"
@click="openDelDialog(scope.row)">
<template #icon>
<t-icon size="20px" name="delete-1" />
</template>
</t-button>
<t-button variant="text"
shape="square"
size="extra-small"
v-if="scope.row.del"
@click="retrunDelDialog(scope.row)">
<template #icon>
<t-icon size="20px" name="rollback" />
</template>
</t-button>
</div>
</template>
</el-table-column>
<el-table-column label="字段名" width="123">
<template #default="scope">
<div v-if="scope.row.edit">
<t-input placeholder="字段名" v-model="scope.row.name" style="padding:1px;height:33px" borderless />
</div>
<div v-else-if="!scope.row.id" style="color:green"><strong>{{scope.row.name}}</strong></div>
<div v-else-if="scope.row.del" style="color:gray"><del>{{scope.row.name}}</del></div>
<div v-else-if="scope.row.old" style="color:blue"><u>{{scope.row.name}}</u></div>
<div v-else>{{scope.row.name}}</div>
</template>
</el-table-column>
<el-table-column label="备注" width="140">
<template #default="scope">
<div v-if="scope.row.edit">
<t-input placeholder="备注" v-model="scope.row.notes" style="padding:1px;height:33px" borderless />
</div>
<div v-else>
{{scope.row.notes}}
</div>
</template>
</el-table-column>
<el-table-column label="类型" width="100">
<template #default="scope">
<div v-if="scope.row.edit">
<!--<t-input placeholder="类型" v-model="scope.row.type" style="padding:1px;height:40px" borderless />-->
<el-select v-model="scope.row.type">
<el-option v-for="item in typeList" :key="item" :label="item" :value="item" placeholder="类型" />
</el-select>
</div>
<div v-else>
{{scope.row.type}}
</div>
</template>
</el-table-column>
<el-table-column label="长度" width="75">
<template #default="scope">
<div v-if="scope.row.edit">
<div v-if="scope.row.type == 'varchar'">
<t-input placeholder="长度" v-model="scope.row.long" style="padding:1px;height:33px" borderless type="number" />
</div>
<div v-else>\</div>
</div>
<div v-else>
<span v-if="scope.row.long">{{scope.row.long }}</span>
<span v-else>\</span>
</div>
</template>
</el-table-column>
</el-table>
</div>
<!-- 新增 悬浮按钮 -->
<t-fab :icon="iconFuncAdd"
draggable="true"
style="bottom: 100px;"
@click="openCreateDialog"
class="fab-button" />
<!-- <t-fab :icon="iconFuncSave"
draggable="true"
style="bottom: 45px;"
@click="saveDialog"
class="fab-button" />-->
<!-- 确认删除对话框 -->
<t-dialog v-model:visible="delDialogVisible"
cancel-btn="取消"
:confirm-btn="{ content: '确认删除', theme: 'danger' }"
@confirm="delRow">
<!--<div v-if="currentTable.id">是否确认加入删除清单可撤回</div>
<div v-if="!currentTable.id">是否确认永久删除不可撤销</div>-->
<div>是否确认永久删除不可撤销</div>
</t-dialog>
<t-dialog v-model:visible="newDialogVisible"
cancel-btn="取消"
:confirm-btn="{ content: '确认保存', theme: 'danger' }"
@confirm="saveDialog">
<!--<div v-if="currentTable.id">是否确认加入删除清单可撤回</div>
<div v-if="!currentTable.id">是否确认永久删除不可撤销</div>-->
<div>是否确认保存修改不可撤销</div>
</t-dialog>
<!-- 保存确认 -->
<t-dialog v-model:visible="editDialogVisible"
cancel-btn="取消"
:confirm-btn="{ content: '确认提交', theme: 'danger' }"
@confirm="saveChanges">
<div style="color:red;font-size:18px">操作不可逆, 是否确定提交?</div>
<br />
<!-- 新增 -->
<div>
<div style="color:blue" v-if="addList.length > 0">新增字段</div>
<!--<div style="color:blue" v-else>无新增字段</div>-->
<span v-for="item in addList" style="white-space: nowrap;">{{ item.name }}&nbsp;&nbsp;</span>
</div>
<!-- 修改 -->
<div>
<div style="color:green" v-if="updateList.length > 0">修改字段</div>
<!--<div style="color:green" v-else>无修改字段</div>-->
<span v-for="item in updateList" style="white-space: nowrap;">{{ item.name }}&nbsp;&nbsp;</span>
</div>
<!-- 删除 -->
<div>
<div style="color:gray" v-if="delList.length > 0">删除字段</div>
<!--<div style="color:gray" v-else>无删除字段</div>-->
<span v-for="item in delList" style="white-space: nowrap;">{{ item.name }}&nbsp;&nbsp;</span>
</div>
</t-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, watch, inject, h } from 'vue';
import { Toast } from 'tdesign-mobile-vue';
import { AddIcon, SaveIcon } from 'tdesign-icons-vue-next';
const $http = inject('$http')
// 初始化加载数据
onMounted(() => {
getTable()
});
const allTableCount = ref(0)
const dbradio = ref('tbl_snlist');
const dblist = ref([
{
value: "tbl_snlist",
label: "snlist"
},
{
value: "tbl_importinfo",
label: "importinfo"
}
])
const dbchange = (val) => {
dbradio.value = val
getTable()
}
const typeList = ref([
"varchar",
"int",
"tinyint",
"bigint",
"double",
"datetime",
"text",
])
const iconFuncAdd = () => h(AddIcon, { size: '24px' });
const iconFuncSave = () => h(SaveIcon, { size: '24px' });
const tableNames = ref([]);
var currentTable = reactive({
id: null,
name: '',
notes: '',
type: '',
long: null,
old: null,
del: false,
edit: false,
})
// 获取数据库表
const getTable = () => {
let requestBody = {
key: "getTable",
CmdType: "sys",
DBQueryData: [
{
key: "@DBName",
value: localStorage.getItem('selectdb'),
valueType: "string",
},
{
key: "@TBName",
value: dbradio.value,
valueType: "string",
}
]
}
const dbtb = localStorage.getItem('selectdb')
//console.log(requestBody)
$http.post('DataHandle/R_InfoCommon', requestBody)
.then(rs => {
//console.log(rs)
if (rs.data.isok) {
allTableCount.value = rs.data.response.length
tableNames.value = []
let tbexclude = 0
if (dbradio.value == 'tbl_snlist') {
tbexclude = 34
} else if (dbradio.value == 'tbl_importinfo') {
tbexclude = 9
}
rs.data.response.forEach((item, index) => {
if (index >= tbexclude) {
tableNames.value.push({
id: index + 1,
name: item.name,
type: item.type,
notes: item.notes,
long: Object.prototype.toString.call(item.long) === '[object Number]' ? item.long : null,
edit: false,
old: null,
del: false,
})
}
});
//console.log(tableNames.value)
} else {
Toast(rs.data.message)
}
})
.catch(error => {
// 处理错误情况
console.error(error);
})
.finally(() => {
})
}
// 确认保存
const saveDialog = () => {
addList.value = []
delList.value = []
updateList.value = []
tableNames.value.forEach((item, index) => {
if (item.id == null) { // 新增
addList.value.push(item)
//addUts(item)
} else if (item.del) { // 删除
delList.value.push(item)
//delUts(item)
} else if (item.old) { // 更新
updateList.value.push(item)
//updateUts(item)
}
});
editDialogVisible.value = true
}
const editDialogVisible = ref(false);
// 新增行
const openCreateDialog = () => {
tableNames.value.push({
id: null,
name: '',
type: 'varchar',
notes: '',
long: 255,
edit: true,
old: null,
del: false,
})
window.scrollTo({
top: document.body.scrollHeight,
behavior: 'smooth' // 可选,用于平滑滚动
});
setTimeout(() => {
window.scrollTo({
top: document.body.scrollHeight,
behavior: 'smooth' // 可选,用于平滑滚动
});
}, 233)
};
// 打开修改
const openUpdateDialog = (row) => {
//console.log(row)
if (row.id != null) {
if (row.edit) {
if (row.old.name == row.name && row.old.type == row.type && row.old.notes == row.notes && row.old.long == row.long) {
row.old = null
}
} else {
row.old = JSON.parse(JSON.stringify(row))
}
}
if (row.edit) {
newDialogVisible.value = true
}
// 切换编辑模式
row.edit = !row.edit;
};
const delDialogVisible = ref(false);
const newDialogVisible = ref(false);
// 打开删除确认
const openDelDialog = (row) => {
currentTable = {}
//console.log(row)
currentTable = row
delDialogVisible.value = true
row.edit = false
}
// 确认删除行
const delRow = () => {
currentTable.del = true
let delindex = -1
tableNames.value.forEach((item, index) => {
if (currentTable == item && currentTable.id == null) {
delindex = index
}
});
if (delindex != -1) {
tableNames.value.splice(delindex, 1); // 使用 splice 来删除元素
}
saveDialog()
}
// 撤回删除行
const retrunDelDialog = (row) => {
row.del = false
}
const updateList = ref([])
const addList = ref([])
const delList = ref([])
// 保存修改
const saveChanges = (row) => {
// 这里实现保存逻辑,发送请求到服务器
addList.value.forEach(item => {
addUts(item)
})
delList.value.forEach(item => {
delUts(item)
})
updateList.value.forEach(item => {
updateUts(item)
})
Toast('操作完成!')
tableNames.value = []
setTimeout(() => {
getTable()
}, 555)
}
// 新增
const addUts = (item) => {
//console.log(item)
let requestBody = {
key: "addTable",
CmdType: "sys",
DBQueryData: [{
key: "@dbName",
value: localStorage.getItem('selectdb'),
valueType: "string",
}, {
key: "@TBName",
value: dbradio.value,
valueType: "string",
}, {
key: "@name",
value: item.name,
valueType: "string",
}, {
key: "@type",
value: item.type,
valueType: "string",
}, {
key: "@long",
value: item.long ? "(" + item.long + ")" : "",
valueType: "string",
}, {
key: "@notes",
value: item.notes,
valueType: "string",
},]
}
$http.post('DataHandle/CUD_InfoCommon', requestBody)
.then(rs => {
//console.log(rs)
if (rs.data.isok) {
} else {
Toast(rs.data.message)
}
})
.catch(error => {
// 处理错误情况
console.error(error);
})
}
// 删除
const delUts = (item) => {
let requestBody = {
key: "deleteTable",
CmdType: "sys",
DBQueryData: [{
key: "@dbName",
value: localStorage.getItem('selectdb'),
valueType: "string",
}, {
key: "@TBName",
value: dbradio.value,
valueType: "string",
}, {
key: "@name",
value: item.name,
valueType: "string",
}]
}
$http.post('DataHandle/CUD_InfoCommon', requestBody)
.then(rs => {
//console.log(rs)
if (rs.data.isok) {
} else {
Toast(rs.data.message)
}
})
.catch(error => {
// 处理错误情况
console.error(error);
})
}
// 更新
const updateUts = (item) => {
let requestBody = {
key: "updateTable",
CmdType: "sys",
DBQueryData: [{
key: "@dbName",
value: localStorage.getItem('selectdb'),
valueType: "string",
}, {
key: "@TBName",
value: dbradio.value,
valueType: "string",
}, {
key: "@name",
value: item.name,
valueType: "string",
}, {
key: "@oldName",
value: item.old.name,
valueType: "string",
}, {
key: "@type",
value: item.type,
valueType: "string",
}, {
key: "@long",
value: item.type == 'varchar' ? (item.long ? '(' + item.long + ')' : '(255)') : '',
valueType: "string",
}, {
key: "@notes",
value: item.notes,
valueType: "string",
},]
}
$http.post('DataHandle/CUD_InfoCommon', requestBody)
.then(rs => {
//console.log(rs)
if (rs.data.isok) {
} else {
Toast(rs.data.message)
}
})
.catch(error => {
// 处理错误情况
console.error(error);
})
}
</script>
<style scoped>
.container {
min-height: 85vh;
}
.fab-button {
position: fixed;
right: 16px;
}
/* PC端样式 */
@media (min-width: 1024px) {
.container {
margin-top: 5px;
}
}
</style>

View File

@@ -0,0 +1,41 @@
<template>
<div>Home</div>
</template>
<script setup>
import { ref, inject, onMounted } from 'vue';
const $http = inject('$http')
// 注入方法
const checkLoginStatus = inject('checkLoginStatus');
checkLoginStatus();
onMounted(() => {
getUserInfo();
})
// 获取用户登录信息并保存日志
const getUserInfo = async () => {
let requestBody = {
"UserName": localStorage.getItem('username'),
"Database": localStorage.getItem('selectdb'),
}
$http.post('Login/LogRecord', requestBody)
.then(rs => {
//console.log(rs)
if (rs.data.isok) {
} else {
console.log(rs.data.message)
}
})
.catch(error => {
// 处理错误情况
console.error(error);
})
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,172 @@
<template>
<div class="bodyclass">
<el-select v-model="nowModel"
placeholder="选择机型"
style="width: 150px;">
<el-option v-for="item in models"
:key="item.value"
:label="item.label"
:value="item.value" />
</el-select>
<span>&nbsp;</span>
<el-button type="info" @click="handleImportClick" v-show="nowModel">导入Excel</el-button>
<input type="file" ref="fileInput" @change="handleFileChange" style="display: none;" accept=".xlsx, .xls" />
<span>&nbsp;</span>
<el-tag type="success" closable @close="tagClose(tag)" v-if="fileName" size="large">
{{ fileName }}
</el-tag>
<span>&nbsp;</span>
<el-button type="danger" @click="" v-show="fileDate.length > 0">保存到数据库</el-button>
</div>
<div style="min-height: 85vh; margin-top: 3px;">
<el-table :data="fileDate" style="width: 100%">
<el-table-column v-for="header in headers"
:key="header"
:prop="header"
:label="header"
width="180">
<template #header>
<span :style="{ color: !tableNames.includes(header) ? 'red' : 'green' }">
{{ header }}
</span>
</template>
<template #default="scope">
<span :style="{ color: !tableNames.includes(header) ? 'red' : 'green' }">
{{ scope.row[header] }}
</span>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script setup>
import { ref, inject, onMounted } from 'vue';
import { Toast } from 'tdesign-mobile-vue';
import * as XLSX from 'xlsx';
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
const $http = inject('$http')
const value = ref('')
const models = ref([
{
value: 'models1',
label: 'models1',
},
{
value: 'models2',
label: 'models2',
},
{
value: 'models3',
label: 'models3',
}
])
const nowModel = ref('')
const fileInput = ref(null)
const headers = ref([])
const fileDate = ref([])
const fileName = ref('')
const handleImportClick = () => {
fileInput.value.click()
}
const handleFileChange = async (e) => {
const file = e.target.files[0]
if (!file) return
try {
// 读取Excel文件
const data = await file.arrayBuffer()
const workbook = XLSX.read(data, { type: 'array' })
const worksheet = workbook.Sheets[workbook.SheetNames[0]]
const jsonData = XLSX.utils.sheet_to_json(worksheet)
headers.value = Object.keys(jsonData[0])
fileDate.value = jsonData
// 这里可以处理解析后的数据,例如:
// 1. 发送到服务器
//await $http.post('/api/upload', jsonData)
// 2. 或者更新models数据示例
// models.value = jsonData.map(item => ({
// value: item.模型ID,
// label: item.模型名称
// }))
fileName.value = file.name
console.log('导入成功:', jsonData)
Toast('文件导入成功')
} catch (error) {
console.error('导入失败:', error)
Toast('文件导入失败')
} finally {
e.target.value = '' // 清空选择允许重复上传
}
}
const tableNames = ref([])
const tagClose = () => {
fileDate.value = []
fileName.value = ''
}
// 获取数据库表
const getTable = () => {
let requestBody = {
key: "getTable",
CmdType: "sys",
DBQueryData: [
{
key: "@DBName",
value: localStorage.getItem('selectdb'),
valueType: "string",
},
{
key: "@TBName",
value: "tbl_importinfo",
valueType: "string",
}
]
}
$http.post('DataHandle/R_InfoCommon', requestBody)
.then(rs => {
//console.log(rs)
if (rs.data.isok) {
tableNames.value = []
rs.data.response.forEach((item, index) => {
tableNames.value.push(item.name)
});
//console.log(tableNames.value)
} else {
Toast(rs.data.message)
}
})
.catch(error => {
// 处理错误情况
console.error(error);
})
.finally(() => {
})
}
// 初始化加载数据
onMounted(() => {
getTable()
});
</script>
<style scoped>
@media (min-width: 1024px) {
.bodyclass {
margin-top: 5px
}
}
</style>

View File

@@ -0,0 +1,457 @@
<template>
<div class="container">
<!-- 顶部导航栏 -->
<t-navbar class="custom-navbar">
<!-- 左侧区域 -->
<template #left>
<t-button variant="text"
shape="square"
@click="handleLeftClick('home')">
<template #icon>
<t-icon size="23px" name="home" />
</template>
</t-button>
<!-- 控制页面路由菜单 -->
<t-dropdown-menu ref="parentRef" style="height:32px;border:none">
<t-dropdown-item :value="currentTitle.routes" :label="currentTitle.label">
<div v-for="(group,i) in menuValue" :key="i" >
<div class="demo-dropdown-item_label" v-if="group.label != '管理' || (username == 'MomoWen' || username == 'Admin')">
{{ group.label }}
</div>
<t-radio-group @change="menuChange" :key="i" v-if="group.label != '管理' || (username == 'MomoWen' || username == 'Admin')">
<view v-for="(item, index) in group.options" :key="item.value" :class="`card ${currentTitle.routes == item.value ? 'card--active' : ''}`">
<icon v-if="currentTitle.routes == item.value" name="check" class="card__icon" />
<t-radio :value="item" :label="item.label" icon="none" borderless />
</view>
</t-radio-group>
</div>
</t-dropdown-item>
</t-dropdown-menu>
<!-- 数据库控制菜单 -->
<t-dropdown-menu style="height:32px;border:none">
<t-dropdown-item :options="product.options" :value="product.value" @change="databaseChanges" :disabled="disDbDrop" />
</t-dropdown-menu>
</template>
<!-- 右侧区域 -->
<template #right>
<t-popover v-model="showPopover"
trigger="click"
placement="bottom-right"
:overlay-style="{ width: '120px' }">
<t-avatar class="avatar-example--small external-class-content" shape="circle" size="33px">{{ username[0] }}</t-avatar>
<template #content>
<t-button size="large" theme="primary" variant="text" @click="handleAction('profile')">个人中心</t-button>
<t-button size="large" theme="danger" variant="text" @click="handleAction('logout')">退出登录</t-button>
</template>
</t-popover>
<t-button class="theme-toggle" @click="toggleTheme" size="33px">
{{ isDarkMode ? '🌞' : '🌙' }}
</t-button>
</template>
</t-navbar>
<!-- 导航栏 -->
<!-- 页面内容 -->
<!--<router-view></router-view>-->
</div>
</template>
<script setup>
import { ref, computed, onMounted, reactive, inject } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { Toast } from 'tdesign-mobile-vue'
import { useDark, useToggle } from '@vueuse/core'
const isDark = useDark()
const toggleDark = useToggle(isDark)
const router = useRouter()
const route = useRoute()
const showSidebar = ref(false)
const $http = inject('$http')
const username = localStorage.getItem('username')
const product = reactive({
value: '',
options: [],
})
// 菜单值
const menuValue = reactive([
{
label: "控制台",
options: [
{
label: '主页',
value: '/home',
disabled: true,
},
],
},
{
label: "数据",
options: [
{
label: '测试数据',
value: '/testlog',
disabled: true,
},
],
},
{
label: "管理",
options: [
{
label: '导入数据',
value: '/inputcolorbox',
disabled: true,
},
{
label: '用户管理',
value: '/usermanage',
disabled: true,
},
{
label: '数据库管理',
value: '/dbmanage',
disabled: true,
},
{
label: '用户日志',
value: '/loginlog',
disabled: true,
}
],
},
])
const currentTitle = reactive({
label: '主页',
routes: '/home',
}) // 当前页面标题
const showPopover = ref(false) // 气泡菜单显示状态
// 左侧按钮点击处理
const handleLeftClick = (type) => {
// type 可能的值: 'home' 按钮
switch (type) {
case 'home':
let hm = {
"label": "主页",
"value": "/home",
"disabled": true
}
menuChange(hm)
router.push('/home')
break;
default:
console.log('跳转错误!');
break;
}
}
// 菜单操作处理
const handleAction = (action) => {
// action 可能的值: 'profile' 个人中心 / 'logout' 退出登录
showPopover.value = false
switch (action) {
case 'profile':
disDbDrop.value = true
router.push('/user')
break;
case 'logout':
router.push('/login')
break;
default:
console.log('跳转错误!');
break;
}
}
const disDbDrop = ref(true)
// 数据库变更处理
const databaseChanges = (type) => {
if (type != localStorage.getItem('selectdb')) {
Toast({
className: 'toast-root--success',
theme: 'success',
direction: 'column',
message: '数据库切换为:' + type,
});
localStorage.setItem('selectdb', type)
setTimeout(() => {
location.reload(true);
}, 666);
}
}
const parentRef = ref(null)
// 菜单处理
const menuChange = (val) => {
currentTitle.label = val.label;
currentTitle.routes = val.value;
//console.log(val)
if (val.value != '/home') {
disDbDrop.value = true
} else {
disDbDrop.value = false
}
parentRef.value.collapseMenu();
router.push(val.value)
}
// 判断是否管理员
const isAdmin = (page) => {
return (page.label == "管理" && (username == 'MomoWen' || username == 'Admin'))
}
// 控制深色模式
const isDarkMode = ref(false)
const webToDark = (dark) => {
// 获取 <html> 元素的类名
let ishtmldark = null;
if (document.documentElement.className.indexOf('dark') !== -1) {
ishtmldark = false
} else {
ishtmldark = true
}
if (dark) {
document.documentElement.setAttribute('theme-mode', 'dark')
isDarkMode.value = true
if (ishtmldark) {
toggleDark()
}
} else {
document.documentElement.removeAttribute('theme-mode')
isDarkMode.value = false
if (!ishtmldark) {
toggleDark()
}
}
}
const toggleTheme = () => {
isDarkMode.value = !isDarkMode.value
let root = document.documentElement;
if (!isDarkMode.value) {
// 如果当前是暗色主题,切换到亮色主题
root.style.setProperty('--color-background', '#ffffff');
root.style.setProperty('--color-background-soft', '#f8f8f8');
root.style.setProperty('--color-background-mute', '#f2f2f2');
root.style.setProperty('--color-border', 'rgba(60, 60, 60, 0.12)');
root.style.setProperty('--color-border-hover', 'rgba(60, 60, 60, 0.29)');
root.style.setProperty('--color-heading', '#2c3e50');
root.style.setProperty('--color-text', '#2c3e50');
root.setAttribute('data-theme', 'light');
if (root.className.indexOf('dark') !== -1) {
toggleDark()
}
setTimeout(() => {
root.removeAttribute('theme-mode');
}, 20);
} else {
// 如果当前是亮色主题,切换到暗色主题
root.style.setProperty('--color-background', '#242424');
root.style.setProperty('--color-background-soft', '#222222');
root.style.setProperty('--color-background-mute', '#282828');
root.style.setProperty('--color-border', 'rgba(84, 84, 84, 0.48)');
root.style.setProperty('--color-border-hover', 'rgba(84, 84, 84, 0.65)');
root.style.setProperty('--color-heading', '#ffffff');
root.style.setProperty('--color-text', 'rgba(235, 235, 235, 0.64)');
root.setAttribute('data-theme', 'dark');
if (!(root.className.indexOf('dark') !== -1)) {
toggleDark()
}
setTimeout(() => {
root.setAttribute('theme-mode', 'dark');
}, 20);
}
}
var alreadyLogin = ref(false)
const checkLoginStatus = () => {
alreadyLogin.value = localStorage.getItem('login') == 'true'
}
// 获取服务器信息
const onDbLogout = () => {
let requestBody = {
key: "getDbName",
CmdType: "sys",
DBQueryData: []
}
$http.post('DataHandle/R_InfoCommon', requestBody)
.then(rs => {
//console.log(rs)
if (rs.data.isok) {
product.options = []
rs.data.response.forEach(item => {
if (item.Database.slice(0, 3) == "uts" && item.Database != "uts_manage") {
let dbname = {
label: item.Database,
value: item.Database
}
product.options.push(dbname)
}
})
} else {
Toast(rs.data.message)
}
})
.catch(error => {
// 处理错误情况
console.error(error);
})
.finally(() => {
})
}
// 初始化
onMounted(() => {
// 初始检查登录状态
checkLoginStatus()
// 初始化数据库列表
onDbLogout()
if (localStorage.getItem('selectdb')) {
product.value = localStorage.getItem('selectdb')
} else {
product.value = "uts_blw"
localStorage.setItem('selectdb', product.value)
}
findLabelByValue();
// 主题初始化
const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)')
const handleColorSchemeChange = (e) => {
webToDark(e.matches)
}
const url = window.location.href;
// 找到最后一个 '/' 的位置
const lastSlashIndex = url.lastIndexOf('/');
// 获取最后一个 '/' 后面的内容
const lastSegment = url.substring(lastSlashIndex);
if (lastSegment == '/home') {
disDbDrop.value = false
}
// 初始设置
webToDark(darkModeQuery.matches)
// 监听系统主题变化
darkModeQuery.addEventListener('change', handleColorSchemeChange)
// 清理监听器
return () => {
darkModeQuery.removeEventListener('change', handleColorSchemeChange)
}
})
// 确定当前地址
const findLabelByValue = () => {
const url = window.location.href;
// 找到最后一个 '/' 的位置
const lastSlashIndex = url.lastIndexOf('/');
// 获取最后一个 '/' 后面的内容
const lastSegment = url.substring(lastSlashIndex);
for (const menu of menuValue) {
for (const option of menu.options) {
if (option.value == lastSegment) {
currentTitle.routes = lastSegment
currentTitle.label = option.label
}
}
}
}
</script>
<style scoped>
/* 主题切换按钮 */
.theme-toggle {
margin-left: 8px;
padding: 8px 12px;
background: var(--toggle-bg);
color: var(--toggle-text);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
}
/* 横向布局 */
.t-radio--block {
padding: 3px 10px 3px 10px;
}
.horizontal-box {
width: calc(100% - 32px);
display: flex;
align-items: center;
margin: 16px;
}
.horizontal-box .card {
flex: 1;
margin: 0;
}
.horizontal-box .card + .card {
margin-left: 12px;
}
.demo-desc {
font-size: 12px;
color: rgba(0, 0, 0, 0.6);
margin-bottom: 16px;
}
.card {
display: block;
position: relative;
margin: 8px;
border-radius: 6px;
overflow: hidden;
box-sizing: border-box;
border: 1.5px solid #fff;
height: 35px;
}
.card--active {
border-color: #0052d9;
}
.card--active::after {
content: '';
display: block;
position: absolute;
left: 0;
top: 0;
width: 0;
border: 8px solid #0052d9;
border-bottom-color: transparent;
border-right-color: transparent;
}
.card__icon {
display: block;
color: #fff;
position: absolute;
left: 1.5px;
top: 1.5px;
z-index: 10;
font-size: 14px;
}
</style>

View File

@@ -0,0 +1,427 @@
<template>
<h1 style="margin:100px 0px 100px 0px; text-align:center">登录AUTS</h1>
<div class="login-container">
<t-form @submit="handleSubmit">
<t-input v-model="form.username"
label="账号"
placeholder="请输入账号"
@focus="MoveToEnd"
clearable />
<t-input v-model="form.password"
label="密码"
placeholder="请输入密码"
@focus="MoveToEnd"
type="password"
clearable />
<t-input v-model="form.code"
maxlength="4"
:placeholder="captchaSrc ? '请输入验证码' : '点击获取验证码'"
label="验证码"
@focus="getCaptchaSrc"
clearable>
<template #suffix>
<div class="suffix" v-if="captchaSrc">
<t-image class="image"
:src="captchaSrc"
@click="generateCaptcha"
mode="heightFix"
aria-role="img"
aria-label="验证码" />
</div>
</template>
</t-input>
<t-checkbox v-model="form.remember">记住密码</t-checkbox>
<t-button v-if="!isLocked"
theme="primary"
type="submit"
block
:loading="loading">登录</t-button>
<t-button v-if="isLocked"
theme="danger"
type="submit"
ghost
disabled>密码错误账号已锁定</t-button>
</t-form>
</div>
</template>
<script setup>
import { inject, ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { Toast } from 'tdesign-mobile-vue'
const router = useRouter()
// 存储数据
//sessionStorage.setItem('test', 'a123');
//sessionStorage.getItem('username');
//sessionStorage.removeItem('username');
const successMethod = () => {
console.log('验证成功')
}
const form = ref({
username: '',
password: '',
remember: false,
code: ''
})
// 状态定义
const isDarkMode = ref(false)
const loading = ref(false)
const submitDisabled = ref(false) // 提交禁用状态
const isLocked = ref(false) // 账号锁定状态
const errorAttempts = ref(0) // 错误尝试次数
const $http = inject('$http')
const LOCK_STORAGE_KEY = 'loginLockUntil' // 锁定时间存储键
const ATTEMPTS_STORAGE_KEY = 'loginErrorAttempts'
const captchaSrc = ref('')
const captchaValue = ref('')
/*localStorage.removeItem('token')*/
const checkLoginStatus = inject('checkLoginStatus');
checkLoginStatus();
// 初始化认证状态
const initAuthState = () => {
const storedLockUntil = localStorage.getItem(LOCK_STORAGE_KEY)
const storedAttempts = localStorage.getItem(ATTEMPTS_STORAGE_KEY)
errorAttempts.value = parseInt(storedAttempts) || 0
if (storedLockUntil) {
const remainingTime = parseInt(storedLockUntil) - Date.now()
if (remainingTime > 0) {
isLocked.value = true
setupUnlockTimer(remainingTime)
} else {
// 清理过期数据
localStorage.removeItem(LOCK_STORAGE_KEY)
localStorage.removeItem(ATTEMPTS_STORAGE_KEY)
errorAttempts.value = 0
}
}
}
// 设置解锁定时器
const setupUnlockTimer = (duration) => {
isLocked.value = true
const timer = setTimeout(() => {
isLocked.value = false
errorAttempts.value = 0
localStorage.removeItem(LOCK_STORAGE_KEY)
localStorage.removeItem(ATTEMPTS_STORAGE_KEY)
clearTimeout(timer)
}, duration)
}
// 初始化认证状态
onMounted(() => {
//mountedToken()
initAuthState()
localStorage.setItem('login', false)
getCaptchaSrc()
});
const mountedToken = () => {
$http.post('Login/Helloooo', '')
.then(rs => {
//console.log(rs)
if (rs.data.isok) {
router.push('/home')
/*Toast({
className: 'toast-root--success',
theme: 'success',
direction: 'column',
message: '登录成功!',
});*/
} else {
Toast(rs.data.message)
}
})
.catch(error => {
// 处理错误情况
console.error(error);
})
.finally(() => {
})
}
// 新增深色模式状态
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
isDarkMode.value = true;
} else {
isDarkMode.value = false;
}
const clearUsername = () => {
form.value.username = ''
}
// 滚动页面
const MoveToEnd = () => {
// 滚动到页面底部
window.scrollTo({
top: document.body.scrollHeight,
behavior: 'smooth' // 可选,用于平滑滚动
});
}
const getCaptchaSrc = () => {
if (captchaValue.value == "") {
generateCaptcha()
}
MoveToEnd()
}
// 生成验证码
const generateCaptcha = () => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const chars = '0123456789';
let captcha = '';
canvas.width = 86;
canvas.height = 30;
// 生成验证码字符串
for (let i = 0; i < 4; i++) {
captcha += chars.charAt(Math.floor(Math.random() * chars.length));
}
captchaValue.value = captcha; // 存储验证码值用于验证
// 绘制背景
ctx.fillStyle = isDarkMode.value ? '#242424' : '#FFFFFF';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 绘制噪点
ctx.fillStyle = isDarkMode.value ? '#FFFFFF' : '#000000';
for (let i = 0; i < 33; i++) {
const x = Math.random() * canvas.width;
const y = Math.random() * canvas.height;
ctx.beginPath();
ctx.arc(x, y, 1, 0, Math.PI * 2, false);
ctx.fill();
}
// 绘制验证码
ctx.font = '24px Arial';
let xnd = 10;
for (let i = 0; i < captcha.length; i++) {
const angle = (Math.random() * 30) - 15; // -10度到+10度
// 保存当前状态
ctx.save();
// 旋转并绘制文字
ctx.translate(xnd, 20);
ctx.rotate(angle * Math.PI / 180);
ctx.fillText(captcha[i], 0, 0);
// 恢复状态
ctx.restore();
// 更新下一个字符的x位置
xnd += 15 + Math.abs(angle) / 2; // 根据角度调整字符间距
}
// 添加5-10条干扰条纹
ctx.strokeStyle = '#6F4A2F';
const bug = Math.floor(Math.random() * 6) + 3
for (let i = 0; i < bug; i++) {
ctx.beginPath();
ctx.moveTo(Math.random() * canvas.width, Math.random() * canvas.height);
ctx.lineTo(Math.random() * canvas.width, Math.random() * canvas.height);
ctx.stroke();
}
// 将canvas转换为图片URL
captchaSrc.value = canvas.toDataURL('image/png');
}
// 验证验证码
const verifyCaptcha = (inputValue) => {
// 验证输入的验证码是否正确
return inputValue === captchaValue.value;
}
// 登录
const handleSubmit = () => {
if (isLocked.value || submitDisabled.value) return
if (!form.value.username) {
Toast('请输入账号!')
return
}
if (!form.value.password) {
Toast('请输入密码!')
return
}
if (!form.value.code) {
Toast('请输入验证码!')
return
}
if (!verifyCaptcha(form.value.code)) {
Toast('验证码错误!')
generateCaptcha()
return
}
loading.value = true
submitDisabled.value = true
const formdata = {
username: form.value.username,
password: form.value.password
}
$http.post('Login/Login', formdata)
.then(rs => {
//console.log(rs)
if (rs.data.isok) {
// 登录成功处理...
errorAttempts.value = 0
localStorage.removeItem(ATTEMPTS_STORAGE_KEY)
localStorage.removeItem(LOCK_STORAGE_KEY)
// 存储token
localStorage.setItem('token', rs.data.response.accessToken)
localStorage.setItem('username', rs.data.response.userName)
localStorage.setItem('login', true)
localStorage.setItem('uid', rs.data.response.id)
// 记住账号和密码
if (form.value.remember) {
localStorage.setItem('rememberedUsername', form.value.username)
localStorage.setItem('rememberedPassword', form.value.password)
} else {
localStorage.removeItem('rememberedPassword')
}
Toast({
className: 'toast-root--success',
theme: 'success',
direction: 'column',
message: '登录成功,欢迎:' + rs.data.response.userName + '',
});
router.push('/home')
} else {
Toast(rs.data.message)
errorAttempts.value++
localStorage.setItem(ATTEMPTS_STORAGE_KEY, errorAttempts.value)
if (errorAttempts.value >= 5) {
const lockUntil = Date.now() + 3600000
localStorage.setItem(LOCK_STORAGE_KEY, lockUntil)
setupUnlockTimer(3600000)
}
}
})
.catch(error => {
// 处理错误情况
console.error(error);
})
.finally(() => {
setTimeout(() => {
loading.value = false
}, 300)
setTimeout(() => {
submitDisabled.value = false
}, 30000)
})
}
// 初始化记住的账号和密码
form.value.username = localStorage.getItem('rememberedUsername') || ''
form.value.password = localStorage.getItem('rememberedPassword') || ''
form.value.remember = form.value.username !== '' && form.value.password !== ''
</script>
<style scoped>
.login-container {
padding: 16px;
}
/* 基础样式 */
.container {
min-height: 100vh;
padding: 40px 16px;
transition: background-color 0.3s, color 0.3s;
background-color: var(--bg-color);
color: var(--text-color);
}
.title {
text-align: center;
margin-bottom: 40px;
font-size: 24px;
color: var(--primary-color);
}
.login-container {
max-width: 400px;
margin: 0 auto;
padding: 24px;
background: var(--card-bg);
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
/* 输入框间距优化 */
.t-form {
display: flex;
margin: 18px 10px 10px 10px;
flex-direction: column;
gap: 25px;
}
/* 按钮样式优化 */
.t-button {
margin-top: 5px;
height: 40px;
font-weight: 400;
}
.t-checkbox--block {
padding: 0;
}
/* 主题切换按钮 */
.theme-toggle {
position: fixed;
top: 16px;
right: 16px;
padding: 8px 16px;
background: var(--toggle-bg);
color: var(--toggle-text);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
z-index: 100;
}
/* 响应式设计 */
@media (max-width: 480px) {
.container {
padding: 24px 12px;
}
.login-container {
padding: 5px;
border-radius: 8px;
}
.title {
font-size: 20px;
margin-bottom: 32px;
}
}
@media (min-width: 768px) {
.login-container {
padding: 5px;
border-radius: 16px;
}
.title {
font-size: 28px;
}
}
</style>

View File

@@ -0,0 +1,164 @@
<template>
<div class="tagclass" style="display:inline;">
<el-tag type="info">总日志数</el-tag>
<span>&nbsp;</span>
<el-tag type="success">{{ allLogsCount }}</el-tag>
<span>&nbsp;</span>
<el-input-number v-model="nowPage" min="1" :max="maxPage" size="small"
step-strictly="true"
@change="onChangeStepper">
<template #suffix>
<span>/{{ maxPage }}</span>
</template>
</el-input-number>
</div>
<t-list>
<div v-for="user in logs" :key="user.ID" class="user-list-div"
style="position: relative; width: auto; padding: 3px; height:48px; border-radius: 3px; margin: 2px;">
<span class="user-name" style="position: absolute; top: 3px; left: 4px; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
{{ user.ID }}:
<span style="font-size: 14px; font-weight: 500">{{ user.UserName }} | {{ user.Database }}</span>
</span>
<span class="user-ip" style="position: absolute; top: 25px; left: 4px; font-size: 12px; "></span>
<span class="user-time" style="position: absolute; bottom: 3px; left: 4px; font-size: 11px; ">{{ user.Ip }} | {{ user.Location }}</span>
<span class="user-location" style="position: absolute; top: 3px; right: 4px; font-size: 11px; ">
<span>
<icon name="logo-android-filled" v-if="user.Device == 'Android'" />
<icon name="logo-windows-filled" v-else-if="user.Device == 'windows'" />
<icon name="logo-apple-filled" v-else-if="user.Device == 'ios' || user.Device == 'Mac'" />
<icon name="help-circle-filled" v-else />
<!-- {{user.Device}}-->
</span>
| {{ user.Browser }}
</span>
<span class="user-location" style="position: absolute; bottom: 3px; right: 4px; font-size: 11px; ">
{{ user.CreationTime }}
</span>
</div>
</t-list>
</template>
<script setup>
import { ref, reactive, onMounted, inject } from 'vue';
import { Toast } from 'tdesign-mobile-vue';
import { Icon } from 'tdesign-icons-vue-next';
const $http = inject('$http')
onMounted(() => {
getPage();
loading("");
})
const maxPage = ref(1)
const nowPage = ref(1)
const allLogsCount = ref(null)
const logs = ref([])
const lastId = ref(0)
// 翻页
const onChangeStepper = (value) => {
if (value == 1) {
nowPage.value = 1
lastId.value = 0
loading('')
} else {
nowPage.value = value
loading(lastId.value)
}
}
const getPage = () => {
let requestBody = {
key: "getLoginLogCount",
CmdType: "sys",
DBQueryData: [],
}
$http.post('DataHandle/R_InfoCommon', requestBody)
.then(rs => {
//console.log(rs)
if (rs.data.isok) {
allLogsCount.value = rs.data.response[0].LogCount
maxPage.value = Math.ceil(rs.data.response[0].LogCount / 25)
} else {
Toast(rs.data.message)
}
})
.catch(error => {
// 处理错误情况
console.error(error);
})
}
// 加载页面
const loading = (last_id) => {
let requestBody = {
key: "getLoginLog",
CmdType: "",
DBQueryData: [],
isFunction: true,
FunParameters: [last_id == 0 ? "" : "" + last_id]
}
//console.log(JSON.stringify(requestBody))
$http.post('DataHandle/R_InfoCommon', JSON.stringify(requestBody))
.then(rs => {
//console.log(rs)
if (rs.data.isok) {
logs.value = rs.data.response.map(item => {
return {
...item,
CreationTime: formatDateTime(item.CreationTime)
}
})
if (rs.data.response.length == 25) {
lastId.value = rs.data.response[24].ID
}
//console.log(lastId.value)
} else {
Toast(rs.data.message)
}
})
.catch(error => {
// 处理错误情况
console.error(error);
})
}
const formatDateTime = (isoString) => {
const date = new Date(isoString);
// 使用padStart确保时间部分是两位数
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, '0'); // 月份是从0开始的
const day = date.getDate().toString().padStart(2, '0');
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
const seconds = date.getSeconds().toString().padStart(2, '0');
// 拼接成指定的格式
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
</script>
<style scoped>
.user-list-div {
border: 1px solid #E9E9E960;
background-color: #80808030;
transition: all 0.3s ease;
}
.user-list-div:hover {
border-color: #409eff;
background-color: #80808010;
/* box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); 可选阴影 */
}
@media (min-width: 1024px) {
.tagclass {
margin-top: 5px
}
}
</style>

View File

@@ -0,0 +1,566 @@
<template>
<div class="query-container">
<!-- 查询条件区域 -->
<el-collapse v-model="activeCollapse" class="collapse-panel">
<el-collapse-item title="基础条件" name="queryConditions">
<div class="condition-section">
<el-form :model="queryForm" class="condition-form" size="small">
<div class="condition-row">
<!-- 机型下拉框 -->
<el-form-item label="机型" class="condition-item">
<el-select v-model="queryForm.model"
placeholder="全部机型"
style="width:211px;"
clearable>
<el-option v-for="item in machineModels"
:key="item.value"
:label="item.label"
:value="item.value" />
</el-select>
</el-form-item>
<!-- 当机型不为空时显示其他条件 -->
<template v-if="queryForm.model">
<!-- 条形码输入 -->
<el-form-item label="条码" class="condition-item">
<el-input v-model="queryForm.barcode"
placeholder="请输入条形码"
style="width:211px;"
clearable />
</el-form-item>
<!-- 日期范围选择 -->
<el-form-item label="日期" class="condition-item">
<div class="date-range">
<el-date-picker v-model="queryForm.startDate"
type="date"
style="width:120px;"
placeholder="开始日期"
value-format="YYYY-MM-DD" />
<span>&nbsp;-&nbsp;</span>
<el-date-picker v-model="queryForm.endDate"
type="date"
style="width:120px;"
placeholder="结束日期"
value-format="YYYY-MM-DD" />
</div>
</el-form-item>
<!-- 查询条件选择 -->
<el-form-item label="条件" class="condition-item">
<el-select v-model="queryForm.queryType"
style="width:211px;"
placeholder="请选择">
<el-option label="BARCODE" value="BARCODE" />
<!-- 其他选项根据实际接口返回添加 -->
</el-select>
</el-form-item>
<!-- 当条件选择不为空时显示模糊匹配和数值范围 -->
<template v-if="queryForm.queryType">
<!-- 模糊匹配 - 仅在数值范围没有值时显示 -->
<el-form-item v-if="!hasNumberRangeValue"
label="模糊"
class="condition-item">
<el-input v-model="queryForm.keyword"
style="width:211px;"
placeholder="输入关键词" />
</el-form-item>
<!-- 数值范围 - 仅在模糊匹配没有值时显示 -->
<el-form-item v-if="!hasKeywordValue"
label="数值"
class="condition-item">
<div class="number-range">
<el-input v-model="queryForm.minValue"
style="width:100px;"
placeholder="最小值" />
<span>&nbsp;-&nbsp;</span>
<el-input v-model="queryForm.maxValue"
style="width:100px;"
placeholder="最大值" />
</div>
</el-form-item>
</template>
</template>
</div>
</el-form>
</div>
</el-collapse-item>
</el-collapse>
<!-- 勾选参数区域 -->
<el-collapse v-model="activeCollapse" class="collapse-panel">
<el-collapse-item title="选择参数" name="selectParams">
<div class="param-section">
<div class="param-header">
<el-checkbox v-model="allParamsSelected" size="small"
@change="handleSelectAll">
全选
</el-checkbox>
<el-checkbox v-model="resParamsSelected" size="small"
@change="handleSelectAll">
选择Results
</el-checkbox>
</div>
<div class="param-list">
<el-checkbox-group v-model="selectedParams" size="small">
<div class="param-grid">
<el-checkbox v-for="param in availableParams"
:key="param.value"
:label="param.value"
class="param-item">
{{ param.label }}
</el-checkbox>
</div>
</el-checkbox-group>
</div>
</div>
</el-collapse-item>
</el-collapse>
<!-- 操作按钮 - 主要修改点优化移动端按钮布局 -->
<div class="action-buttons">
<el-button type="primary"
icon="Search" size="small"
@click="handleSearch"
class="action-btn">
查询
</el-button>
<el-button icon="Refresh" size="small"
@click="handleReset"
class="action-btn">重置</el-button>
<!-- 导出按钮 -->
<el-button type="success"
icon="Download"
size="small"
@click="handleExportExcel"
class="action-btn">
导出Excel
</el-button>
</div>
<!-- 分页器 -->
<div class="pagination">
<el-pagination v-model:current-page="currentPage"
v-model:page-size="pageSize"
size="small"
:page-sizes="[50, 100, 200, 300]"
layout="total, sizes, prev, pager, next"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange" />
</div>
<!-- 数据表格 -->
<div class="data-section">
<el-table :data="tableData"
stripe
size="small"
border
style="width: 100%">
<el-table-column v-for="col in tableColumns"
:key="col.prop"
:prop="col.prop"
:label="col.label"
min-width="120" />
</el-table>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, watch, inject, computed } from 'vue';
import { Toast } from 'tdesign-mobile-vue';
import { AddIcon, SaveIcon } from 'tdesign-icons-vue-next';
import { ElMessage, ElMessageBox, ElCollapse, ElCollapseItem } from 'element-plus'
import { RefreshRight, Refresh, Search } from '@element-plus/icons-vue'
import dayjs from 'dayjs'
const $http = inject('$http')
// ---------- 数据定义 ----------
const queryForm = reactive({
model: null, // 机型
barcode: null, // 条形码
startDate: '', // 开始日期
endDate: '', // 结束日期
queryType: null, // 查询条件
keyword: null, // 字符模糊匹配
minValue: null, // 最小值
maxValue: null // 最大值
});
// 机型选项
const machineModels = ref([]);
const activeCollapse = ref(['queryConditions', 'selectParams']); // 默认展开
// 可用参数列表
const availableParams = ref([
{ label: 'COLORBOX_BARCODE', value: 'COLORBOX_BARCODE' },
{ label: 'OUTBOXCODE', value: 'OUTBOXCODE' },
{ label: 'MACCODE', value: 'MACCODE' },
{ label: 'UNITWEIGTH', value: 'UNITWEIGTH' },
{ label: 'LABEL_PRINT_CNT', value: 'LABEL_PRINT_CNT' },
{ label: 'S1', value: 'S1' },
{ label: 'S4', value: 'S4' },
{ label: 'S5', value: 'S5' },
{ label: 'S6', value: 'S6' },
{ label: 'S7', value: 'S7' },
{ label: 'S10', value: 'S10' },
{ label: 'RESULT1', value: 'RESULT1' },
{ label: 'RESULT4', value: 'RESULT4' },
{ label: 'RESULT7', value: 'RESULT7' },
{ label: 'RESULT10', value: 'RESULT10' },
{ label: 'MO', value: 'MO' },
{ label: 'SHIPPINGLOADER', value: 'SHIPPINGLOADER' },
{ label: 'SHIPPINGDATETIME', value: 'SHIPPINGDATETIME' },
{ label: '589_LCM_SN', value: '589_LCM_SN' },
{ label: '589_KEYBOARD_SN', value: '589_KEYBOARD_SN' },
{ label: 'RESULT2', value: 'RESULT2' },
{ label: 'RESULT3', value: 'RESULT3' },
{ label: 'RESULT6', value: 'RESULT6' },
{ label: 'RESULT12', value: 'RESULT12' },
{ label: 'SHIPPINGCODE', value: 'SHIPPINGCODE' },
{ label: '589_LCM_SN', value: '589_LCM_SN' },
{ label: '589_MODULEBOARD_SN', value: '589_MODULEBOARD_SN' },
{ label: '189_MAINBOARD_SN', value: '189_MAINBOARD_SN' }
]);
// 选中的参数
const selectedParams = ref([]);
// 表格数据
const tableData = ref([]);
const tableColumns = ref([
{ prop: 'id', label: 'ID' },
{ prop: 'barcode', label: '条形码' },
{ prop: 'result', label: '结果' }
]);
// 分页数据
const currentPage = ref(1);
const pageSize = ref(50);
const total = ref(0);
// ---------- 计算属性 ----------
// 检查是否有数值范围值
const hasNumberRangeValue = computed(() => {
return queryForm.minValue || queryForm.maxValue;
});
// 检查是否有模糊匹配值
const hasKeywordValue = computed(() => {
return queryForm.keyword;
});
// 全选参数控制
const allParamsSelected = computed({
get: () => availableParams.value.length > 0 &&
selectedParams.value.length === availableParams.value.length,
set: (value) => {
selectedParams.value = value
? availableParams.value.map(p => p.value)
: [];
}
});
const resParamsSelected = computed({
get: () => availableParams.value.length > 0 &&
selectedParams.value.length === availableParams.value.length,
set: (value) => {
selectedParams.value = value
? availableParams.value.map(p => p.value)
: [];
}
});
// ---------- 生命周期 ----------
onMounted(() => {
fetchMachineModels();
fetchAvailableParams();
});
// ---------- 方法定义 ----------
// 获取机型列表
const fetchMachineModels = async () => {
try {
// 实际接口调用:
// const response = await $http.get('/api/models');
// machineModels.value = response.data.map(item => ({
// value: item.id,
// label: item.name
// }));
// 模拟数据
machineModels.value = [
{ value: 'model1', label: '机型1' },
{ value: 'model2', label: '机型2' },
{ value: 'model3', label: '机型3' }
];
} catch (error) {
console.error('获取机型失败:', error);
}
};
// 获取可用参数列表
const fetchAvailableParams = async () => {
try {
// 实际接口调用:
// const response = await $http.get('/api/parameters');
// availableParams.value = response.data.map(item => ({
// value: item.id,
// label: item.name
// }));
// 模拟数据 - 默认使用上面定义的初始数据
} catch (error) {
console.error('获取参数列表失败:', error);
}
};
// 全选/取消全选
const handleSelectAll = (val) => {
selectedParams.value = val
? availableParams.value.map(p => p.value)
: [];
};
// 查询数据
const handleSearch = async () => {
try {
currentPage.value = 1;
await fetchTableData();
} catch (error) {
console.error('查询失败:', error);
}
};
// 重置查询
const handleReset = () => {
Object.keys(queryForm).forEach(key => {
// 日期字段需要空字符串而非null
if (key.includes('Date')) {
queryForm[key] = '';
} else {
queryForm[key] = null;
}
});
selectedParams.value = [];
tableData.value = [];
currentPage.value = 1;
pageSize.value = 50;
};
// 获取表格数据
const fetchTableData = async () => {
try {
// 实际查询接口调用:
// const params = {
// ...queryForm,
// selectedParams: selectedParams.value.join(','),
// page: currentPage.value,
// size: pageSize.value
// };
// const response = await $http.get('/api/query', { params });
// tableData.value = response.data.list;
// total.value = response.data.total;
// 模拟数据
console.log('调用查询接口:', {
...queryForm,
selectedParams: selectedParams.value,
page: currentPage.value,
size: pageSize.value
});
// 模拟响应
tableData.value = [
{ id: 1, barcode: 'SN123456', result: '通过' },
{ id: 2, barcode: 'SN789012', result: '失败' },
{ id: 3, barcode: 'SN345678', result: '通过' },
{ id: 4, barcode: 'SN901234', result: '通过' },
{ id: 5, barcode: 'SN567890', result: '失败' }
];
total.value = 5;
} catch (error) {
console.error('获取表格数据失败:', error);
}
};
// 分页大小变化
const handleSizeChange = (newSize) => {
pageSize.value = newSize;
fetchTableData();
};
// 当前页变化
const handleCurrentChange = (newPage) => {
currentPage.value = newPage;
fetchTableData();
};
// 导出Excel
const handleExportExcel = async () => {
try {
console.log('导出Excel参数:', {
...queryForm,
selectedParams: selectedParams.value
});
// 实际导出接口调用:
// const params = {
// ...queryForm,
// selectedParams: selectedParams.value.join(',')
// };
// window.location.href = `/api/export?${new URLSearchParams(params)}`;
} catch (error) {
console.error('导出失败:', error);
}
};
</script>
<style scoped>
.query-container {
padding: 5px;
border-radius: 5px;
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.1);
}
/* 查询条件区域样式 */
.condition-section {
margin-bottom: 5px;
}
.condition-form {
display: flex;
flex-direction: column;
}
.condition-row {
display: flex;
flex-wrap: wrap;
gap: 5px;
}
.condition-item {
flex: 1;
min-width: 250px;
margin-bottom: 5px;
}
.date-range,
.number-range {
display: flex;
align-items: center;
}
.date-separator {
padding: 0 5px;
}
/* 参数区域样式 */
.param-section {
margin-bottom: 5px;
padding: 5px;
}
/* 参数列表网格布局 */
.param-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 2px;
}
.param-item {
margin-right: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.param-header {
display: flex;
justify-content: flex-start;
align-items: center;
margin-bottom: 6px;
font-weight: bold;
}
/* 按钮区域 - 主要优化点:确保移动端按钮在一行显示 */
.action-buttons {
margin: 5px 0;
display: flex;
justify-content: flex-end;
gap: 12px;
}
.action-btn {
flex-shrink: 1; /* 允许按钮宽度缩小 */
min-width: auto; /* 去除最小宽度限制 */
}
/* 表格区域 */
.data-section {
position: relative;
}
.pagination {
margin: 2px 0;
display: flex;
justify-content: center;
}
.collapse-panel {
margin-bottom: 1px;
border: none;
border-radius: 1px;
box-shadow: 0 1px 1px rgba(0,0,0,0.1);
}
:deep(.el-collapse-item__header) {
font-weight: bold;
padding: 0 2px;
border-radius: 1px;
}
:deep(.el-collapse-item__content) {
padding: 2px;
}
:deep(.el-collapse) {
--el-collapse-header-height: 25px;
}
/* 移动端适配 */
@media (max-width: 768px) {
.condition-row {
flex-direction: column;
}
.condition-item {
min-width: 100%;
}
.param-list {
flex-direction: column;
gap: 3px;
}
.param-item {
margin-right: 0;
}
/* 参数列表网格布局 */
:deep(.param-grid) {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(123px, 1fr)) !important;
gap: 2px;
}
/* 移动端按钮优化 - 保持一行显示 */
.action-buttons {
flex-wrap: nowrap; /* 防止按钮换行 */
overflow-x: auto; /* 如果按钮过多允许水平滚动 */
justify-content: flex-start; /* 从左对齐 */
gap: 1px; /* 减小按钮间距 */
}
.action-btn {
flex-shrink: 0; /* 不收缩按钮 */
padding: 4px 6px; /* 减小按钮内边距 */
font-size: 12px; /* 缩小字体 */
white-space: nowrap; /* 确保文本不换行 */
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,347 @@
<template>
<div class="form-container">
<div>
<h2 style="text-align:center">个人中心</h2>
<div class="header">
<el-button v-if="isEditing"
type="danger" round
@click="handleEditSave">保存</el-button>
<el-button v-if="!isEditing"
type="success" round
@click="handleEditSave">编辑</el-button>
</div>
</div>
<el-form ref="formRef"
:model="userInfo"
:rules="formRules"
label-width="83px"
:disabled="!isEditing">
<!-- 用户名 -->
<el-form-item label="用户名" prop="userName">
<!--<el-input v-model="userInfo.userName" readonly disabled />-->
<el-tag size="large" type="primary">{{ userInfo.userName }}</el-tag>
</el-form-item>
<!-- 公司 -->
<el-form-item label="所属公司" prop="userName">
<!--<el-input v-model="userInfo.customerName" readonly disabled />-->
<el-tag size="large" type="success">{{ userInfo.customerName }}</el-tag>
</el-form-item>
<!-- 微信 -->
<el-form-item label="微信" prop="weiXin">
<el-input v-model="userInfo.weiXin" clearable />
</el-form-item>
<!-- 邮箱 -->
<el-form-item label="邮箱" prop="email">
<el-input v-model="userInfo.email" clearable />
</el-form-item>
<!-- 手机号 -->
<el-form-item label="手机号" prop="mobile">
<el-input v-model="userInfo.mobile" clearable />
</el-form-item>
<!-- 密码 -->
<el-form-item label="修改密码" prop="password">
<el-input v-model="userInfo.password"
type="password"
show-password clearable
placeholder="不修改密码则留空" />
</el-form-item>
<!-- 确认密码动态显示 -->
<el-form-item v-if="userInfo.password"
label="确认密码"
prop="confPassword">
<el-input v-model="userInfo.confPassword"
type="password"
show-password clearable
placeholder="请确认密码" />
</el-form-item>
<!-- 旧密码动态显示 -->
<el-form-item v-if="userInfo.password"
label="旧密码"
prop="oldPassword">
<el-input v-model="userInfo.oldPassword"
type="password"
show-password clearable
placeholder="请输入旧密码" />
</el-form-item>
</el-form>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, watch, inject, computed } from 'vue';
import { Toast } from 'tdesign-mobile-vue';
import { useRouter, useRoute } from 'vue-router'
// 初始化加载数据
onMounted(() => {
loadingUser()
});
const $http = inject('$http')
const router = useRouter()
const userInfo = ref({
userName: '',
companyId: '',
customerName: '',
weiXin: '',
email: '',
mobile: '',
password: '',
confPassword: '',
oldPassword: '',
currentPassword: ''
})
const companies = ref([])
// 初始化用户信息
const loadingUser = () => {
$http.post('Company/GetCompanyInfo', {
IsAll: true,
ID: 0
})
.then(rs => {
//console.log(rs)
if (rs.data.isok) {
companies.value = rs.data.response
companies.value.forEach(item => {
item.label = item.customerName
item.value = item.id
/* product.options.push({
label: item.customerName,
value: item.customerName
})*/
});
var formdata = {
IsAll: false,
ID: localStorage.getItem('uid')
}
// 调用API获取用户信息
$http.post('Users/GetUserInfo', formdata)
.then(rs => {
//console.log(rs)
if (rs.data.isok) {
userInfo.value = rs.data.response
const companyMap = new Map(companies.value.map(company => [company.id, company.customerName]));
userInfo.value.customerName = companyMap.get(userInfo.value.companyId);
//console.log(userInfo.value)
} else {
Toast(rs.data.message)
}
})
.catch(error => {
// 处理错误情况
console.error(error);
})
.finally(() => {
})
} else {
Toast(rs.data.message)
}
})
.catch(error => {
// 处理错误情况
console.error(error);
})
.finally(() => {
})
}
// TODO 头像修改(未做)
const openAvatarDialog = (user) => {
console.log(user)
}
// 重置密码
const handleResetPassword = (user) => {
// TODO: 调用重置密码API
var us = {
id: user.id
}
$http.post('Users/NewPassWord', us)
.then(rs => {
//console.log(rs)
if (rs.data.isok) {
} else {
Toast(rs.data.message)
}
})
.catch(error => {
// 处理错误情况
console.error(error);
})
};
// 表单状态控制
const isEditing = ref(false)
const formRef = ref(null)
// 验证规则
const validatePassword = (rule, value, callback) => {
if (value) {
const reg = /^[\w!@#$%^&*()]{6,20}$/
if (!reg.test(value)) {
callback(new Error('6-20位英文/数字/特殊字符'))
} else if (userInfo.value.confPassword && value != userInfo.value.confPassword) {
callback(new Error('两次输入密码不一致'))
}
}
callback()
}
const formRules = reactive({
email: [
{ type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
],
mobile: [
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
],
password: [
{ validator: validatePassword, trigger: 'blur' }
],
confPassword: [
{ required: true, message: '请再次输入密码', trigger: 'blur' },
{
validator: (_, value, cb) => {
if (value !== userInfo.value.password) {
cb(new Error('两次输入密码不一致'))
}
cb()
}, trigger: 'blur'
}
],
oldPassword: [
{ required: true, message: '请输入旧密码', trigger: 'blur' }
]
})
// 处理编辑/保存
const handleEditSave = () => {
if (!isEditing.value) {
isEditing.value = true
return
}
let form = {
Id: userInfo.value.id,
UserName: userInfo.value.userName,
CompanyId: userInfo.value.companyId,
Mobile: userInfo.value.mobile,
WeiXin: userInfo.value.weiXin,
Email: userInfo.value.email,
}
//Toast("保存成功!")
//return
if (userInfo.value.password) {
$http.post('Login/Login', {
username: userInfo.value.userName,
password: userInfo.value.oldPassword
})
.then(rs => {
//console.log(rs)
if (rs.data.isok) {
$http.post('Users/EditUser', form)
.then(rs => {
//console.log(rs)
let psw = {
Id: userInfo.value.id,
PlaintextPwd: userInfo.value.confPassword
}
if (rs.data.isok) {
$http.post('Users/ResetPassWord', psw)
.then(rs => {
//console.log(rs)
if (rs.data.isok) {
isEditing.value = false
Toast("保存成功!")
setTimeout(() => {
router.push('/login')
}, 233)
} else {
Toast(rs.data.message)
}
})
.catch(error => {
// 处理错误情况
console.error(error);
})
} else {
Toast(rs.data.message)
}
})
.catch(error => {
// 处理错误情况
console.error(error);
})
} else {
Toast(rs.data.message)
}
})
.catch(error => {
// 处理错误情况
console.error(error);
})
} else {
$http.post('Users/EditUser', form)
.then(rs => {
//console.log(rs)
if (rs.data.isok) {
isEditing.value = false
Toast("保存成功!")
} else {
Toast(rs.data.message)
}
})
.catch(error => {
// 处理错误情况
console.error(error);
})
}
}
</script>
<style scoped>
.form-container {
max-width: 600px;
padding: 20px;
/*margin: 20px;*/
}
.header {
margin-bottom: 20px;
text-align: left;
}
/* PC端样式 */
@media (min-width: 1024) {
}
</style>

View File

@@ -0,0 +1,722 @@
<template>
<div class="container">
<!-- 用户列表 -->
<!-- <div>
<div><t-input label="搜索" placeholder="搜索格式:公司 用户名" @change="SearchUsers" /></div>
<div>
<t-dropdown-menu style="height:32px;border:none">
<t-dropdown-item :options="product.options" :value="product.value" @change="databaseChanges" :disabled="disDbDrop" />
</t-dropdown-menu>
</div>
</div>-->
<div style="display: flex; align-items: center; gap: 10px">
<!-- 输入框部分 - 自适应剩余宽度 -->
<div style="flex: 1">
<t-input label="搜索"
placeholder="公司 用户名"
clearable
@change="SearchUsers" />
</div>
<!-- 下拉菜单部分 - 固定宽度 -->
<div style="width: 90px">
<t-dropdown-menu style="height:32px; border:none; width:100%">
<t-dropdown-item :options="product.options"
:value="product.value"
@change="databaseChanges" />
</t-dropdown-menu>
</div>
</div>
<t-list :async-loading="loading">
<div v-for="user in nowUsers" :key="user.id" class="user-list-div" @click="openEditDialog(user)"
style="position: relative; width: auto; padding: 5px; height: 55px; border-radius: 3px; margin: 2px;">
<!-- 头像 -->
<t-avatar class="avatar-example--small external-class-content" shape="circle" size="44px" style="margin-right: 10px;" @click.stop="openAvatarDialog(user)">{{ user.userName[0] + user.userName[1] }}</t-avatar>
<!-- 主体 -->
<span class="user-name" style="position: absolute; top: 3px; left: 55px; font-size: 12px; text-align: left; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">用户<span style="font-size: 14px; font-weight: 500">{{ user.userName }}</span></span>
<span class="user-company" style="position: absolute; bottom: 3px; left: 55px; font-size: 12px; ">公司:<span style="font-size:15px;font-weight:bold">{{ user.customerName }}</span></span>
<!-- 操作 -->
<span class="user-actions" style="position: absolute; bottom: 3px; right: 0px;">
<t-button theme="danger"
@click.stop="openDelDialog(user)"
variant="outline"
:disabled="!((user.userName != 'MomoWen') && (user.userName != 'Admin'))"
size="extra-small">删除</t-button>
<span>&nbsp;</span>
<!-- <t-button theme="primary"
@click="openEditDialog(user)"
variant="outline"
size="extra-small">修改</t-button>-->
</span>
</div>
</t-list>
<!-- 删除对话框 -->
<t-dialog v-model:visible="isDelDialogVis"
close-on-overlay-click
content="操作不可逆,确认删除?"
cancel-btn="取消"
:confirm-btn="{ content: '确认删除', theme: 'danger' }"
@confirm="isDelUser"
@cancel="isDelUserCancel"
@close="isDelUserCancel">
</t-dialog>
<!-- 编辑/新增对话框 -->
<t-dialog v-model:visible="editDialogVisible"
:header="currentUser.id ? '编辑用户' : '新增用户'"
cancel-btn="取消"
confirm-btn="保存"
@confirm="handleSave">
<t-form>
<t-form-item label="用户名">
<t-input v-model="currentUser.userName" :disabled="!!currentUser.id" />
</t-form-item>
<t-form-item label="所属公司">
<!--<t-input v-model="currentUser.company" />-->
<t-dropdown-menu>
<t-dropdown-item :options="companies"
:value="currentUser.companyId"
zIndex="19999"
@change="currentUserComChange" />
</t-dropdown-menu>
</t-form-item>
<t-form-item label="手机号">
<t-input v-model="currentUser.mobile" />
</t-form-item>
<t-form-item label="微信号">
<t-input v-model="currentUser.weiXin" />
</t-form-item>
<t-form-item label="Email">
<t-input v-model="currentUser.email" />
</t-form-item>
<t-form-item label="权限管理">
<div class="permission-group-btn">
<t-button size="extra-small"
theme="primary"
variant="outline"
:disabled="!((currentUser.userName != 'MomoWen') && (currentUser.userName != 'Admin'))"
@click="permissionBtn('pages')">展开权限菜单</t-button>
</div>
</t-form-item>
<t-checkbox :checked="currentUser.resetpsw"
:label="currentUser.id ? '重置密码' : '初始化密码'"
:disabled="currentUser.id ? false : true"
icon="rectangle"/>
</t-form>
</t-dialog>
<!-- 页面权限弹出层 -->
<t-popup v-model:visible="popupVisible.pages"
placement="bottom"
:style="{ height: '92%' }"
header="权限设置">
<!-- 数据库权限 -->
<div style="padding:10px">
<t-checkbox :block="false" v-model="allSelected" label="全选" @change="checkboxGroupChangeAll" />
<t-checkbox-group class="box" borderless @change="checkboxGroupChange" v-model="selectedCheckboxes">
<span v-for="(item, index) in dbOptions" :key="index">
<t-checkbox :block="false" :value="item.label" :label="item.label" />
<span>&nbsp;&nbsp;</span>
</span>
</t-checkbox-group>
</div>
<!-- 表权限 -->
<div style="padding:10px">
</div>
<div class="popup-footer">
<t-button theme="default" size="extra-small" @click="popupVisible.pages = false"> </t-button>
<div style="display: inline-block; width:10px"></div>
<t-button theme="primary" size="extra-small" @click="handleFormConfirm"> </t-button>
</div>
</t-popup>
<!-- 新增 悬浮按钮 -->
<t-fab icon="+"
draggable="true"
style="font-size:25px"
@click="openCreateDialog"
class="fab-button" />
</div>
</template>
<script setup>
import { ref, reactive, onMounted, watch, inject, computed } from 'vue';
import { Toast } from 'tdesign-mobile-vue';
import { ElTree } from 'element-plus'
// 初始化加载数据
onMounted(() => {
loading.value = true;
loadingUsers()
loading.value = false;
});
// 用户数据
const users = ref([]);
const loading = ref(true);
const tableData = reactive([]);
// 对话框控制
const editDialogVisible = ref(false);
const currentUser = reactive({
id: 0,
userName: '',
companyId: null,
companyName: '',
mobile: '',
weiXin: '',
email: '',
resetpsw: true,
permissions: []
});
const companies = ref([]);
const $http = inject('$http')
const dbNames = ref([])
// 初始化用户列表
const loadingUsers = () => {
var formdata = {
IsAll: true,
ID: 0
}
// 调用API获取用户列表
$http.post('Company/GetCompanyInfo', formdata)
.then(rs => {
//console.log(rs)
if (rs.data.isok) {
companies.value = rs.data.response
product.options = [{ value: '全选', label: '全选' }]
companies.value.forEach(item => {
item.label = item.customerName
item.value = item.id
product.options.push({
label: item.customerName,
value: item.customerName
})
});
//console.log(product)
$http.post('Users/GetUserInfo', formdata)
.then(rsu => {
if (rsu.data.isok) {
const companyMap = new Map(companies.value.map(company => [company.id, company.customerName]));
users.value = rsu.data.response.sort((a, b) => {
const nameA = a.userName.toUpperCase(); // 转换为大写以进行不区分大小写的比较
const nameB = b.userName.toUpperCase(); // 转换为大写以进行不区分大小写的比较
if (nameA < nameB) {
return -1;
}
if (nameA > nameB) {
return 1;
}
// 如果userName相同则保持原始顺序稳定排序
return 0;
});
//console.log(users.value)
users.value.forEach(user => {
// 检查companyId是否存在于公司映射中
if (companyMap.has(user.companyId)) {
// 如果存在,将公司名称添加到用户对象中
user.customerName = companyMap.get(user.companyId);
}
});
if (search.value) {
SearchUsers(search.value)
} else {
nowUsers.value = JSON.parse(JSON.stringify(users.value))
}
} else {
Toast(rsu.data.message)
}
})
.catch(error => {
// 处理错误情况
console.error(error);
})
} else {
Toast(rs.data.message)
}
})
.catch(error => {
// 处理错误情况
console.error(error);
})
.finally(() => {
})
let requestBody = {
key: "getDbName",
CmdType: "sys",
DBQueryData: []
}
$http.post('DataHandle/R_InfoCommon', requestBody)
.then(rs => {
//console.log(rs)
if (rs.data.isok) {
dbNames.value = [];
rs.data.response.forEach(item => {
if (item.Database.slice(0, 3) == "uts" && item.Database != "uts_manage") {
dbNames.value.push(item.Database)
//console.log(dbNames.value)
}
})
} else {
Toast(rs.data.message)
}
})
.catch(error => {
// 处理错误情况
console.error(error);
})
.finally(() => {
})
}
const product = reactive({
value: '全选',
options: [
{
value: '全选',
label: '全选'
}
],
})
// 搜索
const databaseChanges = (db) => {
if (db == "全选") {
SearchUsers("")
} else {
SearchUsers(db)
}
}
const search = ref('')
const nowUsers = ref([]);
const SearchUsers = (s) => {
search.value = s
if (s) {
//nowUsers.value = users.value.filter(user => user.userName.includes(s));
const keywords = s.trim().split(/\s+/); // 按空格分割关键词
nowUsers.value = users.value.filter(user =>
keywords.every(key =>
(user.userName || '').toLowerCase().includes(key.toLowerCase()) ||
(user.customerName || '').toLowerCase().includes(key.toLowerCase())
)
);
} else {
nowUsers.value = JSON.parse(JSON.stringify(users.value))
}
}
// 打开编辑公司对话框
const currentUserComChange = (cid) => {
//console.log(cid)
const usc = companies.value.forEach(item => {
if (cid == item.id) {
// 将公司名称和id添加到用户对象中
currentUser.companyId = item.id
currentUser.companyName = item.companyName
}
})
};
// 打开编辑对话框
const openEditDialog = (user) => {
Object.assign(currentUser, user);
currentUser.resetpsw = false;
editDialogVisible.value = true;
};
// TODO 头像修改(未做)
const openAvatarDialog = (user) => {
//console.log(user)
}
const isDelDialogVis = ref(false)
// 打开删除用户确认框
const openDelDialog = (user) => {
isDelDialogVis.value = true;
Object.assign(currentUser, user);
};
// 取消删除
const isDelUserCancel = () => {
isDelDialogVis.value = false;
}
// 确认删除
const isDelUser = () => {
// TODO 删除用户API
var us = {
id: currentUser.id
}
$http.post('Users/DelUser', us)
.then(rs => {
//console.log(rs)
if (rs.data.isok) {
Toast({
className: 'toast-root--success',
theme: 'success',
direction: 'column',
message: '删除成功!',
});
loadingUsers()
} else {
Toast(rs.data.message)
}
})
.catch(error => {
// 处理错误情况
console.error(error);
})
}
// 打开新增对话框
const openCreateDialog = () => {
Object.assign(currentUser, {
id: 0,
userName: '',
companyId: null,
companyName: '',
mobile: '',
weiXin: '',
email: '',
resetpsw: true,
permissions: []
});
editDialogVisible.value = true;
};
const selectedCheckboxes = ref([]);
const allSelected = computed({
get() {
return selectedCheckboxes.value.includes('all');
},
set(isChecked) {
if (isChecked) {
selectedCheckboxes.value = ['all', ...dbOptions.value.map(opt => opt.label)];
} else {
selectedCheckboxes.value = [];
}
}
});
const checkboxGroupChange = (check) => {
selectedCheckboxes.value = check;
if (check.length === dbOptions.value.length && !check.includes('all')) {
selectedCheckboxes.value = ['all', ...check];
} else {
selectedCheckboxes.value = check.filter(item => item !== 'all');
}
};
const checkboxGroupChangeAll = (isChecked) => {
allSelected.value = isChecked;
};
/* const checkboxGroupChange = (check) => {
const selected = new Set(check); // 使用Set便于判断存在性
const hasAll = selected.has('all');
const labels = dbOptions.value.map(opt => opt.label);
const filtered = check.filter(item => item !== 'all');
// 情况1选中了"all"(可能单独或与其他一起)
if (hasAll) {
// 如果当前选中仅"all",则全选
if (check.length === 1) {
selectedCheckboxes.value = [...labels, 'all'];
}
// 如果已全选且包含"all",则维持
else if (filtered.length === labels.length) {
selectedCheckboxes.value = [...labels, 'all'];
}
// 如果选中了"all"但未全选,移除"all"
else {
selectedCheckboxes.value = filtered;
}
return;
}
// 情况2未选中"all",但勾选了全部选项
if (filtered.length === labels.length) {
selectedCheckboxes.value = [...labels, 'all']; // 自动补全"all"
}
// 情况3正常选择
else {
selectedCheckboxes.value = filtered;
}
}*/
// 保存处理
const handleSave = () => {
if (currentUser.id != 0) {
let us = {
Id: currentUser.id,
UserName: currentUser.userName,
CompanyId: currentUser.companyId,
Mobile: currentUser.mobile,
WeiXin: currentUser.weiXin,
Email: currentUser.email,
IsValid: true
}
// TODO: 调用更新用户API
$http.post('Users/EditUser', us)
.then(rs => {
//console.log(rs)
if (rs.data.isok) {
if (currentUser.resetpsw == true) {
handleResetPassword(currentUser)
}
Toast({
className: 'toast-root--success',
theme: 'success',
direction: 'column',
message: '修改成功!',
});
} else {
Toast(rs.data.message)
}
})
.catch(error => {
// 处理错误情况
console.error(error);
})
.finally(() => {
loadingUsers()
})
} else {
// TODO: 调用新增用户API
let us = {
Id: currentUser.id,
UserName: currentUser.userName,
CompanyId: currentUser.companyId,
Mobile: currentUser.mobile,
WeiXin: currentUser.weiXin,
Email: currentUser.email
}
$http.post('Users/AddUser', us)
.then(rs => {
//console.log(rs)
if (rs.data.isok) {
Toast({
className: 'toast-root--success',
theme: 'success',
direction: 'column',
message: '新增成功!',
});
} else {
Toast(rs.data.message)
}
})
.catch(error => {
// 处理错误情况
console.error(error);
})
.finally(() => {
loadingUsers()
})
}
};
// 重置密码
const handleResetPassword = (user) => {
// TODO: 调用重置密码API
var us = {
id: user.id
}
$http.post('Users/NewPassWord', us)
.then(rs => {
//console.log(rs)
if (rs.data.isok) {
} else {
Toast(rs.data.message)
}
})
.catch(error => {
// 处理错误情况
console.error(error);
})
};
// 弹出层控制
const popupVisible = reactive({
pages: false,
model: false,
db: false,
});
// 页面权限表单数据
const formItems = ref([
{ label: '/home', isOpen: true },
{ label: '/user', isOpen: true },
{ label: '/usermanage', isOpen: true },
{ label: '/scopemanage', isOpen: true },
/* { label: '/', isOpen: true },
{ label: '/', isOpen: true },
{ label: '/', isOpen: true },*/
]);
// 数据库权限多选数据
const dbOptions = ref([]);
// 按钮点击处理
const permissionBtn = (type) => {
switch (type) {
case 'pages':
// TODO: 这里可以添加初始化表单数据的接口调用
dbOptions.value = []
dbNames.value.forEach(db => {
let dbv = { label: db, isOpen: false }
dbOptions.value.push(dbv)
})
popupVisible.pages = true;
break;
case 'db':
// TODO: 这里可以添加初始化多选数据的接口调用
dbOptions.value = []
dbNames.value.forEach(db => {
let dbv = { label: db, isOpen: false }
dbOptions.value.push(dbv)
})
popupVisible.db = true;
break;
case 'model':
// TODO: 这里可以添加初始化多选数据的接口调用
popupVisible.model = true;
break;
default:
console.error('未知操作类型');
}
};
// 全选或取消全选
const isAllOpenPages = (tof) => {
if (tof) {
formItems.value.forEach(item => {
item.isOpen = true;
});
} else {
formItems.value.forEach(item => {
item.isOpen = false;
});
}
}
// 全选或取消全选
const isAllOpenDB = (tof) => {
//console.log(tof)
if (tof) {
dbOptions.value.forEach(item => {
item.isOpen = true;
});
} else {
dbOptions.value.forEach(item => {
item.isOpen = false;
});
}
}
// 表单确认处理
const handleFormConfirm = () => {
// TODO: 调用接口保存 formItems.value
//console.log('提交表单数据:', formItems.value);
popupVisible.pages = false;
};
// 机型权限确认处理
const handleModelConfirm = () => {
// TODO: 调用接口保存 selectedModels.value
//console.log('选中机型:', selectedModels.value);
popupVisible.model = false;
};
// 数据库权限确认处理
const handleDBConfirm = () => {
// TODO: 调用接口保存 selectedDBs.value
//console.log('选中数据库:', selectedDBs.value);
popupVisible.db = false;
};
</script>
<style scoped>
.container {
padding: 10px;
}
.fab-button {
position: fixed;
bottom: 32px;
right: 16px;
}
/* .t-dialog__body {
overflow-y: auto !important;
}
*/
.t-list {
z-index: 0;
position: relative;
}
.container {
padding: 0;
}
/* 底部操作栏 */
.popup-footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 10px;
display: block;
box-shadow: 0 -2px 10px rgba(0,0,0,0.1);
}
/* 多选组样式 */
/*.checkbox-group {
padding: 16px;
height: calc(100% - 60px);
overflow-y: auto;
}*/
.t-dropdown-item {
top: 145px !important
}
.user-list-div {
border: 1px solid #E9E9E960;
background-color: #80808030;
transition: all 0.3s ease;
}
.user-list-div:hover {
border-color: #409eff;
background-color: #80808010;
/* box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); 可选阴影 */
}
/* PC端样式 */
@media (min-width: 768px) {
}
</style>

View File

@@ -0,0 +1,100 @@
import { createRouter, createWebHistory } from 'vue-router';
import Home from '../pages/home/index.vue';
import Login from '../pages/login/index.vue';
import User from '../pages/user/index.vue';
import UserManage from '../pages/userManage/index.vue';
import LoginLog from '../pages/loginlog/index.vue';
import DbManage from '../pages/dbmanage/index.vue';
import InputColorbox from '../pages/inputimportantinfo/index.vue';
import StationLog from '../pages/stationlog/index.vue';
import TestLog from '../pages/testlog/index.vue';
const routes = [
{
path: '/',
// 动态重定向到登录页或主页
redirect: () => {
const isAuthenticated = localStorage.getItem('token');
return isAuthenticated ? '/home' : '/login';
}
},
{
path: '/login',
name: '登录',
component: Login
},
{
path: '/home',
name: '主页',
component: Home,
meta: { requiresAuth: true } // 需要认证的路由
},
{
path: '/user',
name: '个人中心',
component: User,
meta: { requiresAuth: true } // 需要认证的路由
},
{
path: '/usermanage',
name: '用户管理',
component: UserManage,
meta: { requiresAuth: true } // 需要认证的路由
},
{
path: '/loginlog',
name: '用户日志',
component: LoginLog,
meta: { requiresAuth: true } // 需要认证的路由
},
{
path: '/dbmanage',
name: '数据库管理',
component: DbManage,
meta: { requiresAuth: true } // 需要认证的路由
},
{
path: '/inputcolorbox',
name: '数据导入',
component: InputColorbox,
meta: { requiresAuth: true } // 需要认证的路由
},
{
path: '/stationlog',
name: '数据导入',
component: StationLog,
meta: { requiresAuth: true } // 需要认证的路由
},
{
path: '/testlog',
name: '测试数据',
component: TestLog,
meta: { requiresAuth: true } // 需要认证的路由
},
];
const router = createRouter({
history: createWebHistory(),
routes
});
// 路由守卫
router.beforeEach((to, from, next) => {
const isAuthenticated = localStorage.getItem('token') // 用localStorage存储token
const username = localStorage.getItem('username') // 获取用户名
if (to.meta.requiresAuth && !isAuthenticated) {
// 如果需要认证且没有token则跳转到登录页面
next('/login')
} else if ((to.path == '/usermanage' || to.path == '/scopemanage') && (username != 'Admin' && username != 'MomoWen')) {
// 如果访问的是用户管理页面但用户名不是Admin则跳转到主页
next('/home')
} else {
// 其他情况正常放行
sessionStorage.setItem('currentRoute', to.fullPath)
next()
}
})
export default router;

View File

@@ -0,0 +1,10 @@
import { defineConfig } from 'vite';
import plugin from '@vitejs/plugin-vue';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [plugin()],
server: {
port: 8809,
}
})