From c6b379107683ae21a4f1cd07ef5e3f2a48894f3b Mon Sep 17 00:00:00 2001
From: liangbin <15536829364@163.com>
Date: Thu, 15 Jan 2026 16:50:32 +0800
Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=E9=A1=B9=E7=9B=AE?=
=?UTF-8?q?=E5=88=9B=E5=BB=BAUniapp=E6=A8=A1=E7=89=88=20vue3=E4=B8=8Euview?=
=?UTF-8?q?-plus=E6=A1=86=E6=9E=B6=E6=90=AD=E5=BB=BA?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.gitignore | 46 +++++++
README.md | 158 ++++++++++++++++++++++
index.html | 13 ++
package.json | 35 +++++
src/App.vue | 12 ++
src/api/index.js | 2 +
src/api/modules/auth.js | 21 +++
src/api/modules/user.js | 25 ++++
src/api/request.js | 163 +++++++++++++++++++++++
src/components/common/base-button.vue | 71 ++++++++++
src/components/common/custom-card.vue | 91 +++++++++++++
src/components/common/empty-state.vue | 71 ++++++++++
src/components/common/index.js | 5 +
src/components/common/lazy-image.vue | 108 +++++++++++++++
src/components/common/page-container.vue | 93 +++++++++++++
src/components/layout/index.js | 3 +
src/components/layout/layout-footer.vue | 107 +++++++++++++++
src/components/layout/layout-header.vue | 125 +++++++++++++++++
src/components/layout/layout-sidebar.vue | 137 +++++++++++++++++++
src/main.js | 17 +++
src/manifest.json | 75 +++++++++++
src/pages.json | 86 ++++++++++++
src/pages/demo/index.vue | 3 +
src/pages/index/index.vue | 12 ++
src/pages/login/index.vue | 138 +++++++++++++++++++
src/pages/register/index.vue | 3 +
src/pages/user/index.vue | 3 +
src/store/app.js | 49 +++++++
src/store/index.js | 2 +
src/store/user.js | 67 ++++++++++
src/styles/uni.scss | 42 ++++++
src/styles/variable.scss | 29 ++++
src/utils/format.js | 121 +++++++++++++++++
src/utils/index.js | 3 +
src/utils/storage.js | 91 +++++++++++++
src/utils/validate.js | 114 ++++++++++++++++
vite.config.js | 26 ++++
37 files changed, 2167 insertions(+)
create mode 100644 .gitignore
create mode 100644 README.md
create mode 100644 index.html
create mode 100644 package.json
create mode 100644 src/App.vue
create mode 100644 src/api/index.js
create mode 100644 src/api/modules/auth.js
create mode 100644 src/api/modules/user.js
create mode 100644 src/api/request.js
create mode 100644 src/components/common/base-button.vue
create mode 100644 src/components/common/custom-card.vue
create mode 100644 src/components/common/empty-state.vue
create mode 100644 src/components/common/index.js
create mode 100644 src/components/common/lazy-image.vue
create mode 100644 src/components/common/page-container.vue
create mode 100644 src/components/layout/index.js
create mode 100644 src/components/layout/layout-footer.vue
create mode 100644 src/components/layout/layout-header.vue
create mode 100644 src/components/layout/layout-sidebar.vue
create mode 100644 src/main.js
create mode 100644 src/manifest.json
create mode 100644 src/pages.json
create mode 100644 src/pages/demo/index.vue
create mode 100644 src/pages/index/index.vue
create mode 100644 src/pages/login/index.vue
create mode 100644 src/pages/register/index.vue
create mode 100644 src/pages/user/index.vue
create mode 100644 src/store/app.js
create mode 100644 src/store/index.js
create mode 100644 src/store/user.js
create mode 100644 src/styles/uni.scss
create mode 100644 src/styles/variable.scss
create mode 100644 src/utils/format.js
create mode 100644 src/utils/index.js
create mode 100644 src/utils/storage.js
create mode 100644 src/utils/validate.js
create mode 100644 vite.config.js
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4bbdc8f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,46 @@
+# 依赖
+node_modules/
+package-lock.json
+yarn.lock
+pnpm-lock.yaml
+
+# 构建输出
+dist/
+unpackage/
+.hbuilderx/
+
+# 环境变量
+.env.local
+.env.*.local
+
+# 日志
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+
+# 编辑器
+.vscode/
+.idea/
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+# 操作系统
+.DS_Store
+Thumbs.db
+
+# 测试
+coverage/
+.nyc_output/
+
+# 临时文件
+*.tmp
+*.temp
+.cache/
+
+# 其他
+.trae/
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..eec5c88
--- /dev/null
+++ b/README.md
@@ -0,0 +1,158 @@
+# uniapp-vue3-template
+
+基于 Vue3 + Vite + Pinia + uview-plus 的 UniApp 项目模板
+
+## 技术栈
+
+- **核心框架**: Vue3 + Vite
+- **状态管理**: Pinia(配置持久化存储)
+- **UI组件库**: uview-plus
+- **日期处理**: dayjs
+- **多端支持**: H5、微信小程序
+
+## 项目结构
+
+```
+uniapp-vue3-template/
+├── api/ # API 接口层
+│ ├── modules/ # 业务接口模块
+│ │ ├── auth.js # 登录注册接口
+│ │ └── user.js # 用户相关接口
+│ ├── request.js # HTTP 请求封装
+│ └── index.js # 接口统一导出
+├── components/ # 组件目录
+│ ├── common/ # 公共组件
+│ │ ├── lazy-image.vue # 图片懒加载
+│ │ ├── empty-state.vue # 空状态组件
+│ │ └── page-container.vue # 页面容器
+│ └── layout/ # 布局组件
+│ ├── layout-header.vue # 头部导航
+│ ├── layout-footer.vue # 底部标签栏
+│ └── layout-sidebar.vue # 侧边栏
+├── pages/ # 主包页面
+│ ├── index/ # 首页
+│ ├── login/ # 登录页
+│ ├── register/ # 注册页
+│ ├── user/ # 个人中心
+│ └── demo/ # 组件演示
+├── store/ # Pinia 状态管理
+│ ├── user.js # 用户状态
+│ ├── app.js # 应用状态
+│ └── index.js # 状态管理入口
+├── styles/ # 样式文件
+│ ├── uni.scss # 全局样式
+│ └── variable.scss # SCSS 变量
+├── subpkg/ # 分包目录
+│ ├── goods/ # 商品模块
+│ └── order/ # 订单模块
+├── utils/ # 工具函数
+│ ├── storage.js # 本地存储
+│ ├── validate.js # 表单验证
+│ ├── format.js # 格式化工具
+│ └── index.js # 工具导出
+├── App.vue # 应用入口
+├── main.js # 应用配置
+├── manifest.json # 应用配置文件
+├── pages.json # 页面配置
+├── package.json # 项目依赖
+└── vite.config.js # Vite 配置
+```
+
+## 快速开始
+
+### 安装依赖
+
+```bash
+npm install
+```
+
+### 开发模式
+
+```bash
+# H5 开发
+npm run dev:h5
+
+# 微信小程序开发
+npm run dev:mp-weixin
+```
+
+### 构建生产
+
+```bash
+# H5 构建
+npm run build:h5
+
+# 微信小程序构建
+npm run build:mp-weixin
+```
+
+## 功能特性
+
+- ✅ Pinia 状态管理 + 持久化存储
+- ✅ uview-plus 组件库集成
+- ✅ dayjs 日期处理
+- ✅ HTTP 请求封装(拦截器)
+- ✅ 路由分包加载
+- ✅ 图片懒加载组件
+- ✅ 页面缓存策略
+- ✅ 登录/注册功能模块
+- ✅ 基础布局组件(头部、底部、侧边栏)
+- ✅ 常用工具函数
+
+## API 使用
+
+```javascript
+import { get, post } from '@/api'
+import { login, register } from '@/api/modules/auth'
+
+// GET 请求
+const list = await get('/user/list', { page: 1, pageSize: 10 })
+
+// POST 请求
+const res = await login({ username: 'admin', password: '123456' })
+```
+
+## Store 使用
+
+```javascript
+import { useUserStore } from '@/store/user'
+
+const userStore = useUserStore()
+
+// 设置用户信息
+userStore.setUserInfo({ id: '1', username: 'admin' })
+
+// 获取状态
+const isLoggedIn = userStore.isLoggedIn
+
+// 退出登录
+userStore.logout()
+```
+
+## 页面缓存
+
+```javascript
+import { useAppStore } from '@/store/app'
+
+const appStore = useAppStore()
+
+// 添加页面缓存
+appStore.addCachePage('user-profile')
+
+// 移除页面缓存
+appStore.removeCachePage('user-profile')
+
+// 清空缓存
+appStore.clearCache()
+```
+
+## 注意事项
+
+1. 首次运行需要安装依赖
+2. 开发 H5 时访问 `http://localhost:3000`
+3. 微信小程序需要配置 AppID
+4. 记得修改 API 请求的 BASE_URL
+
+## License
+
+MIT
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..63f6485
--- /dev/null
+++ b/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+ uniapp-vue3-template
+
+
+
+
+
+
+
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..6ea7f84
--- /dev/null
+++ b/package.json
@@ -0,0 +1,35 @@
+{
+ "name": "uniapp-vue3-template",
+ "version": "1.0.0",
+ "description": "基于Vue3 + Vite + Pinia + uview-plus的UniApp项目模板",
+ "main": "main.ts",
+ "scripts": {
+ "dev:h5": "uni",
+ "dev:mp-weixin": "uni -p mp-weixin",
+ "build:h5": "uni build",
+ "build:mp-weixin": "uni build -p mp-weixin",
+ "preview:h5": "uni preview"
+ },
+ "dependencies": {
+ "@dcloudio/uni-app": "3.0.0-4010520240507001",
+ "@dcloudio/uni-components": "3.0.0-4010520240507001",
+ "@dcloudio/uni-h5": "3.0.0-4010520240507001",
+ "@dcloudio/uni-mp-weixin": "3.0.0-4010520240507001",
+ "dayjs": "^1.11.10",
+ "pinia": "^2.1.7",
+ "pinia-plugin-persistedstate": "^3.2.1",
+ "uview-plus": "^3.2.7",
+ "vue": "^3.4.21"
+ },
+ "devDependencies": {
+ "@dcloudio/types": "^3.4.8",
+ "@dcloudio/uni-automator": "3.0.0-4010520240507001",
+ "@dcloudio/uni-cli-shared": "3.0.0-4010520240507001",
+ "@dcloudio/uni-stacktracey": "3.0.0-4010520240507001",
+ "@dcloudio/vite-plugin-uni": "3.0.0-4010520240507001",
+ "sass": "1.63.2",
+ "sass-loader": "10.4.1",
+ "typescript": "^5.3.3",
+ "vite": "^5.1.4"
+ }
+}
diff --git a/src/App.vue b/src/App.vue
new file mode 100644
index 0000000..16040f6
--- /dev/null
+++ b/src/App.vue
@@ -0,0 +1,12 @@
+
+
+ UniApp 加载中...
+
+
+
+
+
+
diff --git a/src/api/index.js b/src/api/index.js
new file mode 100644
index 0000000..74f0b02
--- /dev/null
+++ b/src/api/index.js
@@ -0,0 +1,2 @@
+export * from './auth'
+export * from './user'
diff --git a/src/api/modules/auth.js b/src/api/modules/auth.js
new file mode 100644
index 0000000..c9b333d
--- /dev/null
+++ b/src/api/modules/auth.js
@@ -0,0 +1,21 @@
+import { get, post } from '../request'
+
+export function login(data) {
+ return post('/auth/login', data)
+}
+
+export function register(data) {
+ return post('/auth/register', data)
+}
+
+export function logout() {
+ return post('/auth/logout')
+}
+
+export function getSmsCode(phone) {
+ return get('/auth/smscode', { phone })
+}
+
+export function resetPassword(data) {
+ return post('/auth/resetPassword', data)
+}
diff --git a/src/api/modules/user.js b/src/api/modules/user.js
new file mode 100644
index 0000000..4ba51dd
--- /dev/null
+++ b/src/api/modules/user.js
@@ -0,0 +1,25 @@
+import { get, post } from '../request'
+
+export function getUserProfile() {
+ return get('/user/profile')
+}
+
+export function updateUserProfile(data) {
+ return post('/user/profile', data)
+}
+
+export function updateAvatar(avatar) {
+ return post('/user/avatar', { avatar })
+}
+
+export function bindPhone(phone, smsCode) {
+ return post('/user/bindPhone', { phone, smsCode })
+}
+
+export function getUserList(params) {
+ return get('/user/list', params)
+}
+
+export function getUserDetail(id) {
+ return get(`/user/detail/${id}`)
+}
diff --git a/src/api/request.js b/src/api/request.js
new file mode 100644
index 0000000..6ad81e1
--- /dev/null
+++ b/src/api/request.js
@@ -0,0 +1,163 @@
+import { useUserStore } from '@/store/user'
+
+const BASE_URL = 'https://api.example.com'
+
+const requestInterceptor = (config) => {
+ const userStore = useUserStore()
+
+ if (config.header) {
+ config.header = {
+ ...config.header,
+ 'Content-Type': 'application/json',
+ 'Authorization': userStore.token ? `Bearer ${userStore.token}` : ''
+ }
+ } else {
+ config.header = {
+ 'Content-Type': 'application/json',
+ 'Authorization': userStore.token ? `Bearer ${userStore.token}` : ''
+ }
+ }
+
+ return config
+}
+
+const responseInterceptor = (response) => {
+ const { statusCode, data } = response
+
+ if (statusCode === 200) {
+ if (data.code === 0 || data.success) {
+ return data.data || data.result
+ } else {
+ uni.showToast({
+ title: data.message || '请求失败',
+ icon: 'none'
+ })
+ return Promise.reject(data)
+ }
+ } else if (statusCode === 401) {
+ uni.showToast({
+ title: '登录已过期,请重新登录',
+ icon: 'none'
+ })
+ const userStore = useUserStore()
+ userStore.clearUser()
+ setTimeout(() => {
+ uni.reLaunch({
+ url: '/pages/login/index'
+ })
+ }, 1500)
+ return Promise.reject(response)
+ } else if (statusCode === 403) {
+ uni.showToast({
+ title: '没有权限访问',
+ icon: 'none'
+ })
+ return Promise.reject(response)
+ } else if (statusCode === 404) {
+ uni.showToast({
+ title: '请求的资源不存在',
+ icon: 'none'
+ })
+ return Promise.reject(response)
+ } else if (statusCode >= 500) {
+ uni.showToast({
+ title: '服务器错误,请稍后重试',
+ icon: 'none'
+ })
+ return Promise.reject(response)
+ } else {
+ uni.showToast({
+ title: '网络请求失败',
+ icon: 'none'
+ })
+ return Promise.reject(response)
+ }
+}
+
+function request(options) {
+ const { url, method = 'GET', data, header, loading = true, loadingText = '加载中...' } = options
+
+ const config = {
+ url: url.startsWith('http') ? url : `${BASE_URL}${url}`,
+ method,
+ data,
+ header: requestInterceptor({ header }),
+ timeout: 15000
+ }
+
+ return new Promise((resolve, reject) => {
+ if (loading) {
+ uni.showLoading({
+ title: loadingText,
+ mask: true
+ })
+ }
+
+ uni.request({
+ ...config,
+ success: (res) => {
+ const result = responseInterceptor(res)
+ resolve(result)
+ },
+ fail: (err) => {
+ uni.showToast({
+ title: '网络请求失败',
+ icon: 'none'
+ })
+ reject(err)
+ },
+ complete: () => {
+ if (loading) {
+ uni.hideLoading()
+ }
+ }
+ })
+ })
+}
+
+export function get(url, params, options) {
+ return request({
+ url,
+ method: 'GET',
+ data: params,
+ ...options
+ })
+}
+
+export function post(url, data, options) {
+ return request({
+ url,
+ method: 'POST',
+ data,
+ ...options
+ })
+}
+
+export function put(url, data, options) {
+ return request({
+ url,
+ method: 'PUT',
+ data,
+ ...options
+ })
+}
+
+export function del(url, data, options) {
+ return request({
+ url,
+ method: 'DELETE',
+ data,
+ ...options
+ })
+}
+
+export function patch(url, data, options) {
+ return request({
+ url,
+ method: 'PATCH',
+ data,
+ ...options
+ })
+}
+
+export default request
diff --git a/src/components/common/base-button.vue b/src/components/common/base-button.vue
new file mode 100644
index 0000000..a2ac41a
--- /dev/null
+++ b/src/components/common/base-button.vue
@@ -0,0 +1,71 @@
+
+
+
+
+
diff --git a/src/components/common/custom-card.vue b/src/components/common/custom-card.vue
new file mode 100644
index 0000000..be74d6d
--- /dev/null
+++ b/src/components/common/custom-card.vue
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/common/empty-state.vue b/src/components/common/empty-state.vue
new file mode 100644
index 0000000..ec29a78
--- /dev/null
+++ b/src/components/common/empty-state.vue
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+ {{ description }}
+
+
+
+
+
+
+
+
+
diff --git a/src/components/common/index.js b/src/components/common/index.js
new file mode 100644
index 0000000..43313e2
--- /dev/null
+++ b/src/components/common/index.js
@@ -0,0 +1,5 @@
+export { default as LazyImage } from './lazy-image.vue'
+export { default as EmptyState } from './empty-state.vue'
+export { default as PageContainer } from './page-container.vue'
+export { default as BaseButton } from './base-button.vue'
+export { default as CustomCard } from './custom-card.vue'
diff --git a/src/components/common/lazy-image.vue b/src/components/common/lazy-image.vue
new file mode 100644
index 0000000..073d0bc
--- /dev/null
+++ b/src/components/common/lazy-image.vue
@@ -0,0 +1,108 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/common/page-container.vue b/src/components/common/page-container.vue
new file mode 100644
index 0000000..450f9fb
--- /dev/null
+++ b/src/components/common/page-container.vue
@@ -0,0 +1,93 @@
+
+
+
+
+
+ {{ loadingText }}
+
+
+
+
+
+
+
diff --git a/src/components/layout/index.js b/src/components/layout/index.js
new file mode 100644
index 0000000..49d6e6f
--- /dev/null
+++ b/src/components/layout/index.js
@@ -0,0 +1,3 @@
+export { default as LayoutHeader } from './layout-header.vue'
+export { default as LayoutFooter } from './layout-footer.vue'
+export { default as LayoutSidebar } from './layout-sidebar.vue'
diff --git a/src/components/layout/layout-footer.vue b/src/components/layout/layout-footer.vue
new file mode 100644
index 0000000..1d975d5
--- /dev/null
+++ b/src/components/layout/layout-footer.vue
@@ -0,0 +1,107 @@
+
+
+
+
+
+
+
diff --git a/src/components/layout/layout-header.vue b/src/components/layout/layout-header.vue
new file mode 100644
index 0000000..e379773
--- /dev/null
+++ b/src/components/layout/layout-header.vue
@@ -0,0 +1,125 @@
+
+
+
+
+
+
+
diff --git a/src/components/layout/layout-sidebar.vue b/src/components/layout/layout-sidebar.vue
new file mode 100644
index 0000000..54aa44d
--- /dev/null
+++ b/src/components/layout/layout-sidebar.vue
@@ -0,0 +1,137 @@
+
+
+
+
+
+
+
diff --git a/src/main.js b/src/main.js
new file mode 100644
index 0000000..4f2b402
--- /dev/null
+++ b/src/main.js
@@ -0,0 +1,17 @@
+import { createApp } from 'vue'
+import { createPinia } from 'pinia'
+import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
+import uviewPlus from 'uview-plus'
+import App from './App.vue'
+
+import '@/styles/uni.scss'
+
+const app = createApp(App)
+
+const pinia = createPinia()
+pinia.use(piniaPluginPersistedstate)
+
+app.use(pinia)
+app.use(uviewPlus)
+
+app.mount('#app')
diff --git a/src/manifest.json b/src/manifest.json
new file mode 100644
index 0000000..db4b2a5
--- /dev/null
+++ b/src/manifest.json
@@ -0,0 +1,75 @@
+{
+ "name": "uniapp-vue3-template",
+ "appid": "",
+ "description": "基于Vue3 + Vite + Pinia + uview-plus的UniApp项目模板",
+ "versionName": "1.0.0",
+ "versionCode": "100",
+ "transformPx": false,
+ "networkTimeout": {
+ "request": 10000,
+ "connectSocket": 10000,
+ "uploadFile": 10000,
+ "downloadFile": 10000
+ },
+ "app-plus": {
+ "usingComponents": true,
+ "nvueStyleCompiler": "uni-app",
+ "compilerVersion": 3,
+ "splashscreen": {
+ "alwaysShowBeforeRender": true,
+ "waiting": true,
+ "autoclose": true,
+ "delay": 0
+ },
+ "modules": {},
+ "distribute": {
+ "android": {
+ "permissions": [""],
+ "abiFilters": ["armeabi-v7a", "arm64-v8a"]
+ },
+ "ios": {
+ "dSYMs": false
+ },
+ "sdkConfigs": {}
+ },
+ "screenOrientation": ["portrait-primary"]
+ },
+ "quickapp": {},
+ "mp-weixin": {
+ "appid": "",
+ "setting": {
+ "urlCheck": false,
+ "es6": true,
+ "minified": true,
+ "postcss": true
+ },
+ "usingComponents": true
+ },
+ "mp-alipay": {
+ "usingComponents": true
+ },
+ "mp-baidu": {
+ "usingComponents": true
+ },
+ "mp-toutiao": {
+ "usingComponents": true
+ },
+ "h5": {
+ "title": "uniapp-vue3-template",
+ "router": {
+ "mode": "hash",
+ "base": "./"
+ },
+ "devServer": {
+ "port": 5173,
+ "disableHostCheck": true,
+ "https": false
+ },
+ "optimization": {
+ "treeShaking": {
+ "enable": true
+ }
+ }
+ },
+ "vueVersion": "3"
+}
diff --git a/src/pages.json b/src/pages.json
new file mode 100644
index 0000000..49cfc04
--- /dev/null
+++ b/src/pages.json
@@ -0,0 +1,86 @@
+{
+ "pages": [
+ {
+ "path": "pages/index/index",
+ "style": {
+ "navigationBarTitleText": "首页",
+ "enablePullDownRefresh": true
+ }
+ },
+ {
+ "path": "pages/login/index",
+ "style": {
+ "navigationBarTitleText": "登录",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/register/index",
+ "style": {
+ "navigationBarTitleText": "注册",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/user/index",
+ "style": {
+ "navigationBarTitleText": "个人中心",
+ "enablePullDownRefresh": false
+ }
+ },
+ {
+ "path": "pages/demo/index",
+ "style": {
+ "navigationBarTitleText": "组件演示",
+ "enablePullDownRefresh": false
+ }
+ }
+ ],
+ "globalStyle": {
+ "navigationBarTextStyle": "black",
+ "navigationBarTitleText": "uniapp-vue3-template",
+ "navigationBarBackgroundColor": "#ffffff",
+ "backgroundColor": "#f8f8f8",
+ "backgroundTextStyle": "dark",
+ "app-plus": {
+ "background": "#efeff4"
+ }
+ },
+ "tabBar": {
+ "color": "#999999",
+ "selectedColor": "#2979ff",
+ "borderStyle": "black",
+ "backgroundColor": "#ffffff",
+ "height": "50px",
+ "fontSize": "12px",
+ "iconWidth": "24px",
+ "list": [
+ {
+ "pagePath": "pages/index/index",
+ "text": "首页",
+ "iconPath": "static/tabbar/home.png",
+ "selectedIconPath": "static/tabbar/home-active.png"
+ },
+ {
+ "pagePath": "pages/demo/index",
+ "text": "演示",
+ "iconPath": "static/tabbar/demo.png",
+ "selectedIconPath": "static/tabbar/demo-active.png"
+ },
+ {
+ "pagePath": "pages/user/index",
+ "text": "我的",
+ "iconPath": "static/tabbar/user.png",
+ "selectedIconPath": "static/tabbar/user-active.png"
+ }
+ ]
+ },
+ "easycom": {
+ "autoscan": true,
+ "custom": {
+ "^u--(.*)": "uview-plus/components/u--$1/u--$1.vue",
+ "^up-(.*)": "uview-plus/components/u-$1/u-$1.vue",
+ "^u-(.*)": "uview-plus/components/u-$1/u-$1.vue"
+ }
+ }
+}
diff --git a/src/pages/demo/index.vue b/src/pages/demo/index.vue
new file mode 100644
index 0000000..254689a
--- /dev/null
+++ b/src/pages/demo/index.vue
@@ -0,0 +1,3 @@
+
+ 这是一个演示页面
+
diff --git a/src/pages/index/index.vue b/src/pages/index/index.vue
new file mode 100644
index 0000000..36676e3
--- /dev/null
+++ b/src/pages/index/index.vue
@@ -0,0 +1,12 @@
+
+ 首页
+
+
+
\ No newline at end of file
diff --git a/src/pages/login/index.vue b/src/pages/login/index.vue
new file mode 100644
index 0000000..823f7d9
--- /dev/null
+++ b/src/pages/login/index.vue
@@ -0,0 +1,138 @@
+
+
+
+
+ logo
+
+ 登录
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 忘记密码?
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/pages/register/index.vue b/src/pages/register/index.vue
new file mode 100644
index 0000000..3a7b84a
--- /dev/null
+++ b/src/pages/register/index.vue
@@ -0,0 +1,3 @@
+
+ 注册
+
\ No newline at end of file
diff --git a/src/pages/user/index.vue b/src/pages/user/index.vue
new file mode 100644
index 0000000..5a497fe
--- /dev/null
+++ b/src/pages/user/index.vue
@@ -0,0 +1,3 @@
+
+ 用户中心
+
diff --git a/src/store/app.js b/src/store/app.js
new file mode 100644
index 0000000..0e7ff8f
--- /dev/null
+++ b/src/store/app.js
@@ -0,0 +1,49 @@
+import { defineStore } from 'pinia'
+import { ref } from 'vue'
+
+export const useAppStore = defineStore('app', () => {
+ const theme = ref('light')
+ const pageCache = ref([])
+ const systemInfo = ref(null)
+
+ function setTheme(value) {
+ theme.value = value
+ }
+
+ function addCachePage(pageName) {
+ if (!pageCache.value.includes(pageName)) {
+ pageCache.value.push(pageName)
+ }
+ }
+
+ function removeCachePage(pageName) {
+ const index = pageCache.value.indexOf(pageName)
+ if (index > -1) {
+ pageCache.value.splice(index, 1)
+ }
+ }
+
+ function clearCache() {
+ pageCache.value = []
+ }
+
+ function setSystemInfo(info) {
+ systemInfo.value = info
+ }
+
+ return {
+ theme,
+ pageCache,
+ systemInfo,
+ setTheme,
+ addCachePage,
+ removeCachePage,
+ clearCache,
+ setSystemInfo
+ }
+}, {
+ persist: {
+ key: 'app-store',
+ paths: ['theme', 'pageCache']
+ }
+})
diff --git a/src/store/index.js b/src/store/index.js
new file mode 100644
index 0000000..80a7757
--- /dev/null
+++ b/src/store/index.js
@@ -0,0 +1,2 @@
+export { useUserStore } from './user'
+export { useAppStore } from './app'
diff --git a/src/store/user.js b/src/store/user.js
new file mode 100644
index 0000000..40b56d3
--- /dev/null
+++ b/src/store/user.js
@@ -0,0 +1,67 @@
+import { defineStore } from 'pinia'
+import { ref } from 'vue'
+
+export const useUserStore = defineStore('user', () => {
+ const token = ref('')
+ const refreshToken = ref('')
+ const userInfo = ref(null)
+ const isLoggedIn = ref(false)
+
+ function setToken(value) {
+ token.value = value
+ isLoggedIn.value = !!value
+ }
+
+ function setRefreshToken(value) {
+ refreshToken.value = value
+ }
+
+ function setUserInfo(info) {
+ userInfo.value = info
+ if (info.token) {
+ token.value = info.token
+ isLoggedIn.value = true
+ }
+ if (info.refreshToken) {
+ refreshToken.value = info.refreshToken
+ }
+ }
+
+ function updateUserInfo(info) {
+ if (userInfo.value) {
+ userInfo.value = { ...userInfo.value, ...info }
+ }
+ }
+
+ function logout() {
+ token.value = ''
+ refreshToken.value = ''
+ userInfo.value = null
+ isLoggedIn.value = false
+ }
+
+ function clearUser() {
+ token.value = ''
+ refreshToken.value = ''
+ userInfo.value = null
+ isLoggedIn.value = false
+ }
+
+ return {
+ token,
+ refreshToken,
+ userInfo,
+ isLoggedIn,
+ setToken,
+ setRefreshToken,
+ setUserInfo,
+ updateUserInfo,
+ logout,
+ clearUser
+ }
+}, {
+ persist: {
+ key: 'user-store',
+ paths: ['token', 'refreshToken', 'userInfo', 'isLoggedIn']
+ }
+})
diff --git a/src/styles/uni.scss b/src/styles/uni.scss
new file mode 100644
index 0000000..112224a
--- /dev/null
+++ b/src/styles/uni.scss
@@ -0,0 +1,42 @@
+@charset "utf-8";
+
+@import '@/styles/variable.scss';
+
+*,
+:before,
+:after {
+ box-sizing: border-box;
+}
+
+body,
+html {
+ width: 100%;
+ height: 100%;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+ font-size: 28rpx;
+ line-height: 1.5;
+ color: $u-text-color;
+ background-color: $u-bg-color;
+}
+
+body,
+page {
+ margin: 0;
+ padding: 0;
+ width: 100%;
+ height: 100%;
+}
+
+view,
+text,
+button,
+input,
+textarea,
+image {
+ box-sizing: border-box;
+}
+
+#app {
+ width: 100%;
+ height: 100%;
+}
diff --git a/src/styles/variable.scss b/src/styles/variable.scss
new file mode 100644
index 0000000..e3931e3
--- /dev/null
+++ b/src/styles/variable.scss
@@ -0,0 +1,29 @@
+$u-border-color: #e4e7ed;
+$u-border-color-light: #ebeef5;
+$u-primary: #2979ff;
+$u-warning: #ffb21a;
+$u-success: #19c160;
+$u-error: #fa3534;
+$u-info: #909399;
+$u-text-color: #333333;
+$u-text-color-grey: #999999;
+$u-bg-color: #f8f8f8;
+$u-content-color: #606266;
+$u-primary-light: #e6f2ff;
+$u-warning-light: #fff8e6;
+$u-success-light: #e6f9e6;
+$u-error-light: #fff2f0;
+$u-info-light: #f4f7fc;
+$u-primary-dark: #1976d2;
+$u-warning-dark: #ff9800;
+$u-success-dark: #159854;
+$u-error-dark: #d32f2f;
+$u-info-dark: #606266;
+$u-primary-disabled: #c5d8f6;
+$u-warning-disabled: #fff8e6;
+$u-success-disabled: #e6f9e6;
+$u-error-disabled: #fff2f0;
+$u-info-disabled: #f4f7fc;
+$u-main-color: #2979ff;
+ $u-tips-color: #909399;
+ $u-light-color: #f8f8f8;
diff --git a/src/utils/format.js b/src/utils/format.js
new file mode 100644
index 0000000..e8499d8
--- /dev/null
+++ b/src/utils/format.js
@@ -0,0 +1,121 @@
+import dayjs from 'dayjs'
+import 'dayjs/locale/zh-cn'
+import relativeTime from 'dayjs/plugin/relativeTime'
+
+dayjs.locale('zh-cn')
+dayjs.extend(relativeTime)
+
+export function formatDate(date, format = 'YYYY-MM-DD') {
+ return dayjs(date).format(format)
+}
+
+export function formatDateTime(date, format = 'YYYY-MM-DD HH:mm:ss') {
+ return dayjs(date).format(format)
+}
+
+export function formatTime(date, format = 'HH:mm') {
+ return dayjs(date).format(format)
+}
+
+export function formatRelativeTime(date) {
+ return dayjs(date).fromNow()
+}
+
+export function formatDuration(seconds) {
+ if (seconds < 60) {
+ return `${seconds}秒`
+ } else if (seconds < 3600) {
+ const minutes = Math.floor(seconds / 60)
+ const secs = seconds % 60
+ return secs > 0 ? `${minutes}分${secs}秒` : `${minutes}分钟`
+ } else if (seconds < 86400) {
+ const hours = Math.floor(seconds / 3600)
+ const minutes = Math.floor((seconds % 3600) / 60)
+ return minutes > 0 ? `${hours}小时${minutes}分` : `${hours}小时`
+ } else {
+ const days = Math.floor(seconds / 86400)
+ const hours = Math.floor((seconds % 86400) / 3600)
+ return hours > 0 ? `${days}天${hours}小时` : `${days}天`
+ }
+}
+
+export function formatPrice(price, options) {
+ const { prefix = '¥', decimals = 2 } = options || {}
+ const num = typeof price === 'string' ? parseFloat(price) : price
+ if (isNaN(num)) return `${prefix}0.00`
+ return `${prefix}${num.toFixed(decimals)}`
+}
+
+export function formatNumber(num, decimals = 0) {
+ const value = typeof num === 'string' ? parseFloat(num) : num
+ if (isNaN(value)) return '0'
+ return value.toLocaleString('zh-CN', { minimumFractionDigits: decimals, maximumFractionDigits: decimals })
+}
+
+export function formatFileSize(bytes) {
+ if (bytes === 0) return '0B'
+ const k = 1024
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))}${sizes[i]}`
+}
+
+export function formatPhone(phone) {
+ return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
+}
+
+export function formatIdCard(idCard) {
+ return idCard.replace(/(\d{3})\d{10}(\d{3}[xX]?)/, '$1**********$2')
+}
+
+export function formatThousands(num, separator = ',') {
+ const value = typeof num === 'string' ? parseFloat(num) : num
+ if (isNaN(value)) return '0'
+ const [integer, decimal] = String(value).split('.')
+ const reg = new RegExp(`\\B(?=(\\d{3})+(?!\\d))`, 'g')
+ return `${integer.replace(reg, separator)}${decimal ? '.' + decimal : ''}`
+}
+
+export function getCurrentDate() {
+ const date = new Date()
+ const weekMap = [0, 1, 2, 3, 4, 5, 6]
+ const weekDay = weekMap[date.getDay()]
+ return {
+ year: date.getFullYear(),
+ month: date.getMonth() + 1,
+ day: date.getDate(),
+ week: weekDay
+ }
+}
+
+export function isToday(date) {
+ return dayjs(date).isSame(dayjs(), 'day')
+}
+
+export function isYesterday(date) {
+ return dayjs(date).isSame(dayjs().subtract(1, 'day'), 'day')
+}
+
+export function getWeekStartAndEnd(date = Date.now()) {
+ const d = dayjs(date)
+ return {
+ start: d.startOf('week'),
+ end: d.endOf('week')
+ }
+}
+
+export function getMonthStartAndEnd(date = Date.now()) {
+ const d = dayjs(date)
+ return {
+ start: d.startOf('month'),
+ end: d.endOf('month')
+ }
+}
+
+export function getYearStartAndEnd(date = Date.now()) {
+ const d = dayjs(date)
+ return {
+ start: d.startOf('year'),
+ end: d.endOf('year')
+ }
+}
diff --git a/src/utils/index.js b/src/utils/index.js
new file mode 100644
index 0000000..94a2ae0
--- /dev/null
+++ b/src/utils/index.js
@@ -0,0 +1,3 @@
+export * from './storage'
+export * from './validate'
+export * from './format'
diff --git a/src/utils/storage.js b/src/utils/storage.js
new file mode 100644
index 0000000..7d3ba99
--- /dev/null
+++ b/src/utils/storage.js
@@ -0,0 +1,91 @@
+const STORAGE_PREFIX = 'UNIAPP_'
+
+function getKey(key, usePrefix = true) {
+ return usePrefix ? `${STORAGE_PREFIX}${key}` : key
+}
+
+export function setStorage(key, value, options = {}) {
+ const { prefix = true, expires } = options
+ const storageData = {
+ value,
+ expires: expires ? Date.now() + expires : null,
+ timestamp: Date.now()
+ }
+ try {
+ uni.setStorageSync(getKey(key, prefix), storageData)
+ return true
+ } catch (e) {
+ console.error('setStorage error:', e)
+ return false
+ }
+}
+
+export function getStorage(key, options = {}) {
+ const { prefix = true } = options
+ try {
+ const data = uni.getStorageSync(getKey(key, prefix))
+ if (!data) return null
+
+ if (data.expires && Date.now() > data.expires) {
+ removeStorage(key, { prefix })
+ return null
+ }
+
+ return data.value
+ } catch (e) {
+ console.error('getStorage error:', e)
+ return null
+ }
+}
+
+export function removeStorage(key, options = {}) {
+ const { prefix = true } = options
+ try {
+ uni.removeStorageSync(getKey(key, prefix))
+ return true
+ } catch (e) {
+ console.error('removeStorage error:', e)
+ return false
+ }
+}
+
+export function clearStorage(prefixOnly = false) {
+ try {
+ if (prefixOnly) {
+ const keys = uni.getStorageInfoSync().keys
+ keys.forEach(key => {
+ if (key.startsWith(STORAGE_PREFIX)) {
+ uni.removeStorageSync(key)
+ }
+ })
+ } else {
+ uni.clearStorageSync()
+ }
+ return true
+ } catch (e) {
+ console.error('clearStorage error:', e)
+ return false
+ }
+}
+
+export function getStorageInfo() {
+ try {
+ const info = uni.getStorageInfoSync()
+ return {
+ currentSize: info.currentSize,
+ limitSize: info.limitSize,
+ keys: info.keys
+ }
+ } catch (e) {
+ console.error('getStorageInfo error:', e)
+ return null
+ }
+}
+
+export const storage = {
+ set: setStorage,
+ get: getStorage,
+ remove: removeStorage,
+ clear: clearStorage,
+ info: getStorageInfo
+}
diff --git a/src/utils/validate.js b/src/utils/validate.js
new file mode 100644
index 0000000..e14d014
--- /dev/null
+++ b/src/utils/validate.js
@@ -0,0 +1,114 @@
+export function isEmpty(value) {
+ if (value === null || value === undefined) return true
+ if (typeof value === 'string') return value.trim() === ''
+ if (Array.isArray(value)) return value.length === 0
+ if (typeof value === 'object') return Object.keys(value).length === 0
+ return false
+}
+
+export function validate(value, rules) {
+ for (const rule of rules) {
+ if (rule.required && isEmpty(value)) {
+ return { isValid: false, message: rule.message || '该项为必填项' }
+ }
+
+ if (!isEmpty(value)) {
+ if (rule.pattern && !rule.pattern.test(String(value))) {
+ return { isValid: false, message: rule.message || '格式不正确' }
+ }
+
+ if (rule.min !== undefined && Number(value) < rule.min) {
+ return { isValid: false, message: rule.message || `不能小于${rule.min}` }
+ }
+
+ if (rule.max !== undefined && Number(value) > rule.max) {
+ return { isValid: false, message: rule.message || `不能大于${rule.max}` }
+ }
+
+ if (rule.minLength !== undefined && String(value).length < rule.minLength) {
+ return { isValid: false, message: rule.message || `长度不能小于${rule.minLength}` }
+ }
+
+ if (rule.maxLength !== undefined && String(value).length > rule.maxLength) {
+ return { isValid: false, message: rule.message || `长度不能大于${rule.maxLength}` }
+ }
+ }
+ }
+
+ return { isValid: true, message: '' }
+}
+
+export const validators = {
+ required: (message = '该项为必填项') => ({
+ required: true,
+ message
+ }),
+
+ mobile: (message = '请输入正确的手机号') => ({
+ required: true,
+ pattern: /^1[3-9]\d{9}$/,
+ message
+ }),
+
+ email: (message = '请输入正确的邮箱地址') => ({
+ required: true,
+ pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
+ message
+ }),
+
+ idCard: (message = '请输入正确的身份证号') => ({
+ required: true,
+ pattern: /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/,
+ message
+ }),
+
+ url: (message = '请输入正确的网址') => ({
+ required: true,
+ pattern: /^https?:\/\/[^\s]+$/,
+ message
+ }),
+
+ number: (message = '请输入数字') => ({
+ required: true,
+ pattern: /^\d+$/,
+ message
+ }),
+
+ minLength: (length, message) => ({
+ minLength: length,
+ message: message || `长度不能小于${length}`
+ }),
+
+ maxLength: (length, message) => ({
+ maxLength: length,
+ message: message || `长度不能大于${length}`
+ }),
+
+ min: (value, message) => ({
+ min: value,
+ message: message || `不能小于${value}`
+ }),
+
+ max: (value, message) => ({
+ max: value,
+ message: message || `不能大于${value}`
+ })
+}
+
+export function validateForm(formData, formRules) {
+ const errors = {}
+ let isValid = true
+
+ for (const key in formRules) {
+ const value = formData[key]
+ const rules = formRules[key]
+ const result = validate(value, rules)
+
+ if (!result.isValid) {
+ errors[key] = result.message
+ isValid = false
+ }
+ }
+
+ return { isValid, errors }
+}
diff --git a/vite.config.js b/vite.config.js
new file mode 100644
index 0000000..827067f
--- /dev/null
+++ b/vite.config.js
@@ -0,0 +1,26 @@
+import { defineConfig } from 'vite'
+import uni from '@dcloudio/vite-plugin-uni'
+import { resolve } from 'path'
+import { fileURLToPath, URL } from 'node:url'
+
+export default defineConfig({
+ plugins: [uni()],
+ resolve: {
+ alias: {
+ '@': fileURLToPath(new URL('./src', import.meta.url)),
+ }
+ },
+ css: {
+ preprocessorOptions: {
+ scss: {
+ additionalData: `
+ @import "uview-plus/theme.scss";
+ @import "@/styles/variable.scss";
+ `
+ }
+ }
+ },
+ optimizeDeps: {
+ include: ['uview-plus']
+ }
+})