init project

This commit is contained in:
2026-01-12 19:13:10 +08:00
commit 5dc52ce6df
21 changed files with 1500 additions and 0 deletions

29
.gitignore vendored Normal file
View File

@@ -0,0 +1,29 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# uni-app
unpackage
.hbuilderx

21
App.vue Normal file
View File

@@ -0,0 +1,21 @@
<script setup>
import { onLaunch, onShow, onHide } from '@dcloudio/uni-app'
onLaunch(() => {
console.log('App Launch')
})
onShow(() => {
console.log('App Show')
})
onHide(() => {
console.log('App Hide')
})
</script>
<style lang="scss">
/*每个页面公共css */
@import '@/styles/common.scss';
</style>

136
README.md Normal file
View File

@@ -0,0 +1,136 @@
# nl-uniapp
基于 Vue3 + Vite + Ant Design Vue 的 uni-app 项目模板
## 技术栈
- **框架**: Vue 3
- **构建工具**: Vite
- **UI组件库**: Ant Design Vue 4.x
- **状态管理**: Pinia
- **HTTP请求**: Axios (已封装)
- **样式预处理**: Sass/SCSS
## 项目结构
```
nl-uniapp/
├── pages/ # 页面目录
│ └── index/ # 首页
├── src/
│ ├── api/ # API接口
│ │ └── index.js # 接口定义
│ ├── stores/ # Pinia状态管理
│ │ └── user.js # 用户状态
│ ├── utils/ # 工具函数
│ │ └── request.js # axios封装
│ └── styles/ # 样式文件
│ ├── common.scss # 公共样式
│ └── variables.scss # 样式变量
├── static/ # 静态资源
├── App.vue # 应用入口
├── main.js # 主入口文件
├── pages.json # 页面配置
├── manifest.json # 应用配置
├── vite.config.js # Vite配置
└── package.json # 项目配置
```
## 快速开始
### 安装依赖
```bash
npm install
# 或
yarn install
# 或
pnpm install
```
### 开发运行
```bash
# H5
npm run dev:h5
# 微信小程序
npm run dev:mp-weixin
# 支付宝小程序
npm run dev:mp-alipay
# 其他平台...
```
### 构建打包
```bash
# H5
npm run build:h5
# 微信小程序
npm run build:mp-weixin
# 其他平台...
```
## 功能特性
### 1. Axios封装
已封装好的axios请求工具位于 `src/utils/request.js`,包含:
- 请求/响应拦截器
- 自动添加token
- 统一错误处理
- Loading状态管理
- 支持GET、POST、PUT、DELETE、文件上传等方法
使用示例:
```javascript
import { api } from '@/api'
// GET请求
const res = await api.getList({ page: 1 })
// POST请求
const res = await api.create({ name: '测试' })
```
### 2. 状态管理
使用Pinia进行状态管理示例store位于 `src/stores/user.js`
### 3. API接口管理
所有API接口统一管理在 `src/api/index.js`,方便维护和调用
### 4. 样式系统
- 支持SCSS预处理器
- 统一的样式变量定义
- 公共样式类
## 注意事项
1. **Ant Design Vue兼容性**: Ant Design Vue主要针对H5端在小程序端可能不完全兼容建议
- H5端使用Ant Design Vue组件
- 小程序端使用uni-app原生组件或uni-ui组件库
2. **API配置**: 请在 `src/utils/request.js` 中修改 `baseURL` 为你的实际API地址
3. **Token管理**: Token会自动从store中获取并添加到请求头请确保正确设置token
## 开发建议
1. 页面开发时优先使用uni-app原生组件以保证跨平台兼容性
2. 样式使用rpx单位以适配不同屏幕尺寸
3. API请求统一使用封装好的request工具
4. 状态管理使用Pinia避免直接使用localStorage
## License
MIT

11
env.example Normal file
View File

@@ -0,0 +1,11 @@
# 环境变量配置示例
# 复制此文件为 .env.development 和 .env.production 并修改相应配置
# 开发环境
VITE_APP_BASE_API=/api
VITE_APP_TITLE=nl-uniapp
# 生产环境
# VITE_APP_BASE_API=https://your-api-domain.com
# VITE_APP_TITLE=nl-uniapp

17
index.html Normal file
View File

@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>nl-uniapp</title>
<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>
</head>
<body>
<div id="app"></div>
<script type="module" src="/main.js"></script>
</body>
</html>

18
main.js Normal file
View File

@@ -0,0 +1,18 @@
import { createSSRApp } from 'vue'
import App from './App.vue'
import Antd from 'ant-design-vue'
import 'ant-design-vue/dist/reset.css'
import { createPinia } from 'pinia'
export function createApp() {
const app = createSSRApp(App)
const pinia = createPinia()
app.use(pinia)
app.use(Antd)
return {
app
}
}

116
manifest.json Normal file
View File

@@ -0,0 +1,116 @@
{
"name": "nl-uniapp",
"appid": "__UNI__NLUNAPP",
"description": "uniapp项目",
"versionName": "1.0.0",
"versionCode": "100",
"transformPx": false,
"app-plus": {
"usingComponents": true,
"nvueStyleCompiler": "uni-app",
"compilerVersion": 3,
"splashscreen": {
"alwaysShowBeforeRender": true,
"waiting": true,
"autoclose": true,
"delay": 0
},
"modules": {},
"distribute": {
"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": {},
"sdkConfigs": {}
}
},
"quickapp": {},
"mp-weixin": {
"appid": "",
"setting": {
"urlCheck": false,
"es6": true,
"enhance": true,
"postcss": true,
"preloadBackgroundData": false,
"minified": true,
"newFeature": false,
"coverView": true,
"nodeModules": false,
"autoAudits": false,
"showShadowRootInWxmlPanel": true,
"scopeDataCheck": false,
"uglifyFileName": false,
"checkInvalidKey": true,
"checkSiteMap": true,
"uploadWithSourceMap": true,
"compileHotReLoad": false,
"lazyloadPlaceholderEnable": false,
"useMultiFrameRuntime": true,
"useApiHook": true,
"useApiHostProcess": true,
"babelSetting": {
"ignore": [],
"disablePlugins": [],
"outputPath": ""
},
"enableEngineNative": false,
"useIsolateContext": true,
"userConfirmedBundleSwitch": false,
"packNpmManually": false,
"packNpmRelationList": [],
"minifyWXSS": true,
"showES6CompileOption": false,
"minifyWXML": true,
"useStaticServer": true
},
"usingComponents": true,
"permission": {
"scope.userLocation": {
"desc": "你的位置信息将用于小程序位置接口的效果展示"
}
},
"optimization": {
"subPackages": true
}
},
"mp-alipay": {
"usingComponents": true
},
"mp-baidu": {
"usingComponents": true
},
"mp-toutiao": {
"usingComponents": true
},
"uniStatistics": {
"enable": false
},
"vueVersion": "3",
"h5": {
"devServer": {
"port": 3000,
"disableHostCheck": true
},
"router": {
"mode": "hash"
}
}
}

66
package.json Normal file
View File

@@ -0,0 +1,66 @@
{
"name": "nl-uniapp",
"version": "1.0.0",
"description": "uniapp项目 - Vue3 + Vite + Ant Design Vue",
"main": "main.js",
"scripts": {
"install": "npm install",
"dev:h5": "uni",
"dev:mp-weixin": "uni -p mp-weixin",
"dev:mp-alipay": "uni -p mp-alipay",
"dev:mp-baidu": "uni -p mp-baidu",
"dev:mp-toutiao": "uni -p mp-toutiao",
"dev:mp-qq": "uni -p mp-qq",
"dev:mp-kuaishou": "uni -p mp-kuaishou",
"dev:mp-lark": "uni -p mp-lark",
"dev:quickapp-webview": "uni -p quickapp-webview",
"dev:quickapp-webview-huawei": "uni -p quickapp-webview-huawei",
"dev:quickapp-webview-union": "uni -p quickapp-webview-union",
"build:h5": "uni build",
"build:mp-weixin": "uni build -p mp-weixin",
"build:mp-alipay": "uni build -p mp-alipay",
"build:mp-baidu": "uni build -p mp-baidu",
"build:mp-toutiao": "uni build -p mp-toutiao",
"build:mp-qq": "uni build -p mp-qq",
"build:mp-kuaishou": "uni build -p mp-kuaishou",
"build:mp-lark": "uni build -p mp-lark",
"build:quickapp-webview": "uni build -p quickapp-webview",
"build:quickapp-webview-huawei": "uni build -p quickapp-webview-huawei",
"build:quickapp-webview-union": "uni build -p quickapp-webview-union"
},
"dependencies": {
"@dcloudio/uni-app": "^3.0.0",
"@dcloudio/uni-app-plus": "^3.0.0",
"@dcloudio/uni-components": "^3.0.0",
"@dcloudio/uni-h5": "^3.0.0",
"@dcloudio/uni-mp-alipay": "^3.0.0",
"@dcloudio/uni-mp-baidu": "^3.0.0",
"@dcloudio/uni-mp-kuaishou": "^3.0.0",
"@dcloudio/uni-mp-lark": "^3.0.0",
"@dcloudio/uni-mp-qq": "^3.0.0",
"@dcloudio/uni-mp-toutiao": "^3.0.0",
"@dcloudio/uni-mp-weixin": "^3.0.0",
"@dcloudio/uni-quickapp-webview": "^3.0.0",
"ant-design-vue": "^4.0.0",
"axios": "^1.6.0",
"pinia": "^2.1.0",
"vue": "^3.3.0"
},
"devDependencies": {
"@dcloudio/types": "^3.4.0",
"@dcloudio/uni-automator": "^3.0.0",
"@dcloudio/uni-cli-shared": "^3.0.0",
"@dcloudio/vite-plugin-uni": "^3.0.0",
"@vitejs/plugin-vue": "^4.5.0",
"sass": "^1.69.0",
"vite": "^5.0.0"
},
"browserslist": [
"Android >= 4.4",
"ios >= 9"
],
"uni-app": {
"scripts": {}
}
}

37
pages.json Normal file
View File

@@ -0,0 +1,37 @@
{
"easycom": {
"autoscan": true,
"custom": {
"^u-(.*)": "uview-ui/components/u-$1/u-$1.vue"
}
},
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "首页"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "nl-uniapp",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8"
},
"tabBar": {
"color": "#7A7E83",
"selectedColor": "#3cc51f",
"borderStyle": "black",
"backgroundColor": "#ffffff",
"list": [
{
"pagePath": "pages/index/index",
"iconPath": "static/tab-home.png",
"selectedIconPath": "static/tab-home-current.png",
"text": "首页"
}
]
}
}

291
pages/index/index.vue Normal file
View File

@@ -0,0 +1,291 @@
<template>
<view class="container">
<!-- H5端使用Ant Design Vue组件 -->
<!-- #ifdef H5 -->
<a-card title="欢迎使用 nl-uniapp" :bordered="false">
<a-space direction="vertical" :size="16" style="width: 100%">
<a-typography-title :level="3">项目信息</a-typography-title>
<a-descriptions :column="1" bordered>
<a-descriptions-item label="框架">Vue 3</a-descriptions-item>
<a-descriptions-item label="构建工具">Vite</a-descriptions-item>
<a-descriptions-item label="UI组件库">Ant Design Vue</a-descriptions-item>
<a-descriptions-item label="状态管理">Pinia</a-descriptions-item>
<a-descriptions-item label="HTTP请求">Axios</a-descriptions-item>
</a-descriptions>
<a-divider />
<a-space direction="vertical" :size="12" style="width: 100%">
<a-typography-title :level="4">功能演示</a-typography-title>
<a-button type="primary" block @click="handleGetRequest" :loading="loading">
测试GET请求
</a-button>
<a-button type="primary" block @click="handlePostRequest" :loading="loading">
测试POST请求
</a-button>
<a-button type="default" block @click="handleShowMessage">
显示消息提示
</a-button>
</a-space>
<a-divider />
<a-alert
message="提示"
description="这是一个基于 Vue3 + Vite + Ant Design Vue 的 uniapp 项目模板"
type="info"
show-icon
/>
</a-space>
</a-card>
<!-- #endif -->
<!-- 小程序端使用uni-app原生组件 -->
<!-- #ifndef H5 -->
<view class="card">
<view class="title">欢迎使用 nl-uniapp</view>
<view class="info-section">
<view class="info-title">项目信息</view>
<view class="info-item">
<text class="label">框架</text>
<text class="value">Vue 3</text>
</view>
<view class="info-item">
<text class="label">构建工具</text>
<text class="value">Vite</text>
</view>
<view class="info-item">
<text class="label">UI组件库</text>
<text class="value">Ant Design Vue</text>
</view>
<view class="info-item">
<text class="label">状态管理</text>
<text class="value">Pinia</text>
</view>
<view class="info-item">
<text class="label">HTTP请求</text>
<text class="value">Axios</text>
</view>
</view>
<view class="divider"></view>
<view class="demo-section">
<view class="demo-title">功能演示</view>
<button class="btn btn-primary" @click="handleGetRequest" :loading="loading">
测试GET请求
</button>
<button class="btn btn-primary" @click="handlePostRequest" :loading="loading">
测试POST请求
</button>
<button class="btn btn-default" @click="handleShowMessage">
显示消息提示
</button>
</view>
<view class="divider"></view>
<view class="alert">
<text class="alert-title">提示</text>
<text class="alert-desc">这是一个基于 Vue3 + Vite + Ant Design Vue uniapp 项目模板</text>
</view>
</view>
<!-- #endif -->
</view>
</template>
<script setup>
import { ref } from 'vue'
import { api } from '@/api'
// #ifdef H5
import { message } from 'ant-design-vue'
// #endif
const loading = ref(false)
// GET请求示例
const handleGetRequest = async () => {
try {
loading.value = true
const res = await api.getList({ page: 1, pageSize: 10 })
console.log('GET请求成功:', res)
// #ifdef H5
message.success('GET请求成功请查看控制台')
// #endif
// #ifndef H5
uni.showToast({
title: 'GET请求成功',
icon: 'success'
})
// #endif
} catch (error) {
console.error('GET请求失败:', error)
// #ifdef H5
message.error('GET请求失败')
// #endif
// #ifndef H5
uni.showToast({
title: 'GET请求失败',
icon: 'none'
})
// #endif
} finally {
loading.value = false
}
}
// POST请求示例
const handlePostRequest = async () => {
try {
loading.value = true
const res = await api.create({ name: '测试数据', value: '123' })
console.log('POST请求成功:', res)
// #ifdef H5
message.success('POST请求成功请查看控制台')
// #endif
// #ifndef H5
uni.showToast({
title: 'POST请求成功',
icon: 'success'
})
// #endif
} catch (error) {
console.error('POST请求失败:', error)
// #ifdef H5
message.error('POST请求失败')
// #endif
// #ifndef H5
uni.showToast({
title: 'POST请求失败',
icon: 'none'
})
// #endif
} finally {
loading.value = false
}
}
// 消息提示示例
const handleShowMessage = () => {
// #ifdef H5
message.info('这是一个消息提示')
// #endif
// #ifndef H5
uni.showToast({
title: '这是一个消息提示',
icon: 'none'
})
// #endif
}
</script>
<style lang="scss" scoped>
.container {
padding: 16px;
min-height: 100vh;
background-color: #f5f5f5;
}
// 小程序端样式
.card {
background: #fff;
border-radius: 8px;
padding: 24px;
margin-bottom: 16px;
}
.title {
font-size: 20px;
font-weight: bold;
margin-bottom: 24px;
text-align: center;
}
.info-section {
margin-bottom: 24px;
}
.info-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 16px;
}
.info-item {
display: flex;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
.label {
color: #666;
width: 120px;
}
.value {
color: #333;
flex: 1;
}
}
.divider {
height: 1px;
background-color: #e8e8e8;
margin: 24px 0;
}
.demo-section {
margin-bottom: 24px;
}
.demo-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 16px;
}
.btn {
width: 100%;
margin-bottom: 12px;
padding: 12px;
border-radius: 4px;
font-size: 16px;
&.btn-primary {
background-color: #1890ff;
color: #fff;
}
&.btn-default {
background-color: #fff;
color: #333;
border: 1px solid #d9d9d9;
}
}
.alert {
padding: 12px;
background-color: #e6f7ff;
border: 1px solid #91d5ff;
border-radius: 4px;
.alert-title {
display: block;
font-weight: bold;
margin-bottom: 8px;
color: #1890ff;
}
.alert-desc {
display: block;
color: #666;
font-size: 14px;
line-height: 1.5;
}
}
</style>

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

@@ -0,0 +1,77 @@
import request from '@/utils/request'
import { useUserStore } from '@/stores/user'
// 获取token的辅助函数
function getToken() {
try {
const userStore = useUserStore()
return userStore.getToken() || ''
} catch (e) {
return ''
}
}
// 示例API接口
export const api = {
// 获取用户信息
getUserInfo() {
return request.get('/user/info')
},
// 登录
login(data) {
return request.post('/user/login', data)
},
// 退出登录
logout() {
return request.post('/user/logout')
},
// 获取列表数据
getList(params) {
return request.get('/list', params)
},
// 创建数据
create(data) {
return request.post('/create', data)
},
// 更新数据
update(id, data) {
return request.put(`/update/${id}`, data)
},
// 删除数据
delete(id) {
return request.delete(`/delete/${id}`)
},
// 上传文件
uploadFile(filePath) {
return new Promise((resolve, reject) => {
const baseURL = import.meta.env.VITE_APP_BASE_API || (process.env.NODE_ENV === 'development' ? '/api' : 'https://your-api-domain.com')
uni.uploadFile({
url: baseURL + '/upload',
filePath: filePath,
name: 'file',
header: {
Authorization: `Bearer ${getToken()}`
},
success: (res) => {
try {
const data = JSON.parse(res.data)
resolve(data)
} catch (e) {
resolve(res.data)
}
},
fail: (err) => {
reject(err)
}
})
})
}
}

25
src/components/README.md Normal file
View File

@@ -0,0 +1,25 @@
# 组件目录
此目录用于存放自定义组件。
## 使用方式
在页面中引入组件:
```vue
<template>
<view>
<custom-component />
</view>
</template>
<script setup>
import CustomComponent from '@/components/CustomComponent.vue'
</script>
```
## 组件命名规范
- 组件文件名使用 PascalCase大驼峰命名
- 组件名与文件名保持一致

55
src/stores/user.js Normal file
View File

@@ -0,0 +1,55 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useUserStore = defineStore('user', () => {
const token = ref('')
const userInfo = ref(null)
// 设置token
function setToken(val) {
token.value = val
// 可以同时存储到本地
uni.setStorageSync('token', val)
}
// 获取token
function getToken() {
if (!token.value) {
token.value = uni.getStorageSync('token') || ''
}
return token.value
}
// 设置用户信息
function setUserInfo(info) {
userInfo.value = info
uni.setStorageSync('userInfo', info)
}
// 获取用户信息
function getUserInfo() {
if (!userInfo.value) {
userInfo.value = uni.getStorageSync('userInfo') || null
}
return userInfo.value
}
// 退出登录
function logout() {
token.value = ''
userInfo.value = null
uni.removeStorageSync('token')
uni.removeStorageSync('userInfo')
}
return {
token,
userInfo,
setToken,
getToken,
setUserInfo,
getUserInfo,
logout
}
})

122
src/styles/common.scss Normal file
View File

@@ -0,0 +1,122 @@
/* 全局样式 */
page {
background-color: #f5f5f5;
font-size: 14px;
color: #333;
}
/* 通用容器 */
.container {
padding: 16px;
}
/* 通用卡片 */
.card {
background: #fff;
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
}
/* 通用按钮 */
.btn {
padding: 12px 24px;
border-radius: 4px;
border: none;
font-size: 14px;
cursor: pointer;
&.btn-primary {
background-color: #1890ff;
color: #fff;
}
&.btn-success {
background-color: #52c41a;
color: #fff;
}
&.btn-warning {
background-color: #faad14;
color: #fff;
}
&.btn-error {
background-color: #f5222d;
color: #fff;
}
}
/* 通用文本 */
.text-primary {
color: #1890ff;
}
.text-success {
color: #52c41a;
}
.text-warning {
color: #faad14;
}
.text-error {
color: #f5222d;
}
.text-center {
text-align: center;
}
.text-right {
text-align: right;
}
/* 通用布局 */
.flex {
display: flex;
}
.flex-center {
display: flex;
align-items: center;
justify-content: center;
}
.flex-between {
display: flex;
align-items: center;
justify-content: space-between;
}
.flex-column {
display: flex;
flex-direction: column;
}
/* 通用间距 */
.mt-xs { margin-top: 4px; }
.mt-sm { margin-top: 8px; }
.mt-md { margin-top: 16px; }
.mt-lg { margin-top: 24px; }
.mb-xs { margin-bottom: 4px; }
.mb-sm { margin-bottom: 8px; }
.mb-md { margin-bottom: 16px; }
.mb-lg { margin-bottom: 24px; }
.ml-xs { margin-left: 4px; }
.ml-sm { margin-left: 8px; }
.ml-md { margin-left: 16px; }
.ml-lg { margin-left: 24px; }
.mr-xs { margin-right: 4px; }
.mr-sm { margin-right: 8px; }
.mr-md { margin-right: 16px; }
.mr-lg { margin-right: 24px; }
.p-xs { padding: 4px; }
.p-sm { padding: 8px; }
.p-md { padding: 16px; }
.p-lg { padding: 24px; }

34
src/styles/variables.scss Normal file
View File

@@ -0,0 +1,34 @@
// 主题颜色
$primary-color: #1890ff;
$success-color: #52c41a;
$warning-color: #faad14;
$error-color: #f5222d;
$info-color: #1890ff;
// 文字颜色
$text-color: #333333;
$text-color-secondary: #666666;
$text-color-disabled: #999999;
// 背景颜色
$bg-color: #ffffff;
$bg-color-secondary: #f5f5f5;
// 边框颜色
$border-color: #e8e8e8;
$border-radius: 4px;
// 间距
$spacing-xs: 4px;
$spacing-sm: 8px;
$spacing-md: 16px;
$spacing-lg: 24px;
$spacing-xl: 32px;
// 字体大小
$font-size-xs: 12px;
$font-size-sm: 14px;
$font-size-md: 16px;
$font-size-lg: 18px;
$font-size-xl: 20px;

164
src/utils/index.js Normal file
View File

@@ -0,0 +1,164 @@
/**
* 工具函数集合
*/
/**
* 格式化日期
* @param {Date|string|number} date 日期
* @param {string} format 格式,默认 'YYYY-MM-DD HH:mm:ss'
* @returns {string} 格式化后的日期字符串
*/
export function formatDate(date, format = 'YYYY-MM-DD HH:mm:ss') {
const d = new Date(date)
const year = d.getFullYear()
const month = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
const hour = String(d.getHours()).padStart(2, '0')
const minute = String(d.getMinutes()).padStart(2, '0')
const second = String(d.getSeconds()).padStart(2, '0')
return format
.replace('YYYY', year)
.replace('MM', month)
.replace('DD', day)
.replace('HH', hour)
.replace('mm', minute)
.replace('ss', second)
}
/**
* 防抖函数
* @param {Function} func 要防抖的函数
* @param {number} wait 等待时间(毫秒)
* @returns {Function} 防抖后的函数
*/
export function debounce(func, wait = 300) {
let timeout
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout)
func(...args)
}
clearTimeout(timeout)
timeout = setTimeout(later, wait)
}
}
/**
* 节流函数
* @param {Function} func 要节流的函数
* @param {number} limit 时间限制(毫秒)
* @returns {Function} 节流后的函数
*/
export function throttle(func, limit = 300) {
let inThrottle
return function executedFunction(...args) {
if (!inThrottle) {
func(...args)
inThrottle = true
setTimeout(() => (inThrottle = false), limit)
}
}
}
/**
* 深拷贝
* @param {any} obj 要拷贝的对象
* @returns {any} 拷贝后的对象
*/
export function deepClone(obj) {
if (obj === null || typeof obj !== 'object') return obj
if (obj instanceof Date) return new Date(obj.getTime())
if (obj instanceof Array) return obj.map(item => deepClone(item))
if (typeof obj === 'object') {
const clonedObj = {}
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
clonedObj[key] = deepClone(obj[key])
}
}
return clonedObj
}
}
/**
* 生成唯一ID
* @returns {string} 唯一ID
*/
export function generateId() {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
}
/**
* 存储数据到本地
* @param {string} key 键名
* @param {any} value 值
*/
export function setStorage(key, value) {
try {
uni.setStorageSync(key, value)
} catch (e) {
console.error('存储数据失败:', e)
}
}
/**
* 从本地获取数据
* @param {string} key 键名
* @param {any} defaultValue 默认值
* @returns {any} 存储的值
*/
export function getStorage(key, defaultValue = null) {
try {
const value = uni.getStorageSync(key)
return value !== '' ? value : defaultValue
} catch (e) {
console.error('获取数据失败:', e)
return defaultValue
}
}
/**
* 删除本地存储
* @param {string} key 键名
*/
export function removeStorage(key) {
try {
uni.removeStorageSync(key)
} catch (e) {
console.error('删除数据失败:', e)
}
}
/**
* 显示提示消息
* @param {string} title 提示内容
* @param {string} icon 图标类型
* @param {number} duration 显示时长
*/
export function showToast(title, icon = 'none', duration = 2000) {
uni.showToast({
title,
icon,
duration
})
}
/**
* 显示加载提示
* @param {string} title 提示内容
*/
export function showLoading(title = '加载中...') {
uni.showLoading({
title,
mask: true
})
}
/**
* 隐藏加载提示
*/
export function hideLoading() {
uni.hideLoading()
}

205
src/utils/request.js Normal file
View File

@@ -0,0 +1,205 @@
import axios from 'axios'
import { useUserStore } from '@/stores/user'
// 创建axios实例
const service = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_API || (process.env.NODE_ENV === 'development' ? '/api' : 'https://your-api-domain.com'),
timeout: 10000,
headers: {
'Content-Type': 'application/json;charset=UTF-8'
}
})
// 请求拦截器
service.interceptors.request.use(
config => {
// 在发送请求之前做些什么
const userStore = useUserStore()
// 添加token
if (userStore.token) {
config.headers.Authorization = `Bearer ${userStore.token}`
}
// 显示loading可根据需要自定义
if (config.showLoading !== false) {
uni.showLoading({
title: '加载中...',
mask: true
})
}
return config
},
error => {
// 对请求错误做些什么
uni.hideLoading()
console.error('请求错误:', error)
return Promise.reject(error)
}
)
// 响应拦截器
service.interceptors.response.use(
response => {
// 隐藏loading
uni.hideLoading()
const res = response.data
// 如果返回的状态码为200说明接口请求成功可以正常拿到数据
// 否则的话抛出错误
if (res.code !== undefined && res.code !== 200) {
// 根据不同的错误码,做相应的处理
handleError(res.code, res.message || '请求失败')
// 返回一个reject的Promise让错误信息能够被catch到
return Promise.reject(new Error(res.message || '请求失败'))
} else {
// 返回成功的数据
return res
}
},
error => {
// 隐藏loading
uni.hideLoading()
// 对响应错误做点什么
console.error('响应错误:', error)
if (error.response) {
// 服务器返回了错误状态码
const { status, data } = error.response
switch (status) {
case 401:
// 未授权清除token并跳转到登录页
handleError(401, '未授权,请重新登录')
try {
const userStore = useUserStore()
userStore.logout()
// 注意:如果登录页面不存在,请注释掉下面的跳转
// uni.reLaunch({
// url: '/pages/login/login'
// })
} catch (e) {
console.error('退出登录失败:', e)
}
break
case 403:
handleError(403, '拒绝访问')
break
case 404:
handleError(404, '请求错误,未找到该资源')
break
case 500:
handleError(500, '服务器错误')
break
default:
handleError(status, data?.message || `连接错误${status}`)
}
} else if (error.request) {
// 请求已经发出,但没有收到响应
handleError(-1, '网络连接失败,请检查网络')
} else {
// 发送请求时出了点问题
handleError(-1, error.message || '请求失败')
}
return Promise.reject(error)
}
)
// 错误处理函数
function handleError(code, message) {
// 可以根据不同的错误码做不同的处理
uni.showToast({
title: message,
icon: 'none',
duration: 2000
})
}
// 封装请求方法
const request = {
/**
* GET请求
* @param {string} url 请求地址
* @param {object} params 请求参数
* @param {object} config 请求配置
*/
get(url, params = {}, config = {}) {
return service({
method: 'get',
url,
params,
...config
})
},
/**
* POST请求
* @param {string} url 请求地址
* @param {object} data 请求数据
* @param {object} config 请求配置
*/
post(url, data = {}, config = {}) {
return service({
method: 'post',
url,
data,
...config
})
},
/**
* PUT请求
* @param {string} url 请求地址
* @param {object} data 请求数据
* @param {object} config 请求配置
*/
put(url, data = {}, config = {}) {
return service({
method: 'put',
url,
data,
...config
})
},
/**
* DELETE请求
* @param {string} url 请求地址
* @param {object} params 请求参数
* @param {object} config 请求配置
*/
delete(url, params = {}, config = {}) {
return service({
method: 'delete',
url,
params,
...config
})
},
/**
* 上传文件
* @param {string} url 请求地址
* @param {FormData} formData 表单数据
* @param {object} config 请求配置
*/
upload(url, formData, config = {}) {
return service({
method: 'post',
url,
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
},
...config
})
}
}
export default request

3
static/.gitkeep Normal file
View File

@@ -0,0 +1,3 @@
# 静态资源目录
# 请在此目录放置图片、字体等静态资源

26
tsconfig.json Normal file
View File

@@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}

11
tsconfig.node.json Normal file
View File

@@ -0,0 +1,11 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.js"]
}

36
vite.config.js Normal file
View File

@@ -0,0 +1,36 @@
import { defineConfig } from 'vite'
import uni from '@dcloudio/vite-plugin-uni'
import vue from '@vitejs/plugin-vue'
import path from 'path'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
uni()
],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src')
}
},
css: {
preprocessorOptions: {
scss: {
additionalData: `@import "@/styles/variables.scss";`
}
}
},
server: {
port: 3000,
open: true,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
})