mirror of
https://gitee.com/hhyykk/ipms-sjy-ui.git
synced 2025-07-29 02:05:08 +08:00
Compare commits
36 Commits
feature-pm
...
feature-pr
Author | SHA1 | Date | |
---|---|---|---|
b7d868e397 | |||
f5c91daba2 | |||
fa8f650aa1 | |||
63fee3b541 | |||
583ce6d661 | |||
52a4cc210c | |||
2acbe2b228 | |||
a0e4ac5964 | |||
713b2cba0c | |||
edf2a5f188 | |||
31c73b5764 | |||
504911bd06 | |||
3b80a8a953 | |||
9013643093 | |||
1ce91dee0a | |||
2154f9c3e7 | |||
1e45f4d6ba | |||
bc4ecedb43 | |||
0aa66c1420 | |||
e87a5d8ba3 | |||
10726d7c4e | |||
83fc308eeb | |||
0480625210 | |||
e419951a2d | |||
76bc2a3916 | |||
5b7487eb4f | |||
f70f722fda | |||
7a68dccf9b | |||
7a6c93d7e7 | |||
a0bb5bb1d0 | |||
9cf69f42ba | |||
d9b9a0581e | |||
c8df5ed632 | |||
14a8fc6434 | |||
265dc4cdc8 | |||
38530fff07 |
12
.env
12
.env
@ -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
|
||||
|
@ -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
248
README.md
@ -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 免费开源的中后台模版,具备如下特性:
|
||||
|
||||

|
||||
|
||||
* **最新技术栈**:使用 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
|
||||
|
||||

|
||||
|
||||
② Spring Cloud 微服务架构:<https://cloud.iocoder.cn>
|
||||
|
||||

|
||||
|
||||
## 内置功能
|
||||
|
||||
系统内置多种多种业务功能,可以用于快速你的业务系统:
|
||||
|
||||
* 系统功能
|
||||
* 基础设施
|
||||
* 工作流程
|
||||
* 支付系统
|
||||
* 会员中心
|
||||
* 数据报表
|
||||
* 商城系统
|
||||
* 微信公众号
|
||||
* ERP 系统
|
||||
* CRM 系统
|
||||
|
||||
### 系统功能
|
||||
|
||||
| | 功能 | 描述 |
|
||||
|-----|-------|---------------------------------|
|
||||
| | 用户管理 | 用户是系统操作者,该功能主要完成系统用户配置 |
|
||||
| ⭐️ | 在线用户 | 当前系统中活跃用户状态监控,支持手动踢下线 |
|
||||
| | 角色管理 | 角色菜单权限分配、设置角色按机构进行数据范围权限划分 |
|
||||
| | 菜单管理 | 配置系统菜单、操作权限、按钮权限标识等,本地缓存提供性能 |
|
||||
| | 部门管理 | 配置系统组织机构(公司、部门、小组),树结构展现支持数据权限 |
|
||||
| | 岗位管理 | 配置系统用户所属担任职务 |
|
||||
| 🚀 | 租户管理 | 配置系统租户,支持 SaaS 场景下的多租户功能 |
|
||||
| 🚀 | 租户套餐 | 配置租户套餐,自定每个租户的菜单、操作、按钮的权限 |
|
||||
| | 字典管理 | 对系统中经常使用的一些较为固定的数据进行维护 |
|
||||
| 🚀 | 短信管理 | 短信渠道、短息模板、短信日志,对接阿里云、腾讯云等主流短信平台 |
|
||||
| 🚀 | 邮件管理 | 邮箱账号、邮件模版、邮件发送日志,支持所有邮件平台 |
|
||||
| 🚀 | 站内信 | 系统内的消息通知,提供站内信模版、站内信消息 |
|
||||
| 🚀 | 操作日志 | 系统正常操作日志记录和查询,集成 Swagger 生成日志内容 |
|
||||
| ⭐️ | 登录日志 | 系统登录日志记录查询,包含登录异常 |
|
||||
| 🚀 | 错误码管理 | 系统所有错误码的管理,可在线修改错误提示,无需重启服务 |
|
||||
| | 通知公告 | 系统通知公告信息发布维护 |
|
||||
| 🚀 | 敏感词 | 配置系统敏感词,支持标签分组 |
|
||||
| 🚀 | 应用管理 | 管理 SSO 单点登录的应用,支持多种 OAuth2 授权方式 |
|
||||
| 🚀 | 地区管理 | 展示省份、城市、区镇等城市信息,支持 IP 对应城市 |
|
||||
|
||||

|
||||
|
||||
### 工作流程
|
||||
|
||||
| | 功能 | 描述 |
|
||||
|----|-------|-----------------------------------------|
|
||||
| 🚀 | 流程模型 | 配置工作流的流程模型,支持 BPMN 和仿钉钉/飞书设计器 |
|
||||
| 🚀 | 流程表单 | 拖动表单元素生成相应的工作流表单,覆盖 Element UI 所有的表单组件 |
|
||||
| 🚀 | 用户分组 | 自定义用户分组,可用于工作流的审批分组 |
|
||||
| 🚀 | 我的流程 | 查看我发起的工作流程,支持新建、取消流程等操作,高亮流程图、审批时间线 |
|
||||
| 🚀 | 待办任务 | 查看自己【未】审批的工作任务,支持通过、不通过、转派、委派、退回、加减签等操作 |
|
||||
| 🚀 | 已办任务 | 查看自己【已】审批的工作任务,支持流程预测,展示未来审批人信息 |
|
||||
| 🚀 | OA 请假 | 作为业务自定义接入工作流的使用示例,只需创建请求对应的工作流程,即可进行审批 |
|
||||
|
||||

|
||||
|
||||
| BPMN 设计器 | 钉钉/飞书设计器 |
|
||||
|------------------------------|--------------------------------|
|
||||
|  |  |
|
||||
|
||||
### 支付系统
|
||||
|
||||
| | 功能 | 描述 |
|
||||
|-----|------|---------------------------|
|
||||
| 🚀 | 商户信息 | 管理商户信息,支持 Saas 场景下的多商户功能 |
|
||||
| 🚀 | 应用信息 | 配置商户的应用信息,对接支付宝、微信等多个支付渠道 |
|
||||
| 🚀 | 支付订单 | 查看用户发起的支付宝、微信等的【支付】订单 |
|
||||
| 🚀 | 退款订单 | 查看用户发起的支付宝、微信等的【退款】订单 |
|
||||
|
||||
ps:核心功能已经实现,正在对接微信小程序中...
|
||||
|
||||
### 基础设施
|
||||
|
||||
| | 功能 | 描述 |
|
||||
|----|----------|----------------------------------------------|
|
||||
| 🚀 | 代码生成 | 前后端代码的生成(Java、Vue、SQL、单元测试),支持 CRUD 下载 |
|
||||
| 🚀 | 系统接口 | 基于 Swagger 自动生成相关的 RESTful API 接口文档 |
|
||||
| 🚀 | 数据库文档 | 基于 Screw 自动生成数据库文档,支持导出 Word、HTML、MD 格式 |
|
||||
| | 表单构建 | 拖动表单元素生成相应的 HTML 代码,支持导出 JSON、Vue 文件 |
|
||||
| 🚀 | 配置管理 | 对系统动态配置常用参数,支持 SpringBoot 加载 |
|
||||
| ⭐️ | 定时任务 | 在线(添加、修改、删除)任务调度包含执行结果日志 |
|
||||
| 🚀 | 文件服务 | 支持将文件存储到 S3(MinIO、阿里云、腾讯云、七牛云)、本地、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 实现单元测试,保证功能的正确性、代码的质量等 |
|
||||
|
||||

|
||||
|
||||
### 数据报表
|
||||
|
||||
| | 功能 | 描述 |
|
||||
|-----|-------|--------------------|
|
||||
| 🚀 | 报表设计器 | 支持数据报表、图形报表、打印设计等 |
|
||||
| 🚀 | 大屏设计器 | 拖拽生成数据大屏,内置几十种图表组件 |
|
||||
|
||||
### 微信公众号
|
||||
|
||||
| | 功能 | 描述 |
|
||||
|-----|--------|-------------------------------|
|
||||
| 🚀 | 账号管理 | 配置接入的微信公众号,可支持多个公众号 |
|
||||
| 🚀 | 数据统计 | 统计公众号的用户增减、累计用户、消息概况、接口分析等数据 |
|
||||
| 🚀 | 粉丝管理 | 查看已关注、取关的粉丝列表,可对粉丝进行同步、打标签等操作 |
|
||||
| 🚀 | 消息管理 | 查看粉丝发送的消息列表,可主动回复粉丝消息 |
|
||||
| 🚀 | 自动回复 | 自动回复粉丝发送的消息,支持关注回复、消息回复、关键字回复 |
|
||||
| 🚀 | 标签管理 | 对公众号的标签进行创建、查询、修改、删除等操作 |
|
||||
| 🚀 | 菜单管理 | 自定义公众号的菜单,也可以从公众号同步菜单 |
|
||||
| 🚀 | 素材管理 | 管理公众号的图片、语音、视频等素材,支持在线播放语音、视频 |
|
||||
| 🚀 | 图文草稿箱 | 新增常用的图文素材到草稿箱,可发布到公众号 |
|
||||
| 🚀 | 图文发表记录 | 查看已发布成功的图文素材,支持删除操作 |
|
||||
|
||||
### 商城系统
|
||||
|
||||
演示地址:<https://doc.iocoder.cn/mall-preview/>
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### ERP 系统
|
||||
|
||||
演示地址:<https://doc.iocoder.cn/erp-preview/>
|
||||
|
||||

|
||||
|
||||
### CRM 系统
|
||||
|
||||
演示地址:<https://doc.iocoder.cn/crm-preview/>
|
||||
|
||||

|
||||
|
||||
## 🐷 演示图
|
||||
|
||||
### 系统功能
|
||||
|
||||
| 模块 | biu | biu | biu |
|
||||
|----------|-----------------------------|---------------------------|--------------------------|
|
||||
| 登录 & 首页 |  |  |  |
|
||||
| 用户 & 应用 |  |  |  |
|
||||
| 租户 & 套餐 |  |  | - |
|
||||
| 部门 & 岗位 |  |  | - |
|
||||
| 菜单 & 角色 |  |  | - |
|
||||
| 审计日志 |  |  | - |
|
||||
| 短信 |  |  |  |
|
||||
| 字典 & 敏感词 |  |  |  |
|
||||
| 错误码 & 通知 |  |  | - |
|
||||
|
||||
### 工作流程
|
||||
|
||||
| 模块 | biu | biu | biu |
|
||||
|---------|---------------------------------|---------------------------------|---------------------------------|
|
||||
| 流程模型 |  |  |  |
|
||||
| 表单 & 分组 |  |  | - |
|
||||
| 我的流程 |  |  |  |
|
||||
| 待办 & 已办 |  |  |  |
|
||||
| OA 请假 |  |  |  |
|
||||
|
||||
### 基础设施
|
||||
|
||||
| 模块 | biu | biu | biu |
|
||||
|---------------|-------------------------------|-----------------------------|---------------------------|
|
||||
| 代码生成 |  |  | - |
|
||||
| 文档 |  |  | - |
|
||||
| 文件 & 配置 |  |  |  |
|
||||
| 定时任务 |  |  | - |
|
||||
| API 日志 |  |  | - |
|
||||
| MySQL & Redis |  |  | - |
|
||||
| 监控平台 |  |  |  |
|
||||
|
||||
### 支付系统
|
||||
|
||||
| 模块 | biu | biu | biu |
|
||||
|---------|---------------------------|---------------------------------|---------------------------------|
|
||||
| 商家 & 应用 |  |  |  |
|
||||
| 支付 & 退款 |  |  | --- |
|
||||
|
||||
### 数据报表
|
||||
|
||||
| 模块 | biu | biu | biu |
|
||||
|-------|---------------------------------|---------------------------------|---------------------------------------|
|
||||
| 报表设计器 |  |  |  |
|
||||
| 大屏设计器 |  |  |  |
|
||||
# 启动服务
|
||||
npm run dev
|
Binary file not shown.
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 93 KiB |
BIN
public/logo.gif
BIN
public/logo.gif
Binary file not shown.
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 120 KiB |
48
src/api/cms/customerCompany/index.ts
Normal file
48
src/api/cms/customerCompany/index.ts
Normal 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`})
|
||||
},
|
||||
}
|
43
src/api/infra/category/index.ts
Normal file
43
src/api/infra/category/index.ts
Normal 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 })
|
||||
}
|
||||
}
|
@ -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 })
|
||||
}
|
||||
|
66
src/api/pms/project/index.ts
Normal file
66
src/api/pms/project/index.ts
Normal 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 |
@ -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>
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
// 图片上传错误提示
|
||||
|
@ -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! }))
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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>
|
||||
|
@ -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: `扫码后点击"确认",即可完成登录`,
|
||||
|
@ -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'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
|
@ -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 数据校验级别
|
||||
|
@ -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/21,Vue 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>
|
||||
|
@ -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--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>-->
|
||||
|
||||
<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--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--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--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--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--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'-->
|
||||
|
||||
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 {-->
|
||||
<!-- &--peoples {-->
|
||||
<!-- color: #40c9c6;-->
|
||||
<!-- }-->
|
||||
|
||||
&--message {
|
||||
color: #36a3f7;
|
||||
}
|
||||
<!-- &--message {-->
|
||||
<!-- color: #36a3f7;-->
|
||||
<!-- }-->
|
||||
|
||||
&--money {
|
||||
color: #f4516c;
|
||||
}
|
||||
<!-- &--money {-->
|
||||
<!-- color: #f4516c;-->
|
||||
<!-- }-->
|
||||
|
||||
&--shopping {
|
||||
color: #34bfa3;
|
||||
}
|
||||
<!-- &--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--icon {-->
|
||||
<!-- transition: all 0.38s ease-out;-->
|
||||
<!-- }-->
|
||||
|
||||
.#{$prefix-cls}__item--peoples {
|
||||
background: #40c9c6;
|
||||
}
|
||||
<!-- .#{$prefix-cls}__item--peoples {-->
|
||||
<!-- background: #40c9c6;-->
|
||||
<!-- }-->
|
||||
|
||||
.#{$prefix-cls}__item--message {
|
||||
background: #36a3f7;
|
||||
}
|
||||
<!-- .#{$prefix-cls}__item--message {-->
|
||||
<!-- background: #36a3f7;-->
|
||||
<!-- }-->
|
||||
|
||||
.#{$prefix-cls}__item--money {
|
||||
background: #f4516c;
|
||||
}
|
||||
<!-- .#{$prefix-cls}__item--money {-->
|
||||
<!-- background: #f4516c;-->
|
||||
<!-- }-->
|
||||
|
||||
.#{$prefix-cls}__item--shopping {
|
||||
background: #34bfa3;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<!-- .#{$prefix-cls}__item--shopping {-->
|
||||
<!-- background: #34bfa3;-->
|
||||
<!-- }-->
|
||||
<!-- }-->
|
||||
<!-- }-->
|
||||
<!--}-->
|
||||
<!--</style>-->
|
||||
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -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: '',
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
|
108
src/views/cms/customerCompany/CustomerCompanyForm.vue
Normal file
108
src/views/cms/customerCompany/CustomerCompanyForm.vue
Normal 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>
|
221
src/views/cms/customerCompany/index.vue
Normal file
221
src/views/cms/customerCompany/index.vue
Normal 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>
|
131
src/views/infra/category/CategoryForm.vue
Normal file
131
src/views/infra/category/CategoryForm.vue
Normal 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>
|
210
src/views/infra/category/FolderTree.vue
Normal file
210
src/views/infra/category/FolderTree.vue
Normal 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>
|
194
src/views/infra/category/index.vue
Normal file
194
src/views/infra/category/index.vue
Normal 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>
|
@ -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" />
|
||||
|
437
src/views/pms/project/ProjectForm.vue
Normal file
437
src/views/pms/project/ProjectForm.vue
Normal 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>
|
373
src/views/pms/project/bpm/ProjectBpmCreate.vue
Normal file
373
src/views/pms/project/bpm/ProjectBpmCreate.vue
Normal 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>
|
123
src/views/pms/project/bpm/ProjectBpmDetail.vue
Normal file
123
src/views/pms/project/bpm/ProjectBpmDetail.vue
Normal 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>
|
245
src/views/pms/project/index.vue
Normal file
245
src/views/pms/project/index.vue
Normal 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>
|
Reference in New Issue
Block a user