commit 16b481e2808be4b2fc7526a3f7263636c9bfff21 Author: liuhuibin <1650243281@qq.com> Date: Mon Apr 27 14:18:08 2026 +0800 first commit diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3bbfb51 --- /dev/null +++ b/.gitignore @@ -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 + +.env +.env.development + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +package-lock.json \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..eebc91a --- /dev/null +++ b/.prettierrc @@ -0,0 +1,18 @@ +{ + "printWidth": 120, + "tabWidth": 2, + "useTabs": true, + "semi": true, + "singleQuote": true, + "jsxSingleQuote": true, + "bracketSpacing": true, + "bracketSameLine": true, + "quoteProps": "as-needed", + "trailingComma": "all", + "singleAttributePerLine": false, + "rangeStart": 0, + "requirePragma": false, + "insertPragma": false, + "proseWrap": "preserve", + "endOfLine": "lf" +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..befaff8 --- /dev/null +++ b/README.md @@ -0,0 +1,197 @@ +# Vite Vue3 Template + +> 一个开箱即用的 Vue3 + Vite 项目模板,采用模块化架构设计,适合快速构建中大型 Vue 应用。 + +--- + +## 技术栈 + +| 技术 | 版本 | 用途 | +| ------------ | ------- | ---------- | +| Vue | ^3.4.37 | 核心框架 | +| Vite | ^5.4.1 | 构建工具 | +| Vue Router | 4 | 路由管理 | +| Vuex | ^4.0.2 | 状态管理 | +| Element Plus | ^2.8.1 | UI 组件库 | +| Axios | ^1.7.7 | HTTP 请求 | +| Tailwind CSS | ^3.4.10 | 原子化 CSS | +| Less | ^4.2.0 | CSS 预处理 | +| Sass | ^1.77.8 | CSS 预处理 | + +--- + +## 目录结构 + +``` +vite-vue3-template/ +├── public/ # 公共静态资源 +├── src/ +│ ├── api/ # API 接口管理 +│ ├── assets/ # 静态资源 +│ │ ├── images/ # 图片资源 +│ │ └── styles/ # 样式文件 +│ ├── components/ # 公共组件 +│ ├── layouts/ # 布局组件 +│ ├── plugins/ # 插件配置 +│ ├── router/ # 路由配置 +│ ├── store/ # Vuex 状态管理 +│ ├── utils/ # 工具函数 +│ ├── views/ # 页面视图 +│ ├── App.vue # 根组件 +│ └── main.js # 入口文件 +├── index.html # HTML 模板 +├── vite.config.js # Vite 配置 +├── package.json # 项目配置 +└── README.md # 项目说明 +``` + +--- + +## 快速开始 + +### 1. 克隆项目 + +```bash +git clone +cd vite-vue3-template +``` + +### 2. 安装依赖 + +```bash +npm install +# 或使用 pnpm +pnpm install +# 或使用 yarn +yarn install +``` + +### 3. 启动开发服务器 + +```bash +npm run dev +``` + +访问 http://localhost:5173 查看项目 + +### 4. 生产构建 + +```bash +npm run build +``` + +构建产物将输出到 `dist` 目录 + +### 5. 预览构建结果 + +```bash +npm run preview +``` + +### 6. 清理项目(可选) + +```bash +npm run rf +``` + +清除 `dist` 目录和 `node_modules` + +--- + +## 项目特性 + +### 核心功能 + +- ✅ **模块化架构** - 清晰的目录结构,易于维护和扩展 +- ✅ **组件自动导入** - 本地组件和 Element Plus 按需自动导入 +- ✅ **路由模块化管理** - 按功能拆分路由,统一路由守卫 +- ✅ **Vuex 持久化** - 自动持久化用户数据和配置 +- ✅ **Axios 封装** - 统一的请求拦截、错误处理 +- ✅ **智能代码分包** - Vue 核心库和 Element Plus 独立打包 + +### 开发体验 + +- 🚀 **快速热更新** - Vite 提供极速的开发体验 +- 📦 **路径别名** - 使用 `@` 简化导入路径 +- 🎨 **多样式方案** - 支持 Less、SCSS、Tailwind CSS +- 🌐 **开发代理** - 内置 `/api` 代理配置 +- 📝 **工具函数** - 验证、格式化、存储等常用工具 + +### 生产优化 + +- ⚡ **esbuild 压缩** - 更快的构建速度 +- 🧹 **自动清理** - 生产环境移除 console 和 debugger +- 📊 **代码拆分** - CSS 和 JS 自动拆分 +- 🔒 **Source Map 关闭** - 保护源码,减小包体积 + +--- + +## 配置说明 + +### 路径别名 + +在 [vite.config.js:34-40](vite.config.js#L34-L40) 中配置了路径别名,可在项目中使用: + +```js +import MyComponent from '@/components/MyComponent.vue' +``` + +### 开发代理 + +开发环境下,`/api` 请求会被代理到后端服务: + +```js +// vite.config.js +proxy: { + '/api': { + target: 'http://localhost:3000', // 后端地址 + changeOrigin: true, + rewrite: (path) => path.replace(/^\/api/, ''), + } +} +``` + +### Vuex 持久化 + +在 [src/store/index.js](src/store/index.js) 中配置了持久化,自动保存以下数据: + +```js +{ + user: { + userData: state.user.userData, + token: state.user.token, + }, + app: { + theme: state.app.theme, + language: state.app.language, + } +} +``` + +--- + +## 添加新功能 + +### 添加新页面 + +1. 在 `src/views/` 下创建页面组件 +2. 在 `src/router/modules/` 下创建路由模块 +3. 在 `src/router/index.js` 中引入路由模块 + +### 添加新 API + +1. 在 `src/api/modules/` 下创建 API 模块 +2. 在 `src/api/index.js` 中导出 + +### 添加新状态 + +1. 在 `src/store/modules/` 下创建状态模块 +2. 在 `src/store/index.js` 中注册模块 + +### 添加新组件 + +创建在 `src/components/base/` 或 `src/components/common/` 下,组件会自动导入。 + +## License + +[Apache-2.0 license](./LICENSE) diff --git a/index.html b/index.html new file mode 100644 index 0000000..45431de --- /dev/null +++ b/index.html @@ -0,0 +1,16 @@ + + + + + + + + Vite Vue3 Template + + + +
+ + + + \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..2709f2d --- /dev/null +++ b/package.json @@ -0,0 +1,30 @@ +{ + "name": "vite-vue3-template", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "axios": "^1.7.7", + "element-plus": "^2.8.1", + "less": "^4.2.0", + "naive-ui": "^2.44.1", + "vue": "^3.4.37", + "vue-router": "4", + "vuex": "^4.0.2", + "vuex-persist": "^3.1.3" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.1.2", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.44", + "sass": "^1.77.8", + "tailwindcss": "^3.4.10", + "unplugin-vue-components": "^0.22.4", + "vite": "^5.4.1" + } +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..7b75c83 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..4751341 --- /dev/null +++ b/src/App.vue @@ -0,0 +1,9 @@ + + + diff --git a/src/api/index.js b/src/api/index.js new file mode 100644 index 0000000..1058f6d --- /dev/null +++ b/src/api/index.js @@ -0,0 +1,5 @@ +/** + * API统一导出 + */ + +export * as userApi from './modules/user'; diff --git a/src/api/modules/user.js b/src/api/modules/user.js new file mode 100644 index 0000000..51ddc54 --- /dev/null +++ b/src/api/modules/user.js @@ -0,0 +1,14 @@ +import request from '../request'; + +/** + * 用户相关接口 + */ + +// 用户登录 +export function login(data) { + return request({ + url: '/user/login', + method: 'post', + data, + }); +} diff --git a/src/api/request.js b/src/api/request.js new file mode 100644 index 0000000..d4f921f --- /dev/null +++ b/src/api/request.js @@ -0,0 +1,69 @@ +import axios from 'axios'; +import { ElMessage } from 'element-plus'; + +// 创建axios实例 +const service = axios.create({ + baseURL: import.meta.env.VITE_APP_BASE_API, + timeout: 15000, + headers: { + 'Content-Type': 'application/json;charset=UTF-8', + }, +}); + +// 请求拦截器 +service.interceptors.request.use( + (config) => { + // 在发送请求之前做些什么 + const token = localStorage.getItem('token'); + if (token) { + config.headers['Authorization'] = `Bearer ${token}`; + } + return config; + }, + (error) => { + // 对请求错误做些什么 + console.error('请求错误:', error); + return Promise.reject(error); + }, +); + +// 响应拦截器 +service.interceptors.response.use( + (response) => { + const res = response.data; + + // 根据后端返回的状态码进行处理 + if (res.code !== 200 && res.code !== 0) { + ElMessage({ + message: res.message || '请求失败', + type: 'error', + duration: 3000, + }); + + // 401: Token过期 + if (res.code === 401) { + // 可以在这里处理token过期的逻辑,比如跳转到登录页 + localStorage.removeItem('token'); + window.location.href = '/login'; + } + + return Promise.reject(new Error(res.message || '请求失败')); + } else { + return res; + } + }, + (error) => { + console.error('响应错误:', error); + + // 处理HTTP状态码错误 + if (error.response) { + ElMessage.error(error.response.status); + } else { + ElMessage.error('网络连接失败'); + } + + return Promise.reject(error); + }, +); + +export default service; diff --git a/src/assets/images/vite.svg b/src/assets/images/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/src/assets/images/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/images/vue.svg b/src/assets/images/vue.svg new file mode 100644 index 0000000..770e9d3 --- /dev/null +++ b/src/assets/images/vue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/styles/base/common.css b/src/assets/styles/base/common.css new file mode 100644 index 0000000..28a0dbd --- /dev/null +++ b/src/assets/styles/base/common.css @@ -0,0 +1,36 @@ +/* 清除浮动 */ +.clearfix::after { + content: ''; + display: block; + clear: both; +} + +/* 滚动条样式 */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-thumb { + background-color: rgba(0, 0, 0, 0.2); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background-color: rgba(0, 0, 0, 0.3); +} + +::-webkit-scrollbar-track { + background-color: transparent; +} + +/* 动画 */ +.fade-enter-active, +.fade-leave-active { + transition: opacity 0.3s; +} + +.fade-enter-from, +.fade-leave-to { + opacity: 0; +} \ No newline at end of file diff --git a/src/assets/styles/base/element-variables.scss b/src/assets/styles/base/element-variables.scss new file mode 100644 index 0000000..e113d06 --- /dev/null +++ b/src/assets/styles/base/element-variables.scss @@ -0,0 +1,5 @@ +:root { + --el-color-primary: #3237C8 !important; + --el-color-primary-light-3: #3237C8 !important; + --el-color-primary-dark-2: #3237C8 !important; +} \ No newline at end of file diff --git a/src/assets/styles/base/reset.css b/src/assets/styles/base/reset.css new file mode 100644 index 0000000..5569559 --- /dev/null +++ b/src/assets/styles/base/reset.css @@ -0,0 +1,69 @@ +/* CSS Reset - 样式重置 */ +*, +*::before, +*::after { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html, +body { + height: 100%; + width: 100%; +} + +html { + font-size: 16px; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + color: #333; + background-color: #fff; +} + +/* 列表样式重置 */ +ul, +ol { + list-style: none; +} + +/* 链接样式重置 */ +a { + text-decoration: none; + color: inherit; +} + +/* 图片样式 */ +img { + max-width: 100%; + height: auto; + display: block; +} + +/* 表格样式重置 */ +table { + border-collapse: collapse; + border-spacing: 0; +} + +/* 按钮样式重置 */ +button, +input, +select, +textarea { + font-family: inherit; + font-size: inherit; + line-height: inherit; +} + +button { + cursor: pointer; + border: none; + outline: none; + background: none; +} diff --git a/src/assets/styles/base/tailwind.css b/src/assets/styles/base/tailwind.css new file mode 100644 index 0000000..b5c61c9 --- /dev/null +++ b/src/assets/styles/base/tailwind.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/src/assets/styles/base/variables.css b/src/assets/styles/base/variables.css new file mode 100644 index 0000000..9e773de --- /dev/null +++ b/src/assets/styles/base/variables.css @@ -0,0 +1,46 @@ +/* CSS 变量定义 */ +:root { + /* 主题色 */ + --primary-color: #409eff; + --success-color: #67c23a; + --warning-color: #e6a23c; + --danger-color: #f56c6c; + --info-color: #909399; + + /* 文本颜色 */ + --text-primary: #303133; + --text-regular: #606266; + --text-secondary: #909399; + --text-placeholder: #c0c4cc; + + /* 边框颜色 */ + --border-base: #dcdfe6; + --border-light: #e4e7ed; + --border-lighter: #ebeef5; + --border-extra-light: #f2f6fc; + + /* 背景色 */ + --bg-color: #ffffff; + --bg-page: #f5f7fa; + --bg-overlay: rgba(0, 0, 0, 0.5); + + /* 间距 */ + --spacing-xs: 4px; + --spacing-sm: 8px; + --spacing-md: 16px; + --spacing-lg: 24px; + --spacing-xl: 32px; + + /* 圆角 */ + --border-radius-sm: 2px; + --border-radius-base: 4px; + --border-radius-lg: 8px; + + /* 阴影 */ + --box-shadow-light: 0 2px 4px rgba(0, 0, 0, 0.12); + --box-shadow-base: 0 2px 12px 0 rgba(0, 0, 0, 0.1); + --box-shadow-dark: 0 4px 16px 0 rgba(0, 0, 0, 0.15); + + /* 过渡 */ + --transition-base: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} diff --git a/src/assets/styles/index.less b/src/assets/styles/index.less new file mode 100644 index 0000000..49b7ac5 --- /dev/null +++ b/src/assets/styles/index.less @@ -0,0 +1,22 @@ +:root { + font-family: 'Microsoft YaHei UI', 'PingFang SC', 'Noto Sans SC', sans-serif; + line-height: 1.5; + font-weight: 400; + color: #16253d; + background-color: #edf2f8; + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + min-width: 1280px; + min-height: 100vh; + overflow-x: auto; + background-color: #edf2f8; +} + +#app { + min-height: 100vh; +} diff --git a/src/components/HelloWorld.vue b/src/components/HelloWorld.vue new file mode 100644 index 0000000..f8aa4d1 --- /dev/null +++ b/src/components/HelloWorld.vue @@ -0,0 +1,24 @@ + + + diff --git a/src/components/layout/GlobalShell.vue b/src/components/layout/GlobalShell.vue new file mode 100644 index 0000000..12aedd3 --- /dev/null +++ b/src/components/layout/GlobalShell.vue @@ -0,0 +1,537 @@ + + + + + diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..227913a --- /dev/null +++ b/src/main.js @@ -0,0 +1,25 @@ +import { createApp } from 'vue'; +import App from './App.vue'; +import router from './router'; +import store from './store'; + +// 样式导入 +import '@/assets/styles/base/reset.css'; +import '@/assets/styles/base/variables.css'; +import '@/assets/styles/base/common.css'; +import '@/assets/styles/base/tailwind.css'; +import '@/assets/styles/index.less'; + +// 插件 +import setupPlugins from './plugins'; + +const app = createApp(App); + +// 注册插件 +setupPlugins(app); + +// 注册路由和状态管理 +app.use(router); +app.use(store); + +app.mount('#app'); diff --git a/src/plugins/element-plus.js b/src/plugins/element-plus.js new file mode 100644 index 0000000..0086324 --- /dev/null +++ b/src/plugins/element-plus.js @@ -0,0 +1,11 @@ +/** + * Element Plus 插件配置 + */ + +import ElementPlus from 'element-plus'; +import 'element-plus/dist/index.css'; +import '@/assets/styles/base/element-variables.scss'; + +export default function setupElementPlus(app) { + app.use(ElementPlus); +} diff --git a/src/plugins/index.js b/src/plugins/index.js new file mode 100644 index 0000000..4d249fc --- /dev/null +++ b/src/plugins/index.js @@ -0,0 +1,11 @@ +/** + * 插件统一注册 + */ + +import setupElementPlus from './element-plus'; + +export default function setupPlugins(app) { + // 注册 Element Plus + setupElementPlus(app); + +} diff --git a/src/router/guards.js b/src/router/guards.js new file mode 100644 index 0000000..37a1784 --- /dev/null +++ b/src/router/guards.js @@ -0,0 +1,44 @@ +/** + * 全局前置守卫 + */ +export function setupRouterGuards(router) { + // 全局前置守卫 + router.beforeEach((to, from, next) => { + // 设置页面标题 + if (to.meta.title) { + document.title = to.meta.title; + } + + // 示例:权限验证 + // const token = localStorage.getItem('token'); + // if (!token && to.path !== '/login') { + // next('/login'); + // } else { + // next(); + // } + + next(); + }); + + // 全局后置钩子 + router.afterEach((to, from) => { + // 可以在这里处理页面跳转后的逻辑 + // 例如:埋点、页面滚动等 + }); + + // 错误处理 + router.onError((error) => { + // 检查接口返回的状态码是否为500 + if (error.response && error.response.status === 500) { + router.push({ + name: 'ServerError', + }); + } else if (error.response && error.response.status === 404) { + router.push({ + name: 'NotFound', + }); + } else { + console.error('路由错误:', error); + } + }); +} diff --git a/src/router/index.js b/src/router/index.js new file mode 100644 index 0000000..4603096 --- /dev/null +++ b/src/router/index.js @@ -0,0 +1,23 @@ +import { createRouter, createWebHashHistory } from 'vue-router'; +import { setupRouterGuards } from './guards'; + +import homeRoutes from './modules/home'; +import userRoutes from './modules/user'; +import supervisionRoutes from './modules/supervision'; +import tenderingRoutes from './modules/tendering'; + +const routes = [ + ...homeRoutes, + ...userRoutes, + ...supervisionRoutes, + ...tenderingRoutes, +]; + +const router = createRouter({ + history: createWebHashHistory(import.meta.env.BASE_URL), + routes, +}); + +setupRouterGuards(router); + +export default router; diff --git a/src/router/modules/home.js b/src/router/modules/home.js new file mode 100644 index 0000000..b403b79 --- /dev/null +++ b/src/router/modules/home.js @@ -0,0 +1,15 @@ +/** + * 首页路由配置 + */ + +export default [ + { + path: '/', + name: 'Home', + component: () => import('@/views/home/index.vue'), + meta: { + title: '首页', + keepAlive: true, + }, + }, +]; diff --git a/src/router/modules/supervision.js b/src/router/modules/supervision.js new file mode 100644 index 0000000..3f197ab --- /dev/null +++ b/src/router/modules/supervision.js @@ -0,0 +1,18 @@ +export default [ + { + path: '/supervision/plans', + name: 'SupervisionPlanManagement', + component: () => import('@/views/supervision/plan/index.vue'), + meta: { + title: '监管计划', + }, + }, + { + path: '/supervision/videos', + name: 'SupervisionVideoManagement', + component: () => import('@/views/supervision/video/index.vue'), + meta: { + title: '监控视频', + }, + }, +]; diff --git a/src/router/modules/tendering.js b/src/router/modules/tendering.js new file mode 100644 index 0000000..18402ce --- /dev/null +++ b/src/router/modules/tendering.js @@ -0,0 +1,26 @@ +export default [ + { + path: '/tendering/tender-projects', + name: 'TenderProjectManagement', + component: () => import('@/views/tendering/tender/index.vue'), + meta: { + title: '招标项目管理', + }, + }, + { + path: '/tendering/award-projects', + name: 'AwardProjectManagement', + component: () => import('@/views/tendering/award/index.vue'), + meta: { + title: '中标项目管理', + }, + }, + { + path: '/tendering/sector-management', + name: 'SectorManagement', + component: () => import('@/views/tendering/sector/index.vue'), + meta: { + title: '板块管理', + }, + }, +]; diff --git a/src/router/modules/user.js b/src/router/modules/user.js new file mode 100644 index 0000000..ed60a1e --- /dev/null +++ b/src/router/modules/user.js @@ -0,0 +1,14 @@ +/** + * 用户相关路由配置 + */ + +export default [ + { + path: '/user', + name: 'User', + component: () => import('@/views/user/index.vue'), + meta: { + title: '用户中心', + }, + }, +]; diff --git a/src/store/getters.js b/src/store/getters.js new file mode 100644 index 0000000..8573d2f --- /dev/null +++ b/src/store/getters.js @@ -0,0 +1,19 @@ +/** + * 全局 getters + */ + +const getters = { + // 用户模块 + userData: (state) => state.user.userData, + token: (state) => state.user.token, + userInfo: (state) => state.user.userInfo, + isLoggedIn: (state) => state.user.isLoggedIn, + + // 应用模块 + theme: (state) => state.app.theme, + language: (state) => state.app.language, + collapsed: (state) => state.app.collapsed, + loading: (state) => state.app.loading, +}; + +export default getters; diff --git a/src/store/index.js b/src/store/index.js new file mode 100644 index 0000000..12379e6 --- /dev/null +++ b/src/store/index.js @@ -0,0 +1,35 @@ +import { createStore } from 'vuex'; +import VuexPersist from 'vuex-persist'; +import getters from './getters'; + +// 导入模块 +import user from './modules/user'; +import app from './modules/app'; + +// vuex 持久化配置 +const vuexPersist = new VuexPersist({ + key: 'vuex', + storage: localStorage, + reducer: (state) => ({ + user: { + userData: state.user.userData, + token: state.user.token, + }, + app: { + theme: state.app.theme, + language: state.app.language, + }, + }), +}); + +// 创建 store 实例 +const store = createStore({ + modules: { + user, + app, + }, + getters, + plugins: [vuexPersist.plugin], +}); + +export default store; diff --git a/src/store/modules/app.js b/src/store/modules/app.js new file mode 100644 index 0000000..c1f4ebd --- /dev/null +++ b/src/store/modules/app.js @@ -0,0 +1,57 @@ +/** + * 应用模块状态管理 + */ + +const state = { + theme: localStorage.getItem('theme') || 'light', + language: localStorage.getItem('language') || 'zh-CN', + collapsed: false, // 侧边栏折叠状态 + loading: false, // 全局加载状态 +}; + +const mutations = { + SET_THEME(state, theme) { + state.theme = theme; + localStorage.setItem('theme', theme); + }, + SET_LANGUAGE(state, language) { + state.language = language; + localStorage.setItem('language', language); + }, + SET_COLLAPSED(state, collapsed) { + state.collapsed = collapsed; + }, + SET_LOADING(state, loading) { + state.loading = loading; + }, +}; + +const actions = { + setTheme({ commit }, theme) { + commit('SET_THEME', theme); + }, + setLanguage({ commit }, language) { + commit('SET_LANGUAGE', language); + }, + toggleCollapsed({ commit, state }) { + commit('SET_COLLAPSED', !state.collapsed); + }, + setLoading({ commit }, loading) { + commit('SET_LOADING', loading); + }, +}; + +const getters = { + theme: (state) => state.theme, + language: (state) => state.language, + collapsed: (state) => state.collapsed, + loading: (state) => state.loading, +}; + +export default { + namespaced: true, + state, + mutations, + actions, + getters, +}; diff --git a/src/store/modules/user.js b/src/store/modules/user.js new file mode 100644 index 0000000..29e305a --- /dev/null +++ b/src/store/modules/user.js @@ -0,0 +1,66 @@ +/** + * 用户模块状态管理 + */ + +const state = { + userData: {}, + token: localStorage.getItem('token') || '', + userInfo: null, +}; + +const mutations = { + SET_USER_DATA(state, data) { + state.userData = data; + }, + SET_TOKEN(state, token) { + state.token = token; + if (token) { + localStorage.setItem('token', token); + } else { + localStorage.removeItem('token'); + } + }, + SET_USER_INFO(state, userInfo) { + state.userInfo = userInfo; + }, + CLEAR_USER_DATA(state) { + state.userData = {}; + state.token = ''; + state.userInfo = null; + localStorage.removeItem('token'); + }, +}; + +const actions = { + // 设置用户数据 + setUserData({ commit }, data) { + commit('SET_USER_DATA', data); + }, + // 设置token + setToken({ commit }, token) { + commit('SET_TOKEN', token); + }, + // 设置用户信息 + setUserInfo({ commit }, userInfo) { + commit('SET_USER_INFO', userInfo); + }, + // 清除用户数据 + clearUserData({ commit }) { + commit('CLEAR_USER_DATA'); + }, +}; + +const getters = { + userData: (state) => state.userData, + token: (state) => state.token, + userInfo: (state) => state.userInfo, + isLoggedIn: (state) => !!state.token, +}; + +export default { + namespaced: true, + state, + mutations, + actions, + getters, +}; diff --git a/src/utils/format.js b/src/utils/format.js new file mode 100644 index 0000000..6ccd58d --- /dev/null +++ b/src/utils/format.js @@ -0,0 +1,70 @@ +/** + * 格式化工具函数 + */ + +/** + * 格式化日期时间 + */ +export function formatDate(date, format = 'YYYY-MM-DD HH:mm:ss') { + if (!date) return ''; + + 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); +} + +/** + * 格式化金额(千分位) + */ +export function formatMoney(money, decimals = 2) { + if (!money) return '0.00'; + const num = parseFloat(money); + return num.toFixed(decimals).replace(/\d(?=(\d{3})+\.)/g, '$&,'); +} + +/** + * 格式化文件大小 + */ +export function formatFileSize(bytes) { + if (bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return (bytes / Math.pow(k, i)).toFixed(2) + ' ' + sizes[i]; +} + +/** + * 手机号脱敏 + */ +export function formatPhone(phone) { + if (!phone) return ''; + return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2'); +} + +/** + * 身份证号脱敏 + */ +export function formatIdCard(idCard) { + if (!idCard) return ''; + return idCard.replace(/(\d{6})\d{8}(\d{4})/, '$1********$2'); +} + +/** + * 银行卡号脱敏 + */ +export function formatBankCard(cardNo) { + if (!cardNo) return ''; + return cardNo.replace(/(\d{4})\d+(\d{4})/, '$1 **** **** $2'); +} diff --git a/src/utils/index.js b/src/utils/index.js new file mode 100644 index 0000000..ca1d25d --- /dev/null +++ b/src/utils/index.js @@ -0,0 +1,8 @@ +/** + * 工具函数统一导出 + */ + +export { default as storage } from './storage'; +export * from './auth'; +export * from './validate'; +export * from './format'; diff --git a/src/utils/storage.js b/src/utils/storage.js new file mode 100644 index 0000000..88a0d34 --- /dev/null +++ b/src/utils/storage.js @@ -0,0 +1,88 @@ +/** + * 本地存储工具 + */ + +const storage = { + /** + * 设置localStorage + */ + setLocalStorage(key, value) { + try { + localStorage.setItem(key, JSON.stringify(value)); + } catch (error) { + console.error('localStorage set error:', error); + } + }, + + /** + * 获取localStorage + */ + getLocalStorage(key) { + try { + const value = localStorage.getItem(key); + return value ? JSON.parse(value) : null; + } catch (error) { + console.error('localStorage get error:', error); + return null; + } + }, + + /** + * 删除localStorage + */ + removeLocalStorage(key) { + try { + localStorage.removeItem(key); + } catch (error) { + console.error('localStorage remove error:', error); + } + }, + + /** + * 清空localStorage + */ + clearLocalStorage() { + try { + localStorage.clear(); + } catch (error) { + console.error('localStorage clear error:', error); + } + }, + + /** + * 设置sessionStorage + */ + setSession(key, value) { + try { + sessionStorage.setItem(key, JSON.stringify(value)); + } catch (error) { + console.error('sessionStorage set error:', error); + } + }, + + /** + * 获取sessionStorage + */ + getSession(key) { + try { + const value = sessionStorage.getItem(key); + return value ? JSON.parse(value) : null; + } catch (error) { + console.error('sessionStorage get error:', error); + return null; + } + }, + + /** + * 删除sessionStorage + */ + removeSession(key) { + try { + sessionStorage.removeItem(key); + } catch (error) { + console.error('sessionStorage remove error:', error); + } + }, +}; + +export default storage; diff --git a/src/utils/validate.js b/src/utils/validate.js new file mode 100644 index 0000000..1e8253c --- /dev/null +++ b/src/utils/validate.js @@ -0,0 +1,60 @@ +/** + * 验证工具函数 + */ + +/** + * 验证邮箱 + */ +export function isEmail(email) { + const reg = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/; + return reg.test(email); +} + +/** + * 验证手机号 + */ +export function isPhone(phone) { + const reg = /^1[3-9]\d{9}$/; + return reg.test(phone); +} + +/** + * 验证身份证号 + */ +export function isIdCard(idCard) { + const reg = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/; + return reg.test(idCard); +} + +/** + * 验证URL + */ +export function isUrl(url) { + const reg = + /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/; + return reg.test(url); +} + +/** + * 验证密码强度(至少包含数字和字母,长度6-20) + */ +export function isStrongPassword(password) { + const reg = /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{6,20}$/; + return reg.test(password); +} + +/** + * 验证中文 + */ +export function isChinese(str) { + const reg = /^[\u4e00-\u9fa5]+$/; + return reg.test(str); +} + +/** + * 验证数字 + */ +export function isNumber(num) { + const reg = /^[0-9]+$/; + return reg.test(num); +} diff --git a/src/views/home/index.vue b/src/views/home/index.vue new file mode 100644 index 0000000..6c3e536 --- /dev/null +++ b/src/views/home/index.vue @@ -0,0 +1,23 @@ + + + diff --git a/src/views/supervision/plan/index.vue b/src/views/supervision/plan/index.vue new file mode 100644 index 0000000..aa893de --- /dev/null +++ b/src/views/supervision/plan/index.vue @@ -0,0 +1,1040 @@ + + + + + diff --git a/src/views/supervision/video/index.vue b/src/views/supervision/video/index.vue new file mode 100644 index 0000000..3535a11 --- /dev/null +++ b/src/views/supervision/video/index.vue @@ -0,0 +1,1282 @@ + + + + + diff --git a/src/views/tendering/award/index.vue b/src/views/tendering/award/index.vue new file mode 100644 index 0000000..e316790 --- /dev/null +++ b/src/views/tendering/award/index.vue @@ -0,0 +1,191 @@ + + + diff --git a/src/views/tendering/components/ProjectCrudPanel.vue b/src/views/tendering/components/ProjectCrudPanel.vue new file mode 100644 index 0000000..bbd967d --- /dev/null +++ b/src/views/tendering/components/ProjectCrudPanel.vue @@ -0,0 +1,1403 @@ + + + + + diff --git a/src/views/tendering/sector/index.vue b/src/views/tendering/sector/index.vue new file mode 100644 index 0000000..04f9fcd --- /dev/null +++ b/src/views/tendering/sector/index.vue @@ -0,0 +1,441 @@ + + + + + diff --git a/src/views/tendering/tender/index.vue b/src/views/tendering/tender/index.vue new file mode 100644 index 0000000..51f022c --- /dev/null +++ b/src/views/tendering/tender/index.vue @@ -0,0 +1,158 @@ + + + diff --git a/src/views/user/index.vue b/src/views/user/index.vue new file mode 100644 index 0000000..343afb0 --- /dev/null +++ b/src/views/user/index.vue @@ -0,0 +1,22 @@ + + + + + diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..2a142e6 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,8 @@ +// tailwind.config.js +export default { + content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'], + theme: { + extend: {}, + }, + plugins: [], +}; diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..5d4c804 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,72 @@ +import { defineConfig } from 'vite'; +import vue from '@vitejs/plugin-vue'; +import { fileURLToPath, URL } from 'node:url'; +import Components from 'unplugin-vue-components/vite'; +import { ElementPlusResolver, NaiveUiResolver } from 'unplugin-vue-components/resolvers'; + +export default defineConfig(() => { + return { + base: './', + plugins: [ + vue(), + Components({ + resolvers: [ + ElementPlusResolver(), + NaiveUiResolver(), + ], + dirs: ['src/components'], + dts: false, + }), + ], + + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + }, + extensions: ['.js', '.vue', '.json', '.ts'], + }, + + server: { + host: '0.0.0.0', + port: 9527, + open: false, + }, + + build: { + outDir: 'dist', + assetsDir: 'assets', + assetsInlineLimit: 4096, + cssCodeSplit: true, + sourcemap: false, + chunkSizeWarningLimit: 2000, + + rollupOptions: { + output: { + manualChunks: { + vue: ['vue', 'vue-router', 'vuex'], + 'element-plus': ['element-plus'], + }, + chunkFileNames: 'js/[name]-[hash].js', + entryFileNames: 'js/[name]-[hash].js', + assetFileNames: '[ext]/[name]-[hash].[ext]', + }, + }, + minify: 'esbuild', + esbuildOptions: { + drop: ['console', 'debugger'], + }, + }, + + css: { + preprocessorOptions: { + scss: { + api: 'modern-compiler', + }, + }, + }, + + optimizeDeps: { + include: ['vue', 'vue-router', 'vuex', 'axios', 'element-plus/es'], + }, + }; +});