36 Commits

Author SHA1 Message Date
b7d868e397 Merge branch 'gitee-master' into feature-project
# Conflicts:
#	README.md
2024-11-25 18:48:03 +08:00
f5c91daba2 Merge branch 'gitee-master' into feature-project 2024-11-22 11:13:17 +08:00
fa8f650aa1 Merge branch 'gitee-master' into feature-project
# Conflicts:
#	README.md
2024-11-18 08:54:42 +08:00
63fee3b541 Merge branch 'gitee-master' into feature-project 2024-10-28 10:40:13 +08:00
583ce6d661 [fix] 更改描述 2024-10-14 15:09:13 +08:00
52a4cc210c Merge branch 'gitee-master' into feature-project
# Conflicts:
#	src/views/bpm/processInstance/detail/index.vue
2024-10-14 11:08:39 +08:00
2acbe2b228 [fix] 标记bug 2024-10-09 14:40:46 +08:00
a0e4ac5964 Merge branch 'gitee-master' into feature-project
# Conflicts:
#	.env.local
#	src/components/UploadFile/src/useUpload.ts
#	src/router/modules/remaining.ts
#	src/utils/dict.ts
2024-10-08 14:23:33 +08:00
713b2cba0c [fix] 修改说明 2024-07-18 11:41:56 +08:00
edf2a5f188 [fix] 修改字典 2024-07-18 11:38:14 +08:00
31c73b5764 [fix] 修改字典 2024-07-18 11:37:47 +08:00
504911bd06 Merge branch 'refs/heads/gitee-master' into feature-project
# Conflicts:
#	README.md
#	src/utils/dict.ts
#	src/views/Login/components/LoginForm.vue
2024-07-18 11:32:46 +08:00
3b80a8a953 [fix] 修改说明 2024-07-18 10:23:18 +08:00
9013643093 [fix] 修改首页描述 2024-07-18 10:21:12 +08:00
1ce91dee0a fix: upload组件resolve问题 2024-07-16 02:32:09 +00:00
2154f9c3e7 chore: 文件目录树按钮显隐优化 2024-07-16 02:32:09 +00:00
1e45f4d6ba feat: 首页 2024-07-16 02:32:09 +00:00
bc4ecedb43 [fix] 修改项目名称,添加项目信息附件归档 2024-07-15 22:10:57 +08:00
0aa66c1420 [fix] 修改logo,项目立项前端微调 2024-07-15 18:15:11 +08:00
e87a5d8ba3 chore: 文件上传增加category等参数 2024-07-15 14:46:48 +08:00
10726d7c4e refactor: 重构文件目录系统 2024-07-15 14:27:34 +08:00
83fc308eeb [feat] 添加文件夹 2024-07-11 21:28:21 +08:00
0480625210 [fix] 修改上传为禁止删除 2024-07-09 20:19:52 +08:00
e419951a2d [feat] 增加上传禁止删除 2024-07-09 20:19:33 +08:00
76bc2a3916 [fix] 修改登录默认信息 2024-07-09 20:18:28 +08:00
5b7487eb4f !1 merge: upload和城市选择的重构
Merge pull request !1 from Zweihander/yx-dev-from-feature-project
2024-07-09 08:56:52 +00:00
f70f722fda Merge branch 'feature-project' of gitee.com:hhyykk/ipms-sjy-ui into yx-dev-from-feature-project
Signed-off-by: Zweihander <978345079@qq.com>
2024-07-09 08:49:34 +00:00
7a68dccf9b refactor: 将省份和城市级联改为select框联动 2024-07-09 16:42:48 +08:00
7a6c93d7e7 refactor: 修改upload相关组件 2024-07-09 15:40:51 +08:00
a0bb5bb1d0 [fix] 项目信息交互优化 2024-07-08 20:51:14 +08:00
9cf69f42ba [fix] 项目信息交互优化 2024-07-08 20:50:59 +08:00
d9b9a0581e refactor: 重写uploadFile组件 2024-07-08 17:41:28 +08:00
c8df5ed632 [feat] 新增项目信息管理模块业务流程表单 2024-07-07 16:02:33 +08:00
14a8fc6434 [feat] 新增项目信息管理模块 2024-07-06 20:22:49 +08:00
265dc4cdc8 [feat] 新增客户管理模块 2024-07-03 15:35:57 +08:00
38530fff07 [feat] init 2024-07-03 12:57:29 +08:00
34 changed files with 2897 additions and 1194 deletions

12
.env
View File

@ -1,5 +1,5 @@
# 标题
VITE_APP_TITLE=芋道管理系统
VITE_APP_TITLE=项目管理系统
# 项目本地运行端口号
VITE_PORT=80
@ -14,12 +14,12 @@ VITE_APP_TENANT_ENABLE=true
VITE_APP_CAPTCHA_ENABLE=true
# 文档地址的开关
VITE_APP_DOCALERT_ENABLE=true
VITE_APP_DOCALERT_ENABLE=false
# 百度统计
VITE_APP_BAIDU_CODE = a1ff8825baa73c3a78eb96aa40325abc
VITE_APP_BAIDU_CODE=a1ff8825baa73c3a78eb96aa40325abc
# 默认账户密码
VITE_APP_DEFAULT_LOGIN_TENANT = 芋道源码
VITE_APP_DEFAULT_LOGIN_USERNAME = admin
VITE_APP_DEFAULT_LOGIN_PASSWORD = admin123
VITE_APP_DEFAULT_LOGIN_TENANT=设计院
VITE_APP_DEFAULT_LOGIN_USERNAME=admin
VITE_APP_DEFAULT_LOGIN_PASSWORD=admin123

View File

@ -28,4 +28,4 @@ VITE_BASE_PATH=/
VITE_MALL_H5_DOMAIN='http://localhost:3000'
# 验证码的开关
VITE_APP_CAPTCHA_ENABLE=false
VITE_APP_CAPTCHA_ENABLE=true

248
README.md
View File

@ -1,39 +1,3 @@
**严肃声明:现在、未来都不会有商业版本,所有代码全部开源!**
**「我喜欢写代码,乐此不疲」**
**「我喜欢做开源,以此为乐」**
我 🐶 在上海艰苦奋斗,早中晚在 top3 大厂认真搬砖,夜里为开源做贡献。
如果这个项目让你有所收获,记得 Star 关注哦,这对我是非常不错的鼓励与支持。
## 🐶 新手必读
* nodejs > 16.18.0 && pnpm > 8.6.0 (强制使用pnpm)
* 演示地址【Vue3 + element-plus】<http://dashboard-vue3.yudao.iocoder.cn>
* 演示地址【Vue3 + vben(ant-design-vue)】:<http://dashboard-vben.yudao.iocoder.cn>
* 演示地址【Vue2 + element-ui】<http://dashboard.yudao.iocoder.cn>
* 启动文档:<https://doc.iocoder.cn/quick-start/>
* 视频教程:<https://doc.iocoder.cn/video/>
## 🐯 平台简介
**芋道**,以开发者为中心,打造中国第一流的快速开发平台,全部开源,个人与企业可 100% 免费使用。
* 采用 [vue-element-plus-admin](https://gitee.com/kailong110120130/vue-element-plus-admin) 实现
* 改换 saas自动引入等功能
* 使用 Element Plus 免费开源的中后台模版,具备如下特性:
![首页](public/home.png)
* **最新技术栈**:使用 Vue3、Vite4 等前端前沿技术开发
* **TypeScript**: 应用程序级 JavaScript 的语言
* **主题**: 可配置的主题
* **国际化**:内置完善的国际化方案
* **权限**:内置完善的动态路由权限生成方案
* **组件**:二次封装了多个常用的组件
* **示例**:内置丰富的示例
## 技术栈
| 框架 | 说明 | 版本 |
@ -65,202 +29,22 @@
| ESLint | 脚本代码检查 |
| DotENV | env 文件高亮 |
## 🔥 后端架构
支持 Spring Boot、Spring Cloud 两种架构:
### 提交规范
1. 从分支创建新功能分支命名规范feature-{功能名称}-{时间}-作者
2. 提交规范: 根据功能类型划分以下几种提交类型
- [fix] 修复bug/优化xxxx
- [feat] 新增xxxx功能
- [perf] 性能优化
- [hotfix] 紧急修复bug
3. 功能自行测试,通过后提交代码全部完成后提交PR
① Spring Boot 单体架构:<https://doc.iocoder.cn>
### 快速开始
# 安装 pnpm提升依赖的安装速度
npm config set registry https://mirrors.huaweicloud.com/repository/npm/
npm install -g pnpm
# 安装依赖
pnpm install
![架构图](/.image/common/ruoyi-vue-pro-architecture.png)
② Spring Cloud 微服务架构:<https://cloud.iocoder.cn>
![架构图](/.image/common/yudao-cloud-architecture.png)
## 内置功能
系统内置多种多种业务功能,可以用于快速你的业务系统:
* 系统功能
* 基础设施
* 工作流程
* 支付系统
* 会员中心
* 数据报表
* 商城系统
* 微信公众号
* ERP 系统
* CRM 系统
### 系统功能
| | 功能 | 描述 |
|-----|-------|---------------------------------|
| | 用户管理 | 用户是系统操作者,该功能主要完成系统用户配置 |
| ⭐️ | 在线用户 | 当前系统中活跃用户状态监控,支持手动踢下线 |
| | 角色管理 | 角色菜单权限分配、设置角色按机构进行数据范围权限划分 |
| | 菜单管理 | 配置系统菜单、操作权限、按钮权限标识等,本地缓存提供性能 |
| | 部门管理 | 配置系统组织机构(公司、部门、小组),树结构展现支持数据权限 |
| | 岗位管理 | 配置系统用户所属担任职务 |
| 🚀 | 租户管理 | 配置系统租户,支持 SaaS 场景下的多租户功能 |
| 🚀 | 租户套餐 | 配置租户套餐,自定每个租户的菜单、操作、按钮的权限 |
| | 字典管理 | 对系统中经常使用的一些较为固定的数据进行维护 |
| 🚀 | 短信管理 | 短信渠道、短息模板、短信日志,对接阿里云、腾讯云等主流短信平台 |
| 🚀 | 邮件管理 | 邮箱账号、邮件模版、邮件发送日志,支持所有邮件平台 |
| 🚀 | 站内信 | 系统内的消息通知,提供站内信模版、站内信消息 |
| 🚀 | 操作日志 | 系统正常操作日志记录和查询,集成 Swagger 生成日志内容 |
| ⭐️ | 登录日志 | 系统登录日志记录查询,包含登录异常 |
| 🚀 | 错误码管理 | 系统所有错误码的管理,可在线修改错误提示,无需重启服务 |
| | 通知公告 | 系统通知公告信息发布维护 |
| 🚀 | 敏感词 | 配置系统敏感词,支持标签分组 |
| 🚀 | 应用管理 | 管理 SSO 单点登录的应用,支持多种 OAuth2 授权方式 |
| 🚀 | 地区管理 | 展示省份、城市、区镇等城市信息,支持 IP 对应城市 |
![功能图](/.image/common/system-feature.png)
### 工作流程
| | 功能 | 描述 |
|----|-------|-----------------------------------------|
| 🚀 | 流程模型 | 配置工作流的流程模型,支持 BPMN 和仿钉钉/飞书设计器 |
| 🚀 | 流程表单 | 拖动表单元素生成相应的工作流表单,覆盖 Element UI 所有的表单组件 |
| 🚀 | 用户分组 | 自定义用户分组,可用于工作流的审批分组 |
| 🚀 | 我的流程 | 查看我发起的工作流程,支持新建、取消流程等操作,高亮流程图、审批时间线 |
| 🚀 | 待办任务 | 查看自己【未】审批的工作任务,支持通过、不通过、转派、委派、退回、加减签等操作 |
| 🚀 | 已办任务 | 查看自己【已】审批的工作任务,支持流程预测,展示未来审批人信息 |
| 🚀 | OA 请假 | 作为业务自定义接入工作流的使用示例,只需创建请求对应的工作流程,即可进行审批 |
![功能图](/.image/common/bpm-feature.png)
| BPMN 设计器 | 钉钉/飞书设计器 |
|------------------------------|--------------------------------|
| ![](/.image/工作流设计器-bpmn.jpg) | ![](/.image/工作流设计器-simple.jpg) |
### 支付系统
| | 功能 | 描述 |
|-----|------|---------------------------|
| 🚀 | 商户信息 | 管理商户信息,支持 Saas 场景下的多商户功能 |
| 🚀 | 应用信息 | 配置商户的应用信息,对接支付宝、微信等多个支付渠道 |
| 🚀 | 支付订单 | 查看用户发起的支付宝、微信等的【支付】订单 |
| 🚀 | 退款订单 | 查看用户发起的支付宝、微信等的【退款】订单 |
ps核心功能已经实现正在对接微信小程序中...
### 基础设施
| | 功能 | 描述 |
|----|----------|----------------------------------------------|
| 🚀 | 代码生成 | 前后端代码的生成Java、Vue、SQL、单元测试支持 CRUD 下载 |
| 🚀 | 系统接口 | 基于 Swagger 自动生成相关的 RESTful API 接口文档 |
| 🚀 | 数据库文档 | 基于 Screw 自动生成数据库文档,支持导出 Word、HTML、MD 格式 |
| | 表单构建 | 拖动表单元素生成相应的 HTML 代码,支持导出 JSON、Vue 文件 |
| 🚀 | 配置管理 | 对系统动态配置常用参数,支持 SpringBoot 加载 |
| ⭐️ | 定时任务 | 在线(添加、修改、删除)任务调度包含执行结果日志 |
| 🚀 | 文件服务 | 支持将文件存储到 S3MinIO、阿里云、腾讯云、七牛云、本地、FTP、数据库等 |
| 🚀 | API 日志 | 包括 RESTful API 访问日志、异常日志两部分,方便排查 API 相关的问题 |
| | MySQL 监控 | 监视当前系统数据库连接池状态可进行分析SQL找出系统性能瓶颈 |
| | Redis 监控 | 监控 Redis 数据库的使用情况,使用的 Redis Key 管理 |
| 🚀 | 消息队列 | 基于 Redis 实现消息队列Stream 提供集群消费Pub/Sub 提供广播消费 |
| 🚀 | Java 监控 | 基于 Spring Boot Admin 实现 Java 应用的监控 |
| 🚀 | 链路追踪 | 接入 SkyWalking 组件,实现链路追踪 |
| 🚀 | 日志中心 | 接入 SkyWalking 组件,实现日志中心 |
| 🚀 | 服务保障 | 基于 Redis 实现分布式锁、幂等、限流功能,满足高并发场景 |
| 🚀 | 日志服务 | 轻量级日志中心,查看远程服务器的日志 |
| 🚀 | 单元测试 | 基于 JUnit + Mockito 实现单元测试,保证功能的正确性、代码的质量等 |
![功能图](/.image/common/infra-feature.png)
### 数据报表
| | 功能 | 描述 |
|-----|-------|--------------------|
| 🚀 | 报表设计器 | 支持数据报表、图形报表、打印设计等 |
| 🚀 | 大屏设计器 | 拖拽生成数据大屏,内置几十种图表组件 |
### 微信公众号
| | 功能 | 描述 |
|-----|--------|-------------------------------|
| 🚀 | 账号管理 | 配置接入的微信公众号,可支持多个公众号 |
| 🚀 | 数据统计 | 统计公众号的用户增减、累计用户、消息概况、接口分析等数据 |
| 🚀 | 粉丝管理 | 查看已关注、取关的粉丝列表,可对粉丝进行同步、打标签等操作 |
| 🚀 | 消息管理 | 查看粉丝发送的消息列表,可主动回复粉丝消息 |
| 🚀 | 自动回复 | 自动回复粉丝发送的消息,支持关注回复、消息回复、关键字回复 |
| 🚀 | 标签管理 | 对公众号的标签进行创建、查询、修改、删除等操作 |
| 🚀 | 菜单管理 | 自定义公众号的菜单,也可以从公众号同步菜单 |
| 🚀 | 素材管理 | 管理公众号的图片、语音、视频等素材,支持在线播放语音、视频 |
| 🚀 | 图文草稿箱 | 新增常用的图文素材到草稿箱,可发布到公众号 |
| 🚀 | 图文发表记录 | 查看已发布成功的图文素材,支持删除操作 |
### 商城系统
演示地址:<https://doc.iocoder.cn/mall-preview/>
![功能图](/.image/common/mall-feature.png)
![功能图](/.image/common/mall-preview.png)
### ERP 系统
演示地址:<https://doc.iocoder.cn/erp-preview/>
![功能图](/.image/common/erp-feature.png)
### CRM 系统
演示地址:<https://doc.iocoder.cn/crm-preview/>
![功能图](/.image/common/crm-feature.png)
## 🐷 演示图
### 系统功能
| 模块 | biu | biu | biu |
|----------|-----------------------------|---------------------------|--------------------------|
| 登录 & 首页 | ![登录](/.image/登录.jpg) | ![首页](/.image/首页.jpg) | ![个人中心](/.image/个人中心.jpg) |
| 用户 & 应用 | ![用户管理](/.image/用户管理.jpg) | ![令牌管理](/.image/令牌管理.jpg) | ![应用管理](/.image/应用管理.jpg) |
| 租户 & 套餐 | ![租户管理](/.image/租户管理.jpg) | ![租户套餐](/.image/租户套餐.png) | - |
| 部门 & 岗位 | ![部门管理](/.image/部门管理.jpg) | ![岗位管理](/.image/岗位管理.jpg) | - |
| 菜单 & 角色 | ![菜单管理](/.image/菜单管理.jpg) | ![角色管理](/.image/角色管理.jpg) | - |
| 审计日志 | ![操作日志](/.image/操作日志.jpg) | ![登录日志](/.image/登录日志.jpg) | - |
| 短信 | ![短信渠道](/.image/短信渠道.jpg) | ![短信模板](/.image/短信模板.jpg) | ![短信日志](/.image/短信日志.jpg) |
| 字典 & 敏感词 | ![字典类型](/.image/字典类型.jpg) | ![字典数据](/.image/字典数据.jpg) | ![敏感词](/.image/敏感词.jpg) |
| 错误码 & 通知 | ![错误码管理](/.image/错误码管理.jpg) | ![通知公告](/.image/通知公告.jpg) | - |
### 工作流程
| 模块 | biu | biu | biu |
|---------|---------------------------------|---------------------------------|---------------------------------|
| 流程模型 | ![流程模型-列表](/.image/流程模型-列表.jpg) | ![流程模型-设计](/.image/流程模型-设计.jpg) | ![流程模型-定义](/.image/流程模型-定义.jpg) |
| 表单 & 分组 | ![流程表单](/.image/流程表单.jpg) | ![用户分组](/.image/用户分组.jpg) | - |
| 我的流程 | ![我的流程-列表](/.image/我的流程-列表.jpg) | ![我的流程-发起](/.image/我的流程-发起.jpg) | ![我的流程-详情](/.image/我的流程-详情.jpg) |
| 待办 & 已办 | ![任务列表-审批](/.image/任务列表-审批.jpg) | ![任务列表-待办](/.image/任务列表-待办.jpg) | ![任务列表-已办](/.image/任务列表-已办.jpg) |
| OA 请假 | ![OA请假-列表](/.image/OA请假-列表.jpg) | ![OA请假-发起](/.image/OA请假-发起.jpg) | ![OA请假-详情](/.image/OA请假-详情.jpg) |
### 基础设施
| 模块 | biu | biu | biu |
|---------------|-------------------------------|-----------------------------|---------------------------|
| 代码生成 | ![代码生成](/.image/代码生成.jpg) | ![生成效果](/.image/生成效果.jpg) | - |
| 文档 | ![系统接口](/.image/系统接口.jpg) | ![数据库文档](/.image/数据库文档.jpg) | - |
| 文件 & 配置 | ![文件配置](/.image/文件配置.jpg) | ![文件管理](/.image/文件管理2.jpg) | ![配置管理](/.image/配置管理.jpg) |
| 定时任务 | ![定时任务](/.image/定时任务.jpg) | ![任务日志](/.image/任务日志.jpg) | - |
| API 日志 | ![访问日志](/.image/访问日志.jpg) | ![错误日志](/.image/错误日志.jpg) | - |
| MySQL & Redis | ![MySQL](/.image/MySQL.jpg) | ![Redis](/.image/Redis.jpg) | - |
| 监控平台 | ![Java监控](/.image/Java监控.jpg) | ![链路追踪](/.image/链路追踪.jpg) | ![日志中心](/.image/日志中心.jpg) |
### 支付系统
| 模块 | biu | biu | biu |
|---------|---------------------------|---------------------------------|---------------------------------|
| 商家 & 应用 | ![商户信息](/.image/商户信息.jpg) | ![应用信息-列表](/.image/应用信息-列表.jpg) | ![应用信息-编辑](/.image/应用信息-编辑.jpg) |
| 支付 & 退款 | ![支付订单](/.image/支付订单.jpg) | ![退款订单](/.image/退款订单.jpg) | --- |
### 数据报表
| 模块 | biu | biu | biu |
|-------|---------------------------------|---------------------------------|---------------------------------------|
| 报表设计器 | ![数据报表](/.image/报表设计器-数据报表.jpg) | ![图形报表](/.image/报表设计器-图形报表.jpg) | ![报表设计器-打印设计](/.image/报表设计器-打印设计.jpg) |
| 大屏设计器 | ![大屏列表](/.image/大屏设计器-列表.jpg) | ![大屏预览](/.image/大屏设计器-预览.jpg) | ![大屏编辑](/.image/大屏设计器-编辑.jpg) |
# 启动服务
npm run dev

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 120 KiB

View File

@ -0,0 +1,48 @@
import request from '@/config/axios'
// 客户公司 VO
export interface CustomerCompanyVO {
id: number // 主键
contacts: string // 联系人
name: string // 公司名称
address: string // 地址
phone: string // 电话
}
// 客户公司 API
export const CustomerCompanyApi = {
// 查询客户公司分页
getCustomerCompanyPage: async (params: any) => {
return await request.get({ url: `/cms/customer-company/page`, params })
},
// 查询客户公司详情
getCustomerCompany: async (id: number) => {
return await request.get({ url: `/cms/customer-company/get?id=` + id })
},
// 新增客户公司
createCustomerCompany: async (data: CustomerCompanyVO) => {
return await request.post({ url: `/cms/customer-company/create`, data })
},
// 修改客户公司
updateCustomerCompany: async (data: CustomerCompanyVO) => {
return await request.put({ url: `/cms/customer-company/update`, data })
},
// 删除客户公司
deleteCustomerCompany: async (id: number) => {
return await request.delete({ url: `/cms/customer-company/delete?id=` + id })
},
// 导出客户公司 Excel
exportCustomerCompany: async (params) => {
return await request.download({ url: `/cms/customer-company/export-excel`, params })
},
// 获取客户公司列表
getCustomerCompanyList: async () => {
return await request.get({ url: `/cms/customer-company/list`})
},
}

View File

@ -0,0 +1,43 @@
import request from '@/config/axios'
// 文件目录 VO
export interface CategoryVO {
id: number // 主键
code: string // 编号
name: string // 文件夹名称
parentId: number // 父id
description: string // 描述
}
// 文件目录 API
export const CategoryApi = {
// 查询文件目录列表
getCategoryList: async (params?: any) => {
return await request.get({ url: `/infra/category/list`, params })
},
// 查询文件目录详情
getCategory: async (id: number) => {
return await request.get({ url: `/infra/category/get?id=` + id })
},
// 新增文件目录
createCategory: async (data: CategoryVO) => {
return await request.post({ url: `/infra/category/create`, data })
},
// 修改文件目录
updateCategory: async (data: CategoryVO) => {
return await request.put({ url: `/infra/category/update`, data })
},
// 删除文件目录
deleteCategory: async (id: number) => {
return await request.delete({ url: `/infra/category/delete?id=` + id })
},
// 导出文件目录 Excel
exportCategory: async (params?: any) => {
return await request.download({ url: `/infra/category/export-excel`, params })
}
}

View File

@ -4,6 +4,7 @@ export interface FilePageReqVO extends PageParam {
path?: string
type?: string
createTime?: Date[]
categoryId?: number
}
// 文件预签名地址 Response VO
@ -16,6 +17,12 @@ export interface FilePresignedUrlRespVO {
url: string
}
export interface UploadFileParams {
path?: string
categoryId?: string
categoryPath?: string
}
// 查询文件列表
export const getFilePage = (params: FilePageReqVO) => {
return request.get({ url: '/infra/file/page', params })
@ -43,3 +50,8 @@ export const createFile = (data: any) => {
export const updateFile = (data: any) => {
return request.upload({ url: '/infra/file/upload', data })
}
// 上传文件
export const updateFileEx = (data: any, params?: any) => {
return request.upload({ url: '/infra/file/uploadEx', params, data })
}

View File

@ -0,0 +1,66 @@
import request from '@/config/axios'
// 项目基本信息 VO
export interface ProjectVO {
id: number // 主键
name: string // 名称
code: string // 项目编号
customerUser: string // 客户联系人
drawingCompany: string // 出图公司
trackingDepId: number // 跟踪部门id
endTime: Date // 落地时间
possibility: string // 落地可能性
funnelExpectation: string // 漏斗预期
entrustMethod: string // 委托方式
reason: string // 未落地原因
processStatus: string // 审批状态
trackingCode: string // 跟踪编号
type: string // 类型
customerCompanyId: number // 客户公司id
projectManagerId: number // 项目经理id
startTime: Date // 开始时间
situation: string // 跟踪情况
reviewFileUrl: string // 评审附件
confirmation: boolean // 是否落地
winFileUrl: string // 中标附件
contractAmount: string // 预计合同金额
customerPhone: string // 客户电话
provinceId: number // 省份id
cityId: number // 城市id
trackingDeptName: string // 跟踪部门名称
projectManagerName: string // 项目经理名称
customerCompanyName: string // 客户公司名称
}
// 项目基本信息 API
export const ProjectApi = {
// 查询项目基本信息分页
getProjectPage: async (params: any) => {
return await request.get({ url: `/pms/project/page`, params })
},
// 查询项目基本信息详情
getProject: async (id: number) => {
return await request.get({ url: `/pms/project/get?id=` + id })
},
// 新增项目基本信息
createProject: async (data: ProjectVO) => {
return await request.post({ url: `/pms/project/create`, data })
},
// 修改项目基本信息
updateProject: async (data: ProjectVO) => {
return await request.put({ url: `/pms/project/update`, data })
},
// 删除项目基本信息
deleteProject: async (id: number) => {
return await request.delete({ url: `/pms/project/delete?id=` + id })
},
// 导出项目基本信息 Excel
exportProject: async (params) => {
return await request.download({ url: `/pms/project/export-excel`, params })
},
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 98 KiB

View File

@ -7,8 +7,9 @@
:auto-upload="autoUpload"
:before-upload="beforeUpload"
:disabled="disabled"
:delete-disabled="delDisabled"
:drag="drag"
:http-request="httpRequest"
:http-request="handleRequest"
:limit="props.limit"
:multiple="props.limit > 1"
:on-error="excelUploadError"
@ -46,7 +47,7 @@
下载
</el-link>
</div>
<div class="ml-10px">
<div class="ml-10px" v-if="!delDisabled">
<el-button link type="danger" @click="handleRemove(row.file)"> 删除</el-button>
</div>
</div>
@ -68,9 +69,15 @@
</template>
<script lang="ts" setup>
import { propTypes } from '@/utils/propTypes'
import type { UploadInstance, UploadProps, UploadRawFile, UploadUserFile } from 'element-plus'
import type {
UploadInstance,
UploadProps,
UploadRawFile,
UploadRequestOptions,
UploadUserFile
} from 'element-plus'
import { isString } from '@/utils/is'
import { useUpload } from '@/components/UploadFile/src/useUpload'
import { useUpload, ResponseFile } from '@/components/UploadFile/src/useUpload'
import { UploadFile } from 'element-plus/es/components/upload/src/upload'
defineOptions({ name: 'UploadFile' })
@ -79,14 +86,16 @@ const message = useMessage() // 消息弹窗
const emit = defineEmits(['update:modelValue'])
const props = defineProps({
modelValue: propTypes.oneOfType<string | string[]>([String, Array<String>]).isRequired,
modelValue: propTypes.oneOfType<ResponseFile[]>([]).isRequired,
fileType: propTypes.array.def(['doc', 'xls', 'ppt', 'txt', 'pdf']), // 文件类型, 例如['png', 'jpg', 'jpeg']
fileSize: propTypes.number.def(5), // 大小限制(MB)
limit: propTypes.number.def(5), // 数量限制
autoUpload: propTypes.bool.def(true), // 自动上传
drag: propTypes.bool.def(false), // 拖拽上传
isShowTip: propTypes.bool.def(true), // 是否显示提示
disabled: propTypes.bool.def(false) // 是否禁用上传组件 ==> 非必传(默认为 false
disabled: propTypes.bool.def(false), // 是否禁用上传组件 ==> 非必传(默认为 false
delDisabled: propTypes.bool.def(false), // 是否禁用删除
categoryPath: propTypes.string.def('') // 文件目录路径
})
// ========== 上传相关 ==========
@ -97,6 +106,15 @@ const uploadNumber = ref<number>(0)
const { uploadUrl, httpRequest } = useUpload()
// 自定义上传
const handleRequest = async (f: UploadRequestOptions) => {
if (!!props.categoryPath) {
return httpRequest(f, { categoryPath: props.categoryPath })
} else {
return httpRequest(f)
}
}
// 文件上传之前判断
const beforeUpload: UploadProps['beforeUpload'] = (file: UploadRawFile) => {
if (fileList.value.length >= props.limit) {
@ -130,10 +148,12 @@ const beforeUpload: UploadProps['beforeUpload'] = (file: UploadRawFile) => {
// 文件上传成功
const handleFileSuccess: UploadProps['onSuccess'] = (res: any): void => {
message.success('上传成功')
console.log(fileList.value,res);
// 删除自身
const index = fileList.value.findIndex((item) => item.response?.data === res.data)
fileList.value.splice(index, 1)
uploadList.value.push({ name: res.data, url: res.data })
uploadList.value.push({ name: res.data.name ?? res.data.url, url: res.data.url })
if (uploadList.value.length == uploadNumber.value) {
fileList.value.push(...uploadList.value)
uploadList.value = []
@ -164,35 +184,24 @@ const handlePreview: UploadProps['onPreview'] = (uploadFile) => {
// 监听模型绑定值变动
watch(
() => props.modelValue,
(val: string | string[]) => {
(val: ResponseFile[]) => {
if (!val) {
fileList.value = [] // fix处理掉缓存表单重置后上传组件的内容并没有重置
return
}
fileList.value = [] // 保障数据为空
// 情况1字符串
if (isString(val)) {
fileList.value.push(
...val.split(',').map((url) => ({ name: url.substring(url.lastIndexOf('/') + 1), url }))
)
return
}
// 情况2数组
fileList.value.push(
...(val as string[]).map((url) => ({ name: url.substring(url.lastIndexOf('/') + 1), url }))
)
fileList.value = val.map(({ name, url }) => ({
name: name ?? url.substring(url.lastIndexOf('/') + 1),
url
}))
},
{ immediate: true, deep: true }
)
// 发送文件链接列表更新
const emitUpdateModelValue = () => {
// 情况1数组结果
let result: string | string[] = fileList.value.map((file) => file.url!)
// 情况2逗号分隔的字符串
if (props.limit === 1 || isString(props.modelValue)) {
result = result.join(',')
}
const result = fileList.value.map(({ name, url }) => ({ name, url }))
console.log(result);
emit('update:modelValue', result)
}
</script>

View File

@ -8,7 +8,7 @@
:class="['upload', drag ? 'no-border' : '']"
:disabled="disabled"
:drag="drag"
:http-request="httpRequest"
:http-request="handleRequest"
:multiple="false"
:on-error="uploadError"
:on-success="uploadSuccess"
@ -47,7 +47,7 @@
</template>
<script lang="ts" setup>
import type { UploadProps } from 'element-plus'
import type { UploadProps, UploadRequestOptions } from 'element-plus'
import { generateUUID } from '@/utils'
import { propTypes } from '@/utils/propTypes'
@ -79,7 +79,8 @@ const props = defineProps({
width: propTypes.string.def('150px'), // 组件宽度 ==> 非必传(默认为 150px
borderradius: propTypes.string.def('8px'), // 组件边框圆角 ==> 非必传(默认为 8px
showDelete: propTypes.bool.def(true), // 是否显示删除按钮
showBtnText: propTypes.bool.def(true) // 是否显示按钮文字
showBtnText: propTypes.bool.def(true), // 是否显示按钮文字
categoryPath: propTypes.string.def('') // 文件目录路径
})
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
@ -101,6 +102,15 @@ const deleteImg = () => {
const { uploadUrl, httpRequest } = useUpload()
// 自定义上传
const handleRequest = async (f: UploadRequestOptions) => {
if (!!props.categoryPath) {
return httpRequest(f, { categoryPath: props.categoryPath })
} else {
return httpRequest(f)
}
}
const editImg = () => {
const dom = document.querySelector(`#${uuid.value} .el-upload__input`)
dom && dom.dispatchEvent(new MouseEvent('click'))
@ -118,7 +128,7 @@ const beforeUpload: UploadProps['beforeUpload'] = (rawFile) => {
// 图片上传成功提示
const uploadSuccess: UploadProps['onSuccess'] = (res: any): void => {
message.success('上传成功')
emit('update:modelValue', res.data)
emit('update:modelValue', res.data.url)
}
// 图片上传错误提示

View File

@ -8,7 +8,7 @@
:class="['upload', drag ? 'no-border' : '']"
:disabled="disabled"
:drag="drag"
:http-request="httpRequest"
:http-request="handleRequest"
:limit="limit"
:multiple="true"
:on-error="uploadError"
@ -42,12 +42,12 @@
</div>
</template>
<script lang="ts" setup>
import type { UploadFile, UploadProps, UploadUserFile } from 'element-plus'
import type { UploadFile, UploadProps, UploadRequestOptions, UploadUserFile } from 'element-plus'
import { ElNotification } from 'element-plus'
import { createImageViewer } from '@/components/ImageViewer'
import { propTypes } from '@/utils/propTypes'
import { useUpload } from '@/components/UploadFile/src/useUpload'
import { useUpload, ResponseFile } from '@/components/UploadFile/src/useUpload'
defineOptions({ name: 'UploadImgs' })
@ -73,7 +73,7 @@ type FileTypes =
| 'image/x-icon'
const props = defineProps({
modelValue: propTypes.oneOfType<string | string[]>([String, Array<String>]).isRequired,
modelValue: propTypes.oneOfType<ResponseFile[]>([]).isRequired,
drag: propTypes.bool.def(true), // 是否支持拖拽上传 ==> 非必传(默认为 true
disabled: propTypes.bool.def(false), // 是否禁用上传组件 ==> 非必传(默认为 false
limit: propTypes.number.def(5), // 最大图片上传数 ==> 非必传(默认为 5张
@ -81,7 +81,8 @@ const props = defineProps({
fileType: propTypes.array.def(['image/jpeg', 'image/png', 'image/gif']), // 图片类型限制 ==> 非必传(默认为 ["image/jpeg", "image/png", "image/gif"]
height: propTypes.string.def('150px'), // 组件高度 ==> 非必传(默认为 150px
width: propTypes.string.def('150px'), // 组件宽度 ==> 非必传(默认为 150px
borderradius: propTypes.string.def('8px') // 组件边框圆角 ==> 非必传(默认为 8px
borderradius: propTypes.string.def('8px'), // 组件边框圆角 ==> 非必传(默认为 8px
categoryPath: propTypes.string.def('') // 文件目录路径
})
const { uploadUrl, httpRequest } = useUpload()
@ -89,6 +90,16 @@ const { uploadUrl, httpRequest } = useUpload()
const fileList = ref<UploadUserFile[]>([])
const uploadNumber = ref<number>(0)
const uploadList = ref<UploadUserFile[]>([])
// 自定义上传
const handleRequest = async (f: UploadRequestOptions) => {
if (!!props.categoryPath) {
return httpRequest(f, { categoryPath: props.categoryPath })
} else {
return httpRequest(f)
}
}
/**
* @description 文件上传之前判断
* @param rawFile 上传的文件
@ -114,7 +125,7 @@ const beforeUpload: UploadProps['beforeUpload'] = (rawFile) => {
// 图片上传成功
interface UploadEmits {
(e: 'update:modelValue', value: string[]): void
(e: 'update:modelValue', value: ResponseFile[]): void
}
const emit = defineEmits<UploadEmits>()
@ -123,7 +134,7 @@ const uploadSuccess: UploadProps['onSuccess'] = (res: any): void => {
// 删除自身
const index = fileList.value.findIndex((item) => item.response?.data === res.data)
fileList.value.splice(index, 1)
uploadList.value.push({ name: res.data, url: res.data })
uploadList.value.push({ name: res.data.name ?? res.data.url, url: res.data.url })
if (uploadList.value.length == uploadNumber.value) {
fileList.value.push(...uploadList.value)
uploadList.value = []
@ -135,22 +146,22 @@ const uploadSuccess: UploadProps['onSuccess'] = (res: any): void => {
// 监听模型绑定值变动
watch(
() => props.modelValue,
(val: string | string[]) => {
(val: ResponseFile[]) => {
if (!val) {
fileList.value = [] // fix处理掉缓存表单重置后上传组件的内容并没有重置
return
}
fileList.value = [] // 保障数据为空
fileList.value.push(
...(val as string[]).map((url) => ({ name: url.substring(url.lastIndexOf('/') + 1), url }))
)
fileList.value = val.map(({ name, url }) => ({
name: name ?? url.substring(url.lastIndexOf('/') + 1),
url
}))
},
{ immediate: true, deep: true }
)
// 发送图片链接列表更新
const emitUpdateModelValue = () => {
let result: string[] = fileList.value.map((file) => file.url!)
let result: ResponseFile[] = fileList.value.map(({ name, url }) => ({ name, url: url! }))
emit('update:modelValue', result)
}
// 删除图片
@ -160,7 +171,7 @@ const handleRemove = (uploadFile: UploadFile) => {
)
emit(
'update:modelValue',
fileList.value.map((file) => file.url!)
fileList.value.map(({ name, url }) => ({ name, url: url! }))
)
}

View File

@ -3,6 +3,11 @@ import CryptoJS from 'crypto-js'
import { UploadRawFile, UploadRequestOptions } from 'element-plus/es/components/upload/src/upload'
import axios from 'axios'
export type ResponseFile = {
name: string
url: string
}
/**
* 获得上传 URL
*/
@ -16,7 +21,7 @@ export const useUpload = () => {
// 是否使用前端直连上传
const isClientUpload = UPLOAD_TYPE.CLIENT === import.meta.env.VITE_UPLOAD_TYPE
// 重写ElUpload上传方法
const httpRequest = async (options: UploadRequestOptions) => {
const httpRequest = async (options: UploadRequestOptions, params?: any) => {
// 模式一:前端上传
if (isClientUpload) {
// 1.1 生成文件名称
@ -40,7 +45,7 @@ export const useUpload = () => {
// 模式二:后端上传
// 重写 el-upload httpRequest 文件上传成功会走成功的钩子,失败走失败的钩子
return new Promise((resolve, reject) => {
FileApi.updateFile({ file: options.file })
FileApi.updateFileEx({ file: options.file }, params)
.then((res) => {
if (res.code === 0) {
resolve(res)

View File

@ -19,6 +19,6 @@ const title = computed(() => appStore.getTitle)
:class="prefixCls"
class="h-[var(--app-footer-height)] bg-[var(--app-content-bg-color)] text-center leading-[var(--app-footer-height)] text-[var(--el-text-color-placeholder)] dark:bg-[var(--el-bg-color)] overflow-hidden"
>
<span class="text-14px">Copyright ©2022-{{ title }}</span>
<span class="text-14px">Copyright ©2024-{{ title }}</span>
</div>
</template>

View File

@ -115,7 +115,7 @@ export default {
},
login: {
welcome: '欢迎使用本系统',
message: '开箱即用的中后台管理系统',
message: '路桥勘察设计院信息管理系统',
tenantname: '租户名称',
username: '用户名',
password: '密码',
@ -369,7 +369,7 @@ export default {
qrSignInFormTitle: '二维码登录',
signUpFormTitle: '注册',
forgetFormTitle: '重置密码',
signInTitle: '开箱即用的中后台管理系统',
signInTitle: '路桥勘察设计院信息管理系统',
signInDesc: '输入您的个人详细信息开始使用!',
policy: '我同意xxx隐私政策',
scanSign: `扫码后点击"确认",即可完成登录`,

View File

@ -640,6 +640,40 @@ const remainingRouter: AppRouteRecordRaw[] = [
component: () => import('@/views/iot/device/detail/index.vue')
}
]
},
{
path: '/pms',
component: Layout,
name: 'pms',
meta: {
hidden: true
},
children: [
{
path: 'project/bpm/ProjectBpmCreate',
component: () => import('@/views/pms/project/bpm/ProjectBpmCreate.vue'),
name: 'ProjectBpmCreate',
meta: {
noCache: true,
hidden: true,
canTo: true,
title: '发起项目立项',
activeMenu: 'pms/project'
}
},
{
path: 'project/bpm/ProjectBpmDetail',
component: () => import('@/views/pms/project/bpm/ProjectBpmDetail.vue'),
name: 'ProjectBpmDetail',
meta: {
noCache: true,
hidden: true,
canTo: true,
title: '查看项目立项详情',
activeMenu: 'pms/project'
}
}
]
}
]

View File

@ -228,6 +228,12 @@ export enum DICT_TYPE {
AI_WRITE_TONE = 'ai_write_tone', // AI 写作语气
AI_WRITE_LANGUAGE = 'ai_write_language', // AI 写作语言
// ============== PMS =================
PROJECT_TYPE = 'project_type', // 项目类型
ENTRUST_METHOD = 'entrust_method', // 委托方式
PROCESS_STATUS = "process_status", // 流程状态
POSSIBILITY_OF_LANDING = "possibility_of_landing", // 可能性
// ========== IOT - 物联网模块 ==========
IOT_NET_TYPE = 'iot_net_type', // IOT 联网方式
IOT_VALIDATE_TYPE = 'iot_validate_type', // IOT 数据校验级别

View File

@ -1,6 +1,6 @@
<template>
<div>
<el-card shadow="never">
<el-card shadow="never" class="mb-15px">
<el-skeleton :loading="loading" animated>
<el-row :gutter="16" justify="space-between">
<el-col :xl="12" :lg="12" :md="12" :sm="24" :xs="24">
@ -56,130 +56,139 @@
</el-card>
</div>
<el-row class="mt-8px" :gutter="8" justify="space-between">
<el-col :xl="16" :lg="16" :md="24" :sm="24" :xs="24" class="mb-8px">
<el-card shadow="never">
<template #header>
<div class="h-3 flex justify-between">
<span>{{ t('workplace.project') }}</span>
<el-link
type="primary"
:underline="false"
href="https://github.com/yudaocode"
target="_blank"
>
{{ t('action.more') }}
</el-link>
</div>
</template>
<el-skeleton :loading="loading" animated>
<el-row>
<el-col
v-for="(item, index) in projects"
:key="`card-${index}`"
:xl="8"
:lg="8"
:md="8"
:sm="24"
:xs="24"
>
<el-card shadow="hover" class="mr-5px mt-5px">
<div class="flex items-center">
<Icon :icon="item.icon" :size="25" class="mr-8px" />
<span class="text-16px">{{ item.name }}</span>
</div>
<div class="mt-12px text-9px text-gray-400">{{ t(item.message) }}</div>
<div class="mt-12px flex justify-between text-12px text-gray-400">
<span>{{ item.personal }}</span>
<span>{{ formatTime(item.time, 'yyyy-MM-dd') }}</span>
</div>
</el-card>
</el-col>
</el-row>
</el-skeleton>
</el-card>
<el-card shadow="never" class="mt-8px">
<el-skeleton :loading="loading" animated>
<el-row :gutter="20" justify="space-between">
<el-col :xl="10" :lg="10" :md="24" :sm="24" :xs="24">
<el-card shadow="hover" class="mb-8px">
<el-skeleton :loading="loading" animated>
<Echart :options="pieOptionsData" :height="280" />
</el-skeleton>
</el-card>
</el-col>
<el-col :xl="14" :lg="14" :md="24" :sm="24" :xs="24">
<el-card shadow="hover" class="mb-8px">
<el-skeleton :loading="loading" animated>
<Echart :options="barOptionsData" :height="280" />
</el-skeleton>
</el-card>
</el-col>
</el-row>
</el-skeleton>
</el-card>
</el-col>
<el-col :xl="8" :lg="8" :md="24" :sm="24" :xs="24" class="mb-8px">
<el-card shadow="never">
<template #header>
<div class="h-3 flex justify-between">
<span>{{ t('workplace.shortcutOperation') }}</span>
</div>
</template>
<el-skeleton :loading="loading" animated>
<el-row>
<el-col v-for="item in shortcut" :key="`team-${item.name}`" :span="8" class="mb-8px">
<div class="flex items-center">
<Icon :icon="item.icon" class="mr-8px" />
<el-link type="default" :underline="false" @click="setWatermark(item.name)">
{{ item.name }}
</el-link>
<div>
<el-card shadow="never" class="mb-15px">
<el-skeleton :loading="loading" animated>
<div class="title1">经营总览</div>
<el-row :gutter="16" justify="space-between">
<el-col :xl="8" :lg="8" :md="8" :sm="24" :xs="24">
<div class="card1">
<div class="card1-content">
<div class="title">总合同额</div>
<CountTo
class="text-20px"
:start-val="0"
:end-val="17521"
:duration="2600"
:suffix="' 万元'"
style="font-size: 24px"
/>
</div>
</el-col>
</el-row>
</el-skeleton>
</el-card>
<el-card shadow="never" class="mt-8px">
<template #header>
<div class="h-3 flex justify-between">
<span>{{ t('workplace.notice') }}</span>
<el-link type="primary" :underline="false">{{ t('action.more') }}</el-link>
</div>
</template>
<el-skeleton :loading="loading" animated>
<div v-for="(item, index) in notice" :key="`dynamics-${index}`">
<div class="flex items-center">
<el-avatar :src="avatar" :size="35" class="mr-16px">
<img src="@/assets/imgs/avatar.gif" alt="" />
</el-avatar>
<div>
<div class="text-14px">
<Highlight :keys="item.keys.map((v) => t(v))">
{{ item.type }} : {{ item.title }}
</Highlight>
</div>
<div class="mt-16px text-12px text-gray-400">
{{ formatTime(item.date, 'yyyy-MM-dd') }}
</div>
<el-divider direction="vertical" />
<div class="card1-content">
<div class="title">本月新增合同</div>
<CountTo
class="text-20px"
:start-val="0"
:end-val="1355"
:duration="2600"
:suffix="' 万元'"
style="font-size: 24px"
/>
</div>
</div>
<el-divider />
</el-col>
<el-col :xl="8" :lg="8" :md="8" :sm="24" :xs="24">
<div class="card1 bg2">
<div class="card1-content">
<div class="title">总产值</div>
<CountTo
class="text-20px"
:start-val="0"
:end-val="269715"
:duration="2600"
:suffix="' 万元'"
style="font-size: 24px"
/>
</div>
<el-divider direction="vertical" />
<div class="card1-content">
<div class="title">本月新增回款</div>
<CountTo
class="text-20px"
:start-val="0"
:end-val="973"
:duration="2600"
:suffix="' 万元'"
style="font-size: 24px"
/>
</div>
</div>
</el-col>
<el-col :xl="8" :lg="8" :md="8" :sm="24" :xs="24">
<div class="card1 bg3">
<div class="card1-content">
<div class="title">回款总额</div>
<CountTo
class="text-20px"
:start-val="0"
:end-val="186314"
:duration="2600"
:suffix="' 万元'"
style="font-size: 24px"
/>
</div>
<el-divider direction="vertical" />
<div class="card1-content">
<div class="title">本月新增汇款额</div>
<CountTo
class="text-20px"
:start-val="0"
:end-val="633"
:duration="2600"
:suffix="' 万元'"
style="font-size: 24px"
/>
</div>
</div>
</el-col>
</el-row>
</el-skeleton>
</el-card>
</div>
<div class="percent">
<div class="title2">经营占比</div>
<el-row :gutter="16" justify="space-between">
<el-col :xl="8" :lg="8" :md="8" :sm="24" :xs="24">
<el-card shadow="never" class="mb-15px">
<div class="card3-content">
<div class="title">本月新增合同占比</div>
<div class="chart">
<Echart :options="chartOptions1" height="100%" />
</div>
</div>
</el-skeleton>
</el-card>
</el-col>
</el-row>
</el-card>
</el-col>
<el-col :xl="8" :lg="8" :md="8" :sm="24" :xs="24">
<el-card shadow="never" class="mb-15px">
<div class="card3-content">
<div class="title">本月新增产值占比</div>
<div class="chart">
<Echart :options="chartOptions2" height="100%"/>
</div>
</div>
</el-card>
</el-col>
<el-col :xl="8" :lg="8" :md="8" :sm="24" :xs="24">
<el-card shadow="never" class="mb-15px">
<div class="card3-content">
<div class="title">本月新增回款占比</div>
<div class="chart">
<Echart :options="chartOptions3" height="100%" />
</div>
</div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script lang="ts" setup>
import { set } from 'lodash-es'
import { EChartsOption } from 'echarts'
import { formatTime } from '@/utils'
import { useUserStore } from '@/store/modules/user'
import { useWatermark } from '@/hooks/web/useWatermark'
import type { WorkplaceTotal, Project, Notice, Shortcut } from './types'
import { pieOptions, barOptions } from './echarts-data'
import { getPieOptions } from './echarts-data'
defineOptions({ name: 'Home' })
@ -189,7 +198,24 @@ const { setWatermark } = useWatermark()
const loading = ref(true)
const avatar = userStore.getUser.avatar
const username = userStore.getUser.nickname
const pieOptionsData = reactive<EChartsOption>(pieOptions) as EChartsOption
// chart data
const chartOptions1 = getPieOptions([
{name: '一所', value: 12},
{name: '二所', value: 55},
{name: '三所', value: 39},
]);
const chartOptions2 = getPieOptions([
{name: '一所', value: 34},
{name: '二所', value: 78},
{name: '三所', value: 22},
]);
const chartOptions3 = getPieOptions([
{name: '一所', value: 89},
{name: '二所', value: 12},
{name: '三所', value: 30},
]);
// 获取统计数
let totalSate = reactive<WorkplaceTotal>({
project: 0,
@ -206,186 +232,71 @@ const getCount = async () => {
totalSate = Object.assign(totalSate, data)
}
// 获取项目数
let projects = reactive<Project[]>([])
const getProject = async () => {
const data = [
{
name: 'ruoyi-vue-pro',
icon: 'akar-icons:github-fill',
message: 'https://github.com/YunaiV/ruoyi-vue-pro',
personal: 'Spring Boot 单体架构',
time: new Date()
},
{
name: 'yudao-ui-admin-vue3',
icon: 'logos:vue',
message: 'https://github.com/yudaocode/yudao-ui-admin-vue3',
personal: 'Vue3 + element-plus',
time: new Date()
},
{
name: 'yudao-ui-admin-vben',
icon: 'logos:vue',
message: 'https://github.com/yudaocode/yudao-ui-admin-vben',
personal: 'Vue3 + vben(antd)',
time: new Date()
},
{
name: 'yudao-cloud',
icon: 'akar-icons:github',
message: 'https://github.com/YunaiV/yudao-cloud',
personal: 'Spring Cloud 微服务架构',
time: new Date()
},
{
name: 'yudao-ui-mall-uniapp',
icon: 'logos:vue',
message: 'https://github.com/yudaocode/yudao-ui-admin-uniapp',
personal: 'Vue3 + uniapp',
time: new Date()
},
{
name: 'yudao-ui-admin-vue2',
icon: 'logos:vue',
message: 'https://github.com/yudaocode/yudao-ui-admin-vue2',
personal: 'Vue2 + element-ui',
time: new Date()
}
]
projects = Object.assign(projects, data)
}
// 获取通知公告
let notice = reactive<Notice[]>([])
const getNotice = async () => {
const data = [
{
title: '系统支持 JDK 8/17/21Vue 2/3',
type: '通知',
keys: ['通知', '8', '17', '21', '2', '3'],
date: new Date()
},
{
title: '后端提供 Spring Boot 2.7/3.2 + Cloud 双架构',
type: '公告',
keys: ['公告', 'Boot', 'Cloud'],
date: new Date()
},
{
title: '全部开源,个人与企业可 100% 直接使用,无需授权',
type: '通知',
keys: ['通知', '无需授权'],
date: new Date()
},
{
title: '国内使用最广泛的快速开发平台,超 300+ 人贡献',
type: '公告',
keys: ['公告', '最广泛'],
date: new Date()
}
]
notice = Object.assign(notice, data)
}
// 获取快捷入口
let shortcut = reactive<Shortcut[]>([])
const getShortcut = async () => {
const data = [
{
name: 'Github',
icon: 'akar-icons:github-fill',
url: 'github.io'
},
{
name: 'Vue',
icon: 'logos:vue',
url: 'vuejs.org'
},
{
name: 'Vite',
icon: 'vscode-icons:file-type-vite',
url: 'https://vitejs.dev/'
},
{
name: 'Angular',
icon: 'logos:angular-icon',
url: 'github.io'
},
{
name: 'React',
icon: 'logos:react',
url: 'github.io'
},
{
name: 'Webpack',
icon: 'logos:webpack',
url: 'github.io'
}
]
shortcut = Object.assign(shortcut, data)
}
// 用户来源
const getUserAccessSource = async () => {
const data = [
{ value: 335, name: 'analysis.directAccess' },
{ value: 310, name: 'analysis.mailMarketing' },
{ value: 234, name: 'analysis.allianceAdvertising' },
{ value: 135, name: 'analysis.videoAdvertising' },
{ value: 1548, name: 'analysis.searchEngines' }
]
set(
pieOptionsData,
'legend.data',
data.map((v) => t(v.name))
)
pieOptionsData!.series![0].data = data.map((v) => {
return {
name: t(v.name),
value: v.value
}
})
}
const barOptionsData = reactive<EChartsOption>(barOptions) as EChartsOption
// 周活跃量
const getWeeklyUserActivity = async () => {
const data = [
{ value: 13253, name: 'analysis.monday' },
{ value: 34235, name: 'analysis.tuesday' },
{ value: 26321, name: 'analysis.wednesday' },
{ value: 12340, name: 'analysis.thursday' },
{ value: 24643, name: 'analysis.friday' },
{ value: 1322, name: 'analysis.saturday' },
{ value: 1324, name: 'analysis.sunday' }
]
set(
barOptionsData,
'xAxis.data',
data.map((v) => t(v.name))
)
set(barOptionsData, 'series', [
{
name: t('analysis.activeQuantity'),
data: data.map((v) => v.value),
type: 'bar'
}
])
}
const getAllApi = async () => {
await Promise.all([
getCount(),
getProject(),
getNotice(),
getShortcut(),
getUserAccessSource(),
getWeeklyUserActivity()
])
await Promise.all([getCount()])
loading.value = false
}
getAllApi()
</script>
<style scoped lang="scss">
.title1 {
font-size: 20px;
font-weight: bold;
margin-bottom: 15px;
}
.card1 {
height: calc((100vh - 400px) * 0.2);
display: flex;
align-items: center;
justify-content: space-between;
background: linear-gradient(0deg, rgba(229, 243, 253, 1) 0%, rgba(241, 249, 254, 1) 100%);
.card1-content {
padding-left: 13%;
width: 49%;
padding-left: 10%;
.title {
font-size: 20px;
margin-bottom: 10%;
}
}
&.bg2 {
background: linear-gradient(0deg, rgba(226, 255, 239, 1) 0%, rgba(243, 255, 243, 1) 100%);
}
&.bg3 {
background: linear-gradient(0deg, rgba(237, 235, 254, 1) 0%, rgba(246, 244, 254, 1) 100%);
}
}
.percent {
padding: 20px;
.title2 {
font-size: 20px;
font-weight: bolder;
margin-bottom: 15px;
}
.card3-content{
.title {
font-size: 18px;
font-weight: bold;
margin-bottom: 20px;
}
.chart{
height: calc((100vh - 400px) * 0.58);
}
}
}
:deep(.card1 >.el-divider--vertical) {
height: 8vh;
}
</style>

View File

@ -1,319 +1,319 @@
<template>
<el-row :class="prefixCls" :gutter="20" justify="space-between">
<el-col :lg="6" :md="12" :sm="12" :xl="6" :xs="24">
<el-card class="mb-20px" shadow="hover">
<el-skeleton :loading="loading" :rows="2" animated>
<template #default>
<div :class="`${prefixCls}__item flex justify-between`">
<div>
<div
:class="`${prefixCls}__item--icon ${prefixCls}__item--peoples p-16px inline-block rounded-6px`"
>
<Icon :size="40" icon="svg-icon:peoples" />
</div>
</div>
<div class="flex flex-col justify-between">
<div :class="`${prefixCls}__item--text text-16px text-gray-500 text-right`"
>{{ t('analysis.newUser') }}
</div>
<CountTo
:duration="2600"
:end-val="102400"
:start-val="0"
class="text-right text-20px font-700"
/>
</div>
</div>
</template>
</el-skeleton>
</el-card>
</el-col>
<!--<template>-->
<!-- <el-row :class="prefixCls" :gutter="20" justify="space-between">-->
<!-- <el-col :lg="6" :md="12" :sm="12" :xl="6" :xs="24">-->
<!-- <el-card class="mb-20px" shadow="hover">-->
<!-- <el-skeleton :loading="loading" :rows="2" animated>-->
<!-- <template #default>-->
<!-- <div :class="`${prefixCls}__item flex justify-between`">-->
<!-- <div>-->
<!-- <div-->
<!-- :class="`${prefixCls}__item&#45;&#45;icon ${prefixCls}__item&#45;&#45;peoples p-16px inline-block rounded-6px`"-->
<!-- >-->
<!-- <Icon :size="40" icon="svg-icon:peoples" />-->
<!-- </div>-->
<!-- </div>-->
<!-- <div class="flex flex-col justify-between">-->
<!-- <div :class="`${prefixCls}__item&#45;&#45;text text-16px text-gray-500 text-right`"-->
<!-- >{{ t('analysis.newUser') }}-->
<!-- </div>-->
<!-- <CountTo-->
<!-- :duration="2600"-->
<!-- :end-val="102400"-->
<!-- :start-val="0"-->
<!-- class="text-right text-20px font-700"-->
<!-- />-->
<!-- </div>-->
<!-- </div>-->
<!-- </template>-->
<!-- </el-skeleton>-->
<!-- </el-card>-->
<!-- </el-col>-->
<el-col :lg="6" :md="12" :sm="12" :xl="6" :xs="24">
<el-card class="mb-20px" shadow="hover">
<el-skeleton :loading="loading" :rows="2" animated>
<template #default>
<div :class="`${prefixCls}__item flex justify-between`">
<div>
<div
:class="`${prefixCls}__item--icon ${prefixCls}__item--message p-16px inline-block rounded-6px`"
>
<Icon :size="40" icon="svg-icon:message" />
</div>
</div>
<div class="flex flex-col justify-between">
<div :class="`${prefixCls}__item--text text-16px text-gray-500 text-right`"
>{{ t('analysis.unreadInformation') }}
</div>
<CountTo
:duration="2600"
:end-val="81212"
:start-val="0"
class="text-right text-20px font-700"
/>
</div>
</div>
</template>
</el-skeleton>
</el-card>
</el-col>
<!-- <el-col :lg="6" :md="12" :sm="12" :xl="6" :xs="24">-->
<!-- <el-card class="mb-20px" shadow="hover">-->
<!-- <el-skeleton :loading="loading" :rows="2" animated>-->
<!-- <template #default>-->
<!-- <div :class="`${prefixCls}__item flex justify-between`">-->
<!-- <div>-->
<!-- <div-->
<!-- :class="`${prefixCls}__item&#45;&#45;icon ${prefixCls}__item&#45;&#45;message p-16px inline-block rounded-6px`"-->
<!-- >-->
<!-- <Icon :size="40" icon="svg-icon:message" />-->
<!-- </div>-->
<!-- </div>-->
<!-- <div class="flex flex-col justify-between">-->
<!-- <div :class="`${prefixCls}__item&#45;&#45;text text-16px text-gray-500 text-right`"-->
<!-- >{{ t('analysis.unreadInformation') }}-->
<!-- </div>-->
<!-- <CountTo-->
<!-- :duration="2600"-->
<!-- :end-val="81212"-->
<!-- :start-val="0"-->
<!-- class="text-right text-20px font-700"-->
<!-- />-->
<!-- </div>-->
<!-- </div>-->
<!-- </template>-->
<!-- </el-skeleton>-->
<!-- </el-card>-->
<!-- </el-col>-->
<el-col :lg="6" :md="12" :sm="12" :xl="6" :xs="24">
<el-card class="mb-20px" shadow="hover">
<el-skeleton :loading="loading" :rows="2" animated>
<template #default>
<div :class="`${prefixCls}__item flex justify-between`">
<div>
<div
:class="`${prefixCls}__item--icon ${prefixCls}__item--money p-16px inline-block rounded-6px`"
>
<Icon :size="40" icon="svg-icon:money" />
</div>
</div>
<div class="flex flex-col justify-between">
<div :class="`${prefixCls}__item--text text-16px text-gray-500 text-right`"
>{{ t('analysis.transactionAmount') }}
</div>
<CountTo
:duration="2600"
:end-val="9280"
:start-val="0"
class="text-right text-20px font-700"
/>
</div>
</div>
</template>
</el-skeleton>
</el-card>
</el-col>
<!-- <el-col :lg="6" :md="12" :sm="12" :xl="6" :xs="24">-->
<!-- <el-card class="mb-20px" shadow="hover">-->
<!-- <el-skeleton :loading="loading" :rows="2" animated>-->
<!-- <template #default>-->
<!-- <div :class="`${prefixCls}__item flex justify-between`">-->
<!-- <div>-->
<!-- <div-->
<!-- :class="`${prefixCls}__item&#45;&#45;icon ${prefixCls}__item&#45;&#45;money p-16px inline-block rounded-6px`"-->
<!-- >-->
<!-- <Icon :size="40" icon="svg-icon:money" />-->
<!-- </div>-->
<!-- </div>-->
<!-- <div class="flex flex-col justify-between">-->
<!-- <div :class="`${prefixCls}__item&#45;&#45;text text-16px text-gray-500 text-right`"-->
<!-- >{{ t('analysis.transactionAmount') }}-->
<!-- </div>-->
<!-- <CountTo-->
<!-- :duration="2600"-->
<!-- :end-val="9280"-->
<!-- :start-val="0"-->
<!-- class="text-right text-20px font-700"-->
<!-- />-->
<!-- </div>-->
<!-- </div>-->
<!-- </template>-->
<!-- </el-skeleton>-->
<!-- </el-card>-->
<!-- </el-col>-->
<el-col :lg="6" :md="12" :sm="12" :xl="6" :xs="24">
<el-card class="mb-20px" shadow="hover">
<el-skeleton :loading="loading" :rows="2" animated>
<template #default>
<div :class="`${prefixCls}__item flex justify-between`">
<div>
<div
:class="`${prefixCls}__item--icon ${prefixCls}__item--shopping p-16px inline-block rounded-6px`"
>
<Icon :size="40" icon="svg-icon:shopping" />
</div>
</div>
<div class="flex flex-col justify-between">
<div :class="`${prefixCls}__item--text text-16px text-gray-500 text-right`"
>{{ t('analysis.totalShopping') }}
</div>
<CountTo
:duration="2600"
:end-val="13600"
:start-val="0"
class="text-right text-20px font-700"
/>
</div>
</div>
</template>
</el-skeleton>
</el-card>
</el-col>
</el-row>
<el-row :gutter="20" justify="space-between">
<el-col :lg="10" :md="24" :sm="24" :xl="10" :xs="24">
<el-card class="mb-20px" shadow="hover">
<el-skeleton :loading="loading" animated>
<Echart :height="300" :options="pieOptionsData" />
</el-skeleton>
</el-card>
</el-col>
<el-col :lg="14" :md="24" :sm="24" :xl="14" :xs="24">
<el-card class="mb-20px" shadow="hover">
<el-skeleton :loading="loading" animated>
<Echart :height="300" :options="barOptionsData" />
</el-skeleton>
</el-card>
</el-col>
<el-col :span="24">
<el-card class="mb-20px" shadow="hover">
<el-skeleton :loading="loading" :rows="4" animated>
<Echart :height="350" :options="lineOptionsData" />
</el-skeleton>
</el-card>
</el-col>
</el-row>
</template>
<script lang="ts" setup>
import { set } from 'lodash-es'
import { EChartsOption } from 'echarts'
<!-- <el-col :lg="6" :md="12" :sm="12" :xl="6" :xs="24">-->
<!-- <el-card class="mb-20px" shadow="hover">-->
<!-- <el-skeleton :loading="loading" :rows="2" animated>-->
<!-- <template #default>-->
<!-- <div :class="`${prefixCls}__item flex justify-between`">-->
<!-- <div>-->
<!-- <div-->
<!-- :class="`${prefixCls}__item&#45;&#45;icon ${prefixCls}__item&#45;&#45;shopping p-16px inline-block rounded-6px`"-->
<!-- >-->
<!-- <Icon :size="40" icon="svg-icon:shopping" />-->
<!-- </div>-->
<!-- </div>-->
<!-- <div class="flex flex-col justify-between">-->
<!-- <div :class="`${prefixCls}__item&#45;&#45;text text-16px text-gray-500 text-right`"-->
<!-- >{{ t('analysis.totalShopping') }}-->
<!-- </div>-->
<!-- <CountTo-->
<!-- :duration="2600"-->
<!-- :end-val="13600"-->
<!-- :start-val="0"-->
<!-- class="text-right text-20px font-700"-->
<!-- />-->
<!-- </div>-->
<!-- </div>-->
<!-- </template>-->
<!-- </el-skeleton>-->
<!-- </el-card>-->
<!-- </el-col>-->
<!-- </el-row>-->
<!-- <el-row :gutter="20" justify="space-between">-->
<!-- <el-col :lg="10" :md="24" :sm="24" :xl="10" :xs="24">-->
<!-- <el-card class="mb-20px" shadow="hover">-->
<!-- <el-skeleton :loading="loading" animated>-->
<!-- <Echart :height="300" :options="pieOptionsData" />-->
<!-- </el-skeleton>-->
<!-- </el-card>-->
<!-- </el-col>-->
<!-- <el-col :lg="14" :md="24" :sm="24" :xl="14" :xs="24">-->
<!-- <el-card class="mb-20px" shadow="hover">-->
<!-- <el-skeleton :loading="loading" animated>-->
<!-- <Echart :height="300" :options="barOptionsData" />-->
<!-- </el-skeleton>-->
<!-- </el-card>-->
<!-- </el-col>-->
<!-- <el-col :span="24">-->
<!-- <el-card class="mb-20px" shadow="hover">-->
<!-- <el-skeleton :loading="loading" :rows="4" animated>-->
<!-- <Echart :height="350" :options="lineOptionsData" />-->
<!-- </el-skeleton>-->
<!-- </el-card>-->
<!-- </el-col>-->
<!-- </el-row>-->
<!--</template>-->
<!--<script lang="ts" setup>-->
<!--import { set } from 'lodash-es'-->
<!--import { EChartsOption } from 'echarts'-->
import { useDesign } from '@/hooks/web/useDesign'
import type { AnalysisTotalTypes } from './types'
import { barOptions, lineOptions, pieOptions } from './echarts-data'
<!--import { useDesign } from '@/hooks/web/useDesign'-->
<!--import type { AnalysisTotalTypes } from './types'-->
<!--// import { barOptions, lineOptions, pieOptions } from './echarts-data'-->
defineOptions({ name: 'Home2' })
<!--defineOptions({ name: 'Home2' })-->
const { t } = useI18n()
const loading = ref(true)
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('panel')
const pieOptionsData = reactive<EChartsOption>(pieOptions) as EChartsOption
<!--const { t } = useI18n()-->
<!--const loading = ref(true)-->
<!--const { getPrefixCls } = useDesign()-->
<!--const prefixCls = getPrefixCls('panel')-->
<!--const pieOptionsData = reactive<EChartsOption>(pieOptions) as EChartsOption-->
let totalState = reactive<AnalysisTotalTypes>({
users: 0,
messages: 0,
moneys: 0,
shoppings: 0
})
<!--let totalState = reactive<AnalysisTotalTypes>({-->
<!-- users: 0,-->
<!-- messages: 0,-->
<!-- moneys: 0,-->
<!-- shoppings: 0-->
<!--})-->
const getCount = async () => {
const data = {
users: 102400,
messages: 81212,
moneys: 9280,
shoppings: 13600
}
totalState = Object.assign(totalState, data)
}
<!--const getCount = async () => {-->
<!-- const data = {-->
<!-- users: 102400,-->
<!-- messages: 81212,-->
<!-- moneys: 9280,-->
<!-- shoppings: 13600-->
<!-- }-->
<!-- totalState = Object.assign(totalState, data)-->
<!--}-->
// 用户来源
const getUserAccessSource = async () => {
const data = [
{ value: 335, name: 'analysis.directAccess' },
{ value: 310, name: 'analysis.mailMarketing' },
{ value: 234, name: 'analysis.allianceAdvertising' },
{ value: 135, name: 'analysis.videoAdvertising' },
{ value: 1548, name: 'analysis.searchEngines' }
]
set(
pieOptionsData,
'legend.data',
data.map((v) => t(v.name))
)
set(pieOptionsData, 'series.data', data)
}
const barOptionsData = reactive<EChartsOption>(barOptions) as EChartsOption
<!--// 用户来源-->
<!--const getUserAccessSource = async () => {-->
<!-- const data = [-->
<!-- { value: 335, name: 'analysis.directAccess' },-->
<!-- { value: 310, name: 'analysis.mailMarketing' },-->
<!-- { value: 234, name: 'analysis.allianceAdvertising' },-->
<!-- { value: 135, name: 'analysis.videoAdvertising' },-->
<!-- { value: 1548, name: 'analysis.searchEngines' }-->
<!-- ]-->
<!-- set(-->
<!-- pieOptionsData,-->
<!-- 'legend.data',-->
<!-- data.map((v) => t(v.name))-->
<!-- )-->
<!-- set(pieOptionsData, 'series.data', data)-->
<!--}-->
<!--const barOptionsData = reactive<EChartsOption>(barOptions) as EChartsOption-->
// 周活跃量
const getWeeklyUserActivity = async () => {
const data = [
{ value: 13253, name: 'analysis.monday' },
{ value: 34235, name: 'analysis.tuesday' },
{ value: 26321, name: 'analysis.wednesday' },
{ value: 12340, name: 'analysis.thursday' },
{ value: 24643, name: 'analysis.friday' },
{ value: 1322, name: 'analysis.saturday' },
{ value: 1324, name: 'analysis.sunday' }
]
set(
barOptionsData,
'xAxis.data',
data.map((v) => t(v.name))
)
set(barOptionsData, 'series', [
{
name: t('analysis.activeQuantity'),
data: data.map((v) => v.value),
type: 'bar'
}
])
}
<!--// 周活跃量-->
<!--const getWeeklyUserActivity = async () => {-->
<!-- const data = [-->
<!-- { value: 13253, name: 'analysis.monday' },-->
<!-- { value: 34235, name: 'analysis.tuesday' },-->
<!-- { value: 26321, name: 'analysis.wednesday' },-->
<!-- { value: 12340, name: 'analysis.thursday' },-->
<!-- { value: 24643, name: 'analysis.friday' },-->
<!-- { value: 1322, name: 'analysis.saturday' },-->
<!-- { value: 1324, name: 'analysis.sunday' }-->
<!-- ]-->
<!-- set(-->
<!-- barOptionsData,-->
<!-- 'xAxis.data',-->
<!-- data.map((v) => t(v.name))-->
<!-- )-->
<!-- set(barOptionsData, 'series', [-->
<!-- {-->
<!-- name: t('analysis.activeQuantity'),-->
<!-- data: data.map((v) => v.value),-->
<!-- type: 'bar'-->
<!-- }-->
<!-- ])-->
<!--}-->
const lineOptionsData = reactive<EChartsOption>(lineOptions) as EChartsOption
<!--const lineOptionsData = reactive<EChartsOption>(lineOptions) as EChartsOption-->
// 每月销售总额
const getMonthlySales = async () => {
const data = [
{ estimate: 100, actual: 120, name: 'analysis.january' },
{ estimate: 120, actual: 82, name: 'analysis.february' },
{ estimate: 161, actual: 91, name: 'analysis.march' },
{ estimate: 134, actual: 154, name: 'analysis.april' },
{ estimate: 105, actual: 162, name: 'analysis.may' },
{ estimate: 160, actual: 140, name: 'analysis.june' },
{ estimate: 165, actual: 145, name: 'analysis.july' },
{ estimate: 114, actual: 250, name: 'analysis.august' },
{ estimate: 163, actual: 134, name: 'analysis.september' },
{ estimate: 185, actual: 56, name: 'analysis.october' },
{ estimate: 118, actual: 99, name: 'analysis.november' },
{ estimate: 123, actual: 123, name: 'analysis.december' }
]
set(
lineOptionsData,
'xAxis.data',
data.map((v) => t(v.name))
)
set(lineOptionsData, 'series', [
{
name: t('analysis.estimate'),
smooth: true,
type: 'line',
data: data.map((v) => v.estimate),
animationDuration: 2800,
animationEasing: 'cubicInOut'
},
{
name: t('analysis.actual'),
smooth: true,
type: 'line',
itemStyle: {},
data: data.map((v) => v.actual),
animationDuration: 2800,
animationEasing: 'quadraticOut'
}
])
}
<!--// 每月销售总额-->
<!--const getMonthlySales = async () => {-->
<!-- const data = [-->
<!-- { estimate: 100, actual: 120, name: 'analysis.january' },-->
<!-- { estimate: 120, actual: 82, name: 'analysis.february' },-->
<!-- { estimate: 161, actual: 91, name: 'analysis.march' },-->
<!-- { estimate: 134, actual: 154, name: 'analysis.april' },-->
<!-- { estimate: 105, actual: 162, name: 'analysis.may' },-->
<!-- { estimate: 160, actual: 140, name: 'analysis.june' },-->
<!-- { estimate: 165, actual: 145, name: 'analysis.july' },-->
<!-- { estimate: 114, actual: 250, name: 'analysis.august' },-->
<!-- { estimate: 163, actual: 134, name: 'analysis.september' },-->
<!-- { estimate: 185, actual: 56, name: 'analysis.october' },-->
<!-- { estimate: 118, actual: 99, name: 'analysis.november' },-->
<!-- { estimate: 123, actual: 123, name: 'analysis.december' }-->
<!-- ]-->
<!-- set(-->
<!-- lineOptionsData,-->
<!-- 'xAxis.data',-->
<!-- data.map((v) => t(v.name))-->
<!-- )-->
<!-- set(lineOptionsData, 'series', [-->
<!-- {-->
<!-- name: t('analysis.estimate'),-->
<!-- smooth: true,-->
<!-- type: 'line',-->
<!-- data: data.map((v) => v.estimate),-->
<!-- animationDuration: 2800,-->
<!-- animationEasing: 'cubicInOut'-->
<!-- },-->
<!-- {-->
<!-- name: t('analysis.actual'),-->
<!-- smooth: true,-->
<!-- type: 'line',-->
<!-- itemStyle: {},-->
<!-- data: data.map((v) => v.actual),-->
<!-- animationDuration: 2800,-->
<!-- animationEasing: 'quadraticOut'-->
<!-- }-->
<!-- ])-->
<!--}-->
const getAllApi = async () => {
await Promise.all([getCount(), getUserAccessSource(), getWeeklyUserActivity(), getMonthlySales()])
loading.value = false
}
<!--const getAllApi = async () => {-->
<!-- await Promise.all([getCount(), getUserAccessSource(), getWeeklyUserActivity(), getMonthlySales()])-->
<!-- loading.value = false-->
<!--}-->
getAllApi()
</script>
<!--getAllApi()-->
<!--</script>-->
<style lang="scss" scoped>
$prefix-cls: #{$namespace}-panel;
<!--<style lang="scss" scoped>-->
<!--$prefix-cls: #{$namespace}-panel;-->
.#{$prefix-cls} {
&__item {
&--peoples {
color: #40c9c6;
}
<!--.#{$prefix-cls} {-->
<!-- &__item {-->
<!-- &&#45;&#45;peoples {-->
<!-- color: #40c9c6;-->
<!-- }-->
&--message {
color: #36a3f7;
}
<!-- &&#45;&#45;message {-->
<!-- color: #36a3f7;-->
<!-- }-->
&--money {
color: #f4516c;
}
<!-- &&#45;&#45;money {-->
<!-- color: #f4516c;-->
<!-- }-->
&--shopping {
color: #34bfa3;
}
<!-- &&#45;&#45;shopping {-->
<!-- color: #34bfa3;-->
<!-- }-->
&:hover {
:deep(.#{$namespace}-icon) {
color: #fff !important;
}
<!-- &:hover {-->
<!-- :deep(.#{$namespace}-icon) {-->
<!-- color: #fff !important;-->
<!-- }-->
.#{$prefix-cls}__item--icon {
transition: all 0.38s ease-out;
}
<!-- .#{$prefix-cls}__item&#45;&#45;icon {-->
<!-- transition: all 0.38s ease-out;-->
<!-- }-->
.#{$prefix-cls}__item--peoples {
background: #40c9c6;
}
<!-- .#{$prefix-cls}__item&#45;&#45;peoples {-->
<!-- background: #40c9c6;-->
<!-- }-->
.#{$prefix-cls}__item--message {
background: #36a3f7;
}
<!-- .#{$prefix-cls}__item&#45;&#45;message {-->
<!-- background: #36a3f7;-->
<!-- }-->
.#{$prefix-cls}__item--money {
background: #f4516c;
}
<!-- .#{$prefix-cls}__item&#45;&#45;money {-->
<!-- background: #f4516c;-->
<!-- }-->
.#{$prefix-cls}__item--shopping {
background: #34bfa3;
}
}
}
}
</style>
<!-- .#{$prefix-cls}__item&#45;&#45;shopping {-->
<!-- background: #34bfa3;-->
<!-- }-->
<!-- }-->
<!-- }-->
<!--}-->
<!--</style>-->

View File

@ -1,308 +1,30 @@
import { EChartsOption } from 'echarts'
const { t } = useI18n()
export const lineOptions: EChartsOption = {
title: {
text: t('analysis.monthlySales'),
left: 'center'
},
xAxis: {
data: [
t('analysis.january'),
t('analysis.february'),
t('analysis.march'),
t('analysis.april'),
t('analysis.may'),
t('analysis.june'),
t('analysis.july'),
t('analysis.august'),
t('analysis.september'),
t('analysis.october'),
t('analysis.november'),
t('analysis.december')
],
boundaryGap: false,
axisTick: {
show: false
}
},
grid: {
left: 20,
right: 20,
bottom: 20,
top: 80,
containLabel: true
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross'
export const getPieOptions: (data: any[]) => EChartsOption = (data) => {
return {
color: ['#2695f9', '#00c7fb', '#3f2da4 '],
tooltip: {
trigger: 'item'
},
padding: [5, 10]
},
yAxis: {
axisTick: {
show: false
}
},
legend: {
data: [t('analysis.estimate'), t('analysis.actual')],
top: 50
},
series: [
{
name: t('analysis.estimate'),
smooth: true,
type: 'line',
data: [100, 120, 161, 134, 105, 160, 165, 114, 163, 185, 118, 123],
animationDuration: 2800,
animationEasing: 'cubicInOut'
legend: {
bottom: '5%',
left: 'center'
},
{
name: t('analysis.actual'),
smooth: true,
type: 'line',
itemStyle: {},
data: [120, 82, 91, 154, 162, 140, 145, 250, 134, 56, 99, 123],
animationDuration: 2800,
animationEasing: 'quadraticOut'
}
]
}
export const pieOptions: EChartsOption = {
title: {
text: t('analysis.userAccessSource'),
left: 'center'
},
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b} : {c} ({d}%)'
},
legend: {
orient: 'vertical',
left: 'left',
data: [
t('analysis.directAccess'),
t('analysis.mailMarketing'),
t('analysis.allianceAdvertising'),
t('analysis.videoAdvertising'),
t('analysis.searchEngines')
series: [
{
type: 'pie',
radius: ['38%', '55%'],
center: ['50%', '45%'],
avoidLabelOverlap: false,
labelLine: {
show: true
},
label: {
formatter: '{b}\n\n{d}%',
fontSize: 16
},
data
}
]
},
series: [
{
name: t('analysis.userAccessSource'),
type: 'pie',
radius: '55%',
center: ['50%', '60%'],
data: [
{ value: 335, name: t('analysis.directAccess') },
{ value: 310, name: t('analysis.mailMarketing') },
{ value: 234, name: t('analysis.allianceAdvertising') },
{ value: 135, name: t('analysis.videoAdvertising') },
{ value: 1548, name: t('analysis.searchEngines') }
]
}
]
}
export const barOptions: EChartsOption = {
title: {
text: t('analysis.weeklyUserActivity'),
left: 'center'
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
grid: {
left: 50,
right: 20,
bottom: 20
},
xAxis: {
type: 'category',
data: [
t('analysis.monday'),
t('analysis.tuesday'),
t('analysis.wednesday'),
t('analysis.thursday'),
t('analysis.friday'),
t('analysis.saturday'),
t('analysis.sunday')
],
axisTick: {
alignWithLabel: true
}
},
yAxis: {
type: 'value'
},
series: [
{
name: t('analysis.activeQuantity'),
data: [13253, 34235, 26321, 12340, 24643, 1322, 1324],
type: 'bar'
}
]
}
export const radarOption: EChartsOption = {
legend: {
data: [t('workplace.personal'), t('workplace.team')]
},
radar: {
// shape: 'circle',
indicator: [
{ name: t('workplace.quote'), max: 65 },
{ name: t('workplace.contribution'), max: 160 },
{ name: t('workplace.hot'), max: 300 },
{ name: t('workplace.yield'), max: 130 },
{ name: t('workplace.follow'), max: 100 }
]
},
series: [
{
name: `xxx${t('workplace.index')}`,
type: 'radar',
data: [
{
value: [42, 30, 20, 35, 80],
name: t('workplace.personal')
},
{
value: [50, 140, 290, 100, 90],
name: t('workplace.team')
}
]
}
]
}
export const wordOptions = {
series: [
{
type: 'wordCloud',
gridSize: 2,
sizeRange: [12, 50],
rotationRange: [-90, 90],
shape: 'pentagon',
width: 600,
height: 400,
drawOutOfBound: true,
textStyle: {
color: function () {
return (
'rgb(' +
[
Math.round(Math.random() * 160),
Math.round(Math.random() * 160),
Math.round(Math.random() * 160)
].join(',') +
')'
)
}
},
emphasis: {
textStyle: {
shadowBlur: 10,
shadowColor: '#333'
}
},
data: [
{
name: 'Sam S Club',
value: 10000,
textStyle: {
color: 'black'
},
emphasis: {
textStyle: {
color: 'red'
}
}
},
{
name: 'Macys',
value: 6181
},
{
name: 'Amy Schumer',
value: 4386
},
{
name: 'Jurassic World',
value: 4055
},
{
name: 'Charter Communications',
value: 2467
},
{
name: 'Chick Fil A',
value: 2244
},
{
name: 'Planet Fitness',
value: 1898
},
{
name: 'Pitch Perfect',
value: 1484
},
{
name: 'Express',
value: 1112
},
{
name: 'Home',
value: 965
},
{
name: 'Johnny Depp',
value: 847
},
{
name: 'Lena Dunham',
value: 582
},
{
name: 'Lewis Hamilton',
value: 555
},
{
name: 'KXAN',
value: 550
},
{
name: 'Mary Ellen Mark',
value: 462
},
{
name: 'Farrah Abraham',
value: 366
},
{
name: 'Rita Ora',
value: 360
},
{
name: 'Serena Williams',
value: 282
},
{
name: 'NCAA baseball tournament',
value: 273
},
{
name: 'Point Break',
value: 265
}
]
}
]
}
}

View File

@ -198,7 +198,7 @@ const loginData = reactive({
captchaEnable: import.meta.env.VITE_APP_CAPTCHA_ENABLE !== 'false',
tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE !== 'false',
loginForm: {
tenantName: '芋道源码',
tenantName: '设计院',
username: 'admin',
password: 'admin123',
captchaVerification: '',

View File

@ -125,21 +125,21 @@
</div>
</el-form-item>
</el-col>
<el-divider content-position="center">萌新必读</el-divider>
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
<el-form-item>
<div class="w-[100%] flex justify-between">
<el-link href="https://doc.iocoder.cn/" target="_blank">📚开发指南</el-link>
<el-link href="https://doc.iocoder.cn/video/" target="_blank">🔥视频教程</el-link>
<el-link href="https://www.iocoder.cn/Interview/good-collection/" target="_blank">
面试手册
</el-link>
<el-link href="http://static.yudao.iocoder.cn/mp/Aix9975.jpeg" target="_blank">
🤝外包咨询
</el-link>
</div>
</el-form-item>
</el-col>
<!-- <el-divider content-position="center">萌新必读</el-divider>-->
<!-- <el-col :span="24" style="padding-right: 10px; padding-left: 10px">-->
<!-- <el-form-item>-->
<!-- <div class="w-[100%] flex justify-between">-->
<!-- <el-link href="https://doc.iocoder.cn/" target="_blank">📚开发指南</el-link>-->
<!-- <el-link href="https://doc.iocoder.cn/video/" target="_blank">🔥视频教程</el-link>-->
<!-- <el-link href="https://www.iocoder.cn/Interview/good-collection/" target="_blank">-->
<!-- 面试手册-->
<!-- </el-link>-->
<!-- <el-link href="http://static.yudao.iocoder.cn/mp/Aix9975.jpeg" target="_blank">-->
<!-- 🤝外包咨询-->
<!-- </el-link>-->
<!-- </div>-->
<!-- </el-form-item>-->
<!-- </el-col>-->
</el-row>
</el-form>
</template>

View File

@ -3,7 +3,7 @@
<div class="chat-empty">
<!-- title -->
<div class="center-container">
<div class="title">芋道 AI</div>
<div class="title">AI对话</div>
<div class="role-list">
<div
class="role-item"

View File

@ -0,0 +1,108 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
v-loading="formLoading"
>
<el-form-item label="公司名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入公司名称" />
</el-form-item>
<el-form-item label="地址" prop="address">
<el-input v-model="formData.address" placeholder="请输入地址" />
</el-form-item>
<el-form-item label="联系人" prop="contacts">
<el-input v-model="formData.contacts" placeholder="请输入联系人" />
</el-form-item>
<el-form-item label="电话" prop="phone">
<el-input v-model="formData.phone" placeholder="请输入电话" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { CustomerCompanyApi, CustomerCompanyVO } from '@/api/cms/customerCompany'
/** 客户公司 表单 */
defineOptions({ name: 'CustomerCompanyForm' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中1修改时的数据加载2提交的按钮禁用
const formType = ref('') // 表单的类型create - 新增update - 修改
const formData = ref({
id: undefined,
contacts: undefined,
name: undefined,
address: undefined,
phone: undefined,
})
const formRules = reactive({
contacts: [{ required: true, message: '联系人不能为空', trigger: 'blur' }],
name: [{ required: true, message: '公司名称不能为空', trigger: 'blur' }],
})
const formRef = ref() // 表单 Ref
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
// 修改时,设置数据
if (id) {
formLoading.value = true
try {
formData.value = await CustomerCompanyApi.getCustomerCompany(id)
} finally {
formLoading.value = false
}
}
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
await formRef.value.validate()
// 提交请求
formLoading.value = true
try {
const data = formData.value as unknown as CustomerCompanyVO
if (formType.value === 'create') {
await CustomerCompanyApi.createCustomerCompany(data)
message.success(t('common.createSuccess'))
} else {
await CustomerCompanyApi.updateCustomerCompany(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
contacts: undefined,
name: undefined,
address: undefined,
phone: undefined,
}
formRef.value?.resetFields()
}
</script>

View File

@ -0,0 +1,221 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="公司名称" prop="name">
<el-input
v-model="queryParams.name"
placeholder="请输入公司名称"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="联系人" prop="contacts">
<el-input
v-model="queryParams.contacts"
placeholder="请输入联系人"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<!-- <el-form-item label="创建时间" prop="createTime">-->
<!-- <el-date-picker-->
<!-- v-model="queryParams.createTime"-->
<!-- value-format="YYYY-MM-DD"-->
<!-- type="date"-->
<!-- placeholder="选择创建时间"-->
<!-- clearable-->
<!-- class="!w-240px"-->
<!-- />-->
<!-- </el-form-item>-->
<!-- <el-form-item label="地址" prop="address">-->
<!-- <el-input-->
<!-- v-model="queryParams.address"-->
<!-- placeholder="请输入地址"-->
<!-- clearable-->
<!-- @keyup.enter="handleQuery"-->
<!-- class="!w-240px"-->
<!-- />-->
<!-- </el-form-item>-->
<el-form-item label="电话" prop="phone">
<el-input
v-model="queryParams.phone"
placeholder="请输入电话"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['cms:customer-company:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['cms:customer-company:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
<!-- <el-table-column label="主键" align="center" prop="id" />-->
<el-table-column label="公司名称" align="center" prop="name" />
<el-table-column label="联系人" align="center" prop="contacts" />
<el-table-column label="电话" align="center" prop="phone" />
<el-table-column label="地址" align="center" prop="address" />
<el-table-column
label="创建时间"
align="center"
prop="createTime"
:formatter="dateFormatter"
width="180px"
/>
<el-table-column label="操作" align="center">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['cms:customer-company:update']"
>
编辑
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['cms:customer-company:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<CustomerCompanyForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts">
import { dateFormatter } from '@/utils/formatTime'
import download from '@/utils/download'
import { CustomerCompanyApi, CustomerCompanyVO } from '@/api/cms/customerCompany'
import CustomerCompanyForm from './CustomerCompanyForm.vue'
/** 客户公司 列表 */
defineOptions({ name: 'CustomerCompany' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const loading = ref(true) // 列表的加载中
const list = ref<CustomerCompanyVO[]>([]) // 列表的数据
const total = ref(0) // 列表的总页数
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
contacts: undefined,
createTime: undefined,
createTime: [],
name: undefined,
address: undefined,
phone: undefined,
})
const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await CustomerCompanyApi.getCustomerCompanyPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await CustomerCompanyApi.deleteCustomerCompany(id)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch {}
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
// 导出的二次确认
await message.exportConfirm()
// 发起导出
exportLoading.value = true
const data = await CustomerCompanyApi.exportCustomerCompany(queryParams)
download.excel(data, '客户公司.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 初始化 **/
onMounted(() => {
getList()
})
</script>

View File

@ -0,0 +1,131 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
v-loading="formLoading"
>
<el-form-item label="文件夹名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入文件夹名称" />
</el-form-item>
<el-form-item label="编号" prop="code">
<el-input v-model="formData.code" placeholder="请输入文件夹编号" />
</el-form-item>
<el-form-item label="上一级" prop="parentId">
<el-tree-select
v-model="formData.parentId"
:data="categoryTree"
:props="defaultProps"
check-strictly
default-expand-all
placeholder="请选择上一级"
/>
</el-form-item>
<el-form-item label="描述" prop="description">
<el-input v-model="formData.description" type="textarea" placeholder="请输入描述信息" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { CategoryApi, CategoryVO } from '@/api/infra/category'
import { defaultProps, handleTree } from '@/utils/tree'
/** 文件目录 表单 */
defineOptions({ name: 'CategoryForm' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中1修改时的数据加载2提交的按钮禁用
const formType = ref('') // 表单的类型create - 新增update - 修改
const formData = ref({
id: undefined,
code: undefined,
name: undefined,
parentId: undefined,
description: undefined,
})
const formRules = reactive({
name: [{ required: true, message: '文件夹名称不能为空', trigger: 'blur' }],
parentId: [{ required: true, message: '上一级不能为空', trigger: 'change' }],
})
const formRef = ref() // 表单 Ref
const categoryTree = ref() // 树形结构
/** 打开弹窗 */
const open = async (type: string, id: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
// 修改时,设置数据
if (type === 'update') {
formLoading.value = true
try {
formData.value = await CategoryApi.getCategory(id)
} finally {
formLoading.value = false
}
}
// 新增时,设置默认上一级
else{
formData.value.parentId = id;
}
await getCategoryTree()
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
await formRef.value.validate()
// 提交请求
formLoading.value = true
try {
const data = formData.value as unknown as CategoryVO
if (formType.value === 'create') {
await CategoryApi.createCategory(data)
message.success(t('common.createSuccess'))
} else {
await CategoryApi.updateCategory(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
code: undefined,
name: undefined,
parentId: undefined,
description: undefined,
}
formRef.value?.resetFields()
}
/** 获得文件目录树 */
const getCategoryTree = async () => {
categoryTree.value = []
const data = await CategoryApi.getCategoryList()
const root: Tree = { id: 0, name: '顶级文件目录', children: [] }
root.children = handleTree(data, 'id', 'parentId')
categoryTree.value.push(root)
}
</script>

View File

@ -0,0 +1,210 @@
<template>
<div className="folder-header" v-if="folderList && folderList.length > 0">
<el-input v-model="folderName" class="!w-340px" clearable placeholder="请输入文件夹名称">
<template #prefix>
<Icon icon="ep:search" />
</template>
</el-input>
<el-space>
<el-button type="danger" plain @click="toggleExpandAll">
<Icon icon="ep:sort" class="mr-5px" /> 展开/折叠
</el-button>
</el-space>
</div>
<div>
<el-tree
ref="treeRef"
v-if="refreshTable"
:load="folderLoading"
:data="folderList"
:expand-on-click-node="false"
:filter-node-method="filterNode"
:props="defaultProps"
:default-expand-all="isExpandAll"
highlight-current
node-key="id"
@node-click="handleNodeClick"
>
<template #default="{ node, data }">
<span class="custom-tree-node">
<span>{{ node.label }}</span>
<span class="btns" :class="{ show: data.id === curCategoryId }">
<el-button type="primary" size="small" plain @click="appendFolder(data.id)"
>新增</el-button
>
<el-button
style="margin-left: 8px"
type="warning"
size="small"
plain
@click="updateFolder(data.id)"
>修改</el-button
>
<el-button
:class="{ hidden: data.children !== undefined }"
style="margin-left: 8px"
type="danger"
size="small"
plain
@click="deleteFolder(data.id)"
>删除</el-button
>
</span>
</span>
</template>
<template #empty>
<el-button type="primary" plain @click="appendFolder(0)">新增文件夹</el-button>
</template>
</el-tree>
</div>
<!-- 表单弹窗添加/修改 -->
<CategoryForm ref="formRef" @success="getFolderList" />
</template>
<script lang="ts" setup>
import { ElTree } from 'element-plus'
import { defaultProps, handleTree } from '@/utils/tree'
import { CategoryApi, CategoryVO } from '@/api/infra/category'
import CategoryForm from './CategoryForm.vue'
import download from '@/utils/download'
defineOptions({ name: 'CategoryFolderTree' })
const emits = defineEmits(['node-click'])
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const folderLoading = ref(false)
const folderName = ref('')
const folderList = ref<Tree[]>([])
const refreshTable = ref(true) // 重新渲染表格状态
const exportLoading = ref(false) // 导出的加载中
const isExpandAll = ref(true) // 是否展开,默认全部展开
const curCategoryId = ref<number>()
const treeRef = ref<InstanceType<typeof ElTree>>()
const formRef = ref()
/** 查询文件夹列表 */
const getFolderList = async () => {
folderLoading.value = true
try {
const data = await CategoryApi.getCategoryList()
folderList.value = handleTree(data, 'id', 'parentId')
// 重载树
refreshTable.value = false
await nextTick()
refreshTable.value = true
} finally {
folderLoading.value = false
}
}
/** 处理部门被点击 */
const handleNodeClick = async (row: { [key: string]: any }) => {
curCategoryId.value = row.id
emits('node-click', row)
}
/** 基于名字过滤 */
const filterNode = (name: string, data: Tree) => {
if (!name) return true
return data.name.includes(name)
}
/** 监听过滤项 */
watch(folderName, (val) => {
treeRef.value!.filter(val)
})
/** 新增文件夹 */
const appendFolder = (id: number) => {
formRef.value.open('create', id)
}
/** 修改文件夹 */
const updateFolder = (id: number) => {
formRef.value.open('update', id)
}
/** 删除文件夹 */
const deleteFolder = async (id: number) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await CategoryApi.deleteCategory(id)
message.success(t('common.delSuccess'))
// 刷新列表
getFolderList()
} catch {}
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
// 导出的二次确认
await message.exportConfirm()
// 发起导出
exportLoading.value = true
const data = await CategoryApi.exportCategory()
download.excel(data, '文件目录.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 展开/折叠操作 */
const toggleExpandAll = async () => {
refreshTable.value = false
isExpandAll.value = !isExpandAll.value
await nextTick()
refreshTable.value = true
}
// invoke
getFolderList()
</script>
<style scoped lang="scss">
.folder-header {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
}
.custom-tree-node {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
padding-right: 8px;
}
.hidden {
visibility: hidden;
}
.show {
visibility: visible !important;
}
:deep(.el-tree-node__children) {
margin-top: 7px;
}
:deep(.el-tree-node__content) {
.btns {
visibility: hidden;
}
&:hover {
.btns {
visibility: visible;
}
}
}
</style>

View File

@ -0,0 +1,194 @@
<template>
<el-row :gutter="20">
<!-- 左侧文件夹树 -->
<el-col :span="6" :xs="24">
<ContentWrap class="h-1/1">
<FolderTree @node-click="handleNodeClick" />
</ContentWrap>
</el-col>
<el-col :span="18" :xs="24">
<!-- 搜索 -->
<ContentWrap>
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="文件名" prop="name">
<el-input
v-model="queryParams.name"
placeholder="请输入用户名称"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
<el-date-picker
v-model="queryParams.createTime"
value-format="YYYY-MM-DD HH:mm:ss"
type="datetimerange"
start-placeholder="开始日期"
end-placeholder="结束日期"
class="!w-240px"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" />搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" />重置</el-button>
<el-upload
v-if="curCategoryId"
:show-file-list="false"
style="margin-left: 12px"
:action="uploadUrl"
:http-request="handleRequest"
multiple
>
<el-button type="primary" plain> <Icon icon="ep:plus" /> 新增 </el-button>
</el-upload>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 文件列表 -->
<ContentWrap>
<el-table
v-loading="fileListLoading"
:data="fileList"
:stripe="true"
:show-overflow-tooltip="true"
row-key="id"
>
<!-- <el-table-column label="id" align="center" prop="id" width="150" />-->
<el-table-column label="文件名称" align="center" prop="name" />
<el-table-column label="文件地址" align="center" prop="url" />
<el-table-column
label="文件大小"
align="center"
prop="size"
width="120"
:formatter="fileSizeFormatter"
/>
<el-table-column
label="创建时间"
align="center"
prop="createTime"
:formatter="dateFormatter"
width="180"
/>
<el-table-column label="操作" align="center" width="200">
<template #default="scope">
<el-button link type="primary" @click="downloadFile(scope.row.url)"> 下载 </el-button>
<el-button link type="danger" @click="deleteFile(scope.row.id)"> 删除 </el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getFileList"
/>
</ContentWrap>
</el-col>
</el-row>
</template>
<script setup lang="ts">
import { CategoryApi, CategoryVO } from '@/api/infra/category'
import * as FileApi from '@/api/infra/file'
import FolderTree from './FolderTree.vue'
import { ElTree, UploadRequestOptions } from 'element-plus'
import { useUpload, ResponseFile } from '@/components/UploadFile/src/useUpload'
import { dateFormatter } from '@/utils/formatTime'
import {fileSizeFormatter} from "@/utils";
/** 文件目录 列表 */
defineOptions({ name: 'Category' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const { uploadUrl, httpRequest } = useUpload()
const queryFormRef = ref() // 搜索的表单
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
createTime: [],
name: undefined
})
const total = ref<number>(0)
// 当前文件夹id
const curCategoryId = ref<number>()
const fileList = ref([])
const fileListLoading = ref(false)
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getFileList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields()
handleQuery()
}
/** 获取文件列表 */
const getFileList = async () => {
fileListLoading.value = true
try {
const data = await FileApi.getFilePage({
...queryParams,
categoryId: curCategoryId.value
})
fileList.value = data.list
total.value = data.total
} finally {
fileListLoading.value = false
}
}
/** 处理文件夹被点击 */
const handleNodeClick = async (data: Tree) => {
curCategoryId.value = data.id
getFileList()
}
/** 上传文件 */
const handleRequest = async (f: UploadRequestOptions) => {
if (!curCategoryId.value) {
message.error('请先选择一个文件目录')
return
}
try {
await httpRequest(f, { categoryId: curCategoryId.value })
message.success('新增成功')
getFileList()
} catch (e) {
message.error('上传文件失败')
console.error(e)
}
}
/** 下载文件 */
const downloadFile = (url: string) => {
window.open(url, '_blank')
}
/** 删除文件 */
const deleteFile = async (id: number) => {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await FileApi.deleteFile(id)
message.success(t('common.delSuccess'))
// 刷新列表
getFileList()
}
</script>

View File

@ -1,5 +1,5 @@
<template>
<doc-alert title="服务监控" url="https://doc.iocoder.cn/server-monitor/" />
<doc-alert title="服务监控" url="https://www.baidu.com" />
<ContentWrap :bodyStyle="{ padding: '0px' }" class="!mb-0">
<IFrame v-if="!loading" v-loading="loading" :src="src" />

View File

@ -0,0 +1,437 @@
<template>
<Dialog :title="dialogTitle" v-model="dialogVisible">
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
v-loading="formLoading"
>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入名称" />
</el-form-item>
</el-col>
<el-col :span="12" v-if="formType !== 'create'">
<el-form-item label="跟踪编号" prop="trackingCode">
<el-input
v-model="formData.trackingCode"
placeholder="请输入跟踪编号"
:disabled="formType !== 'create'"
/>
</el-form-item>
</el-col>
<el-col :span="12" v-if="formType !== 'create'">
<el-form-item label="项目编号" prop="code">
<el-input
v-model="formData.code"
placeholder="请输入项目编号"
:disabled="formType !== 'create'"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="类型" prop="type">
<el-select
v-model="formData.type"
placeholder="请选择类型"
:disabled="formType !== 'create'"
>
<el-option
v-for="dict in getStrDictOptions(DICT_TYPE.PROJECT_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="省份" prop="provinceId">
<el-select
clearable
filterable
placeholder="请选择省份"
v-model="formData.provinceId"
@change="onProvinceChange"
:disabled="formType !== 'create'"
>
<el-option
v-for="item in areaList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="城市" prop="cityId">
<el-select
clearable
filterable
placeholder="请选择城市"
v-model="formData.cityId"
:disabled="formType !== 'create'"
>
<el-option
v-for="item in cityList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="预计金额(万)" prop="contractAmount" :label-width="102">
<el-input v-model="formData.contractAmount" placeholder="请输入预计合同金额" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="客户公司" prop="customerCompanyId">
<el-select
v-model="formData.customerCompanyId"
placeholder="请选择客户公司"
>
<el-option
v-for="item in customerCompanyOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="客户联系人" prop="customerUser">
<el-input v-model="formData.customerUser" placeholder="请输入客户联系人" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="客户电话" prop="customerPhone">
<el-input v-model="formData.customerPhone" placeholder="请输入客户电话" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="出图公司" prop="drawingCompany">
<el-input v-model="formData.drawingCompany" placeholder="请输入出图公司" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item
label="跟踪部门"
prop="trackingDepId"
>
<el-tree-select
v-model=formData.trackingDepId
:data="deptOptions"
:props="defaultProps"
check-strictly
filterable
clearable
:render-after-expand="false"
empty-text="加载中请稍后"
default-expand-all
style="width: 240px"
:disabled="formType !== 'create'"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="项目经理" prop="projectManagerId">
<el-select
v-model="formData.projectManagerId"
placeholder="请选择项目经理"
:disabled="formType !== 'create'"
class="w-1/1"
>
<el-option
v-for="item in userOptions"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item
label="开始时间"
prop="startTime"
:disabled="formType !== 'create'"
>
<el-date-picker
v-model="formData.startTime"
type="date"
value-format="x"
placeholder="选择开始时间"
/>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="跟踪情况" prop="situation">
<el-input v-model="formData.situation" type="textarea" placeholder="请输入跟踪情况" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="落地可能性" prop="possibility">
<el-select v-model="formData.possibility" placeholder="请选择落地可能性">
<el-option
v-for="dict in getStrDictOptions(DICT_TYPE.POSSIBILITY_OF_LANDING)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" v-if="formType !== 'create'">
<el-form-item label="漏斗预期" prop="funnelExpectation">
<el-input v-model="formData.funnelExpectation" placeholder="请输入漏斗预期" disabled />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="委托方式" prop="entrustMethod">
<el-select
v-model="formData.entrustMethod"
placeholder="请选择委托方式"
:disabled="formType !== 'create'"
>
<el-option
v-for="dict in getStrDictOptions(DICT_TYPE.ENTRUST_METHOD)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" v-if="formType !== 'create'">
<el-form-item label="是否落地" prop="confirmation">
<el-select v-model="formData.confirmation" placeholder="请选择是否落地">
<el-option
v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" v-if="formType !== 'create'">
<el-form-item label="审批状态" prop="processStatus">
<el-select
v-model="formData.processStatus"
placeholder="请选择审批状态"
disabled
>
<el-option
v-for="dict in getIntDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" v-if="formData.confirmation === '是' || formData.confirmation === true">
<el-form-item label="落地时间" prop="endTime">
<el-date-picker
v-model="formData.endTime"
type="date"
value-format="x"
placeholder="选择落地时间"
/>
</el-form-item>
</el-col>
<el-col :span="24" v-if="formData.confirmation === '否' || formData.confirmation === false">
<el-form-item label="未落地原因" prop="reason">
<el-input v-model="formData.reason" type="textarea" placeholder="请输入未落地原因" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="评审附件" prop="reviewFileUrl">
<UploadFile v-model="formData.reviewFileUrl" :category-path="reviewPath"/>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="中标附件" prop="winFileUrl">
<UploadFile v-model="formData.winFileUrl" :category-path="winPath"/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="submitForm" type="primary" :disabled="formLoading"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { getStrDictOptions, getIntDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
import { ProjectApi, ProjectVO } from '@/api/pms/project'
import * as AreaApi from '@/api/system/area'
import * as UserApi from '@/api/system/user'
import { useUserStore } from '@/store/modules/user'
import { CustomerCompanyApi, CustomerCompanyVO } from '@/api/cms/customerCompany'
import * as DeptApi from '@/api/system/dept'
import {defaultProps, handleTree} from "@/utils/tree";
/** 项目基本信息 表单 */
defineOptions({ name: 'ProjectForm' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示
const dialogTitle = ref('') // 弹窗的标题
const formLoading = ref(false) // 表单的加载中1修改时的数据加载2提交的按钮禁用
const areaList = ref([]) // 地区列表
const cityList = ref([]); // 城市options
const formType = ref('') // 表单的类型create - 新增update - 修改
const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
const customerCompanyOptions = ref<CustomerCompanyVO[]>([]) //客户公司列表
const deptOptions = ref<any[]>([]) // 部门树形结构
const reviewPath = ref('/根目录/项目管理/项目立项/评审附件') //评审附件路径
const winPath = ref('/根目录/项目管理/项目立项/中标附件') //中标附件路径
const formData = ref({
id: undefined,
name: undefined,
code: undefined,
customerUser: undefined,
drawingCompany: undefined,
trackingDepId: undefined,
endTime: undefined,
possibility: undefined,
funnelExpectation: undefined,
entrustMethod: undefined,
reason: undefined,
processStatus: undefined,
trackingCode: undefined,
type: undefined,
customerCompanyId: undefined,
projectManagerId: undefined,
startTime: undefined,
situation: undefined,
reviewFileUrl: undefined,
confirmation: undefined,
winFileUrl: undefined,
contractAmount: undefined,
customerPhone: undefined,
provinceId: undefined,
cityId: undefined,
})
const formRules = reactive({
name: [{ required: true, message: '名称不能为空', trigger: 'blur' }],
type: [{ required: true, message: '类型不能为空', trigger: 'change' }],
provinceId: [{ required: true, message: '省份不能为空', trigger: 'change' }],
cityId: [{ required: true, message: '城市不能为空', trigger: 'change' }],
projectManagerId: [{ required: true, message: '项目经理不能为空', trigger: 'change' }],
contractAmount: [{ required: true, message: '预测金额不能为空', trigger: 'blur' }],
drawingCompany: [{ required: true, message: '出图公司不能为空', trigger: 'blur' }],
trackingDepId: [{ required: true, message: '跟踪部门不能为空', trigger: 'change' }],
possibility: [{ required: true, message: '落地可能性不能为空', trigger: 'change' }],
entrustMethod: [{ required: true, message: '委托方式不能为空', trigger: 'change' }],
customerCompanyId: [{ required: true, message: '客户公司不能为空', trigger: 'blur' }],
startTime: [{ required: true, message: '开始时间不能为空', trigger: 'blur' }],
customerPhone: [{ required: true, message: '客户电话不能为空', trigger: 'blur' }],
customerUser: [{ required: true, message: '客户联系人不能为空', trigger: 'blur' }],
})
const formRef = ref() // 表单 Ref
/** 打开弹窗 */
const open = async (type: string, id?: number) => {
dialogVisible.value = true
dialogTitle.value = t('action.' + type)
formType.value = type
resetForm()
// 修改时,设置数据
if (id) {
formLoading.value = true
try {
formData.value = await ProjectApi.getProject(id)
} finally {
formLoading.value = false
}
}
// 获得部门列表
deptOptions.value = handleTree(await DeptApi.getSimpleDeptList())
// 获得地区列表
areaList.value = await AreaApi.getAreaTree()
// 获得用户列表
userOptions.value = await UserApi.getSimpleUserList()
if (formType.value === 'create') {
formData.value.projectManagerId = useUserStore().getUser.id
}
// 客户公司列表
customerCompanyOptions.value = await CustomerCompanyApi.getCustomerCompanyList()
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
const submitForm = async () => {
// 校验表单
await formRef.value.validate()
// 提交请求
formLoading.value = true
try {
const data = formData.value as unknown as ProjectVO
if (formType.value === 'create') {
await ProjectApi.createProject(data)
message.success(t('common.createSuccess'))
} else {
await ProjectApi.updateProject(data)
message.success(t('common.updateSuccess'))
}
dialogVisible.value = false
// 发送操作成功的事件
emit('success')
} finally {
formLoading.value = false
}
}
/** 重置表单 */
const resetForm = () => {
formData.value = {
id: undefined,
name: undefined,
code: undefined,
customerUser: undefined,
drawingCompany: undefined,
trackingDepId: undefined,
endTime: undefined,
possibility: undefined,
funnelExpectation: undefined,
entrustMethod: undefined,
reason: undefined,
processStatus: undefined,
trackingCode: undefined,
type: undefined,
customerCompanyId: undefined,
projectManagerId: undefined,
startTime: undefined,
situation: undefined,
reviewFileUrl: undefined,
confirmation: undefined,
winFileUrl: undefined,
contractAmount: undefined,
customerPhone: undefined,
provinceId: undefined,
cityId: undefined,
}
formRef.value?.resetFields()
}
// 省份更新时,刷新城市选项
const onProvinceChange = (value: number)=>{
formData.value.cityId = undefined;
cityList.value = (areaList.value.find(({id})=>id===value) as any)?.children ?? [];
}
</script>

View File

@ -0,0 +1,373 @@
<template>
<el-form
ref="formRef"
v-loading="formLoading"
:model="formData"
:rules="formRules"
label-width="100px"
>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入名称" />
</el-form-item>
</el-col>
<!-- <el-col :span="12">-->
<!-- <el-form-item label="跟踪编号" prop="trackingCode">-->
<!-- <el-input v-model="formData.trackingCode" placeholder="请输入跟踪编号" />-->
<!-- </el-form-item>-->
<!-- </el-col>-->
<!-- <el-col :span="12">-->
<!-- <el-form-item label="项目编号" prop="code">-->
<!-- <el-input v-model="formData.code" placeholder="请输入项目编号" />-->
<!-- </el-form-item>-->
<!-- </el-col>-->
<el-col :span="12">
<el-form-item label="类型" prop="type">
<el-select v-model="formData.type" placeholder="请选择类型">
<el-option
v-for="dict in getStrDictOptions(DICT_TYPE.PROJECT_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="省份" prop="provinceId">
<el-select
clearable
filterable
placeholder="请选择省份"
v-model="formData.provinceId"
@change="onProvinceChange"
>
<el-option
v-for="item in areaList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="城市" prop="cityId">
<el-select
clearable
filterable
placeholder="请选择城市"
v-model="formData.cityId"
>
<el-option
v-for="item in cityList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="预计金额(万)" prop="contractAmount" :label-width="102">
<el-input v-model="formData.contractAmount" placeholder="请输入预计合同金额" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="客户公司" prop="customerCompanyId">
<el-select
v-model="formData.customerCompanyId"
placeholder="请选择客户公司"
>
<el-option
v-for="item in customerCompanyOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="客户联系人" prop="customerUser">
<el-input v-model="formData.customerUser" placeholder="请输入客户联系人" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="客户电话" prop="customerPhone">
<el-input v-model="formData.customerPhone" placeholder="请输入客户电话" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="出图公司" prop="drawingCompany">
<el-input v-model="formData.drawingCompany" placeholder="请输入出图公司" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="跟踪部门" prop="trackingDepId">
<el-tree-select
v-model=formData.trackingDepId
:data="deptOptions"
:props="defaultProps"
check-strictly
filterable
clearable
:render-after-expand="false"
empty-text="加载中请稍后"
default-expand-all
style="width: 240px"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="项目经理" prop="projectManagerId">
<el-select
v-model="formData.projectManagerId"
placeholder="请选择项目经理"
class="w-1/1"
>
<el-option
v-for="item in userList"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="开始时间" prop="startTime">
<el-date-picker
v-model="formData.startTime"
type="date"
value-format="x"
placeholder="选择开始时间"
/>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="跟踪情况" prop="situation">
<el-input v-model="formData.situation" type="textarea" placeholder="请输入跟踪情况" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="落地可能性" prop="possibility">
<el-select v-model="formData.possibility" placeholder="请选择落地可能性">
<el-option
v-for="dict in getStrDictOptions(DICT_TYPE.POSSIBILITY_OF_LANDING)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
<!-- <el-col :span="12">-->
<!-- <el-form-item label="漏斗预期" prop="funnelExpectation">-->
<!-- <el-input v-model="formData.funnelExpectation" placeholder="请输入漏斗预期" />-->
<!-- </el-form-item>-->
<!-- </el-col>-->
<el-col :span="12">
<el-form-item label="委托方式" prop="entrustMethod">
<el-select v-model="formData.entrustMethod" placeholder="请选择委托方式">
<el-option
v-for="dict in getStrDictOptions(DICT_TYPE.ENTRUST_METHOD)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
<!-- <el-col :span="12">-->
<!-- <el-form-item label="是否落地" prop="confirmation">-->
<!-- <el-select v-model="formData.confirmation" placeholder="请选择是否落地">-->
<!-- <el-option-->
<!-- v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"-->
<!-- :key="dict.value"-->
<!-- :label="dict.label"-->
<!-- :value="dict.value"-->
<!-- />-->
<!-- </el-select>-->
<!-- </el-form-item>-->
<!-- </el-col>-->
<!-- <el-col :span="12">-->
<!-- <el-form-item label="审批状态" prop="processStatus">-->
<!-- <el-select-->
<!-- v-model="formData.processStatus"-->
<!-- placeholder="请选择审批状态"-->
<!-- disabled-->
<!-- >-->
<!-- <el-option-->
<!-- v-for="dict in getStrDictOptions(DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS)"-->
<!-- :key="dict.value"-->
<!-- :label="dict.label"-->
<!-- :value="dict.value"-->
<!-- />-->
<!-- </el-select>-->
<!-- </el-form-item>-->
<!-- </el-col>-->
<!-- <el-col :span="12" v-if="formData.confirmation === '是' || formData.confirmation === true">-->
<!-- <el-form-item label="落地时间" prop="endTime">-->
<!-- <el-date-picker-->
<!-- v-model="formData.endTime"-->
<!-- type="date"-->
<!-- value-format="x"-->
<!-- placeholder="选择落地时间"-->
<!-- />-->
<!-- </el-form-item>-->
<!-- </el-col>-->
<!-- <el-col :span="24" v-if="formData.confirmation === '否' || formData.confirmation === false">-->
<!-- <el-form-item label="未落地原因" prop="reason">-->
<!-- <el-input v-model="formData.reason" type="textarea" placeholder="请输入未落地原因" />-->
<!-- </el-form-item>-->
<!-- </el-col>-->
<el-col :span="24">
<el-form-item label="评审附件" prop="reviewFileUrl">
<UploadFile v-model="formData.reviewFileUrl" :category-path="reviewPath"/>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="中标附件" prop="winFileUrl">
<UploadFile v-model="formData.winFileUrl" :category-path="winPath"/>
</el-form-item>
</el-col>
</el-row>
<el-form-item>
<el-button :disabled="formLoading" type="primary" @click="submitForm"> </el-button>
</el-form-item>
</el-form>
</template>
<script lang="ts" setup>
import {DICT_TYPE, getBoolDictOptions, getStrDictOptions} from '@/utils/dict'
import { ProjectApi, ProjectVO } from '@/api/pms/project'
import { useTagsViewStore } from '@/store/modules/tagsView'
import * as DefinitionApi from '@/api/bpm/definition'
import * as UserApi from '@/api/system/user'
import {defaultProps, handleTree} from "@/utils/tree";
import {CustomerCompanyApi, CustomerCompanyVO} from "@/api/cms/customerCompany";
import * as DeptApi from "@/api/system/dept";
import * as AreaApi from "@/api/system/area";
import {useUserStore} from "@/store/modules/user";
defineOptions({ name: 'ProjectBpmCreate' })
const message = useMessage() // 消息弹窗
const { delView } = useTagsViewStore() // 视图操作
const { push, currentRoute } = useRouter() // 路由
const formLoading = ref(false) // 表单的加载中1修改时的数据加载2提交的按钮禁用
const props = {
checkStrictly: true,
children: 'children',
label: 'name',
value: 'id',
isLeaf: 'leaf',
emitPath: false // 用于 cascader 组件:在选中节点改变时,是否返回由该节点所在的各级菜单的值所组成的数组,若设置 false则只返回该节点的值
}
const formData = ref({
id: undefined,
name: undefined,
code: undefined,
customerUser: undefined,
drawingCompany: undefined,
trackingDepId: undefined,
endTime: undefined,
possibility: undefined,
funnelExpectation: undefined,
entrustMethod: undefined,
reason: undefined,
processStatus: undefined,
trackingCode: undefined,
type: undefined,
customerCompanyId: undefined,
projectManagerId: undefined,
startTime: undefined,
situation: undefined,
reviewFileUrl: undefined,
confirmation: undefined,
winFileUrl: undefined,
contractAmount: undefined,
customerPhone: undefined,
provinceId: undefined,
cityId: undefined,
})
const formRules = reactive({
name: [{ required: true, message: '名称不能为空', trigger: 'blur' }],
type: [{ required: true, message: '类型不能为空', trigger: 'change' }],
provinceId: [{ required: true, message: '省份不能为空', trigger: 'change' }],
cityId: [{ required: true, message: '城市不能为空', trigger: 'change' }],
projectManagerId: [{ required: true, message: '项目经理不能为空', trigger: 'change' }],
contractAmount: [{ required: true, message: '预测金额不能为空', trigger: 'blur' }],
drawingCompany: [{ required: true, message: '出图公司不能为空', trigger: 'blur' }],
trackingDepId: [{ required: true, message: '跟踪部门不能为空', trigger: 'change' }],
possibility: [{ required: true, message: '落地可能性不能为空', trigger: 'change' }],
entrustMethod: [{ required: true, message: '委托方式不能为空', trigger: 'change' }],
customerCompanyId: [{ required: true, message: '客户公司不能为空', trigger: 'blur' }],
startTime: [{ required: true, message: '开始时间不能为空', trigger: 'blur' }],
customerPhone: [{ required: true, message: '客户电话不能为空', trigger: 'blur' }],
customerUser: [{ required: true, message: '客户联系人不能为空', trigger: 'blur' }],
})
const formRef = ref() // 表单 Ref
// 指定审批人
const processDefineKey = 'pms_project_init' // 流程定义 Key
const userList = ref<any[]>([]) // 用户列表
const areaList = ref([]) // 地区列表
const cityList = ref([]); // 城市options
const customerCompanyOptions = ref<CustomerCompanyVO[]>([]) //客户公司列表
const deptOptions = ref<any[]>([]) // 部门树形结构
const reviewPath = ref('/根目录/项目管理/项目立项/评审附件') //评审附件路径
const winPath = ref('/根目录/项目管理/项目立项/中标附件') //中标附件路径
/** 提交表单 */
const submitForm = async () => {
// 校验表单
if (!formRef) return
const valid = await formRef.value.validate()
if (!valid) return
// 提交请求
formLoading.value = true
try {
const data = { ...formData.value } as unknown as ProjectVO
await ProjectApi.createProject(data)
message.success('发起成功')
// 关闭当前 Tab
delView(unref(currentRoute))
await push({ name: 'Project' })
} finally {
formLoading.value = false
}
}
/** 初始化 */
onMounted(async () => {
const processDefinitionDetail = await DefinitionApi.getProcessDefinition(
undefined,
processDefineKey
)
if (!processDefinitionDetail) {
message.error('项目立项的流程模型未配置,请检查!')
return
}
// 加载用户列表
userList.value = await UserApi.getSimpleUserList()
// 初始化项目经理为当前用户
formData.value.projectManagerId = useUserStore().getUser.id
// 加载部门
deptOptions.value = handleTree(await DeptApi.getSimpleDeptList())
// 加载地区
areaList.value = await AreaApi.getAreaTree()
// 客户公司列表
customerCompanyOptions.value = await CustomerCompanyApi.getCustomerCompanyList()
})
// 省份更新时,刷新城市选项
const onProvinceChange = (value: number)=>{
formData.value.cityId = undefined;
cityList.value = (areaList.value.find(({id})=>id===value) as any)?.children ?? [];
}
</script>

View File

@ -0,0 +1,123 @@
<template>
<ContentWrap>
<el-descriptions :column="2" border>
<el-descriptions-item label="名称" span="1">
{{ detailData.name }}
</el-descriptions-item>
<el-descriptions-item label="跟踪编号" span="1">
{{ detailData.trackingCode }}
</el-descriptions-item>
<el-descriptions-item label="项目编号" span="1">
{{ detailData.code }}
</el-descriptions-item>
<el-descriptions-item label="类型" span="1">
<dict-tag :type="DICT_TYPE.PROJECT_TYPE" :value="detailData.type" />
</el-descriptions-item>
<el-descriptions-item label="省份" span="1">
{{ detailData.province }}
</el-descriptions-item>
<el-descriptions-item label="城市" span="1">
{{ detailData.city }}
</el-descriptions-item>
<el-descriptions-item label="预计金额(万)" span="1">
{{ detailData.contractAmount }}
</el-descriptions-item>
<el-descriptions-item label="客户公司" :span="1">
{{ detailData.customerCompanyName }}
</el-descriptions-item>
<el-descriptions-item label="客户联系人" :span="1">
{{ detailData.customerUser }}
</el-descriptions-item>
<el-descriptions-item label="客户电话" span="1">
{{ detailData.customerPhone }}
</el-descriptions-item>
<el-descriptions-item label="出图公司" span="1">
{{ detailData.drawingCompany }}
</el-descriptions-item>
<el-descriptions-item label="跟踪部门" span="1">
{{ detailData.trackingDepName }}
</el-descriptions-item>
<el-descriptions-item label="项目经理" span="1">
{{ detailData.projectManagerName }}
</el-descriptions-item>
<el-descriptions-item label="开始时间" span="1">
{{ formatDate(detailData.startTime, 'YYYY-MM-DD') }}
</el-descriptions-item>
<el-descriptions-item label="跟踪情况" span="2">
{{ detailData.situation }}
</el-descriptions-item>
<el-descriptions-item label="落地可能性" span="1">
<dict-tag :type="DICT_TYPE.POSSIBILITY_OF_LANDING" :value="detailData.possibility" />
</el-descriptions-item>
<el-descriptions-item label="漏斗预期" span="1">
{{ detailData.funnelExpectation }}
</el-descriptions-item>
<el-descriptions-item label="委托方式" span="1">
<dict-tag :type="DICT_TYPE.ENTRUST_METHOD" :value="detailData.entrustMethod" />
</el-descriptions-item>
<el-descriptions-item label="是否落地" span="1">
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="detailData.confirmation" />
</el-descriptions-item>
<!-- <el-descriptions-item label="审批状态" span="1">-->
<!-- <dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="detailData.processStatus" />-->
<!-- </el-descriptions-item>-->
<el-descriptions-item label="落地时间" v-if="detailData.confirmation === '是' || detailData.confirmation === true" span="1">
{{ formatDate(detailData.endTime, 'YYYY-MM-DD') }}
</el-descriptions-item>
<el-descriptions-item label="未落地原因" v-if="detailData.confirmation === '否' || detailData.confirmation === false" span="2">
{{ detailData.reason }}
</el-descriptions-item>
</el-descriptions>
<el-descriptions :column="1" border>
<el-descriptions-item label="评审附件">
<UploadFile
v-model="detailData.reviewFileUrl"
:disabled="true"
:del-disabled="true"
/>
</el-descriptions-item>
<el-descriptions-item label="中标附件">
<UploadFile
v-model="detailData.winFileUrl"
:disabled="true"
:del-disabled="true"
/>
</el-descriptions-item>
</el-descriptions>
</ContentWrap>
</template>
<script lang="ts" setup>
import { DICT_TYPE } from '@/utils/dict'
import { formatDate } from '@/utils/formatTime'
import { propTypes } from '@/utils/propTypes'
import {ProjectApi} from "@/api/pms/project";
defineOptions({ name: 'ProjectBpmDetail' })
const { query } = useRoute() // 查询参数
const props = defineProps({
id: propTypes.number.def(undefined)
})
const detailLoading = ref(false) // 表单的加载中
const detailData = ref<any>({}) // 详情数据
const queryId = query.id as unknown as number // 从 URL 传递过来的 id 编号
/** 获得数据 */
const getInfo = async () => {
detailLoading.value = true
try {
detailData.value = await ProjectApi.getProject(props.id || queryId)
} finally {
detailLoading.value = false
}
}
defineExpose({ open: getInfo }) // 提供 open 方法,用于打开弹窗
/** 初始化 **/
onMounted(() => {
getInfo()
})
</script>

View File

@ -0,0 +1,245 @@
<template>
<ContentWrap>
<!-- 搜索工作栏 -->
<el-form
class="-mb-15px"
:model="queryParams"
ref="queryFormRef"
:inline="true"
label-width="68px"
>
<el-form-item label="名称" prop="name">
<el-input
v-model="queryParams.name"
placeholder="请输入名称"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="跟踪编号" prop="trackingCode">
<el-input
v-model="queryParams.trackingCode"
placeholder="请输入跟踪编号"
clearable
@keyup.enter="handleQuery"
class="!w-240px"
/>
</el-form-item>
<el-form-item label="类型" prop="type">
<el-select
v-model="queryParams.type"
placeholder="请选择类型"
clearable
class="!w-240px"
>
<el-option
v-for="dict in getStrDictOptions(DICT_TYPE.PROJECT_TYPE)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="项目经理" prop="projectManagerId">
<el-select
v-model="queryParams.projectManagerId"
clearable
placeholder="请选择项目经理"
class="!w-240px"
>
<el-option
v-for="item in userOptions"
:key="item.id"
:label="item.nickname"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
<el-button
type="primary"
plain
@click="openForm('create')"
v-hasPermi="['pms:project:create']"
>
<Icon icon="ep:plus" class="mr-5px" /> 新增
</el-button>
<el-button
type="success"
plain
@click="handleExport"
:loading="exportLoading"
v-hasPermi="['pms:project:export']"
>
<Icon icon="ep:download" class="mr-5px" /> 导出
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
<el-table-column label="名称" align="center" prop="name" />
<el-table-column label="跟踪编号" align="center" prop="trackingCode" />
<el-table-column label="跟踪部门" align="center" prop="trackingDepName" />
<el-table-column label="项目经理" align="center" prop="projectManagerName" />
<el-table-column label="类型" align="center" prop="type">
<template #default="scope">
<dict-tag :type="DICT_TYPE.PROJECT_TYPE" :value="scope.row.type" />
</template>
</el-table-column>
<el-table-column label="预计合同金额" align="center" prop="contractAmount" />
<el-table-column label="操作" align="center">
<template #default="scope">
<el-button
link
type="primary"
@click="openForm('update', scope.row.id)"
v-hasPermi="['pms:project:update']"
>
编辑
</el-button>
<el-button
v-hasPermi="['bpm:oa-leave:query']"
link
type="primary"
@click="handleProcessDetail(scope.row)"
>
进度
</el-button>
<el-button
link
type="danger"
@click="handleDelete(scope.row.id)"
v-hasPermi="['pms:project:delete']"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
<!-- 表单弹窗添加/修改 -->
<ProjectForm ref="formRef" @success="getList" />
</template>
<script setup lang="ts">
import { getStrDictOptions, DICT_TYPE } from '@/utils/dict'
import download from '@/utils/download'
import { ProjectApi, ProjectVO } from '@/api/pms/project'
import ProjectForm from './ProjectForm.vue'
import * as UserApi from '@/api/system/user'
/** 项目基本信息 列表 */
defineOptions({ name: 'Project' })
const message = useMessage() // 消息弹窗
const { t } = useI18n() // 国际化
const loading = ref(true) // 列表的加载中
const list = ref<ProjectVO[]>([]) // 列表的数据
const total = ref(0) // 列表的总页数
const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
const router = useRouter() // 路由
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
name: undefined,
trackingCode: undefined,
type: undefined,
projectManagerId: undefined
})
const queryFormRef = ref() // 搜索的表单
const exportLoading = ref(false) // 导出的加载中
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await ProjectApi.getProjectPage(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 获取用户列表 **/
const getUserList = async () => {
userOptions.value = await UserApi.getSimpleUserList()
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value.resetFields()
handleQuery()
}
/** 添加/修改操作 */
const formRef = ref()
const openForm = (type: string, id?: number) => {
formRef.value.open(type, id)
}
/** 删除按钮操作 */
const handleDelete = async (id: number) => {
try {
// 删除的二次确认
await message.delConfirm()
// 发起删除
await ProjectApi.deleteProject(id)
message.success(t('common.delSuccess'))
// 刷新列表
await getList()
} catch {}
}
/** 导出按钮操作 */
const handleExport = async () => {
try {
// 导出的二次确认
await message.exportConfirm()
// 发起导出
exportLoading.value = true
const data = await ProjectApi.exportProject(queryParams)
download.excel(data, '项目基本信息.xls')
} catch {
} finally {
exportLoading.value = false
}
}
/** 审批进度 */
const handleProcessDetail = (row) => {
router.push({
name: 'BpmProcessInstanceDetail',
query: {
id: row.processInstanceId
}
})
}
/** 初始化 **/
onMounted(() => {
getList()
// 获得用户列表
getUserList()
})
</script>