first commit
This commit is contained in:
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Auto detect text files and perform LF normalization
|
||||||
|
* text=auto
|
||||||
29
.gitignore
vendored
Normal file
29
.gitignore
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
.env
|
||||||
|
.env.development
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
package-lock.json
|
||||||
18
.prettierrc
Normal file
18
.prettierrc
Normal file
@@ -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"
|
||||||
|
}
|
||||||
201
LICENSE
Normal file
201
LICENSE
Normal file
@@ -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.
|
||||||
197
README.md
Normal file
197
README.md
Normal file
@@ -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 <repository-url>
|
||||||
|
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)
|
||||||
16
index.html
Normal file
16
index.html
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Vite Vue3 Template</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
30
package.json
Normal file
30
package.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
9
src/App.vue
Normal file
9
src/App.vue
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
<GlobalShell>
|
||||||
|
<router-view />
|
||||||
|
</GlobalShell>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import GlobalShell from '@/components/layout/GlobalShell.vue';
|
||||||
|
</script>
|
||||||
5
src/api/index.js
Normal file
5
src/api/index.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
/**
|
||||||
|
* API统一导出
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * as userApi from './modules/user';
|
||||||
14
src/api/modules/user.js
Normal file
14
src/api/modules/user.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import request from '../request';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户相关接口
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 用户登录
|
||||||
|
export function login(data) {
|
||||||
|
return request({
|
||||||
|
url: '/user/login',
|
||||||
|
method: 'post',
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
69
src/api/request.js
Normal file
69
src/api/request.js
Normal file
@@ -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;
|
||||||
1
src/assets/images/vite.svg
Normal file
1
src/assets/images/vite.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
1
src/assets/images/vue.svg
Normal file
1
src/assets/images/vue.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 496 B |
36
src/assets/styles/base/common.css
Normal file
36
src/assets/styles/base/common.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
5
src/assets/styles/base/element-variables.scss
Normal file
5
src/assets/styles/base/element-variables.scss
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
:root {
|
||||||
|
--el-color-primary: #3237C8 !important;
|
||||||
|
--el-color-primary-light-3: #3237C8 !important;
|
||||||
|
--el-color-primary-dark-2: #3237C8 !important;
|
||||||
|
}
|
||||||
69
src/assets/styles/base/reset.css
Normal file
69
src/assets/styles/base/reset.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
3
src/assets/styles/base/tailwind.css
Normal file
3
src/assets/styles/base/tailwind.css
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
46
src/assets/styles/base/variables.css
Normal file
46
src/assets/styles/base/variables.css
Normal file
@@ -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);
|
||||||
|
}
|
||||||
22
src/assets/styles/index.less
Normal file
22
src/assets/styles/index.less
Normal file
@@ -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;
|
||||||
|
}
|
||||||
24
src/components/HelloWorld.vue
Normal file
24
src/components/HelloWorld.vue
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<template>
|
||||||
|
<h1 class="text-4xl">{{ msg }}</h1>
|
||||||
|
<div class="card">
|
||||||
|
<el-button type="primary" @click="count++">count is {{ count }}</el-button>
|
||||||
|
</div>
|
||||||
|
<p class="mb-5">这是一个基于 vite + vue3 的开发模板</p>
|
||||||
|
<p class="mb-5">
|
||||||
|
<a href="https://github.com/anghunk/vite-vue3-template">
|
||||||
|
https://github.com/anghunk/vite-vue3-template
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p>已经引入 vuex,router,axios,element-plus,less 等开发工具</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
count: 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
props: ["msg"],
|
||||||
|
};
|
||||||
|
</script>
|
||||||
537
src/components/layout/GlobalShell.vue
Normal file
537
src/components/layout/GlobalShell.vue
Normal file
@@ -0,0 +1,537 @@
|
|||||||
|
<template>
|
||||||
|
<div class="global-shell">
|
||||||
|
<header class="global-shell__header">
|
||||||
|
<div class="brand-block">
|
||||||
|
<div class="brand-logo" aria-hidden="true">
|
||||||
|
<span>苏</span>
|
||||||
|
</div>
|
||||||
|
<div class="brand-copy">
|
||||||
|
<strong class="brand-copy__title">江苏经营管理平台</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="menu-strip" aria-label="主导航">
|
||||||
|
<div
|
||||||
|
v-for="item in menuItems"
|
||||||
|
:key="item.label"
|
||||||
|
class="menu-entry"
|
||||||
|
:class="{
|
||||||
|
'menu-entry--active': isActiveItem(item),
|
||||||
|
'menu-entry--children': Boolean(item.children?.length),
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="menu-strip__item"
|
||||||
|
@click="handleMenuClick(item)"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="item.children?.length"
|
||||||
|
class="menu-entry__dropdown"
|
||||||
|
role="menu"
|
||||||
|
:aria-label="`${item.label}子菜单`"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-for="child in item.children"
|
||||||
|
:key="child.label"
|
||||||
|
type="button"
|
||||||
|
class="menu-entry__child"
|
||||||
|
:class="{ 'menu-entry__child--active': isActiveItem(child) }"
|
||||||
|
@click="handleMenuClick(child)"
|
||||||
|
>
|
||||||
|
<span class="menu-entry__child-title">{{ child.label }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="action-strip">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="ai-badge"
|
||||||
|
aria-label="AI 助手"
|
||||||
|
@click="handlePrototypeNotice"
|
||||||
|
>
|
||||||
|
<span class="ai-badge__icon">AI</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="log-button"
|
||||||
|
@click="handlePrototypeNotice"
|
||||||
|
>
|
||||||
|
导出工作日志
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="icon-button"
|
||||||
|
aria-label="通知中心"
|
||||||
|
@click="handlePrototypeNotice"
|
||||||
|
>
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||||
|
<path
|
||||||
|
d="M12 4.5a4.5 4.5 0 0 0-4.5 4.5v2.18c0 .57-.17 1.12-.49 1.59L5.5 15.1h13l-1.5-2.33a2.94 2.94 0 0 1-.5-1.59V9A4.5 4.5 0 0 0 12 4.5Z"
|
||||||
|
/>
|
||||||
|
<path d="M9.75 18a2.25 2.25 0 0 0 4.5 0" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="profile-chip"
|
||||||
|
aria-label="管理员信息"
|
||||||
|
@click="handlePrototypeNotice"
|
||||||
|
>
|
||||||
|
<span class="profile-chip__avatar">管</span>
|
||||||
|
<span class="profile-chip__name">管理员</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="global-shell__workspace">
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const menuItems = [
|
||||||
|
{ label: '首页', routeName: 'Home' },
|
||||||
|
{ label: '跟踪项目' },
|
||||||
|
{ label: '预审项目' },
|
||||||
|
{ label: '投标项目' },
|
||||||
|
{ label: '在建项目' },
|
||||||
|
{ label: '对接交流' },
|
||||||
|
{ label: '资料中心' },
|
||||||
|
{ label: '人员档案' },
|
||||||
|
{ label: '模板管理' },
|
||||||
|
{
|
||||||
|
label: '项目监管',
|
||||||
|
routeName: 'SupervisionPlanManagement',
|
||||||
|
activeNames: ['SupervisionPlanManagement', 'SupervisionVideoManagement'],
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: '监管计划',
|
||||||
|
routeName: 'SupervisionPlanManagement',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '监管内容',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '上级来访/督导',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '红黄牌处罚',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '监控视频',
|
||||||
|
routeName: 'SupervisionVideoManagement',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '招投标信息管理',
|
||||||
|
routeName: 'TenderProjectManagement',
|
||||||
|
activeNames: ['TenderProjectManagement', 'AwardProjectManagement', 'SectorManagement'],
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: '招标项目管理',
|
||||||
|
note: '管理招标金额、发布日期与详情链接',
|
||||||
|
routeName: 'TenderProjectManagement',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '中标项目管理',
|
||||||
|
note: '管理中标金额与中标单位信息',
|
||||||
|
routeName: 'AwardProjectManagement',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '板块管理',
|
||||||
|
routeName: 'SectorManagement',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ label: '系统管理' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const handlePrototypeNotice = () => {
|
||||||
|
ElMessage({
|
||||||
|
type: 'info',
|
||||||
|
message: '该功能正在原型完善中',
|
||||||
|
grouping: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const isActiveItem = (item) => {
|
||||||
|
if (item.activeNames?.includes(route.name)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return item.routeName ? route.name === item.routeName : false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMenuClick = (item) => {
|
||||||
|
if (item.routeName) {
|
||||||
|
router.push({ name: item.routeName });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePrototypeNotice();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.global-shell {
|
||||||
|
--shell-header-offset: 74px;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top left, rgba(80, 116, 255, 0.18), transparent 28%),
|
||||||
|
radial-gradient(circle at top right, rgba(29, 199, 177, 0.12), transparent 24%),
|
||||||
|
linear-gradient(180deg, #f4f7fb 0%, #edf2f8 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-shell__header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
min-height: 58px;
|
||||||
|
padding: 8px 20px;
|
||||||
|
border-bottom: 1px solid rgba(22, 37, 66, 0.08);
|
||||||
|
background: rgba(8, 20, 43, 0.92);
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
|
box-shadow: 0 12px 24px rgba(10, 26, 52, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-block {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 206px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-logo {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 10px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
color: #ecf5ff;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
background:
|
||||||
|
linear-gradient(145deg, rgba(92, 133, 255, 0.95), rgba(24, 196, 185, 0.92));
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.24),
|
||||||
|
0 10px 18px rgba(17, 78, 149, 0.24);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-copy__title {
|
||||||
|
color: #f7fbff;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-strip {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 0;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-entry {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-strip__item {
|
||||||
|
position: relative;
|
||||||
|
padding: 6px 9px;
|
||||||
|
border-radius: 999px;
|
||||||
|
color: rgba(224, 233, 247, 0.9);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 400;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition:
|
||||||
|
transform 0.2s ease,
|
||||||
|
color 0.2s ease,
|
||||||
|
background-color 0.2s ease,
|
||||||
|
box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-strip__item::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 9px;
|
||||||
|
right: 9px;
|
||||||
|
bottom: 4px;
|
||||||
|
height: 1px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: linear-gradient(90deg, #86a8ff, #5ef0d7);
|
||||||
|
transform: scaleX(0);
|
||||||
|
transform-origin: center;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-strip__item:hover,
|
||||||
|
.menu-entry--active .menu-strip__item,
|
||||||
|
.menu-entry--children:hover .menu-strip__item {
|
||||||
|
color: #ffffff;
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-strip__item:hover::after,
|
||||||
|
.menu-entry--active .menu-strip__item::after,
|
||||||
|
.menu-entry--children:hover .menu-strip__item::after {
|
||||||
|
transform: scaleX(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-entry__dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 8px);
|
||||||
|
left: 0;
|
||||||
|
min-width: 280px;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid rgba(102, 152, 255, 0.18);
|
||||||
|
border-radius: 18px;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(11, 24, 46, 0.96), rgba(8, 19, 37, 0.98)),
|
||||||
|
rgba(8, 19, 37, 0.96);
|
||||||
|
box-shadow:
|
||||||
|
0 24px 50px rgba(4, 11, 24, 0.3),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transform: translateY(8px);
|
||||||
|
transition:
|
||||||
|
opacity 0.2s ease,
|
||||||
|
transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-entry__dropdown::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 100%;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-entry--children:hover .menu-entry__dropdown {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-entry__child {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border-radius: 14px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
color: #edf4ff;
|
||||||
|
text-align: left;
|
||||||
|
transition:
|
||||||
|
background-color 0.2s ease,
|
||||||
|
transform 0.2s ease,
|
||||||
|
box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-entry__child + .menu-entry__child {
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-entry__child:hover,
|
||||||
|
.menu-entry__child--active {
|
||||||
|
background: linear-gradient(135deg, rgba(92, 133, 255, 0.22), rgba(24, 196, 185, 0.16));
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-entry__child-title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-strip {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-badge,
|
||||||
|
.icon-button,
|
||||||
|
.profile-chip,
|
||||||
|
.log-button {
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
|
||||||
|
transition:
|
||||||
|
transform 0.2s ease,
|
||||||
|
border-color 0.2s ease,
|
||||||
|
background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-badge:hover,
|
||||||
|
.icon-button:hover,
|
||||||
|
.profile-chip:hover,
|
||||||
|
.log-button:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
border-color: rgba(105, 233, 219, 0.36);
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-badge,
|
||||||
|
.icon-button {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 10px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-badge__icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #0e223e;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
background: linear-gradient(135deg, #8fd8ff 0%, #85ffd1 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-button {
|
||||||
|
height: 30px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #eff7ff;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-button svg {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
stroke: #ecf5ff;
|
||||||
|
stroke-width: 1.8;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-chip {
|
||||||
|
height: 30px;
|
||||||
|
padding: 0 10px 0 5px;
|
||||||
|
border-radius: 999px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
color: #eff7ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-chip__avatar {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
color: #0c1f39;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
background: linear-gradient(135deg, #f2f7ff 0%, #aac6ff 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-chip__name {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-shell__workspace {
|
||||||
|
flex: 1;
|
||||||
|
padding: 14px 20px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1480px) {
|
||||||
|
.global-shell {
|
||||||
|
--shell-header-offset: 116px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-shell__header {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-strip {
|
||||||
|
order: 3;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.global-shell {
|
||||||
|
--shell-header-offset: 152px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-shell__header {
|
||||||
|
padding: 8px 12px;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-block {
|
||||||
|
min-width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-copy__title {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-entry__dropdown {
|
||||||
|
min-width: 240px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-strip {
|
||||||
|
width: 100%;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-button {
|
||||||
|
flex: 1;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-shell__workspace {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
25
src/main.js
Normal file
25
src/main.js
Normal file
@@ -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');
|
||||||
11
src/plugins/element-plus.js
Normal file
11
src/plugins/element-plus.js
Normal file
@@ -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);
|
||||||
|
}
|
||||||
11
src/plugins/index.js
Normal file
11
src/plugins/index.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* 插件统一注册
|
||||||
|
*/
|
||||||
|
|
||||||
|
import setupElementPlus from './element-plus';
|
||||||
|
|
||||||
|
export default function setupPlugins(app) {
|
||||||
|
// 注册 Element Plus
|
||||||
|
setupElementPlus(app);
|
||||||
|
|
||||||
|
}
|
||||||
44
src/router/guards.js
Normal file
44
src/router/guards.js
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
23
src/router/index.js
Normal file
23
src/router/index.js
Normal file
@@ -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;
|
||||||
15
src/router/modules/home.js
Normal file
15
src/router/modules/home.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* 首页路由配置
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'Home',
|
||||||
|
component: () => import('@/views/home/index.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '首页',
|
||||||
|
keepAlive: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
18
src/router/modules/supervision.js
Normal file
18
src/router/modules/supervision.js
Normal file
@@ -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: '监控视频',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
26
src/router/modules/tendering.js
Normal file
26
src/router/modules/tendering.js
Normal file
@@ -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: '板块管理',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
14
src/router/modules/user.js
Normal file
14
src/router/modules/user.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* 用户相关路由配置
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
path: '/user',
|
||||||
|
name: 'User',
|
||||||
|
component: () => import('@/views/user/index.vue'),
|
||||||
|
meta: {
|
||||||
|
title: '用户中心',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
19
src/store/getters.js
Normal file
19
src/store/getters.js
Normal file
@@ -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;
|
||||||
35
src/store/index.js
Normal file
35
src/store/index.js
Normal file
@@ -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;
|
||||||
57
src/store/modules/app.js
Normal file
57
src/store/modules/app.js
Normal file
@@ -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,
|
||||||
|
};
|
||||||
66
src/store/modules/user.js
Normal file
66
src/store/modules/user.js
Normal file
@@ -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,
|
||||||
|
};
|
||||||
70
src/utils/format.js
Normal file
70
src/utils/format.js
Normal file
@@ -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');
|
||||||
|
}
|
||||||
8
src/utils/index.js
Normal file
8
src/utils/index.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* 工具函数统一导出
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { default as storage } from './storage';
|
||||||
|
export * from './auth';
|
||||||
|
export * from './validate';
|
||||||
|
export * from './format';
|
||||||
88
src/utils/storage.js
Normal file
88
src/utils/storage.js
Normal file
@@ -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;
|
||||||
60
src/utils/validate.js
Normal file
60
src/utils/validate.js
Normal file
@@ -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);
|
||||||
|
}
|
||||||
23
src/views/home/index.vue
Normal file
23
src/views/home/index.vue
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<template>
|
||||||
|
<section class="home-empty-panel" aria-label="首页内容区"></section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.home-empty-panel {
|
||||||
|
min-height: max(420px, calc(100vh - 122px));
|
||||||
|
border: 1px dashed rgba(23, 41, 74, 0.08);
|
||||||
|
border-radius: 28px;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(255, 255, 255, 0.7), rgba(246, 249, 253, 0.96)),
|
||||||
|
repeating-linear-gradient(
|
||||||
|
90deg,
|
||||||
|
rgba(32, 74, 135, 0.025) 0,
|
||||||
|
rgba(32, 74, 135, 0.025) 1px,
|
||||||
|
transparent 1px,
|
||||||
|
transparent 120px
|
||||||
|
);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.92),
|
||||||
|
0 20px 40px rgba(26, 54, 93, 0.04);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
1040
src/views/supervision/plan/index.vue
Normal file
1040
src/views/supervision/plan/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
1282
src/views/supervision/video/index.vue
Normal file
1282
src/views/supervision/video/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
191
src/views/tendering/award/index.vue
Normal file
191
src/views/tendering/award/index.vue
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
<template>
|
||||||
|
<ProjectCrudPanel
|
||||||
|
title="中标项目管理"
|
||||||
|
storage-key="admin-pc:award-projects"
|
||||||
|
date-range-prop="awardDate"
|
||||||
|
:columns="columns"
|
||||||
|
:form-fields="formFields"
|
||||||
|
:initial-records="initialRecords"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import ProjectCrudPanel from '@/views/tendering/components/ProjectCrudPanel.vue';
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ prop: 'projectCode', label: '项目编号', minWidth: 150 },
|
||||||
|
{ prop: 'projectName', label: '项目名称', minWidth: 220 },
|
||||||
|
{ prop: 'sectionCode', label: '标段编号', minWidth: 150 },
|
||||||
|
{ prop: 'sectionName', label: '标段名称', minWidth: 220 },
|
||||||
|
{ prop: 'constructionCompany', label: '建设单位名称', minWidth: 220 },
|
||||||
|
{ prop: 'awardCompany', label: '中标人名称', minWidth: 220 },
|
||||||
|
{ prop: 'awardAmount', label: '中标价', minWidth: 160, type: 'currency' },
|
||||||
|
{ prop: 'awardDate', label: '中标时间', minWidth: 140 },
|
||||||
|
{ prop: 'detailLink', label: '详情链接', minWidth: 96, width: 96, type: 'link' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const formFields = [
|
||||||
|
{ prop: 'projectCode', label: '项目编号', type: 'text' },
|
||||||
|
{ prop: 'projectName', label: '项目名称', type: 'text' },
|
||||||
|
{ prop: 'sectionCode', label: '标段编号', type: 'text' },
|
||||||
|
{ prop: 'sectionName', label: '标段名称', type: 'text' },
|
||||||
|
{ prop: 'constructionCompany', label: '建设单位名称', type: 'text' },
|
||||||
|
{ prop: 'awardCompany', label: '中标人名称', type: 'text' },
|
||||||
|
{ prop: 'awardAmount', label: '中标价', type: 'amount' },
|
||||||
|
{ prop: 'awardDate', label: '中标时间', type: 'date' },
|
||||||
|
{ prop: 'detailLink', label: '详情链接', type: 'url' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const initialRecords = [
|
||||||
|
{
|
||||||
|
id: 'award_1',
|
||||||
|
projectCode: 'AW-2026-001',
|
||||||
|
projectName: '南京江北新材料园综合配套项目',
|
||||||
|
sectionCode: 'BD-01',
|
||||||
|
sectionName: '园区道路及附属配套工程一标段',
|
||||||
|
constructionCompany: '南京江北新区建设投资集团有限公司',
|
||||||
|
awardCompany: '江苏城建发展有限公司',
|
||||||
|
awardAmount: 17980000,
|
||||||
|
awardDate: '2026-04-22',
|
||||||
|
detailLink: 'https://example.com/award/1',
|
||||||
|
provinceCity: '江苏省南京市',
|
||||||
|
sectorType: '基础设施',
|
||||||
|
projectType: '工程类',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'award_2',
|
||||||
|
projectCode: 'AW-2026-002',
|
||||||
|
projectName: '苏州产业园智慧运维升级项目',
|
||||||
|
sectionCode: 'BD-02',
|
||||||
|
sectionName: '园区智慧运维平台建设标段',
|
||||||
|
constructionCompany: '苏州高新科技产业发展有限公司',
|
||||||
|
awardCompany: '苏州云控科技股份有限公司',
|
||||||
|
awardAmount: 8860000,
|
||||||
|
awardDate: '2026-04-20',
|
||||||
|
detailLink: 'https://example.com/award/2',
|
||||||
|
provinceCity: '江苏省苏州市',
|
||||||
|
sectorType: '智慧园区',
|
||||||
|
projectType: '服务类',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'award_3',
|
||||||
|
projectCode: 'AW-2026-003',
|
||||||
|
projectName: '徐州物流枢纽能源系统优化项目',
|
||||||
|
sectionCode: 'BD-03',
|
||||||
|
sectionName: '能源系统设备采购及调试标段',
|
||||||
|
constructionCompany: '徐州陆港建设管理有限公司',
|
||||||
|
awardCompany: '华东机电工程集团有限公司',
|
||||||
|
awardAmount: 13250000,
|
||||||
|
awardDate: '2026-04-19',
|
||||||
|
detailLink: 'https://example.com/award/3',
|
||||||
|
provinceCity: '江苏省徐州市',
|
||||||
|
sectorType: '能源环保',
|
||||||
|
projectType: '采购类',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'award_4',
|
||||||
|
projectCode: 'AW-2026-004',
|
||||||
|
projectName: '常州高新区智慧安防平台建设项目',
|
||||||
|
sectionCode: 'BD-04',
|
||||||
|
sectionName: '智慧安防平台软硬件集成标段',
|
||||||
|
constructionCompany: '常州高新区数字城市建设有限公司',
|
||||||
|
awardCompany: '江苏智联数字科技有限公司',
|
||||||
|
awardAmount: 5980000,
|
||||||
|
awardDate: '2026-04-17',
|
||||||
|
detailLink: 'https://example.com/award/4',
|
||||||
|
provinceCity: '江苏省常州市',
|
||||||
|
sectorType: '智慧园区',
|
||||||
|
projectType: '采购类',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'award_5',
|
||||||
|
projectCode: 'AW-2026-005',
|
||||||
|
projectName: '南通滨海新区公共服务中心运营项目',
|
||||||
|
sectionCode: 'BD-05',
|
||||||
|
sectionName: '公共服务中心运营管理标段',
|
||||||
|
constructionCompany: '南通滨海新区开发建设有限公司',
|
||||||
|
awardCompany: '南通城服运营管理有限公司',
|
||||||
|
awardAmount: 11360000,
|
||||||
|
awardDate: '2026-04-16',
|
||||||
|
detailLink: 'https://example.com/award/5',
|
||||||
|
provinceCity: '江苏省南通市',
|
||||||
|
sectorType: '公共服务',
|
||||||
|
projectType: '服务类',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'award_6',
|
||||||
|
projectCode: 'AW-2026-006',
|
||||||
|
projectName: '扬州智能制造产业基地设备采购项目',
|
||||||
|
sectionCode: 'BD-06',
|
||||||
|
sectionName: '生产线设备采购二标段',
|
||||||
|
constructionCompany: '扬州经开产业投资有限公司',
|
||||||
|
awardCompany: '华东智能装备供应链有限公司',
|
||||||
|
awardAmount: 14750000,
|
||||||
|
awardDate: '2026-04-14',
|
||||||
|
detailLink: 'https://example.com/award/6',
|
||||||
|
provinceCity: '江苏省扬州市',
|
||||||
|
sectorType: '智能制造',
|
||||||
|
projectType: '采购类',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'award_7',
|
||||||
|
projectCode: 'AW-2026-007',
|
||||||
|
projectName: '镇江城市更新配套路桥施工项目',
|
||||||
|
sectionCode: 'BD-07',
|
||||||
|
sectionName: '主线道路及桥梁施工标段',
|
||||||
|
constructionCompany: '镇江市城市更新建设发展有限公司',
|
||||||
|
awardCompany: '镇江建工集团有限公司',
|
||||||
|
awardAmount: 26880000,
|
||||||
|
awardDate: '2026-04-12',
|
||||||
|
detailLink: 'https://example.com/award/7',
|
||||||
|
provinceCity: '江苏省镇江市',
|
||||||
|
sectorType: '基础设施',
|
||||||
|
projectType: '工程类',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'award_8',
|
||||||
|
projectCode: 'AW-2026-008',
|
||||||
|
projectName: '盐城零碳园区综合咨询服务项目',
|
||||||
|
sectionCode: 'BD-08',
|
||||||
|
sectionName: '零碳园区综合咨询服务标段',
|
||||||
|
constructionCompany: '盐城绿色低碳产业发展有限公司',
|
||||||
|
awardCompany: '江苏绿源咨询顾问有限公司',
|
||||||
|
awardAmount: 3620000,
|
||||||
|
awardDate: '2026-04-10',
|
||||||
|
detailLink: 'https://example.com/award/8',
|
||||||
|
provinceCity: '江苏省盐城市',
|
||||||
|
sectorType: '能源环保',
|
||||||
|
projectType: '服务类',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'award_9',
|
||||||
|
projectCode: 'AW-2026-009',
|
||||||
|
projectName: '连云港港区仓储自动化改造项目',
|
||||||
|
sectionCode: 'BD-09',
|
||||||
|
sectionName: '自动化仓储改造施工标段',
|
||||||
|
constructionCompany: '连云港港口建设运营有限公司',
|
||||||
|
awardCompany: '连云港港机自动化工程有限公司',
|
||||||
|
awardAmount: 8450000,
|
||||||
|
awardDate: '2026-04-08',
|
||||||
|
detailLink: 'https://example.com/award/9',
|
||||||
|
provinceCity: '江苏省连云港市',
|
||||||
|
sectorType: '物流仓储',
|
||||||
|
projectType: '其他',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'award_10',
|
||||||
|
projectCode: 'AW-2026-010',
|
||||||
|
projectName: '宿迁文旅综合体机电安装工程',
|
||||||
|
sectionCode: 'BD-10',
|
||||||
|
sectionName: '机电安装施工总承包标段',
|
||||||
|
constructionCompany: '宿迁文旅产业发展集团有限公司',
|
||||||
|
awardCompany: '江苏弘远机电安装有限公司',
|
||||||
|
awardAmount: 21400000,
|
||||||
|
awardDate: '2026-04-06',
|
||||||
|
detailLink: 'https://example.com/award/10',
|
||||||
|
provinceCity: '江苏省宿迁市',
|
||||||
|
sectorType: '文旅配套',
|
||||||
|
projectType: '工程类',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
</script>
|
||||||
1403
src/views/tendering/components/ProjectCrudPanel.vue
Normal file
1403
src/views/tendering/components/ProjectCrudPanel.vue
Normal file
File diff suppressed because it is too large
Load Diff
441
src/views/tendering/sector/index.vue
Normal file
441
src/views/tendering/sector/index.vue
Normal file
@@ -0,0 +1,441 @@
|
|||||||
|
<template>
|
||||||
|
<section class="sector-page">
|
||||||
|
<div class="sector-card">
|
||||||
|
<div class="sector-toolbar">
|
||||||
|
<h1 class="sector-toolbar__title">板块管理</h1>
|
||||||
|
<el-button type="primary" @click="openCreateRootDialog">新增一级板块</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-empty
|
||||||
|
v-if="!treeData.length"
|
||||||
|
description="暂无板块数据"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<el-tree
|
||||||
|
v-else
|
||||||
|
:data="treeData"
|
||||||
|
:props="treeProps"
|
||||||
|
node-key="id"
|
||||||
|
default-expand-all
|
||||||
|
:expand-on-click-node="false"
|
||||||
|
class="sector-tree"
|
||||||
|
>
|
||||||
|
<template #default="{ data }">
|
||||||
|
<div class="sector-node">
|
||||||
|
<div class="sector-node__main">
|
||||||
|
<span class="sector-node__label">{{ data.label }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sector-node__actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="sector-node__action"
|
||||||
|
@click="openCreateChildDialog(data)"
|
||||||
|
>
|
||||||
|
新增子级
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="sector-node__action"
|
||||||
|
@click="openEditDialog(data)"
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="sector-node__action sector-node__action--danger"
|
||||||
|
@click="deleteNode(data)"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-tree>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
:title="dialogTitle"
|
||||||
|
width="520px"
|
||||||
|
destroy-on-close
|
||||||
|
>
|
||||||
|
<el-form label-width="92px">
|
||||||
|
<el-form-item
|
||||||
|
v-if="currentAction.type === 'create-child'"
|
||||||
|
label="上级板块"
|
||||||
|
>
|
||||||
|
<el-input :model-value="currentAction.parentLabel" disabled />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="板块名称" required>
|
||||||
|
<el-input
|
||||||
|
v-model="formModel.label"
|
||||||
|
maxlength="30"
|
||||||
|
placeholder="请输入板块名称"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="submitForm">保存</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, reactive, ref } from 'vue';
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
|
import storage from '@/utils/storage';
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'admin-pc:sector-tree';
|
||||||
|
|
||||||
|
const treeProps = {
|
||||||
|
children: 'children',
|
||||||
|
label: 'label',
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultTreeData = [
|
||||||
|
{
|
||||||
|
id: 'infra',
|
||||||
|
label: '基础设施',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 'infra-road',
|
||||||
|
label: '道路工程',
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'infra-mep',
|
||||||
|
label: '机电安装',
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'smart-park',
|
||||||
|
label: '智慧园区',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 'smart-park-security',
|
||||||
|
label: '智慧安防',
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'smart-park-operation',
|
||||||
|
label: '智慧运维',
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'energy',
|
||||||
|
label: '能源环保',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 'energy-station',
|
||||||
|
label: '能源站',
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'energy-consulting',
|
||||||
|
label: '绿色咨询',
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const normalizeTree = (nodes) =>
|
||||||
|
(Array.isArray(nodes) ? nodes : []).map((node) => ({
|
||||||
|
id: node.id,
|
||||||
|
label: node.label || '',
|
||||||
|
children: normalizeTree(node.children),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const cloneTree = (nodes) =>
|
||||||
|
nodes.map((node) => ({
|
||||||
|
...node,
|
||||||
|
children: cloneTree(node.children || []),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const treeData = ref(
|
||||||
|
normalizeTree(storage.getLocalStorage(STORAGE_KEY) || defaultTreeData),
|
||||||
|
);
|
||||||
|
|
||||||
|
const dialogVisible = ref(false);
|
||||||
|
const formModel = reactive({
|
||||||
|
label: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentAction = reactive({
|
||||||
|
type: 'create-root',
|
||||||
|
targetId: '',
|
||||||
|
parentLabel: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const dialogTitle = computed(() => {
|
||||||
|
if (currentAction.type === 'create-child') {
|
||||||
|
return '新增子级板块';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentAction.type === 'edit') {
|
||||||
|
return '编辑板块';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '新增一级板块';
|
||||||
|
});
|
||||||
|
|
||||||
|
const persistTree = () => {
|
||||||
|
storage.setLocalStorage(STORAGE_KEY, treeData.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateId = () => `sector_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
||||||
|
|
||||||
|
const flattenTree = (nodes) =>
|
||||||
|
nodes.flatMap((node) => [node, ...flattenTree(node.children || [])]);
|
||||||
|
|
||||||
|
const findNode = (nodes, id) => {
|
||||||
|
for (const node of nodes) {
|
||||||
|
if (node.id === id) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
const matched = findNode(node.children || [], id);
|
||||||
|
if (matched) {
|
||||||
|
return matched;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeNode = (nodes, id) =>
|
||||||
|
nodes
|
||||||
|
.filter((node) => node.id !== id)
|
||||||
|
.map((node) => ({
|
||||||
|
...node,
|
||||||
|
children: removeNode(node.children || [], id),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
formModel.label = '';
|
||||||
|
currentAction.type = 'create-root';
|
||||||
|
currentAction.targetId = '';
|
||||||
|
currentAction.parentLabel = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const openCreateRootDialog = () => {
|
||||||
|
resetForm();
|
||||||
|
currentAction.type = 'create-root';
|
||||||
|
dialogVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const openCreateChildDialog = (node) => {
|
||||||
|
resetForm();
|
||||||
|
currentAction.type = 'create-child';
|
||||||
|
currentAction.targetId = node.id;
|
||||||
|
currentAction.parentLabel = node.label;
|
||||||
|
dialogVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const openEditDialog = (node) => {
|
||||||
|
resetForm();
|
||||||
|
currentAction.type = 'edit';
|
||||||
|
currentAction.targetId = node.id;
|
||||||
|
formModel.label = node.label;
|
||||||
|
dialogVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitForm = () => {
|
||||||
|
const label = formModel.label.trim();
|
||||||
|
|
||||||
|
if (!label) {
|
||||||
|
ElMessage.warning('请输入板块名称');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const duplicated = flattenTree(treeData.value).find(
|
||||||
|
(node) => node.label === label && node.id !== currentAction.targetId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (duplicated) {
|
||||||
|
ElMessage.warning('板块名称已存在');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextTree = cloneTree(treeData.value);
|
||||||
|
|
||||||
|
if (currentAction.type === 'create-root') {
|
||||||
|
nextTree.push({
|
||||||
|
id: generateId(),
|
||||||
|
label,
|
||||||
|
children: [],
|
||||||
|
});
|
||||||
|
} else if (currentAction.type === 'create-child') {
|
||||||
|
const parentNode = findNode(nextTree, currentAction.targetId);
|
||||||
|
if (!parentNode) {
|
||||||
|
ElMessage.error('未找到上级板块');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
parentNode.children.push({
|
||||||
|
id: generateId(),
|
||||||
|
label,
|
||||||
|
children: [],
|
||||||
|
});
|
||||||
|
} else if (currentAction.type === 'edit') {
|
||||||
|
const targetNode = findNode(nextTree, currentAction.targetId);
|
||||||
|
if (!targetNode) {
|
||||||
|
ElMessage.error('未找到待编辑板块');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
targetNode.label = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
treeData.value = nextTree;
|
||||||
|
persistTree();
|
||||||
|
dialogVisible.value = false;
|
||||||
|
|
||||||
|
ElMessage.success(
|
||||||
|
currentAction.type === 'edit' ? '板块已更新' : '板块已保存',
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteNode = async (node) => {
|
||||||
|
const hasChildren = Boolean(node.children?.length);
|
||||||
|
const message = hasChildren
|
||||||
|
? `确认删除“${node.label}”及其下级板块吗?`
|
||||||
|
: `确认删除“${node.label}”吗?`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(message, '删除确认', {
|
||||||
|
type: 'warning',
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
treeData.value = removeNode(treeData.value, node.id);
|
||||||
|
persistTree();
|
||||||
|
ElMessage.success('板块已删除');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.sector-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sector-card {
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid rgba(112, 144, 196, 0.12);
|
||||||
|
border-radius: 2px;
|
||||||
|
background: rgba(255, 255, 255, 0.92);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.92),
|
||||||
|
0 18px 36px rgba(26, 54, 93, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sector-toolbar {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sector-toolbar__title {
|
||||||
|
color: #142949;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sector-tree {
|
||||||
|
margin: -6px;
|
||||||
|
border: 1px solid #d9e3f0;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sector-node {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 46px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sector-node__main {
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sector-node__label {
|
||||||
|
color: #1b2d44;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sector-node__actions {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sector-node__action {
|
||||||
|
color: #1d4ed8;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sector-node__action--danger {
|
||||||
|
color: #da4f60;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.sector-tree .el-tree-node__content) {
|
||||||
|
min-height: 52px;
|
||||||
|
border-bottom: 1px solid #e5ebf3;
|
||||||
|
border-radius: 2px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.sector-tree > .el-tree-node > .el-tree-node__content) {
|
||||||
|
border-left: none;
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.sector-tree .el-tree-node:last-child > .el-tree-node__content) {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.sector-tree .el-tree-node__content:hover) {
|
||||||
|
background: rgba(37, 99, 235, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.sector-toolbar {
|
||||||
|
align-items: stretch;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sector-node {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sector-node__main,
|
||||||
|
.sector-node__actions {
|
||||||
|
width: 100%;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
158
src/views/tendering/tender/index.vue
Normal file
158
src/views/tendering/tender/index.vue
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
<template>
|
||||||
|
<ProjectCrudPanel
|
||||||
|
title="招标项目管理"
|
||||||
|
storage-key="admin-pc:tender-projects"
|
||||||
|
:enable-conversion-action="true"
|
||||||
|
:record-defaults="{ conversionStatus: '未转化' }"
|
||||||
|
date-range-prop="publishedAt"
|
||||||
|
amount-range-prop="tenderAmount"
|
||||||
|
amount-range-unit="万元"
|
||||||
|
:amount-scale="10000"
|
||||||
|
:columns="columns"
|
||||||
|
:form-fields="formFields"
|
||||||
|
:initial-records="initialRecords"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import ProjectCrudPanel from '@/views/tendering/components/ProjectCrudPanel.vue';
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ prop: 'projectName', label: '项目名称', minWidth: 190 },
|
||||||
|
{ prop: 'provinceCity', label: '所属省市', minWidth: 128 },
|
||||||
|
{ prop: 'sectorType', label: '板块类型', minWidth: 112 },
|
||||||
|
{ prop: 'projectType', label: '项目类型', minWidth: 100 },
|
||||||
|
{ prop: 'tenderAmount', label: '项目金额', minWidth: 128, type: 'wanAmount' },
|
||||||
|
{ prop: 'conversionStatus', label: '是否转化', minWidth: 100 },
|
||||||
|
{ prop: 'publishedAt', label: '发布日期', minWidth: 118 },
|
||||||
|
{ prop: 'detailLink', label: '详情链接', minWidth: 96, width: 96, type: 'link' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const formFields = [
|
||||||
|
{ prop: 'projectName', label: '项目名称', type: 'text' },
|
||||||
|
{ prop: 'provinceCity', label: '所属省市', type: 'select' },
|
||||||
|
{ prop: 'sectorType', label: '板块类型', type: 'select' },
|
||||||
|
{
|
||||||
|
prop: 'projectType',
|
||||||
|
label: '项目类型',
|
||||||
|
type: 'select',
|
||||||
|
options: ['服务类', '工程类', '采购类', '其他'],
|
||||||
|
},
|
||||||
|
{ prop: 'tenderAmount', label: '项目金额', type: 'amount' },
|
||||||
|
{ prop: 'publishedAt', label: '发布日期', type: 'date' },
|
||||||
|
{ prop: 'detailLink', label: '详情链接', type: 'url' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const initialRecords = [
|
||||||
|
{
|
||||||
|
id: 'tender_1',
|
||||||
|
projectName: '南京江北新材料园综合配套项目',
|
||||||
|
provinceCity: '江苏省南京市',
|
||||||
|
sectorType: '基础设施',
|
||||||
|
projectType: '工程类',
|
||||||
|
tenderAmount: 18600000,
|
||||||
|
conversionStatus: '已转化',
|
||||||
|
publishedAt: '2026-04-20',
|
||||||
|
detailLink: 'https://example.com/tender/1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tender_2',
|
||||||
|
projectName: '苏州产业园智慧运维升级项目',
|
||||||
|
provinceCity: '江苏省苏州市',
|
||||||
|
sectorType: '智慧园区',
|
||||||
|
projectType: '服务类',
|
||||||
|
tenderAmount: 9200000,
|
||||||
|
conversionStatus: '未转化',
|
||||||
|
publishedAt: '2026-04-18',
|
||||||
|
detailLink: 'https://example.com/tender/2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tender_3',
|
||||||
|
projectName: '无锡区域能源站扩容工程',
|
||||||
|
provinceCity: '江苏省无锡市',
|
||||||
|
sectorType: '能源环保',
|
||||||
|
projectType: '工程类',
|
||||||
|
tenderAmount: 25400000,
|
||||||
|
conversionStatus: '已转化',
|
||||||
|
publishedAt: '2026-04-14',
|
||||||
|
detailLink: 'https://example.com/tender/3',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tender_4',
|
||||||
|
projectName: '常州高新区智慧安防平台建设项目',
|
||||||
|
provinceCity: '江苏省常州市',
|
||||||
|
sectorType: '智慧园区',
|
||||||
|
projectType: '采购类',
|
||||||
|
tenderAmount: 6400000,
|
||||||
|
conversionStatus: '未转化',
|
||||||
|
publishedAt: '2026-04-12',
|
||||||
|
detailLink: 'https://example.com/tender/4',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tender_5',
|
||||||
|
projectName: '南通滨海新区公共服务中心运营项目',
|
||||||
|
provinceCity: '江苏省南通市',
|
||||||
|
sectorType: '公共服务',
|
||||||
|
projectType: '服务类',
|
||||||
|
tenderAmount: 11800000,
|
||||||
|
conversionStatus: '已转化',
|
||||||
|
publishedAt: '2026-04-11',
|
||||||
|
detailLink: 'https://example.com/tender/5',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tender_6',
|
||||||
|
projectName: '扬州智能制造产业基地设备采购项目',
|
||||||
|
provinceCity: '江苏省扬州市',
|
||||||
|
sectorType: '智能制造',
|
||||||
|
projectType: '采购类',
|
||||||
|
tenderAmount: 15200000,
|
||||||
|
conversionStatus: '未转化',
|
||||||
|
publishedAt: '2026-04-09',
|
||||||
|
detailLink: 'https://example.com/tender/6',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tender_7',
|
||||||
|
projectName: '镇江城市更新配套道路施工项目',
|
||||||
|
provinceCity: '江苏省镇江市',
|
||||||
|
sectorType: '基础设施',
|
||||||
|
projectType: '工程类',
|
||||||
|
tenderAmount: 27600000,
|
||||||
|
conversionStatus: '已转化',
|
||||||
|
publishedAt: '2026-04-08',
|
||||||
|
detailLink: 'https://example.com/tender/7',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tender_8',
|
||||||
|
projectName: '盐城零碳园区综合咨询服务项目',
|
||||||
|
provinceCity: '江苏省盐城市',
|
||||||
|
sectorType: '能源环保',
|
||||||
|
projectType: '服务类',
|
||||||
|
tenderAmount: 3850000,
|
||||||
|
conversionStatus: '未转化',
|
||||||
|
publishedAt: '2026-04-06',
|
||||||
|
detailLink: 'https://example.com/tender/8',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tender_9',
|
||||||
|
projectName: '连云港港区仓储自动化改造项目',
|
||||||
|
provinceCity: '江苏省连云港市',
|
||||||
|
sectorType: '物流仓储',
|
||||||
|
projectType: '其他',
|
||||||
|
tenderAmount: 8900000,
|
||||||
|
conversionStatus: '未转化',
|
||||||
|
publishedAt: '2026-04-05',
|
||||||
|
detailLink: 'https://example.com/tender/9',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tender_10',
|
||||||
|
projectName: '宿迁文旅综合体机电安装工程',
|
||||||
|
provinceCity: '江苏省宿迁市',
|
||||||
|
sectorType: '文旅配套',
|
||||||
|
projectType: '工程类',
|
||||||
|
tenderAmount: 22100000,
|
||||||
|
conversionStatus: '已转化',
|
||||||
|
publishedAt: '2026-04-03',
|
||||||
|
detailLink: 'https://example.com/tender/10',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
</script>
|
||||||
22
src/views/user/index.vue
Normal file
22
src/views/user/index.vue
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<template>
|
||||||
|
<div class="user-container">
|
||||||
|
用户中心
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import { useStore } from 'vuex';
|
||||||
|
|
||||||
|
const store = useStore();
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
console.log('用户中心页面加载完成');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.user-container {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
8
tailwind.config.js
Normal file
8
tailwind.config.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
// tailwind.config.js
|
||||||
|
export default {
|
||||||
|
content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
72
vite.config.js
Normal file
72
vite.config.js
Normal file
@@ -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'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user