initial project with login web

This commit is contained in:
2026-01-14 12:57:30 +08:00
commit e3f0ae2b6e
19 changed files with 5252 additions and 0 deletions

23
.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
.DS_Store
node_modules/
dist/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
**/*.log
tests/**/coverage/
tests/e2e/reports
selenium-debug.log
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.local
package-lock.json
yarn.lock

0
README.md Normal file
View File

17
index.html Normal file
View File

@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<script>
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a]') || CSS.supports('top: constant(a)'))
document.write('<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' + (coverSupport ? ', viewport-fit=cover' : '') + '" />')
</script>
<title></title>
<!--preload-links-->
<!--app-context-->
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

27
package.json Normal file
View File

@@ -0,0 +1,27 @@
{
"name": "uniapp_basic_project",
"version": "1.0.0",
"description": "uni-app 基础项目",
"scripts": {
"dev:h5": "uni",
"build:h5": "uni build"
},
"dependencies": {
"@dcloudio/uni-app": "3.0.0-alpha-5000020260104004",
"@dcloudio/uni-components": "3.0.0-alpha-5000020260104004",
"@dcloudio/uni-h5": "3.0.0-alpha-5000020260104004",
"axios": "^1.6.8",
"vant": "^4.8.0",
"vue": "^3.4.21"
},
"devDependencies": {
"@vant/auto-import-resolver": "^1.2.1",
"unplugin-vue-components": "^0.26.0",
"@dcloudio/uni-automator": "3.0.0-alpha-5000020260104004",
"@dcloudio/uni-cli-shared": "3.0.0-alpha-5000020260104004",
"@dcloudio/uni-stacktracey": "3.0.0-alpha-5000020260104004",
"@dcloudio/vite-plugin-uni": "3.0.0-alpha-5000020260104004",
"@vue/runtime-core": "^3.4.21",
"vite": "^5.2.8"
}
}

4342
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

17
src/App.vue Normal file
View File

@@ -0,0 +1,17 @@
<script>
export default {
onLaunch: function() {
console.log('App Launch')
},
onShow: function() {
console.log('App Show')
},
onHide: function() {
console.log('App Hide')
}
}
</script>
<style>
/*每个页面公共css */
</style>

2
src/api/index.js Normal file
View File

@@ -0,0 +1,2 @@
// API 统一导出
export * from "./user";

26
src/api/user.js Normal file
View File

@@ -0,0 +1,26 @@
import { get, post } from "@/utils/http";
// 用户登录
export const login = (data) => {
return post("/user/login", data);
};
// 用户注册
export const register = (data) => {
return post("/user/register", data);
};
// 获取用户信息
export const getUserInfo = () => {
return get("/user/info");
};
// 更新用户信息
export const updateUserInfo = (data) => {
return post("/user/update", data);
};
// 退出登录
export const logout = () => {
return post("/user/logout");
};

BIN
src/assets/bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 554 KiB

22
src/main.js Normal file
View File

@@ -0,0 +1,22 @@
import App from './App'
// #ifndef VUE3
import Vue from 'vue'
import './uni.promisify.adaptor'
Vue.config.productionTip = false
App.mpType = 'app'
const app = new Vue({
...App
})
app.$mount()
// #endif
// #ifdef VUE3
import { createSSRApp } from 'vue'
export function createApp() {
const app = createSSRApp(App)
return {
app
}
}
// #endif

72
src/manifest.json Normal file
View File

@@ -0,0 +1,72 @@
{
"name" : "uniapp_basic_project",
"appid" : "",
"description" : "",
"versionName" : "1.0.0",
"versionCode" : "100",
"transformPx" : false,
/* 5+App */
"app-plus" : {
"usingComponents" : true,
"nvueStyleCompiler" : "uni-app",
"compilerVersion" : 3,
"splashscreen" : {
"alwaysShowBeforeRender" : true,
"waiting" : true,
"autoclose" : true,
"delay" : 0
},
/* */
"modules" : {},
/* */
"distribute" : {
/* android */
"android" : {
"permissions" : [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
]
},
/* ios */
"ios" : {},
/* SDK */
"sdkConfigs" : {}
}
},
/* */
"quickapp" : {},
/* */
"mp-weixin" : {
"appid" : "",
"setting" : {
"urlCheck" : false
},
"usingComponents" : true
},
"mp-alipay" : {
"usingComponents" : true
},
"mp-baidu" : {
"usingComponents" : true
},
"mp-toutiao" : {
"usingComponents" : true
},
"uniStatistics" : {
"enable" : false
},
"vueVersion" : "3"
}

24
src/pages.json Normal file
View File

@@ -0,0 +1,24 @@
{
"pages": [ //pages数组中第一项表示应用启动页参考https://uniapp.dcloud.io/collocation/pages
{
"path": "pages/login",
"style": {
"navigationBarTitleText": "登录",
"navigationStyle": "custom"
}
},
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "uni-app"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "uni-app",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8"
},
"uniIdRouter": {}
}

48
src/pages/index/index.vue Normal file
View File

@@ -0,0 +1,48 @@
<template>
<view class="content">
<image class="logo" src="/static/logo.png"></image>
<view class="text-area">
<text class="title">{{ title }} 111</text>
</view>
</view>
</template>
<script>
export default {
data() {
return {
title: "Hello",
};
},
onLoad() {},
methods: {},
};
</script>
<style>
.content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.logo {
height: 200rpx;
width: 200rpx;
margin-top: 200rpx;
margin-left: auto;
margin-right: auto;
margin-bottom: 50rpx;
}
.text-area {
display: flex;
justify-content: center;
}
.title {
font-size: 36rpx;
color: #8f8f94;
}
</style>

402
src/pages/login.vue Normal file
View File

@@ -0,0 +1,402 @@
<template>
<view class="login-container">
<!-- 顶部背景区域 -->
<view class="header-section">
<image class="bg-image" src="@/assets/bg.png" mode="aspectFill"></image>
<view class="header-content">
<text class="welcome-text">欢迎来到</text>
<text class="title-text">辽宁和兴LMS手持系统</text>
</view>
</view>
<!-- 底部表单区域 -->
<view class="form-section">
<!-- 表单标题 -->
<view class="form-title">
<view class="title-bar"></view>
<text class="title-text">用户登录 User login</text>
</view>
<!-- 输入框组 -->
<view class="input-wrapper">
<!-- 账号输入 -->
<view class="input-group">
<view class="input-icon icon-user"></view>
<input
class="input-field"
type="text"
v-model="formData.username"
placeholder="*请输入邮箱/手机"
placeholder-class="input-placeholder"
/>
</view>
<!-- 密码输入 -->
<view class="input-group">
<view class="input-icon icon-lock"></view>
<input
class="input-field"
type="password"
v-model="formData.password"
placeholder="*请输入登录密码"
placeholder-class="input-placeholder"
/>
</view>
<!-- IP地址和端口号输入 -->
<view class="input-group ip-input-group">
<view class="input-icon icon-shield"></view>
<input
class="input-field ip-field"
type="text"
v-model="formData.ip"
placeholder="请输入IP"
placeholder-class="input-placeholder"
/>
<text class="ip-separator">:</text>
<input
class="input-field port-field"
type="text"
v-model="formData.port"
placeholder="请输入端口号"
placeholder-class="input-placeholder"
/>
</view>
</view>
<!-- 登录按钮 -->
<button class="login-btn" @click="handleLogin" :disabled="isLoading">
<text class="login-btn-text">{{ isLoading ? "登录中..." : "登录" }}</text>
</button>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { setBaseURL } from "@/utils/http";
import { login } from "@/api/user";
// 表单数据
const formData = ref({
ip: "127.0.0.1",
port: "8011",
username: "",
password: "",
});
// 加载状态
const isLoading = ref(false);
// 登录处理
const handleLogin = async () => {
// 表单验证
if (!formData.value.ip) {
uni.showToast({
title: "请输入IP地址",
icon: "none",
});
return;
}
if (!formData.value.port) {
uni.showToast({
title: "请输入端口号",
icon: "none",
});
return;
}
if (!formData.value.username) {
uni.showToast({
title: "请输入账号",
icon: "none",
});
return;
}
if (!formData.value.password) {
uni.showToast({
title: "请输入密码",
icon: "none",
});
return;
}
try {
isLoading.value = true;
// 组合IP地址和端口号
const fullUrl = `${formData.value.ip}:${formData.value.port}`;
// 设置IP地址到http.js
setBaseURL(fullUrl);
// 预留登录接口调用
// const res = await login({
// username: formData.value.username,
// password: formData.value.password,
// });
// 模拟登录成功(实际使用时需要替换为真实接口调用)
uni.showToast({
title: "登录成功",
icon: "success",
});
// 保存登录信息(实际使用时需要根据接口返回的数据处理)
// uni.setStorageSync("token", res.data.token);
// uni.setStorageSync("userInfo", res.data.userInfo);
// 跳转到首页
setTimeout(() => {
uni.reLaunch({
url: "/pages/index/index",
});
}, 1500);
} catch (error) {
console.error("登录失败:", error);
uni.showToast({
title: error.message || "登录失败,请重试",
icon: "none",
});
} finally {
isLoading.value = false;
}
};
// 页面加载时尝试从本地存储读取之前保存的IP
onMounted(() => {
const savedIP = uni.getStorageSync("apiBaseURL");
if (savedIP) {
// 移除http://或https://前缀
let url = savedIP.replace(/^https?:\/\//, "");
// 解析IP和端口号
const parts = url.split(":");
if (parts.length >= 2) {
formData.value.ip = parts[0];
formData.value.port = parts.slice(1).join(":"); // 处理IPv6的情况
} else {
formData.value.ip = url;
formData.value.port = "8011";
}
}
// 如果没有保存的IP使用默认值已在formData中设置
});
</script>
<style scoped>
.login-container {
min-height: 100vh;
background-color: #f5f5f5;
display: flex;
flex-direction: column;
position: relative;
}
/* 顶部背景区域 */
.header-section {
position: relative;
width: 100%;
height: 33vh;
min-height: 400rpx;
overflow: hidden;
background: linear-gradient(180deg, #4a90e2 0%, #7bb3f0 50%, #e8f4fd 100%);
}
.bg-image {
width: 100%;
height: 100%;
opacity: 0.8;
}
.header-content {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: flex-start;
padding: 0 50rpx 80rpx;
box-sizing: border-box;
}
.welcome-text {
font-size: 28rpx;
color: #000000;
margin-bottom: 12rpx;
opacity: 0.9;
}
.header-content .title-text {
font-size: 48rpx;
font-weight: bold;
color: #000000;
letter-spacing: 2rpx;
line-height: 1.4;
}
/* 底部表单区域 */
.form-section {
position: relative;
flex: 1;
background-color: #ffffff;
border-radius: 40rpx 40rpx 0 0;
margin-top: -60rpx;
padding: 60rpx 50rpx 80rpx;
box-sizing: border-box;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.05);
min-height: 67vh;
}
/* 表单标题 */
.form-title {
display: flex;
align-items: center;
margin-bottom: 60rpx;
}
.title-bar {
width: 8rpx;
height: 40rpx;
background-color: #4a90e2;
border-radius: 4rpx;
margin-right: 20rpx;
}
.form-title .title-text {
font-size: 36rpx;
font-weight: 600;
color: #333333;
letter-spacing: 2rpx;
}
/* 输入框包装器 */
.input-wrapper {
margin-bottom: 60rpx;
}
/* 输入框组 */
.input-group {
display: flex;
align-items: center;
background-color: #f8f9fa;
border-radius: 12rpx;
padding: 0 30rpx;
margin-bottom: 30rpx;
height: 100rpx;
border: 2rpx solid transparent;
transition: all 0.3s;
}
.input-group:focus-within {
border-color: #4a90e2;
background-color: #ffffff;
}
.input-icon {
width: 50rpx;
height: 50rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20rpx;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
.icon-user {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='%234a90e2'%3E%3Cpath d='M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z'/%3E%3C/svg%3E");
}
.icon-lock {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='%234a90e2'%3E%3Cpath d='M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z'/%3E%3C/svg%3E");
}
.icon-shield {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='%234a90e2'%3E%3Cpath d='M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z'/%3E%3C/svg%3E");
}
.input-field {
flex: 1;
height: 100%;
font-size: 30rpx;
color: #333333;
background-color: transparent;
border: none;
outline: none;
}
/* IP输入组特殊样式 */
.ip-input-group {
padding: 0 20rpx;
}
.ip-field {
flex: 2;
background-color: #ffffff;
border-radius: 8rpx;
padding: 0 20rpx;
margin-right: 10rpx;
height: 70rpx;
box-sizing: border-box;
}
.port-field {
flex: 1;
background-color: #ffffff;
border-radius: 8rpx;
padding: 0 20rpx;
margin-left: 10rpx;
height: 70rpx;
box-sizing: border-box;
}
.ip-separator {
font-size: 32rpx;
color: #333333;
font-weight: 500;
margin: 0 8rpx;
line-height: 100rpx;
}
.input-placeholder {
color: #999999;
font-size: 28rpx;
}
/* 登录按钮 */
.login-btn {
width: 100%;
height: 96rpx;
background-color: #4a90e2;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
border: none;
margin-top: 40rpx;
box-shadow: 0 4rpx 12rpx rgba(74, 144, 226, 0.3);
transition: all 0.3s;
}
.login-btn:active {
transform: scale(0.98);
box-shadow: 0 2rpx 8rpx rgba(74, 144, 226, 0.2);
}
.login-btn[disabled] {
opacity: 0.6;
background-color: #cccccc;
}
.login-btn-text {
font-size: 36rpx;
font-weight: 600;
color: #ffffff;
letter-spacing: 2rpx;
}
</style>

BIN
src/static/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -0,0 +1,13 @@
uni.addInterceptor({
returnValue (res) {
if (!(!!res && (typeof res === "object" || typeof res === "function") && typeof res.then === "function")) {
return res;
}
return new Promise((resolve, reject) => {
res.then((res) => {
if (!res) return resolve(res)
return res[0] ? reject(res[0]) : resolve(res[1])
});
});
},
});

76
src/uni.scss Normal file
View File

@@ -0,0 +1,76 @@
/**
* 这里是uni-app内置的常用样式变量
*
* uni-app 官方扩展插件及插件市场https://ext.dcloud.net.cn上很多三方插件均使用了这些样式变量
* 如果你是插件开发者建议你使用scss预处理并在插件代码中直接使用这些变量无需 import 这个文件方便用户通过搭积木的方式开发整体风格一致的App
*
*/
/**
* 如果你是App开发者插件使用者你可以通过修改这些变量来定制自己的插件主题实现自定义主题功能
*
* 如果你的项目同样使用了scss预处理你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
*/
/* 颜色变量 */
/* 行为相关颜色 */
$uni-color-primary: #007aff;
$uni-color-success: #4cd964;
$uni-color-warning: #f0ad4e;
$uni-color-error: #dd524d;
/* 文字基本颜色 */
$uni-text-color:#333;//基本色
$uni-text-color-inverse:#fff;//反色
$uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息
$uni-text-color-placeholder: #808080;
$uni-text-color-disable:#c0c0c0;
/* 背景颜色 */
$uni-bg-color:#ffffff;
$uni-bg-color-grey:#f8f8f8;
$uni-bg-color-hover:#f1f1f1;//点击状态颜色
$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色
/* 边框颜色 */
$uni-border-color:#c8c7cc;
/* 尺寸变量 */
/* 文字尺寸 */
$uni-font-size-sm:12px;
$uni-font-size-base:14px;
$uni-font-size-lg:16px;
/* 图片尺寸 */
$uni-img-size-sm:20px;
$uni-img-size-base:26px;
$uni-img-size-lg:40px;
/* Border Radius */
$uni-border-radius-sm: 2px;
$uni-border-radius-base: 3px;
$uni-border-radius-lg: 6px;
$uni-border-radius-circle: 50%;
/* 水平间距 */
$uni-spacing-row-sm: 5px;
$uni-spacing-row-base: 10px;
$uni-spacing-row-lg: 15px;
/* 垂直间距 */
$uni-spacing-col-sm: 4px;
$uni-spacing-col-base: 8px;
$uni-spacing-col-lg: 12px;
/* 透明度 */
$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
/* 文章场景相关 */
$uni-color-title: #2C405A; // 文章标题颜色
$uni-font-size-title:20px;
$uni-color-subtitle: #555555; // 二级标题颜色
$uni-font-size-subtitle:26px;
$uni-color-paragraph: #3F536E; // 文章段落颜色
$uni-font-size-paragraph:15px;

128
src/utils/http.js Normal file
View File

@@ -0,0 +1,128 @@
import axios from "axios";
// 获取存储的IP地址
const getBaseURL = () => {
const storedIP = uni.getStorageSync("apiBaseURL");
if (storedIP) {
// 如果存储的IP不包含协议自动添加http://
return storedIP.startsWith("http") ? storedIP : `http://${storedIP}`;
}
return import.meta.env.VITE_API_BASE_URL || "/api";
};
// 创建 axios 实例
const http = axios.create({
baseURL: getBaseURL(),
timeout: 10000,
headers: {
"Content-Type": "application/json",
},
});
// 设置IP地址的方法供外部调用
export const setBaseURL = (ip) => {
const baseURL = ip.startsWith("http") ? ip : `http://${ip}`;
http.defaults.baseURL = baseURL;
uni.setStorageSync("apiBaseURL", baseURL);
};
// 请求拦截器
http.interceptors.request.use(
(config) => {
// 每次请求前更新baseURL从存储中获取最新的IP
const storedIP = uni.getStorageSync("apiBaseURL");
if (storedIP) {
const baseURL = storedIP.startsWith("http") ? storedIP : `http://${storedIP}`;
config.baseURL = baseURL;
}
// 从本地存储获取 token
const token = uni.getStorageSync("token");
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
// 显示 loading
uni.showLoading({ title: "加载中..." });
return config;
},
(error) => {
uni.hideLoading();
return Promise.reject(error);
}
);
// 响应拦截器
http.interceptors.response.use(
(response) => {
uni.hideLoading();
const { data } = response;
// 根据业务状态码处理(可根据实际接口调整)
if (data.code === 200 || data.code === 0) {
return data;
}
// 业务错误提示
uni.showToast({
title: data.message || "请求失败",
icon: "none",
});
return Promise.reject(data);
},
(error) => {
uni.hideLoading();
// HTTP 错误处理
let message = "网络错误";
if (error.response) {
switch (error.response.status) {
case 401:
message = "登录已过期,请重新登录";
// 清除 token 并跳转登录页
uni.removeStorageSync("token");
uni.reLaunch({ url: "/pages/index/index" });
break;
case 403:
message = "没有权限访问";
break;
case 404:
message = "请求资源不存在";
break;
case 500:
message = "服务器错误";
break;
default:
message = error.response.data?.message || "请求失败";
}
} else if (error.code === "ECONNABORTED") {
message = "请求超时";
}
uni.showToast({ title: message, icon: "none" });
return Promise.reject(error);
}
);
// 封装请求方法
export const get = (url, params, config = {}) => {
return http.get(url, { params, ...config });
};
export const post = (url, data, config = {}) => {
return http.post(url, data, config);
};
export const put = (url, data, config = {}) => {
return http.put(url, data, config);
};
export const del = (url, params, config = {}) => {
return http.delete(url, { params, ...config });
};
export default http;

13
vite.config.js Normal file
View File

@@ -0,0 +1,13 @@
import { defineConfig } from "vite";
import uni from "@dcloudio/vite-plugin-uni";
import Components from "unplugin-vue-components/vite";
import { VantResolver } from "@vant/auto-import-resolver";
export default defineConfig({
plugins: [
uni(),
Components({
resolvers: [VantResolver()],
}),
],
});