commit c6b379107683ae21a4f1cd07ef5e3f2a48894f3b Author: liangbin <15536829364@163.com> Date: Thu Jan 15 16:50:32 2026 +0800 初始化项目创建Uniapp模版 vue3与uview-plus框架搭建 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + \ 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'] + } +})