Compare commits

..

27 Commits

Author SHA1 Message Date
zhangjf
e5d9db10a8 feat: requirement表添加remark字段 2026-02-23 16:42:41 +08:00
zhangjf
852af7ee26 fix: 远程执行命令时加载环境变量
- 使用 bash -l 加载登录 shell 环境变量
- 解决 SSH 非交互式 shell 不加载 .bash_profile 导致 java 命令找不到的问题
2026-02-23 15:51:41 +08:00
zhangjf
112a970563 fix: 服务部署脚本优化,不再上传整个tar.gz
- 本地解压tar.gz提取jar文件
- 只上传服务jar和fund-common*.jar
- 大幅减少上传时间和带宽消耗
2026-02-23 15:35:17 +08:00
zhangjf
2a74f237df fix: 修复部署脚本路径问题
- 使用绝对路径获取项目根目录
- 修正打包文件路径引用
2026-02-23 15:26:23 +08:00
zhangjf
fe51e87c17 fix: 调整服务部署脚本
- 启动脚本路径改为 bin 目录
- 只更新服务jar和fund-common*.jar,不再全量覆盖lib目录
2026-02-23 15:08:04 +08:00
zhangjf
1e346c3a2e feat: 添加生产环境部署脚本
- deploy-config.sh: 部署配置文件(服务器信息、路径配置)
- deploy-mobile.sh: 移动端部署脚本
- deploy-admin.sh: 管理后台部署脚本
- deploy-service.sh: 服务部署脚本(只更新lib目录)
- deploy-all.sh: 全量部署脚本
2026-02-23 15:03:14 +08:00
zhangjf
06dfa26514 fix: 修复需求查询参数名requirementTitle改为requirementName 2026-02-23 14:47:23 +08:00
zhangjf
df2f1cdfa2 fix: 项目列表字段对齐,ProjectVO的id改为projectId,添加customerName 2026-02-23 14:36:59 +08:00
zhangjf
83e8712dfc fix: 新增支出页面添加收款单位必填字段 2026-02-23 14:23:27 +08:00
zhangjf
9498201313 fix: 新增需求页面添加需求编号和客户ID字段,修正需求名称字段 2026-02-23 14:14:38 +08:00
zhangjf
f87ee0b51d feat: 移动端新增项目添加项目编码和项目类型字段 2026-02-23 14:07:23 +08:00
zhangjf
ff9f4d05ad fix: 移动端列表页首次加载pageNum从1开始
**问题:**
- van-list组件的@load事件在挂载时自动触发
- 导致onLoad先执行pageNum++,首次请求时pageNum变成2

**修复:**
- 在onMounted中主动加载第一页数据
- onLoad只处理加载更多逻辑
- 统一所有列表页:customer, project, expense, requirement, receivable
2026-02-23 13:43:43 +08:00
zhangjf
9b545b3f00 fix: 客户列表查询失败问题修复
**问题根因:**
1. PageResult返回字段list与前端期望records不一致
2. CustomerVO字段名与前端期望不一致

**修改内容:**
- fund-common: PageResult字段list改为records
- fund-cust: CustomerVO字段id改为customerId
- fund-mobile: 客户列表页面字段对齐
- fund-admin: 客户管理页面字段对齐
2026-02-23 13:26:07 +08:00
zhangjf
011a6bfb3f fix: 移动端新增客户表单与后端字段对齐
- 新增联系人(contact)字段,设为必填
- 客户编码(customerCode)设为必填
- 移除后端不支持的字段:简称、客户等级、所属行业
- 表单字段与后端CustomerCreateDTO保持一致
2026-02-23 13:14:17 +08:00
zhangjf
965d98cab5 feat: 移动端新增客户页面添加客户编码字段
- 新增客户编码输入框(customerCode)
- 位于客户名称字段之前
2026-02-23 13:05:57 +08:00
zhangjf
69f437dbb3 fix: 移动端表单优化
1. 新增页面必填项标记
   - expense/Add.vue: 支出标题、支出类型、支出金额添加红色星号必填标记
   - 其他使用 van-form 的页面已有 required 属性标记

2. 错误提示优化
   - 将 showToast 改为 showFailToast 显示错误提示
   - showFailToast 使用红色背景,错误信息更清晰可见
   - 涉及文件:
     - expense/Add.vue
     - requirement/Add.vue
     - receivable/Add.vue
     - project/Add.vue
     - customer/Add.vue
     - my/ChangePassword.vue
2026-02-23 12:56:52 +08:00
zhangjf
b5a954f008 fix: 修复列表页浮动添加按钮被底部Tabbar遮挡的问题
将列表页浮动添加按钮的bottom值从24px调整为80px:
- receivable/List.vue
- expense/List.vue
- requirement/List.vue

Tabbar高度64px,添加按钮需要预留足够空间避免被遮挡
2026-02-23 12:43:42 +08:00
zhangjf
205af48cb6 fix: 修复底部工具栏遮挡页面提交按钮的问题
1. 在App.vue中统一添加padding-bottom: 80px
2. 移除各列表页面重复的padding-bottom设置
   - expense/List.vue
   - requirement/List.vue
   - receivable/List.vue
2026-02-23 12:37:20 +08:00
zhangjf
400b7272d4 feat: 移动端新增修改密码功能
1. 新增修改密码页面 (my/ChangePassword.vue)
   - 支持输入旧密码、新密码、确认密码
   - 密码验证:至少6位、两次输入一致性校验
   - 修改成功后自动清除登录信息并跳转到登录页

2. 新增API接口 (updatePassword)
   - PUT /sys/profile/password
   - 参数: oldPassword, newPassword, confirmPassword

3. 更新路由配置
   - 新增 /my/change-password 路由

4. 更新我的页面
   - 修改密码点击跳转到修改密码页面
2026-02-23 12:32:36 +08:00
zhangjf
e7f1b39ac8 style: 移动端首页UI调整
1. 删除顶部标题区域,节省页面垂直空间
2. 快捷操作调整为2行布局(第一行3个,第二行2个)
3. 优化快捷操作图标和文字大小
2026-02-23 12:26:58 +08:00
zhangjf
d3a77c23f1 feat: 移动端首页重构与业务模块完善
1. 首页布局调整
   - 保留今日概览板块
   - 快捷操作板块:新增需求工单、新增应收款、新增支出、新增项目、新增客户
   - 新增业务服务板块:需求工单、应收款管理、支出管理、项目管理、客户管理入口

2. 新增页面
   - 需求工单:列表页(支持搜索)、新增页
   - 支出管理:列表页(支持搜索)、保留新增页
   - 应收款:新增页、列表页添加搜索功能
   - 项目:新增页、列表页优化搜索参数
   - 客户:新增页、列表页优化搜索参数

3. API更新
   - 新增需求工单相关API(getRequirementList、getRequirementById、createRequirement)
   - 新增项目新增API(createProject)
   - 新增客户新增API(createCustomer)
   - 新增应收款新增API(createReceivable)
   - 更新搜索参数为统一的keyword格式

4. 路由更新
   - 新增需求工单列表/新增路由
   - 新增支出管理列表路由
   - 新增应收款新增路由
   - 新增项目新增路由
   - 新增客户新增路由
2026-02-23 11:51:52 +08:00
zhangjf
5e782ac8cc docs: 增加前端API集中管理设计规范
1. 架构设计文档 v1.7 -> v1.8
   新增 6.1.5 API集中管理规范:
   - 目录结构示例:api/index.ts + api/request.ts + api/modules/
   - api/index.ts 代码示例
   - Vue组件调用示例(正确vs错误对比)
   - 规范要求:禁止硬编码、统一入口、路径简化、便于维护

2. Agents.md v1.2 -> v1.3
   新增 5.6 前端API集中管理规范:
   - 目录结构说明
   - 规范要求表格
   - 正确/错误代码示例对比

规范要点:
- 采用独立目录或文件集中管理后台API请求
- Vue组件中禁止直接使用 request.get('/xxx/xxx') 硬编码URL
- 所有API函数从 @/api 统一导出,按模块分组
- API路径变更时只需修改 api/index.ts 一处
2026-02-23 11:26:18 +08:00
zhangjf
2e7fb5f5d4 fix: 修复移动端API路径错误,统一归集到api/index.ts
问题:
- Vue文件中直接使用错误的API路径 /api/v1/xxx
- 导致请求URL重复包含/api/v1,被当作静态资源处理

修复:
1. 重构src/api/index.ts,按模块分类集中定义所有API
   - 用户认证:login, getUserInfo, logout
   - 项目管理:getProjectList, getProjectById
   - 客户管理:getCustomerList
   - 支出管理:createExpense, getExpenseList, getExpenseTypeTree, getTodayExpense
   - 应收款管理:getReceivableList, getUpcomingDueList, getTodayIncome, getUnpaidAmount, getOverdueCount

2. 修复各Vue文件,使用集中的API定义
   - Home.vue: 使用getTodayIncome, getTodayExpense, getUnpaidAmount
   - receivable/List.vue: 使用getReceivableList
   - expense/Add.vue: 使用createExpense, getExpenseTypeTree
   - Login.vue: 使用login

正确的API路径:
- 前端请求: /fund/receipt/receivable/page
- Gateway转发: /api/v1/receipt/receivable/page
2026-02-23 11:18:44 +08:00
zhangjf
610054918a docs: 架构设计文档和Agents.md增加前端部署路径设计
1. 架构设计文档 v1.6 -> v1.7
   新增 6.1 前端部署路径设计章节:
   - 6.1.1 部署路径规划表
   - 6.1.2 Nginx配置示例
   - 6.1.3 前端构建配置(VITE_BASE等)
   - 6.1.4 API请求路径规范
   - 原有章节编号顺延(6.1->6.3, 6.2->6.4)

2. Agents.md v1.1 -> v1.2
   15.2 访问地址 改为 15.2 前端部署路径:
   - 新增前端部署路径规划表
   - 补充移动端H5开发环境访问地址
2026-02-23 11:01:19 +08:00
zhangjf
f8e0a51314 docs: 更新文档中的前端部署路径地址
部署路径更新:
- fund-admin 部署路径: /fadmin/
- fund-mobile 部署路径: /fmobile/
- API网关前缀: /fund

更新内容:
1. 部署运维文档.md
   - 本地开发访问地址添加移动端H5
   - Docker Compose架构图更新前端访问路径
   - 服务清单添加访问路径说明
   - Nginx配置示例更新为子路径部署

2. 单机部署文档.md
   - 部署架构图更新前端路径
   - 前端打包说明添加部署脚本和子路径说明
   - 新增4.5 Nginx配置章节
   - 添加前端解压部署步骤
2026-02-23 10:53:04 +08:00
zhangjf
807f894828 fix: 修复Login.vue登录API路径错误
- 将 '/sys/api/v1/auth/login' 修正为 '/auth/login'
- baseURL已配置为/fund,最终请求地址为 /fund/auth/login
2026-02-23 10:47:30 +08:00
zhangjf
1a5b583c2f feat: fund-mobile支持Nginx子路径/fmobile部署
- 新增.env.development/.env.production环境配置
- vite.config.ts支持VITE_BASE动态base路径
- router使用import.meta.env.BASE_URL
- API baseURL使用环境变量
- 新增vite-env.d.ts类型声明
2026-02-23 10:28:51 +08:00
45 changed files with 2927 additions and 282 deletions

View File

@ -1,8 +1,8 @@
# 资金服务平台 (FundPlatform) - 开发规范 # 资金服务平台 (FundPlatform) - 开发规范
> **文档版本**: v1.1 > **文档版本**: v1.3
> **创建日期**: 2026-02-20 > **创建日期**: 2026-02-20
> **更新日期**: 2026-02-13 > **更新日期**: 2026-02-23
> **适用范围**: 本项目所有开发人员 > **适用范围**: 本项目所有开发人员
--- ---
@ -264,6 +264,52 @@ GET /cust/api/v1/customer/page # 分页查询
| 404 | 资源不存在 | | 404 | 资源不存在 |
| 500 | 服务器内部错误 | | 500 | 服务器内部错误 |
### 5.6 前端API集中管理规范
前端项目必须采用**独立目录或文件集中管理**对后台API的请求便于调整和优化。
**目录结构**
```
fund-admin/src/api/ 或 fund-mobile/src/api/
├── index.ts # API集中定义入口统一导出
├── request.ts # Axios实例配置、拦截器
└── modules/ # 按业务模块拆分(可选)
├── auth.ts # 用户认证API
├── customer.ts # 客户管理API
└── expense.ts # 支出管理API
```
**规范要求**
| 要求 | 说明 |
|------|------|
| 禁止硬编码 | Vue组件中禁止直接使用 `request.get('/xxx/xxx')` 形式 |
| 统一入口 | 所有API函数从 `@/api` 统一导出,按模块分组 |
| 路径简化 | API函数内部使用简化路径由 baseURL 统一添加前缀 |
| 便于维护 | 后端接口变更时,只需修改 api/index.ts 一处 |
**正确示例**
```typescript
// api/index.ts - 集中定义
export function getReceivableList(params: { pageNum: number; pageSize: number }) {
return request.get('/receipt/receivable/page', { params })
}
// Vue组件 - 使用集中定义
import { getReceivableList } from '@/api'
const res = await getReceivableList({ pageNum: 1, pageSize: 10 })
```
**错误示例**
```typescript
// Vue组件 - 硬编码URL禁止
import request from '@/api/request'
const res = await request.get('/receipt/api/v1/receipt/receivable/page')
```
--- ---
## 六、多租户规范 ## 六、多租户规范
@ -911,11 +957,22 @@ public class TokenAuthFilter implements GlobalFilter {
| `TENANT_ID` | 租户标识 | 空值=共享实例,有值=VIP实例 | | `TENANT_ID` | 租户标识 | 空值=共享实例,有值=VIP实例 |
| `SERVER_PORT` | 服务端口 | 可选覆盖application.yml | | `SERVER_PORT` | 服务端口 | 可选覆盖application.yml |
### 15.2 访问地址 ### 15.2 前端部署路径
前端项目采用 **Nginx 子路径部署** 模式:
| 前端项目 | 部署路径 | 访问地址 | 说明 |
|----------|----------|----------|------|
| fund-admin | `/fadmin/` | `http://host/fadmin/` | 管理后台 |
| fund-mobile | `/fmobile/` | `http://host/fmobile/` | 移动端H5 |
| API网关 | `/fund/` | `http://host/fund/` | 后端API统一前缀 |
**开发环境访问地址**
| 服务 | 地址 | | 服务 | 地址 |
|------|------| |------|------|
| 管理后台 | http://localhost:3000 | | 管理后台 | http://localhost:3000 |
| 移动端H5 | http://localhost:8080 |
| API网关 | http://localhost:8000 | | API网关 | http://localhost:8000 |
| Nacos控制台 | http://localhost:8048/nacos | | Nacos控制台 | http://localhost:8048/nacos |

View File

@ -62,6 +62,7 @@ CREATE TABLE IF NOT EXISTS requirement (
receivable_date DATE COMMENT '应收款日期', receivable_date DATE COMMENT '应收款日期',
status VARCHAR(20) DEFAULT 'pending' COMMENT '状态pending-待开发developing-开发中delivered-已交付completed-已完成', status VARCHAR(20) DEFAULT 'pending' COMMENT '状态pending-待开发developing-开发中delivered-已交付completed-已完成',
progress INT DEFAULT 0 COMMENT '开发进度0-100', progress INT DEFAULT 0 COMMENT '开发进度0-100',
remark VARCHAR(500) COMMENT '备注',
attachment_url VARCHAR(500) COMMENT '附件URL', attachment_url VARCHAR(500) COMMENT '附件URL',
created_by BIGINT COMMENT '创建人ID', created_by BIGINT COMMENT '创建人ID',
created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',

View File

@ -17,7 +17,7 @@
│ │ │ │
│ ┌─────────────────────────────────────────────────────────┐ │ │ ┌─────────────────────────────────────────────────────────┐ │
│ │ 前端服务 │ │ │ │ 前端服务 │ │
│ │ fund-admin (Nginx:80) fund-mobile (Nginx:81) │ │ │ │ fund-admin (/fadmin/) fund-mobile (/fmobile/) │ │
│ └─────────────────────────┬───────────────────────────────┘ │ │ └─────────────────────────┬───────────────────────────────┘ │
│ │ │ │ │ │
│ ┌─────────────────────────▼───────────────────────────────┐ │ │ ┌─────────────────────────▼───────────────────────────────┐ │
@ -431,7 +431,17 @@ fundplatform/
#### 4.2.4 前端打包 #### 4.2.4 前端打包
前端项目采用 Nginx 子路径部署方式:
- **管理后台** (fund-admin): 部署路径 `/fadmin/`
- **移动端H5** (fund-mobile): 部署路径 `/fmobile/`
- **API网关前缀**: `/fund`
```bash ```bash
# 使用部署脚本打包(推荐)
./scripts/deploy-frontend-nginx.sh admin # 管理后台
./scripts/deploy-frontend-nginx.sh mobile # 移动端H5
# 或手动打包
# 管理后台打包 # 管理后台打包
cd fund-admin cd fund-admin
npm install npm install
@ -485,6 +495,54 @@ tar -xzf fund-receipt.tar.gz -C /opt/fundplatform/deploy/
tar -xzf fund-report.tar.gz -C /opt/fundplatform/deploy/ tar -xzf fund-report.tar.gz -C /opt/fundplatform/deploy/
tar -xzf fund-file.tar.gz -C /opt/fundplatform/deploy/ tar -xzf fund-file.tar.gz -C /opt/fundplatform/deploy/
# 解压前端发布包
mkdir -p /opt/fundplatform/web
unzip fund-admin-nginx.zip -d /opt/fundplatform/web/admin/
unzip fund-mobile-nginx.zip -d /opt/fundplatform/web/mobile/
# 复制Nginx配置
cp /opt/fundplatform/web/admin/nginx.conf /etc/nginx/conf.d/fadmin.conf
cp /opt/fundplatform/web/mobile/nginx.conf /etc/nginx/conf.d/fmobile.conf
```
### 4.5 Nginx 配置
前端采用子路径部署Nginx 配置示例:
```nginx
# /etc/nginx/conf.d/fundplatform.conf
server {
listen 80;
server_name localhost;
# 管理后台 (部署路径: /fadmin/)
location /fadmin/ {
alias /opt/fundplatform/web/admin/;
try_files $uri $uri/ /fadmin/index.html;
}
# 移动端H5 (部署路径: /fmobile/)
location /fmobile/ {
alias /opt/fundplatform/web/mobile/;
try_files $uri $uri/ /fmobile/index.html;
}
# API代理 (网关前缀: /fund)
location /fund/ {
proxy_pass http://127.0.0.1:8000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
**访问地址**
- 管理后台: `http://服务器IP/fadmin/`
- 移动端H5: `http://服务器IP/fmobile/`
```bash ```bash
# 上传一键管理脚本到deploy目录在解压服务包后执行 # 上传一键管理脚本到deploy目录在解压服务包后执行
# 一键管理脚本位于项目根目录scripts目录需要单独上传 # 一键管理脚本位于项目根目录scripts目录需要单独上传
@ -494,7 +552,7 @@ cp scripts/restart-all.sh /opt/fundplatform/deploy/
cp scripts/status-all.sh /opt/fundplatform/deploy/ cp scripts/status-all.sh /opt/fundplatform/deploy/
``` ```
### 4.5 配置文件修改 ### 4.6 配置文件修改
各服务配置文件需要根据实际环境修改以下配置: 各服务配置文件需要根据实际环境修改以下配置:
@ -521,7 +579,7 @@ spring:
namespace: fund-platform namespace: fund-platform
``` ```
### 4.6 服务管理脚本 ### 4.7 服务管理脚本
每个服务的 `bin` 目录下包含以下脚本: 每个服务的 `bin` 目录下包含以下脚本:

View File

@ -1,8 +1,8 @@
# 资金服务平台 (FundPlatform) - 架构设计文档 # 资金服务平台 (FundPlatform) - 架构设计文档
> **文档版本**: v1.6 > **文档版本**: v1.8
> **创建日期**: 2026-02-13 > **创建日期**: 2026-02-13
> **更新日期**: 2026-02-13 > **更新日期**: 2026-02-23
> **项目名称**: 资金服务平台 > **项目名称**: 资金服务平台
> **项目代号**: fundplatform > **项目代号**: fundplatform
@ -3460,7 +3460,164 @@ public class UserController {
## 六、部署架构 ## 六、部署架构
### 6.1 生产环境部署 ### 6.1 前端部署路径设计
前端项目采用 **Nginx 子路径部署** 模式,通过统一的 Nginx 入口提供静态资源服务。
#### 6.1.1 部署路径规划
| 前端项目 | 部署路径 | 访问地址 | 说明 |
|----------|----------|----------|------|
| fund-admin | `/fadmin/` | `http://host/fadmin/` | 管理后台 |
| fund-mobile | `/fmobile/` | `http://host/fmobile/` | 移动端H5 |
| API网关 | `/fund/` | `http://host/fund/` | 后端API统一前缀 |
#### 6.1.2 Nginx 配置示例
```nginx
server {
listen 80;
server_name localhost;
# 管理后台前端 (部署路径: /fadmin/)
location /fadmin/ {
alias /opt/fundplatform/web/admin/;
try_files $uri $uri/ /fadmin/index.html;
}
# 移动端H5 (部署路径: /fmobile/)
location /fmobile/ {
alias /opt/fundplatform/web/mobile/;
try_files $uri $uri/ /fmobile/index.html;
}
# API代理 (网关前缀: /fund)
location /fund/ {
proxy_pass http://127.0.0.1:8000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
#### 6.1.3 前端构建配置
前端项目需配置 `VITE_BASE` 环境变量以支持子路径部署:
**fund-admin/.env.production**:
```properties
VITE_BASE=/fadmin/
VITE_API_BASE_URL=/fund
```
**fund-mobile/.env.production**:
```properties
VITE_BASE=/fmobile/
VITE_API_BASE_URL=/fund
```
**vite.config.ts** 配置动态 base 路径:
```typescript
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd())
const base = env.VITE_BASE || '/'
return {
base,
// ...其他配置
}
})
```
**Vue Router** 配置动态 base 路径:
```typescript
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes
})
```
#### 6.1.4 API请求路径规范
前端所有API请求统一使用网关前缀 `/fund`由Gateway转发到后端服务
```
前端请求 Gateway转发 后端服务
/fund/auth/login -> /api/v1/auth/login -> fund-sys
/fund/customer/page -> /api/v1/customer/page -> fund-cust
/fund/project/page -> /api/v1/project/page -> fund-proj
/fund/exp/expense -> /api/v1/exp/expense -> fund-exp
/fund/receipt/receivable -> /api/v1/receipt/receivable -> fund-receipt
```
#### 6.1.5 API集中管理规范
前端项目必须采用**独立目录或文件集中管理**对后台API的请求便于调整和优化。
**目录结构示例**
```
fund-mobile/src/
└── api/
├── index.ts # API集中定义入口统一导出
├── request.ts # Axios实例配置、拦截器
└── modules/ # 按业务模块拆分(可选)
├── auth.ts # 用户认证API
├── customer.ts # 客户管理API
└── expense.ts # 支出管理API
```
**api/index.ts 示例**
```typescript
import request from './request'
// ===================== 用户认证 =====================
export function login(data: { username: string; password: string }) {
return request.post('/auth/login', data)
}
export function getUserInfo() {
return request.get('/auth/info')
}
// ===================== 支出管理 =====================
export function createExpense(data: any) {
return request.post('/exp/expense', data)
}
export function getExpenseTypeTree() {
return request.get('/exp/expense-type/tree')
}
// ===================== 应收款管理 =====================
export function getReceivableList(params: { pageNum: number; pageSize: number }) {
return request.get('/receipt/receivable/page', { params })
}
```
**Vue组件调用示例**
```typescript
// 正确使用集中定义的API函数
import { getReceivableList, createExpense } from '@/api'
const res = await getReceivableList({ pageNum: 1, pageSize: 10 })
// 错误直接在组件中硬编码URL
import request from '@/api/request'
const res = await request.get('/receipt/api/v1/receipt/receivable/page')
```
**规范要求**
1. **禁止硬编码**Vue组件中禁止直接使用 `request.get('/xxx/xxx')` 形式调用API
2. **统一入口**所有API函数从 `@/api` 统一导出,按模块分组
3. **路径简化**API函数内部使用简化路径`/auth/login`),由 request.ts 的 baseURL 统一添加前缀
4. **便于维护**:后端接口变更时,只需修改 api/index.ts 一处即可
### 6.3 生产环境部署
``` ```
┌─────────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────────┐
@ -3511,9 +3668,9 @@ public class UserController {
└─────────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────────┘
``` ```
### 6.2 容器化部署 ### 6.4 容器化部署
#### 6.2.1 Docker Compose 配置 #### 6.4.1 Docker Compose 配置
```yaml ```yaml
version: '3.8' version: '3.8'
@ -3978,6 +4135,8 @@ public class GlobalExceptionHandler {
| v1.4 | 2026-02-13 | 补充统一全局上下文 GlobalContext统筹 tid/uid/uname 获取和异步传递 | zhangjf | | v1.4 | 2026-02-13 | 补充统一全局上下文 GlobalContext统筹 tid/uid/uname 获取和异步传递 | zhangjf |
| v1.5 | 2026-02-13 | 补充模块通信与 OpenFeign 参数对象管理策略、分层架构职责说明、MyBatis-Plus 使用规范、Controller 与参数校验规范、事务与测试规范及开发规则总览 | zhangjf | | v1.5 | 2026-02-13 | 补充模块通信与 OpenFeign 参数对象管理策略、分层架构职责说明、MyBatis-Plus 使用规范、Controller 与参数校验规范、事务与测试规范及开发规则总览 | zhangjf |
| v1.6 | 2026-02-13 | 补充单机部署配置配置文件分离架构env.properties+service.properties、打包目录结构、多租户部署配置、日志配置集中化、脚本加载逻辑 | zhangjf | | v1.6 | 2026-02-13 | 补充单机部署配置配置文件分离架构env.properties+service.properties、打包目录结构、多租户部署配置、日志配置集中化、脚本加载逻辑 | zhangjf |
| v1.7 | 2026-02-23 | 新增6.1前端部署路径设计Nginx子路径部署、部署路径规划、Nginx配置示例、前端构建配置、API请求路径规范 | zhangjf |
| v1.8 | 2026-02-23 | 新增6.1.5 API集中管理规范独立目录管理、禁止硬编码、统一入口、路径简化、便于维护 | zhangjf |
--- ---

View File

@ -158,6 +158,7 @@ npm run dev
# 4. 访问系统 # 4. 访问系统
# 管理后台http://localhost:3000 # 管理后台http://localhost:3000
# 移动端H5http://localhost:8080
# 网关地址http://localhost:8000 # 网关地址http://localhost:8000
# Nacos 控制台http://localhost:8048/nacos # Nacos 控制台http://localhost:8048/nacos
# Grafana 监控http://localhost:3000 (Docker环境) 或 http://localhost:3001 (本地开发) # Grafana 监控http://localhost:3000 (Docker环境) 或 http://localhost:3001 (本地开发)
@ -179,8 +180,8 @@ npm run dev
│ │ │ │
│ ┌──────────────┐ │ │ ┌──────────────┐ │
│ │ 前端服务 │ │ │ │ 前端服务 │ │
│ │ fund-admin │ ← 管理后台 (http://localhost:80) │ │ fund-admin │ ← 管理后台 (http://localhost/fadmin/)
│ │ fund-mobile │ ← 移动端H5 (http://localhost:81) │ │ fund-mobile │ ← 移动端H5 (http://localhost/fmobile/)
│ └──────┬───────┘ │ │ └──────┬───────┘ │
│ │ │ │ │ │
│ ┌──────┴───────┐ │ │ ┌──────┴───────┐ │
@ -224,8 +225,8 @@ npm run dev
| 服务 | 端口 | 说明 | | 服务 | 端口 | 说明 |
|------|------|------| |------|------|------|
| fund-admin | 80 | 管理后台前端 | | fund-admin | 80 | 管理后台前端 (访问路径: /fadmin/) |
| fund-mobile | 81 | 移动端H5 | | fund-mobile | 80 | 移动端H5 (访问路径: /fmobile/) |
| gateway | 8000 | API网关 | | gateway | 8000 | API网关 |
| fund-sys | 8100 | 系统管理服务 | | fund-sys | 8100 | 系统管理服务 |
| fund-sys-vip001 | 8101 | 系统服务VIP专属实例 | | fund-sys-vip001 | 8101 | 系统服务VIP专属实例 |
@ -480,14 +481,20 @@ server {
listen 80; listen 80;
server_name test.fundplatform.com; server_name test.fundplatform.com;
# 前端静态资源 # 管理后台前端 (部署路径: /fadmin/)
location / { location /fadmin/ {
root /opt/fundplatform/admin; alias /opt/fundplatform/admin/;
try_files $uri $uri/ /index.html; try_files $uri $uri/ /fadmin/index.html;
} }
# API 代理 # 移动端H5 (部署路径: /fmobile/)
location /api/ { location /fmobile/ {
alias /opt/fundplatform/mobile/;
try_files $uri $uri/ /fmobile/index.html;
}
# API 代理 (网关前缀: /fund)
location /fund/ {
proxy_pass http://gateway/; proxy_pass http://gateway/;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;

View File

@ -21,7 +21,7 @@ export function deleteProject(id: number) {
} }
// 需求工单 // 需求工单
export function getRequirementList(params: { pageNum: number; pageSize: number; requirementName?: string; status?: string }) { export function getRequirementList(params: { pageNum: number; pageSize: number; requirementName?: string; projectName?: string; requirementStatus?: string; priority?: string }) {
return request.get('/requirement/page', { params }) return request.get('/requirement/page', { params })
} }

View File

@ -31,24 +31,18 @@
<el-table :data="tableData" v-loading="loading" border stripe> <el-table :data="tableData" v-loading="loading" border stripe>
<el-table-column prop="customerId" label="客户ID" width="80" /> <el-table-column prop="customerId" label="客户ID" width="80" />
<el-table-column prop="customerCode" label="客户编码" width="120" />
<el-table-column prop="customerName" label="客户名称" min-width="150" /> <el-table-column prop="customerName" label="客户名称" min-width="150" />
<el-table-column prop="customerType" label="客户类型" width="100"> <el-table-column prop="contact" label="联系人" width="120" />
<template #default="{ row }"> <el-table-column prop="phone" label="联系电话" width="140" />
<el-tag v-if="row.customerType === 'ENTERPRISE'" type="primary">企业</el-tag>
<el-tag v-else type="success">个人</el-tag>
</template>
</el-table-column>
<el-table-column prop="contactPerson" label="联系人" width="120" />
<el-table-column prop="contactPhone" label="联系电话" width="140" />
<el-table-column prop="email" label="邮箱" min-width="180" show-overflow-tooltip /> <el-table-column prop="email" label="邮箱" min-width="180" show-overflow-tooltip />
<el-table-column prop="address" label="地址" min-width="200" show-overflow-tooltip /> <el-table-column prop="address" label="地址" min-width="200" show-overflow-tooltip />
<el-table-column prop="status" label="状态" width="80"> <el-table-column prop="status" label="状态" width="80">
<template #default="{ row }"> <template #default="{ row }">
<el-tag v-if="row.status === 'NORMAL'" type="success">正常</el-tag> <el-tag v-if="row.status === 1" type="success">启用</el-tag>
<el-tag v-else type="danger">禁用</el-tag> <el-tag v-else type="danger">禁用</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="createTime" label="创建时间" width="160" />
<el-table-column label="操作" width="280" fixed="right"> <el-table-column label="操作" width="280" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
<el-button link type="primary" :icon="View" @click="handleView(row)">详情</el-button> <el-button link type="primary" :icon="View" @click="handleView(row)">详情</el-button>
@ -81,29 +75,26 @@
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px"> <el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="12"> <el-col :span="12">
<el-form-item label="客户名称" prop="customerName"> <el-form-item label="客户编码" prop="customerCode">
<el-input v-model="form.customerName" placeholder="请输入客户名称" /> <el-input v-model="form.customerCode" placeholder="请输入客户编码" />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="客户类型" prop="customerType"> <el-form-item label="客户名称" prop="customerName">
<el-select v-model="form.customerType" placeholder="请选择" style="width: 100%"> <el-input v-model="form.customerName" placeholder="请输入客户名称" />
<el-option label="企业" value="ENTERPRISE" />
<el-option label="个人" value="INDIVIDUAL" />
</el-select>
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="12"> <el-col :span="12">
<el-form-item label="联系人" prop="contactPerson"> <el-form-item label="联系人" prop="contact">
<el-input v-model="form.contactPerson" placeholder="请输入联系人" /> <el-input v-model="form.contact" placeholder="请输入联系人" />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="联系电话" prop="contactPhone"> <el-form-item label="联系电话" prop="phone">
<el-input v-model="form.contactPhone" placeholder="请输入联系电话" /> <el-input v-model="form.phone" placeholder="请输入联系电话" />
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
@ -117,8 +108,8 @@
<el-col :span="12"> <el-col :span="12">
<el-form-item label="状态" prop="status"> <el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status"> <el-radio-group v-model="form.status">
<el-radio value="NORMAL">正常</el-radio> <el-radio :value="1">启用</el-radio>
<el-radio value="DISABLED">禁用</el-radio> <el-radio :value="0">禁用</el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
</el-col> </el-col>
@ -128,8 +119,8 @@
<el-input v-model="form.address" placeholder="请输入地址" /> <el-input v-model="form.address" placeholder="请输入地址" />
</el-form-item> </el-form-item>
<el-form-item label="备注" prop="remarks"> <el-form-item label="备注" prop="remark">
<el-input v-model="form.remarks" type="textarea" :rows="3" placeholder="请输入备注" /> <el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入备注" />
</el-form-item> </el-form-item>
</el-form> </el-form>
@ -143,21 +134,17 @@
<el-dialog title="客户详情" v-model="detailVisible" width="700px"> <el-dialog title="客户详情" v-model="detailVisible" width="700px">
<el-descriptions :column="2" border> <el-descriptions :column="2" border>
<el-descriptions-item label="客户ID">{{ detailData.customerId }}</el-descriptions-item> <el-descriptions-item label="客户ID">{{ detailData.customerId }}</el-descriptions-item>
<el-descriptions-item label="客户编码">{{ detailData.customerCode }}</el-descriptions-item>
<el-descriptions-item label="客户名称">{{ detailData.customerName }}</el-descriptions-item> <el-descriptions-item label="客户名称">{{ detailData.customerName }}</el-descriptions-item>
<el-descriptions-item label="客户类型">
<el-tag v-if="detailData.customerType === 'ENTERPRISE'" type="primary">企业</el-tag>
<el-tag v-else type="success">个人</el-tag>
</el-descriptions-item>
<el-descriptions-item label="状态"> <el-descriptions-item label="状态">
<el-tag v-if="detailData.status === 'NORMAL'" type="success">正常</el-tag> <el-tag v-if="detailData.status === 1" type="success">启用</el-tag>
<el-tag v-else type="danger">禁用</el-tag> <el-tag v-else type="danger">禁用</el-tag>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="联系人">{{ detailData.contactPerson }}</el-descriptions-item> <el-descriptions-item label="联系人">{{ detailData.contact }}</el-descriptions-item>
<el-descriptions-item label="联系电话">{{ detailData.contactPhone }}</el-descriptions-item> <el-descriptions-item label="联系电话">{{ detailData.phone }}</el-descriptions-item>
<el-descriptions-item label="邮箱" :span="2">{{ detailData.email }}</el-descriptions-item> <el-descriptions-item label="邮箱" :span="2">{{ detailData.email }}</el-descriptions-item>
<el-descriptions-item label="地址" :span="2">{{ detailData.address }}</el-descriptions-item> <el-descriptions-item label="地址" :span="2">{{ detailData.address }}</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">{{ detailData.remarks || '-' }}</el-descriptions-item> <el-descriptions-item label="备注" :span="2">{{ detailData.remark || '-' }}</el-descriptions-item>
<el-descriptions-item label="创建时间" :span="2">{{ detailData.createTime }}</el-descriptions-item>
</el-descriptions> </el-descriptions>
</el-dialog> </el-dialog>
</div> </div>
@ -191,21 +178,21 @@ const formRef = ref<FormInstance>()
const form = reactive({ const form = reactive({
customerId: null as number | null, customerId: null as number | null,
customerCode: '',
customerName: '', customerName: '',
customerType: 'ENTERPRISE', contact: '',
contactPerson: '', phone: '',
contactPhone: '',
email: '', email: '',
address: '', address: '',
status: 'NORMAL', status: 1,
remarks: '' remark: ''
}) })
const rules = reactive<FormRules>({ const rules = reactive<FormRules>({
customerCode: [{ required: true, message: '请输入客户编码', trigger: 'blur' }],
customerName: [{ required: true, message: '请输入客户名称', trigger: 'blur' }], customerName: [{ required: true, message: '请输入客户名称', trigger: 'blur' }],
customerType: [{ required: true, message: '请选择客户类型', trigger: 'change' }], contact: [{ required: true, message: '请输入联系人', trigger: 'blur' }],
contactPerson: [{ required: true, message: '请输入联系人', trigger: 'blur' }], phone: [
contactPhone: [
{ required: true, message: '请输入联系电话', trigger: 'blur' }, { required: true, message: '请输入联系电话', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' } { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
], ],
@ -306,14 +293,14 @@ const handleSubmit = async () => {
const resetForm = () => { const resetForm = () => {
form.customerId = null form.customerId = null
form.customerCode = ''
form.customerName = '' form.customerName = ''
form.customerType = 'ENTERPRISE' form.contact = ''
form.contactPerson = '' form.phone = ''
form.contactPhone = ''
form.email = '' form.email = ''
form.address = '' form.address = ''
form.status = 'NORMAL' form.status = 1
form.remarks = '' form.remark = ''
formRef.value?.clearValidate() formRef.value?.clearValidate()
} }

View File

@ -53,6 +53,7 @@
¥{{ row.amount?.toLocaleString() || '0' }} ¥{{ row.amount?.toLocaleString() || '0' }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="payeeName" label="收款单位" width="140" show-overflow-tooltip />
<el-table-column prop="expenseDate" label="支出日期" width="120" /> <el-table-column prop="expenseDate" label="支出日期" width="120" />
<el-table-column prop="approvalStatus" label="审批状态" width="100"> <el-table-column prop="approvalStatus" label="审批状态" width="100">
<template #default="{ row }"> <template #default="{ row }">
@ -145,6 +146,11 @@
<el-input-number v-model="form.amount" :precision="2" :min="0" style="width: 100%" /> <el-input-number v-model="form.amount" :precision="2" :min="0" style="width: 100%" />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12">
<el-form-item label="收款单位" prop="payeeName">
<el-input v-model="form.payeeName" placeholder="请输入收款单位" />
</el-form-item>
</el-col>
</el-row> </el-row>
<el-row :gutter="20"> <el-row :gutter="20">
@ -199,6 +205,7 @@
<el-descriptions :column="1" border> <el-descriptions :column="1" border>
<el-descriptions-item label="支出标题">{{ approvalData.title }}</el-descriptions-item> <el-descriptions-item label="支出标题">{{ approvalData.title }}</el-descriptions-item>
<el-descriptions-item label="支出金额">¥{{ approvalData.amount?.toLocaleString() || '0' }}</el-descriptions-item> <el-descriptions-item label="支出金额">¥{{ approvalData.amount?.toLocaleString() || '0' }}</el-descriptions-item>
<el-descriptions-item label="收款单位">{{ approvalData.payeeName || '-' }}</el-descriptions-item>
<el-descriptions-item label="支出类型">{{ approvalData.expenseTypeName }}</el-descriptions-item> <el-descriptions-item label="支出类型">{{ approvalData.expenseTypeName }}</el-descriptions-item>
<el-descriptions-item label="关联项目">{{ approvalData.projectName || '-' }}</el-descriptions-item> <el-descriptions-item label="关联项目">{{ approvalData.projectName || '-' }}</el-descriptions-item>
<el-descriptions-item label="申请人">{{ approvalData.applicant }}</el-descriptions-item> <el-descriptions-item label="申请人">{{ approvalData.applicant }}</el-descriptions-item>
@ -229,6 +236,7 @@
<el-descriptions-item label="支出金额"> <el-descriptions-item label="支出金额">
¥{{ detailData.amount?.toLocaleString() || '0' }} ¥{{ detailData.amount?.toLocaleString() || '0' }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="收款单位">{{ detailData.payeeName || '-' }}</el-descriptions-item>
<el-descriptions-item label="支出日期">{{ detailData.expenseDate }}</el-descriptions-item> <el-descriptions-item label="支出日期">{{ detailData.expenseDate }}</el-descriptions-item>
<el-descriptions-item label="审批状态"> <el-descriptions-item label="审批状态">
<el-tag v-if="detailData.approvalStatus === 'DRAFT'" type="info">草稿</el-tag> <el-tag v-if="detailData.approvalStatus === 'DRAFT'" type="info">草稿</el-tag>
@ -312,6 +320,7 @@ const form = reactive({
expenseTypeId: null as number | null, expenseTypeId: null as number | null,
projectId: null as number | null, projectId: null as number | null,
amount: 0, amount: 0,
payeeName: '',
expenseDate: '', expenseDate: '',
applicant: '', applicant: '',
description: '', description: '',
@ -323,6 +332,7 @@ const rules = reactive<FormRules>({
title: [{ required: true, message: '请输入支出标题', trigger: 'blur' }], title: [{ required: true, message: '请输入支出标题', trigger: 'blur' }],
expenseTypeId: [{ required: true, message: '请选择支出类型', trigger: 'change' }], expenseTypeId: [{ required: true, message: '请选择支出类型', trigger: 'change' }],
amount: [{ required: true, message: '请输入支出金额', trigger: 'blur' }], amount: [{ required: true, message: '请输入支出金额', trigger: 'blur' }],
payeeName: [{ required: true, message: '请输入收款单位', trigger: 'blur' }],
expenseDate: [{ required: true, message: '请选择支出日期', trigger: 'change' }], expenseDate: [{ required: true, message: '请选择支出日期', trigger: 'change' }],
applicant: [{ required: true, message: '请输入申请人', trigger: 'blur' }] applicant: [{ required: true, message: '请输入申请人', trigger: 'blur' }]
}) })
@ -552,6 +562,7 @@ const resetForm = () => {
form.expenseTypeId = null form.expenseTypeId = null
form.projectId = null form.projectId = null
form.amount = 0 form.amount = 0
form.payeeName = ''
form.expenseDate = '' form.expenseDate = ''
form.applicant = '' form.applicant = ''
form.description = '' form.description = ''

View File

@ -3,7 +3,7 @@
<el-card shadow="never" class="search-card"> <el-card shadow="never" class="search-card">
<el-form :inline="true" :model="queryParams"> <el-form :inline="true" :model="queryParams">
<el-form-item label="需求标题"> <el-form-item label="需求标题">
<el-input v-model="queryParams.requirementTitle" placeholder="请输入需求标题" clearable style="width: 200px" /> <el-input v-model="queryParams.requirementName" placeholder="请输入需求标题" clearable style="width: 200px" />
</el-form-item> </el-form-item>
<el-form-item label="项目名称"> <el-form-item label="项目名称">
<el-input v-model="queryParams.projectName" placeholder="请输入项目名称" clearable style="width: 180px" /> <el-input v-model="queryParams.projectName" placeholder="请输入项目名称" clearable style="width: 180px" />
@ -40,7 +40,7 @@
<el-table :data="tableData" v-loading="loading" border stripe> <el-table :data="tableData" v-loading="loading" border stripe>
<el-table-column prop="requirementId" label="需求ID" width="80" /> <el-table-column prop="requirementId" label="需求ID" width="80" />
<el-table-column prop="requirementCode" label="需求编号" width="140" /> <el-table-column prop="requirementCode" label="需求编号" width="140" />
<el-table-column prop="requirementTitle" label="需求标题" min-width="200" show-overflow-tooltip /> <el-table-column prop="requirementName" label="需求名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="projectName" label="项目名称" width="150" show-overflow-tooltip /> <el-table-column prop="projectName" label="项目名称" width="150" show-overflow-tooltip />
<el-table-column prop="requirementType" label="需求类型" width="100"> <el-table-column prop="requirementType" label="需求类型" width="100">
<template #default="{ row }"> <template #default="{ row }">
@ -113,8 +113,8 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="需求标题" prop="requirementTitle"> <el-form-item label="需求名称" prop="requirementName">
<el-input v-model="form.requirementTitle" placeholder="请输入需求标题" /> <el-input v-model="form.requirementName" placeholder="请输入需求名称" />
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
@ -132,6 +132,21 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12">
<el-form-item label="客户" prop="customerId">
<el-select v-model="form.customerId" placeholder="请选择客户" filterable style="width: 100%">
<el-option
v-for="item in customerList"
:key="item.customerId"
:label="item.customerName"
:value="item.customerId"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12"> <el-col :span="12">
<el-form-item label="需求类型" prop="requirementType"> <el-form-item label="需求类型" prop="requirementType">
<el-select v-model="form.requirementType" placeholder="请选择" style="width: 100%"> <el-select v-model="form.requirementType" placeholder="请选择" style="width: 100%">
@ -249,8 +264,9 @@
<el-descriptions :column="2" border> <el-descriptions :column="2" border>
<el-descriptions-item label="需求ID">{{ detailData.requirementId }}</el-descriptions-item> <el-descriptions-item label="需求ID">{{ detailData.requirementId }}</el-descriptions-item>
<el-descriptions-item label="需求编号">{{ detailData.requirementCode }}</el-descriptions-item> <el-descriptions-item label="需求编号">{{ detailData.requirementCode }}</el-descriptions-item>
<el-descriptions-item label="需求标题" :span="2">{{ detailData.requirementTitle }}</el-descriptions-item> <el-descriptions-item label="需求名称" :span="2">{{ detailData.requirementName }}</el-descriptions-item>
<el-descriptions-item label="项目名称">{{ detailData.projectName }}</el-descriptions-item> <el-descriptions-item label="项目名称">{{ detailData.projectName }}</el-descriptions-item>
<el-descriptions-item label="客户名称">{{ detailData.customerName }}</el-descriptions-item>
<el-descriptions-item label="需求类型"> <el-descriptions-item label="需求类型">
<el-tag v-if="detailData.requirementType === 'FEATURE'" type="primary">功能</el-tag> <el-tag v-if="detailData.requirementType === 'FEATURE'" type="primary">功能</el-tag>
<el-tag v-else-if="detailData.requirementType === 'BUG'" type="danger">缺陷</el-tag> <el-tag v-else-if="detailData.requirementType === 'BUG'" type="danger">缺陷</el-tag>
@ -293,6 +309,7 @@ import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'elem
import { Search, Refresh, Plus, Edit, Delete, View, Check, Document } from '@element-plus/icons-vue' import { Search, Refresh, Plus, Edit, Delete, View, Check, Document } from '@element-plus/icons-vue'
import { getRequirementList, createRequirement, updateRequirement, deleteRequirement } from '@/api/project' import { getRequirementList, createRequirement, updateRequirement, deleteRequirement } from '@/api/project'
import { getProjectList } from '@/api/project' import { getProjectList } from '@/api/project'
import { getCustomerList } from '@/api/customer'
// //
const uploadUrl = '/file/api/v1/file/upload' const uploadUrl = '/file/api/v1/file/upload'
@ -307,11 +324,12 @@ const submitLoading = ref(false)
const tableData = ref<any[]>([]) const tableData = ref<any[]>([])
const total = ref(0) const total = ref(0)
const projectList = ref<any[]>([]) const projectList = ref<any[]>([])
const customerList = ref<any[]>([])
const queryParams = reactive({ const queryParams = reactive({
pageNum: 1, pageNum: 1,
pageSize: 10, pageSize: 10,
requirementTitle: '', requirementName: '',
projectName: '', projectName: '',
requirementStatus: '', requirementStatus: '',
priority: '' priority: ''
@ -324,8 +342,9 @@ const formRef = ref<FormInstance>()
const form = reactive({ const form = reactive({
requirementId: null as number | null, requirementId: null as number | null,
requirementCode: '', requirementCode: '',
requirementTitle: '', requirementName: '',
projectId: null as number | null, projectId: null as number | null,
customerId: null as number | null,
requirementType: 'FEATURE', requirementType: 'FEATURE',
priority: 'MEDIUM', priority: 'MEDIUM',
requirementStatus: 'PENDING', requirementStatus: 'PENDING',
@ -338,8 +357,9 @@ const form = reactive({
const rules = reactive<FormRules>({ const rules = reactive<FormRules>({
requirementCode: [{ required: true, message: '请输入需求编号', trigger: 'blur' }], requirementCode: [{ required: true, message: '请输入需求编号', trigger: 'blur' }],
requirementTitle: [{ required: true, message: '请输入需求标题', trigger: 'blur' }], requirementName: [{ required: true, message: '请输入需求名称', trigger: 'blur' }],
projectId: [{ required: true, message: '请选择项目', trigger: 'change' }], projectId: [{ required: true, message: '请选择项目', trigger: 'change' }],
customerId: [{ required: true, message: '请选择客户', trigger: 'change' }],
requirementType: [{ required: true, message: '请选择需求类型', trigger: 'change' }], requirementType: [{ required: true, message: '请选择需求类型', trigger: 'change' }],
priority: [{ required: true, message: '请选择优先级', trigger: 'change' }] priority: [{ required: true, message: '请选择优先级', trigger: 'change' }]
}) })
@ -389,6 +409,15 @@ const getFileUrl = (path: string) => {
return `/file/api/v1/file/download/${path}` return `/file/api/v1/file/download/${path}`
} }
const fetchCustomers = async () => {
try {
const res: any = await getCustomerList({ pageNum: 1, pageSize: 1000 })
customerList.value = res.data?.records || []
} catch (e) {
console.error(e)
}
}
const fetchData = async () => { const fetchData = async () => {
loading.value = true loading.value = true
try { try {
@ -417,7 +446,7 @@ const handleSearch = () => {
} }
const handleReset = () => { const handleReset = () => {
queryParams.requirementTitle = '' queryParams.requirementName = ''
queryParams.projectName = '' queryParams.projectName = ''
queryParams.requirementStatus = '' queryParams.requirementStatus = ''
queryParams.priority = '' queryParams.priority = ''
@ -520,8 +549,9 @@ const handleSubmit = async () => {
const resetForm = () => { const resetForm = () => {
form.requirementId = null form.requirementId = null
form.requirementCode = '' form.requirementCode = ''
form.requirementTitle = '' form.requirementName = ''
form.projectId = null form.projectId = null
form.customerId = null
form.requirementType = 'FEATURE' form.requirementType = 'FEATURE'
form.priority = 'MEDIUM' form.priority = 'MEDIUM'
form.requirementStatus = 'PENDING' form.requirementStatus = 'PENDING'
@ -537,6 +567,7 @@ const resetForm = () => {
onMounted(() => { onMounted(() => {
fetchData() fetchData()
fetchProjects() fetchProjects()
fetchCustomers()
}) })
</script> </script>

View File

@ -12,7 +12,7 @@ import java.util.List;
* <li>pageNum当前页码 1 开始</li> * <li>pageNum当前页码 1 开始</li>
* <li>pageSize每页条数</li> * <li>pageSize每页条数</li>
* <li>total总记录数</li> * <li>total总记录数</li>
* <li>list当前页数据列表</li> * <li>records当前页数据列表</li>
* </ul> * </ul>
* *
* @param <T> 列表元素类型 * @param <T> 列表元素类型
@ -31,17 +31,17 @@ public class PageResult<T> implements Serializable {
private long total; private long total;
/** 当前页数据 */ /** 当前页数据 */
private List<T> list; private List<T> records;
public PageResult() { public PageResult() {
this.list = Collections.emptyList(); this.records = Collections.emptyList();
} }
public PageResult(long pageNum, long pageSize, long total, List<T> list) { public PageResult(long pageNum, long pageSize, long total, List<T> records) {
this.pageNum = pageNum; this.pageNum = pageNum;
this.pageSize = pageSize; this.pageSize = pageSize;
this.total = total; this.total = total;
this.list = list != null ? list : Collections.emptyList(); this.records = records != null ? records : Collections.emptyList();
} }
public long getPageNum() { public long getPageNum() {
@ -68,11 +68,11 @@ public class PageResult<T> implements Serializable {
this.total = total; this.total = total;
} }
public List<T> getList() { public List<T> getRecords() {
return list; return records;
} }
public void setList(List<T> list) { public void setRecords(List<T> records) {
this.list = list; this.records = records;
} }
} }

View File

@ -122,7 +122,7 @@ public class CustomerServiceImpl implements CustomerService {
private CustomerVO convertToVO(Customer customer) { private CustomerVO convertToVO(Customer customer) {
CustomerVO vo = new CustomerVO(); CustomerVO vo = new CustomerVO();
vo.setId(customer.getId()); vo.setCustomerId(customer.getId());
vo.setCustomerCode(customer.getCustomerCode()); vo.setCustomerCode(customer.getCustomerCode());
vo.setCustomerName(customer.getCustomerName()); vo.setCustomerName(customer.getCustomerName());
vo.setContact(customer.getContact()); vo.setContact(customer.getContact());

View File

@ -5,7 +5,7 @@ package com.fundplatform.cust.vo;
*/ */
public class CustomerVO { public class CustomerVO {
private Long id; private Long customerId;
private String customerCode; private String customerCode;
private String customerName; private String customerName;
private String contact; private String contact;
@ -15,12 +15,12 @@ public class CustomerVO {
private Integer status; private Integer status;
private String remark; private String remark;
public Long getId() { public Long getCustomerId() {
return id; return customerId;
} }
public void setId(Long id) { public void setCustomerId(Long customerId) {
this.id = id; this.customerId = customerId;
} }
public String getCustomerCode() { public String getCustomerCode() {

View File

@ -0,0 +1,6 @@
# 开发环境配置
# 开发模式无部署前缀
VITE_BASE=/
# API基础路径开发模式使用代理
VITE_API_BASE_URL=

View File

@ -0,0 +1,6 @@
# 生产环境配置
# 部署路径前缀Nginx路由使用
VITE_BASE=/fmobile/
# API基础路径
VITE_API_BASE_URL=/fund

View File

@ -11,7 +11,11 @@ declare module 'vue' {
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']
Tabbar: typeof import('./src/components/Tabbar.vue')['default'] Tabbar: typeof import('./src/components/Tabbar.vue')['default']
VanButton: typeof import('vant/es')['Button']
VanCellGroup: typeof import('vant/es')['CellGroup']
VanDatePicker: typeof import('vant/es')['DatePicker'] VanDatePicker: typeof import('vant/es')['DatePicker']
VanField: typeof import('vant/es')['Field']
VanForm: typeof import('vant/es')['Form']
VanIcon: typeof import('vant/es')['Icon'] VanIcon: typeof import('vant/es')['Icon']
VanList: typeof import('vant/es')['List'] VanList: typeof import('vant/es')['List']
VanNavBar: typeof import('vant/es')['NavBar'] VanNavBar: typeof import('vant/es')['NavBar']

View File

@ -31,6 +31,7 @@ body {
.app-container { .app-container {
min-height: 100vh; min-height: 100vh;
padding-bottom: 80px;
} }
/* 页面切换动画 */ /* 页面切换动画 */

View File

@ -1,6 +1,7 @@
import request from './request' import request from './request'
// 用户认证 // ===================== 用户认证 =====================
export function login(data: { username: string; password: string }) { export function login(data: { username: string; password: string }) {
return request.post('/auth/login', data) return request.post('/auth/login', data)
} }
@ -13,8 +14,13 @@ export function logout() {
return request.post('/auth/logout') return request.post('/auth/logout')
} }
// 项目管理 export function updatePassword(data: { oldPassword: string; newPassword: string; confirmPassword: string }) {
export function getProjectList(params?: { pageNum: number; pageSize: number; projectName?: string }) { return request.put('/sys/profile/password', data)
}
// ===================== 项目管理 =====================
export function getProjectList(params?: { pageNum: number; pageSize: number; keyword?: string }) {
return request.get('/project/page', { params }) return request.get('/project/page', { params })
} }
@ -22,38 +28,78 @@ export function getProjectById(id: number) {
return request.get(`/project/${id}`) return request.get(`/project/${id}`)
} }
// 客户管理 export function createProject(data: any) {
export function getCustomerList(params?: { pageNum: number; pageSize: number; customerName?: string }) { return request.post('/project', data)
}
// ===================== 需求工单管理 =====================
export function getRequirementList(params?: { pageNum: number; pageSize: number; keyword?: string }) {
return request.get('/requirement/page', { params })
}
export function getRequirementById(id: number) {
return request.get(`/requirement/${id}`)
}
export function createRequirement(data: any) {
return request.post('/requirement', data)
}
// ===================== 客户管理 =====================
export function getCustomerList(params?: { pageNum: number; pageSize: number; keyword?: string }) {
return request.get('/customer/page', { params }) return request.get('/customer/page', { params })
} }
// 支出管理 export function getCustomerById(id: number) {
return request.get(`/customer/${id}`)
}
export function createCustomer(data: any) {
return request.post('/customer', data)
}
// ===================== 支出管理 =====================
export function createExpense(data: any) { export function createExpense(data: any) {
return request.post('/exp/expense', data) return request.post('/exp/expense', data)
} }
export function getExpenseList(params: { pageNum: number; pageSize: number }) { export function getExpenseList(params?: { pageNum: number; pageSize: number; title?: string }) {
return request.get('/exp/expense/page', { params }) return request.get('/exp/expense/page', { params })
} }
// 应收款管理 export function getExpenseTypeTree() {
export function getReceivableList(params: { pageNum: number; pageSize: number; status?: string }) { return request.get('/exp/expense-type/tree')
}
export function getTodayExpense() {
return request.get('/exp/expense/stats/today-expense')
}
// ===================== 应收款管理 =====================
export function getReceivableList(params?: { pageNum: number; pageSize: number; status?: string; keyword?: string }) {
return request.get('/receipt/receivable/page', { params }) return request.get('/receipt/receivable/page', { params })
} }
export function getReceivableById(id: number) {
return request.get(`/receipt/receivable/${id}`)
}
export function createReceivable(data: any) {
return request.post('/receipt/receivable', data)
}
export function getUpcomingDueList(daysWithin: number = 7) { export function getUpcomingDueList(daysWithin: number = 7) {
return request.get(`/receipt/receivable/upcoming-due?daysWithin=${daysWithin}`) return request.get(`/receipt/receivable/upcoming-due?daysWithin=${daysWithin}`)
} }
// 统计数据
export function getTodayIncome() { export function getTodayIncome() {
return request.get('/receipt/receivable/stats/today-income') return request.get('/receipt/receivable/stats/today-income')
} }
export function getTodayExpense() {
return request.get('/exp/expense/stats/today-expense')
}
export function getUnpaidAmount() { export function getUnpaidAmount() {
return request.get('/receipt/receivable/stats/unpaid-amount') return request.get('/receipt/receivable/stats/unpaid-amount')
} }
@ -61,8 +107,3 @@ export function getUnpaidAmount() {
export function getOverdueCount() { export function getOverdueCount() {
return request.get('/receipt/receivable/stats/overdue-count') return request.get('/receipt/receivable/stats/overdue-count')
} }
// 支出类型
export function getExpenseTypeTree() {
return request.get('/exp/expense-type/tree')
}

View File

@ -1,7 +1,7 @@
import axios from 'axios' import axios from 'axios'
const request = axios.create({ const request = axios.create({
baseURL: '/fund', baseURL: import.meta.env.VITE_API_BASE_URL || '/fund',
timeout: 15000, timeout: 15000,
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'

View File

@ -2,7 +2,7 @@ import { createRouter, createWebHistory } from 'vue-router'
import { useTenantStore } from '@/stores/tenant' import { useTenantStore } from '@/stores/tenant'
const router = createRouter({ const router = createRouter({
history: createWebHistory(), history: createWebHistory(import.meta.env.BASE_URL),
routes: [ routes: [
{ {
path: '/', path: '/',
@ -10,36 +10,85 @@ const router = createRouter({
component: () => import('@/views/Home.vue'), component: () => import('@/views/Home.vue'),
meta: { title: '首页', requiresAuth: true } meta: { title: '首页', requiresAuth: true }
}, },
// 需求工单
{
path: '/requirement',
name: 'RequirementList',
component: () => import('@/views/requirement/List.vue'),
meta: { title: '需求工单', requiresAuth: true }
},
{
path: '/requirement/add',
name: 'RequirementAdd',
component: () => import('@/views/requirement/Add.vue'),
meta: { title: '新增需求工单', requiresAuth: true }
},
// 支出管理
{
path: '/expense',
name: 'ExpenseList',
component: () => import('@/views/expense/List.vue'),
meta: { title: '支出管理', requiresAuth: true }
},
{ {
path: '/expense/add', path: '/expense/add',
name: 'ExpenseAdd', name: 'ExpenseAdd',
component: () => import('@/views/expense/Add.vue'), component: () => import('@/views/expense/Add.vue'),
meta: { title: '新增支出', requiresAuth: true } meta: { title: '新增支出', requiresAuth: true }
}, },
// 应收款管理
{ {
path: '/receivable', path: '/receivable',
name: 'ReceivableList', name: 'ReceivableList',
component: () => import('@/views/receivable/List.vue'), component: () => import('@/views/receivable/List.vue'),
meta: { title: '应收款列表', requiresAuth: true } meta: { title: '应收款管理', requiresAuth: true }
}, },
{
path: '/receivable/add',
name: 'ReceivableAdd',
component: () => import('@/views/receivable/Add.vue'),
meta: { title: '新增应收款', requiresAuth: true }
},
// 项目管理
{ {
path: '/project', path: '/project',
name: 'ProjectList', name: 'ProjectList',
component: () => import('@/views/project/List.vue'), component: () => import('@/views/project/List.vue'),
meta: { title: '项目列表', requiresAuth: true } meta: { title: '项目管理', requiresAuth: true }
}, },
{
path: '/project/add',
name: 'ProjectAdd',
component: () => import('@/views/project/Add.vue'),
meta: { title: '新增项目', requiresAuth: true }
},
// 客户管理
{ {
path: '/customer', path: '/customer',
name: 'CustomerList', name: 'CustomerList',
component: () => import('@/views/customer/List.vue'), component: () => import('@/views/customer/List.vue'),
meta: { title: '客户列表', requiresAuth: true } meta: { title: '客户管理', requiresAuth: true }
}, },
{
path: '/customer/add',
name: 'CustomerAdd',
component: () => import('@/views/customer/Add.vue'),
meta: { title: '新增客户', requiresAuth: true }
},
// 我的
{ {
path: '/my', path: '/my',
name: 'My', name: 'My',
component: () => import('@/views/my/Index.vue'), component: () => import('@/views/my/Index.vue'),
meta: { title: '我的', requiresAuth: true } meta: { title: '我的', requiresAuth: true }
}, },
{
path: '/my/change-password',
name: 'ChangePassword',
component: () => import('@/views/my/ChangePassword.vue'),
meta: { title: '修改密码', requiresAuth: true }
},
// 登录
{ {
path: '/login', path: '/login',
name: 'Login', name: 'Login',

View File

@ -1,11 +1,5 @@
<template> <template>
<div class="page home"> <div class="page home">
<!-- 顶部标题 -->
<div class="header">
<div class="header-title">资金服务平台</div>
<div class="header-subtitle">Financial Platform</div>
</div>
<!-- 数据概览卡片 --> <!-- 数据概览卡片 -->
<div class="summary-card mac-card fade-in-up"> <div class="summary-card mac-card fade-in-up">
<div class="card-header"> <div class="card-header">
@ -43,41 +37,100 @@
</div> </div>
</div> </div>
<!-- 快捷入口卡片 --> <!-- 快捷操作卡片 -->
<div class="quick-card mac-card fade-in-up delay-1"> <div class="quick-card mac-card fade-in-up delay-1">
<div class="card-header"> <div class="card-header">
<span class="card-title">快捷操作</span> <span class="card-title">快捷操作</span>
</div> </div>
<div class="quick-grid"> <div class="quick-grid">
<div class="quick-item" @click="$router.push('/requirement/add')">
<div class="quick-icon requirement">
<van-icon name="description" />
</div>
<div class="quick-text">新增需求</div>
</div>
<div class="quick-item" @click="$router.push('/receivable/add')">
<div class="quick-icon receivable">
<van-icon name="balance-list-o" />
</div>
<div class="quick-text">新增应收款</div>
</div>
<div class="quick-item" @click="$router.push('/expense/add')"> <div class="quick-item" @click="$router.push('/expense/add')">
<div class="quick-icon"> <div class="quick-icon expense">
<van-icon name="plus" /> <van-icon name="gold-coin-o" />
</div> </div>
<div class="quick-text">新增支出</div> <div class="quick-text">新增支出</div>
</div> </div>
<div class="quick-item" @click="$router.push('/receivable')"> <div class="quick-item" @click="$router.push('/project/add')">
<div class="quick-icon"> <div class="quick-icon project">
<van-icon name="balance-list-o" />
</div>
<div class="quick-text">应收款</div>
</div>
<div class="quick-item" @click="$router.push('/project')">
<div class="quick-icon">
<van-icon name="todo-list-o" /> <van-icon name="todo-list-o" />
</div> </div>
<div class="quick-text">项目</div> <div class="quick-text">新增项目</div>
</div> </div>
<div class="quick-item" @click="$router.push('/customer')"> <div class="quick-item" @click="$router.push('/customer/add')">
<div class="quick-icon"> <div class="quick-icon customer">
<van-icon name="friends-o" /> <van-icon name="friends-o" />
</div> </div>
<div class="quick-text">客户</div> <div class="quick-text">新增客户</div>
</div> </div>
<div class="quick-item" @click="$router.push('/my')"> </div>
<div class="quick-icon"> </div>
<van-icon name="user-o" />
<!-- 业务服务卡片 -->
<div class="service-card mac-card fade-in-up delay-2">
<div class="card-header">
<span class="card-title">业务服务</span>
</div>
<div class="service-list">
<div class="service-item" @click="$router.push('/requirement')">
<div class="service-icon requirement">
<van-icon name="description" />
</div> </div>
<div class="quick-text">我的</div> <div class="service-content">
<div class="service-name">需求工单</div>
<div class="service-desc">管理需求工单流程</div>
</div>
<van-icon name="arrow" class="service-arrow" />
</div>
<div class="service-item" @click="$router.push('/receivable')">
<div class="service-icon receivable">
<van-icon name="balance-list-o" />
</div>
<div class="service-content">
<div class="service-name">应收款管理</div>
<div class="service-desc">跟踪应收款项</div>
</div>
<van-icon name="arrow" class="service-arrow" />
</div>
<div class="service-item" @click="$router.push('/expense')">
<div class="service-icon expense">
<van-icon name="gold-coin-o" />
</div>
<div class="service-content">
<div class="service-name">支出管理</div>
<div class="service-desc">管理支出审批流程</div>
</div>
<van-icon name="arrow" class="service-arrow" />
</div>
<div class="service-item" @click="$router.push('/project')">
<div class="service-icon project">
<van-icon name="todo-list-o" />
</div>
<div class="service-content">
<div class="service-name">项目管理</div>
<div class="service-desc">项目信息管理</div>
</div>
<van-icon name="arrow" class="service-arrow" />
</div>
<div class="service-item" @click="$router.push('/customer')">
<div class="service-icon customer">
<van-icon name="friends-o" />
</div>
<div class="service-content">
<div class="service-name">客户管理</div>
<div class="service-desc">客户信息管理</div>
</div>
<van-icon name="arrow" class="service-arrow" />
</div> </div>
</div> </div>
</div> </div>
@ -86,7 +139,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import request from '@/api/request' import { getTodayIncome, getTodayExpense, getUnpaidAmount } from '@/api'
const summary = ref({ const summary = ref({
todayIncome: 0, todayIncome: 0,
@ -114,9 +167,9 @@ const formatMoney = (value: number) => {
const loadSummary = async () => { const loadSummary = async () => {
try { try {
const [incomeRes, expenseRes, unpaidRes] = await Promise.all([ const [incomeRes, expenseRes, unpaidRes] = await Promise.all([
request.get('/receipt/api/v1/receipt/receivable/stats/today-income'), getTodayIncome(),
request.get('/exp/api/v1/exp/expense/stats/today-expense'), getTodayExpense(),
request.get('/receipt/api/v1/receipt/receivable/stats/unpaid-amount') getUnpaidAmount()
]) ])
summary.value.todayIncome = (incomeRes as any).data || 0 summary.value.todayIncome = (incomeRes as any).data || 0
@ -135,26 +188,7 @@ onMounted(() => {
<style scoped> <style scoped>
.home { .home {
padding: 0 16px; padding: 0 16px;
} padding-top: 20px;
.header {
padding: 60px 0 24px;
text-align: center;
}
.header-title {
font-size: 28px;
font-weight: 700;
color: var(--mac-text);
letter-spacing: 1px;
}
.header-subtitle {
font-size: 13px;
color: var(--mac-text-secondary);
margin-top: 6px;
letter-spacing: 2px;
text-transform: uppercase;
} }
.summary-card { .summary-card {
@ -240,25 +274,29 @@ onMounted(() => {
color: var(--mac-text-secondary); color: var(--mac-text-secondary);
} }
/* 快捷操作样式 */
.quick-card { .quick-card {
margin-bottom: 16px;
padding: 20px; padding: 20px;
} }
.quick-grid { .quick-grid {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
gap: 16px; gap: 12px;
justify-items: center;
} }
.quick-item { .quick-item {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
padding: 16px 8px; padding: 14px 16px;
background: rgba(0, 0, 0, 0.02); background: rgba(0, 0, 0, 0.02);
border-radius: 16px; border-radius: 14px;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all 0.2s ease;
min-width: 90px;
} }
.quick-item:active { .quick-item:active {
@ -267,22 +305,121 @@ onMounted(() => {
} }
.quick-icon { .quick-icon {
width: 48px; width: 44px;
height: 48px; height: 44px;
border-radius: 14px; border-radius: 14px;
background: linear-gradient(135deg, var(--mac-primary), #5AC8FA);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 22px; font-size: 22px;
color: #fff; color: #fff;
margin-bottom: 10px; margin-bottom: 8px;
box-shadow: 0 4px 12px rgba(0, 122, 255, 0.25); }
.quick-icon.requirement {
background: linear-gradient(135deg, #5856D6, #AF52DE);
}
.quick-icon.receivable {
background: linear-gradient(135deg, #34C759, #30D158);
}
.quick-icon.expense {
background: linear-gradient(135deg, #FF3B30, #FF453A);
}
.quick-icon.project {
background: linear-gradient(135deg, #007AFF, #5AC8FA);
}
.quick-icon.customer {
background: linear-gradient(135deg, #FF9500, #FF9F0A);
} }
.quick-text { .quick-text {
font-size: 13px; font-size: 12px;
font-weight: 500; font-weight: 500;
color: var(--mac-text); color: var(--mac-text);
text-align: center;
}
/* 业务服务样式 */
.service-card {
padding: 20px;
margin-bottom: 16px;
}
.service-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.service-item {
display: flex;
align-items: center;
padding: 14px;
background: rgba(0, 0, 0, 0.02);
border-radius: 12px;
cursor: pointer;
transition: all 0.2s ease;
}
.service-item:active {
background: rgba(0, 122, 255, 0.08);
}
.service-icon {
width: 44px;
height: 44px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
color: #fff;
margin-right: 14px;
flex-shrink: 0;
}
.service-icon.requirement {
background: linear-gradient(135deg, #5856D6, #AF52DE);
}
.service-icon.receivable {
background: linear-gradient(135deg, #34C759, #30D158);
}
.service-icon.expense {
background: linear-gradient(135deg, #FF3B30, #FF453A);
}
.service-icon.project {
background: linear-gradient(135deg, #007AFF, #5AC8FA);
}
.service-icon.customer {
background: linear-gradient(135deg, #FF9500, #FF9F0A);
}
.service-content {
flex: 1;
}
.service-name {
font-size: 15px;
font-weight: 600;
color: var(--mac-text);
margin-bottom: 4px;
}
.service-desc {
font-size: 12px;
color: var(--mac-text-secondary);
}
.service-arrow {
color: var(--mac-text-secondary);
font-size: 14px;
} }
</style> </style>

View File

@ -51,7 +51,7 @@
import { ref, reactive } from 'vue' import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { showToast, showSuccessToast } from 'vant' import { showToast, showSuccessToast } from 'vant'
import request from '@/api/request' import { login } from '@/api'
const router = useRouter() const router = useRouter()
const loading = ref(false) const loading = ref(false)
@ -74,7 +74,7 @@ const handleLogin = async () => {
loading.value = true loading.value = true
try { try {
const res: any = await request.post('/sys/api/v1/auth/login', form) const res: any = await login(form)
const data = res.data const data = res.data
localStorage.setItem('token', data.token) localStorage.setItem('token', data.token)
localStorage.setItem('userInfo', JSON.stringify({ localStorage.setItem('userInfo', JSON.stringify({

View File

@ -0,0 +1,144 @@
<template>
<div class="page customer-add">
<van-nav-bar title="新增客户" left-arrow @click-left="$router.back()" />
<van-form @submit="onSubmit">
<van-cell-group inset>
<van-field
v-model="form.customerCode"
name="customerCode"
label="客户编码"
placeholder="请输入客户编码"
:rules="[{ required: true, message: '请输入客户编码' }]"
required
/>
<van-field
v-model="form.customerName"
name="customerName"
label="客户名称"
placeholder="请输入客户名称"
:rules="[{ required: true, message: '请输入客户名称' }]"
required
/>
<van-field
v-model="form.contact"
name="contact"
label="联系人"
placeholder="请输入联系人"
:rules="[{ required: true, message: '请输入联系人' }]"
required
/>
<van-field
v-model="form.phone"
name="phone"
label="联系电话"
type="tel"
placeholder="请输入联系电话"
/>
<van-field
v-model="form.email"
name="email"
label="邮箱"
type="email"
placeholder="请输入邮箱"
/>
<van-field
v-model="form.address"
name="address"
label="地址"
placeholder="请输入地址"
/>
<van-field
v-model="form.remark"
name="remark"
label="备注"
type="textarea"
rows="2"
autosize
placeholder="请输入备注"
/>
</van-cell-group>
<div class="submit-btn">
<van-button round block type="primary" native-type="submit" :loading="submitting">
提交
</van-button>
</div>
</van-form>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { showFailToast, showSuccessToast } from 'vant'
import { createCustomer } from '@/api'
const router = useRouter()
const submitting = ref(false)
const form = ref({
customerCode: '',
customerName: '',
contact: '',
phone: '',
email: '',
address: '',
remark: ''
})
const onSubmit = async () => {
if (!form.value.customerCode) {
showFailToast('请输入客户编码')
return
}
if (!form.value.customerName) {
showFailToast('请输入客户名称')
return
}
if (!form.value.contact) {
showFailToast('请输入联系人')
return
}
submitting.value = true
try {
await createCustomer({
customerCode: form.value.customerCode,
customerName: form.value.customerName,
contact: form.value.contact,
phone: form.value.phone,
email: form.value.email,
address: form.value.address,
remark: form.value.remark
})
showSuccessToast('提交成功')
router.back()
} catch (e: any) {
showFailToast(e.message || '提交失败')
} finally {
submitting.value = false
}
}
</script>
<style scoped>
.customer-add {
background: #f5f5f5;
min-height: 100vh;
}
.van-cell-group {
margin: 12px;
}
.submit-btn {
margin: 24px 16px;
}
</style>

View File

@ -20,19 +20,19 @@
<div class="customer-avatar">{{ item.customerName?.charAt(0) || 'C' }}</div> <div class="customer-avatar">{{ item.customerName?.charAt(0) || 'C' }}</div>
<div class="customer-info"> <div class="customer-info">
<div class="customer-name">{{ item.customerName }}</div> <div class="customer-name">{{ item.customerName }}</div>
<div class="customer-short">{{ item.customerShort || '-' }}</div> <div class="customer-code">{{ item.customerCode || '-' }}</div>
</div> </div>
<van-tag :type="getLevelType(item.level)">{{ getLevelText(item.level) }}</van-tag> <van-tag :type="getStatusType(item.status)">{{ getStatusText(item.status) }}</van-tag>
</div> </div>
<div class="customer-detail"> <div class="customer-detail">
<div class="detail-item" v-if="item.contact">
<van-icon name="user-o" />
<span>{{ item.contact }}</span>
</div>
<div class="detail-item" v-if="item.phone"> <div class="detail-item" v-if="item.phone">
<van-icon name="phone-o" /> <van-icon name="phone-o" />
<span>{{ item.phone }}</span> <span>{{ item.phone }}</span>
</div> </div>
<div class="detail-item" v-if="item.industry">
<van-icon name="cluster-o" />
<span>{{ item.industry }}</span>
</div>
</div> </div>
</div> </div>
</van-list> </van-list>
@ -41,7 +41,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref, onMounted } from 'vue'
import { getCustomerList } from '@/api' import { getCustomerList } from '@/api'
const searchText = ref('') const searchText = ref('')
@ -52,24 +52,12 @@ const list = ref<any[]>([])
const pageNum = ref(1) const pageNum = ref(1)
const pageSize = 10 const pageSize = 10
const getLevelType = (level: string): 'primary' | 'success' | 'warning' | 'danger' | 'default' => { const getStatusType = (status: number): 'primary' | 'success' | 'warning' | 'danger' | 'default' => {
const map: Record<string, 'primary' | 'success' | 'warning' | 'danger' | 'default'> = { return status === 1 ? 'success' : 'default'
'A': 'primary',
'B': 'success',
'C': 'warning',
'D': 'danger'
}
return map[level] || 'default'
} }
const getLevelText = (level: string) => { const getStatusText = (status: number) => {
const map: Record<string, string> = { return status === 1 ? '启用' : '禁用'
'A': 'A类',
'B': 'B类',
'C': 'C类',
'D': 'D类'
}
return map[level] || '普通'
} }
const loadData = async () => { const loadData = async () => {
@ -77,7 +65,7 @@ const loadData = async () => {
const res: any = await getCustomerList({ const res: any = await getCustomerList({
pageNum: pageNum.value, pageNum: pageNum.value,
pageSize, pageSize,
customerName: searchText.value || undefined keyword: searchText.value || undefined
}) })
const records = res.data?.records || [] const records = res.data?.records || []
if (pageNum.value === 1) { if (pageNum.value === 1) {
@ -96,6 +84,8 @@ const loadData = async () => {
} }
const onLoad = () => { const onLoad = () => {
if (loading.value) return
loading.value = true
pageNum.value++ pageNum.value++
loadData() loadData()
} }
@ -110,8 +100,14 @@ const handleSearch = () => {
pageNum.value = 1 pageNum.value = 1
finished.value = false finished.value = false
list.value = [] list.value = []
loading.value = true
loadData() loadData()
} }
onMounted(() => {
loading.value = true
loadData()
})
</script> </script>
<style scoped> <style scoped>
@ -162,7 +158,7 @@ const handleSearch = () => {
color: #333; color: #333;
} }
.customer-short { .customer-code {
font-size: 13px; font-size: 13px;
color: #999; color: #999;
margin-top: 2px; margin-top: 2px;

View File

@ -10,12 +10,12 @@
<div class="form-card mac-card fade-in-up"> <div class="form-card mac-card fade-in-up">
<div class="form-group"> <div class="form-group">
<label>支出标题</label> <label>支出标题 <span class="required">*</span></label>
<input v-model="form.title" placeholder="输入标题" class="mac-input" /> <input v-model="form.title" placeholder="输入标题" class="mac-input" />
</div> </div>
<div class="form-group"> <div class="form-group">
<label>支出类型</label> <label>支出类型 <span class="required">*</span></label>
<div class="mac-select" @click="showTypePicker = true"> <div class="mac-select" @click="showTypePicker = true">
<span :class="{ placeholder: !form.expenseTypeName }"> <span :class="{ placeholder: !form.expenseTypeName }">
{{ form.expenseTypeName || '选择类型' }} {{ form.expenseTypeName || '选择类型' }}
@ -25,13 +25,18 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label>支出金额</label> <label>支出金额 <span class="required">*</span></label>
<div class="amount-input"> <div class="amount-input">
<span class="currency">¥</span> <span class="currency">¥</span>
<input v-model="form.amount" type="number" placeholder="0.00" /> <input v-model="form.amount" type="number" placeholder="0.00" />
</div> </div>
</div> </div>
<div class="form-group">
<label>收款单位 <span class="required">*</span></label>
<input v-model="form.payeeName" placeholder="输入收款单位名称" class="mac-input" />
</div>
<div class="form-group"> <div class="form-group">
<label>支出日期</label> <label>支出日期</label>
<div class="mac-select" @click="showDatePicker = true"> <div class="mac-select" @click="showDatePicker = true">
@ -69,8 +74,8 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, onMounted } from 'vue' import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { showToast, showSuccessToast } from 'vant' import { showToast, showSuccessToast, showFailToast } from 'vant'
import request from '@/api/request' import { createExpense, getExpenseTypeTree } from '@/api'
const router = useRouter() const router = useRouter()
const loading = ref(false) const loading = ref(false)
@ -82,6 +87,7 @@ const form = reactive({
expenseTypeId: null as number | null, expenseTypeId: null as number | null,
expenseTypeName: '', expenseTypeName: '',
amount: '', amount: '',
payeeName: '',
expenseDate: '', expenseDate: '',
description: '' description: ''
}) })
@ -102,15 +108,19 @@ const onDateConfirm = ({ selectedValues }: any) => {
const handleSubmit = async () => { const handleSubmit = async () => {
if (!form.title) { if (!form.title) {
showToast('请输入支出标题') showFailToast('请输入支出标题')
return return
} }
if (!form.expenseTypeId) { if (!form.expenseTypeId) {
showToast('请选择支出类型') showFailToast('请选择支出类型')
return return
} }
if (!form.amount) { if (!form.amount) {
showToast('请输入支出金额') showFailToast('请输入支出金额')
return
}
if (!form.payeeName) {
showFailToast('请输入收款单位')
return return
} }
@ -124,15 +134,15 @@ const handleSubmit = async () => {
amount: parseFloat(form.amount), amount: parseFloat(form.amount),
expenseDate: expenseDateTime, expenseDate: expenseDateTime,
purpose: form.description, purpose: form.description,
payeeName: '待填写' payeeName: form.payeeName
} }
console.log('提交支出数据:', requestData) console.log('提交支出数据:', requestData)
await request.post('/exp/api/v1/exp/expense', requestData) await createExpense(requestData)
showSuccessToast('提交成功') showSuccessToast('提交成功')
router.back() router.back()
} catch (e: any) { } catch (e: any) {
console.error('提交失败:', e) console.error('提交失败:', e)
showToast(e.message || '提交失败') showFailToast(e.message || '提交失败')
} finally { } finally {
loading.value = false loading.value = false
} }
@ -141,7 +151,7 @@ const handleSubmit = async () => {
onMounted(async () => { onMounted(async () => {
// //
try { try {
const res: any = await request.get('/exp/api/v1/exp/expense-type/tree') const res: any = await getExpenseTypeTree()
const types = res.data || [] const types = res.data || []
typeColumns.value = types.map((t: any) => ({ typeColumns.value = types.map((t: any) => ({
text: t.typeName, text: t.typeName,
@ -219,6 +229,11 @@ onMounted(async () => {
white-space: nowrap; white-space: nowrap;
} }
.form-group label .required {
color: var(--mac-danger);
margin-left: 2px;
}
.mac-input { .mac-input {
width: 100%; width: 100%;
padding: 14px 16px; padding: 14px 16px;

View File

@ -0,0 +1,231 @@
<template>
<div class="page expense-list">
<van-nav-bar title="支出管理" left-arrow @click-left="$router.back()" />
<!-- 搜索栏 -->
<div class="search-bar">
<van-search v-model="searchText" placeholder="搜索支出标题" @search="handleSearch" />
</div>
<!-- 支出列表 -->
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list
v-model:loading="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<div class="expense-card" v-for="item in list" :key="item.expenseId">
<div class="expense-header">
<span class="expense-title">{{ item.title }}</span>
<van-tag :type="getStatusType(item.status)">{{ getStatusText(item.status) }}</van-tag>
</div>
<div class="expense-info">
<div class="info-item">
<van-icon name="apps-o" />
<span>{{ item.typeName || '-' }}</span>
</div>
<div class="info-item">
<van-icon name="clock-o" />
<span>{{ item.expenseDate }}</span>
</div>
</div>
<div class="expense-amount">
<div class="amount-item">
<span class="label">支出金额</span>
<span class="value expense-value">¥{{ item.amount?.toLocaleString() }}</span>
</div>
<div class="amount-item" v-if="item.paidAmount">
<span class="label">已支付</span>
<span class="value">¥{{ item.paidAmount?.toLocaleString() }}</span>
</div>
</div>
</div>
</van-list>
</van-pull-refresh>
<!-- 新增按钮 -->
<div class="add-btn" @click="$router.push('/expense/add')">
<van-icon name="plus" />
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { getExpenseList } from '@/api'
const searchText = ref('')
const loading = ref(false)
const finished = ref(false)
const refreshing = ref(false)
const list = ref<any[]>([])
const pageNum = ref(1)
const pageSize = 10
const getStatusType = (status: string): 'primary' | 'success' | 'warning' | 'danger' | 'default' => {
const map: Record<string, 'primary' | 'success' | 'warning' | 'danger' | 'default'> = {
'pending': 'warning',
'approved': 'primary',
'paid': 'success',
'rejected': 'danger'
}
return map[status] || 'default'
}
const getStatusText = (status: string) => {
const map: Record<string, string> = {
'pending': '待审批',
'approved': '已审批',
'paid': '已支付',
'rejected': '已拒绝'
}
return map[status] || status
}
const loadData = async () => {
try {
const res: any = await getExpenseList({
pageNum: pageNum.value,
pageSize,
title: searchText.value || undefined
})
const records = res.data?.records || []
if (pageNum.value === 1) {
list.value = records
} else {
list.value.push(...records)
}
finished.value = records.length < pageSize
} catch (e) {
console.error(e)
finished.value = true
} finally {
loading.value = false
refreshing.value = false
}
}
const onLoad = () => {
if (loading.value) return
loading.value = true
pageNum.value++
loadData()
}
const onRefresh = () => {
pageNum.value = 1
finished.value = false
loadData()
}
const handleSearch = () => {
pageNum.value = 1
finished.value = false
list.value = []
loading.value = true
loadData()
}
onMounted(() => {
loading.value = true
loadData()
})
</script>
<style scoped>
.expense-list {
background: #f5f5f5;
min-height: 100vh;
}
.search-bar {
background: #fff;
padding: 8px 0;
}
.expense-card {
background: #fff;
margin: 12px;
padding: 16px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.expense-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.expense-title {
font-size: 16px;
font-weight: 600;
color: #333;
}
.expense-info {
display: flex;
gap: 16px;
margin-bottom: 12px;
}
.info-item {
display: flex;
align-items: center;
gap: 4px;
font-size: 13px;
color: #666;
}
.expense-amount {
display: flex;
gap: 24px;
padding-top: 12px;
border-top: 1px solid #f0f0f0;
}
.amount-item {
display: flex;
flex-direction: column;
}
.amount-item .label {
font-size: 12px;
color: #999;
}
.amount-item .value {
font-size: 15px;
font-weight: 600;
color: #333;
margin-top: 4px;
}
.expense-value {
color: #FF3B30 !important;
}
.add-btn {
position: fixed;
bottom: 80px;
right: 24px;
width: 56px;
height: 56px;
border-radius: 50%;
background: linear-gradient(135deg, #FF3B30, #FF453A);
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 28px;
box-shadow: 0 4px 12px rgba(255, 59, 48, 0.4);
cursor: pointer;
transition: transform 0.2s ease;
}
.add-btn:active {
transform: scale(0.95);
}
</style>

View File

@ -0,0 +1,145 @@
<template>
<div class="page change-password">
<van-nav-bar title="修改密码" left-arrow @click-left="$router.back()" />
<van-form @submit="onSubmit">
<van-cell-group inset>
<van-field
v-model="form.oldPassword"
type="password"
name="oldPassword"
label="旧密码"
placeholder="请输入旧密码"
:rules="[{ required: true, message: '请输入旧密码' }]"
required
/>
<van-field
v-model="form.newPassword"
type="password"
name="newPassword"
label="新密码"
placeholder="请输入新密码至少6位"
:rules="[
{ required: true, message: '请输入新密码' },
{ pattern: /^.{6,}$/, message: '密码至少6位' }
]"
required
/>
<van-field
v-model="form.confirmPassword"
type="password"
name="confirmPassword"
label="确认密码"
placeholder="请再次输入新密码"
:rules="[
{ required: true, message: '请确认新密码' },
{ validator: validateConfirmPassword, message: '两次密码输入不一致' }
]"
required
/>
</van-cell-group>
<div class="tips">
<van-icon name="info-o" />
<span>密码修改成功后需要重新登录</span>
</div>
<div class="submit-btn">
<van-button round block type="primary" native-type="submit" :loading="submitting">
确认修改
</van-button>
</div>
</van-form>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { showFailToast, showSuccessToast, showConfirmDialog } from 'vant'
import { updatePassword } from '@/api'
const router = useRouter()
const submitting = ref(false)
const form = ref({
oldPassword: '',
newPassword: '',
confirmPassword: ''
})
const validateConfirmPassword = () => {
return form.value.confirmPassword === form.value.newPassword
}
const onSubmit = async () => {
if (form.value.newPassword !== form.value.confirmPassword) {
showFailToast('两次密码输入不一致')
return
}
if (form.value.oldPassword === form.value.newPassword) {
showFailToast('新密码不能与旧密码相同')
return
}
try {
await showConfirmDialog({
title: '确认修改',
message: '密码修改成功后需要重新登录,确定要修改吗?'
})
submitting.value = true
await updatePassword({
oldPassword: form.value.oldPassword,
newPassword: form.value.newPassword,
confirmPassword: form.value.confirmPassword
})
showSuccessToast('密码修改成功')
//
localStorage.removeItem('token')
localStorage.removeItem('userInfo')
//
setTimeout(() => {
router.replace('/login')
}, 1500)
} catch (e: any) {
if (e.message !== 'cancel') {
showFailToast(e.message || '修改失败')
}
} finally {
submitting.value = false
}
}
</script>
<style scoped>
.change-password {
background: #f5f5f5;
min-height: 100vh;
}
.van-cell-group {
margin: 12px;
}
.tips {
display: flex;
align-items: center;
gap: 6px;
margin: 16px 20px;
font-size: 12px;
color: var(--mac-text-secondary);
}
.submit-btn {
margin: 24px 16px;
}
</style>

View File

@ -55,7 +55,7 @@ const userInfo = ref({
}) })
const handleChangePassword = () => { const handleChangePassword = () => {
showToast('功能开发中') router.push('/my/change-password')
} }
const handleAbout = () => { const handleAbout = () => {

View File

@ -0,0 +1,289 @@
<template>
<div class="page project-add">
<van-nav-bar title="新增项目" left-arrow @click-left="$router.back()" />
<van-form @submit="onSubmit">
<van-cell-group inset>
<van-field
v-model="form.projectCode"
name="projectCode"
label="项目编码"
placeholder="请输入项目编码"
:rules="[{ required: true, message: '请输入项目编码' }]"
required
/>
<van-field
v-model="form.projectName"
name="projectName"
label="项目名称"
placeholder="请输入项目名称"
:rules="[{ required: true, message: '请输入项目名称' }]"
required
/>
<van-field
v-model="form.customerName"
is-link
readonly
name="customer"
label="关联客户"
placeholder="请选择客户"
:rules="[{ required: true, message: '请选择客户' }]"
required
@click="showCustomerPicker = true"
/>
<van-field
v-model="projectTypeText"
is-link
readonly
name="projectType"
label="项目类型"
placeholder="请选择类型"
:rules="[{ required: true, message: '请选择项目类型' }]"
required
@click="showTypePicker = true"
/>
<van-field
v-model="form.budgetAmount"
name="budgetAmount"
label="预算金额"
type="number"
placeholder="请输入预算金额"
>
<template #button>
<span></span>
</template>
</van-field>
<van-field
v-model="form.startDate"
is-link
readonly
name="startDate"
label="开始日期"
placeholder="请选择日期"
@click="showStartPicker = true"
/>
<van-field
v-model="form.endDate"
is-link
readonly
name="endDate"
label="结束日期"
placeholder="请选择日期"
@click="showEndPicker = true"
/>
<van-field
v-model="form.projectManager"
name="projectManager"
label="项目经理"
placeholder="请输入项目经理"
/>
<van-field
v-model="form.remark"
name="remark"
label="备注"
type="textarea"
rows="2"
autosize
placeholder="请输入备注"
/>
</van-cell-group>
<div class="submit-btn">
<van-button round block type="primary" native-type="submit" :loading="submitting">
提交
</van-button>
</div>
</van-form>
<!-- 项目类型选择器 -->
<van-popup v-model:show="showTypePicker" position="bottom" round>
<van-picker
:columns="projectTypeOptions"
@confirm="onTypeConfirm"
@cancel="showTypePicker = false"
/>
</van-popup>
<!-- 开始日期选择器 -->
<van-popup v-model:show="showStartPicker" position="bottom" round>
<van-date-picker
v-model="selectedStartDate"
@confirm="onStartDateConfirm"
@cancel="showStartPicker = false"
/>
</van-popup>
<!-- 结束日期选择器 -->
<van-popup v-model:show="showEndPicker" position="bottom" round>
<van-date-picker
v-model="selectedEndDate"
@confirm="onEndDateConfirm"
@cancel="showEndPicker = false"
/>
</van-popup>
<!-- 客户选择器 -->
<van-popup v-model:show="showCustomerPicker" position="bottom" round>
<van-picker
:columns="customerOptions"
@confirm="onCustomerConfirm"
@cancel="showCustomerPicker = false"
/>
</van-popup>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { showFailToast, showSuccessToast } from 'vant'
import { createProject, getCustomerList } from '@/api'
const router = useRouter()
const submitting = ref(false)
const showStartPicker = ref(false)
const showEndPicker = ref(false)
const showTypePicker = ref(false)
const showCustomerPicker = ref(false)
const form = ref({
projectCode: '',
projectName: '',
customerId: null as number | null,
customerName: '',
projectType: '',
budgetAmount: '',
startDate: '',
endDate: '',
projectManager: '',
remark: ''
})
const selectedStartDate = ref([
new Date().getFullYear().toString(),
(new Date().getMonth() + 1).toString().padStart(2, '0'),
new Date().getDate().toString().padStart(2, '0')
])
const selectedEndDate = ref([
new Date().getFullYear().toString(),
(new Date().getMonth() + 1).toString().padStart(2, '0'),
new Date().getDate().toString().padStart(2, '0')
])
const projectTypeOptions = [
{ text: '开发项目', value: '开发项目' },
{ text: '运维项目', value: '运维项目' },
{ text: '咨询项目', value: '咨询项目' },
{ text: '集成项目', value: '集成项目' },
{ text: '其他项目', value: '其他项目' }
]
const projectTypeText = computed(() => {
const item = projectTypeOptions.find(t => t.value === form.value.projectType)
return item?.text || ''
})
const customerOptions = ref<{ text: string; value: number }[]>([])
const onTypeConfirm = ({ selectedOptions }: any) => {
form.value.projectType = selectedOptions[0].value
showTypePicker.value = false
}
const onStartDateConfirm = ({ selectedValues }: any) => {
form.value.startDate = selectedValues.join('-')
showStartPicker.value = false
}
const onEndDateConfirm = ({ selectedValues }: any) => {
form.value.endDate = selectedValues.join('-')
showEndPicker.value = false
}
const onCustomerConfirm = ({ selectedOptions }: any) => {
form.value.customerId = selectedOptions[0].value
form.value.customerName = selectedOptions[0].text
showCustomerPicker.value = false
}
const loadOptions = async () => {
try {
const customerRes = await getCustomerList({ pageNum: 1, pageSize: 100 })
customerOptions.value = ((customerRes as any).data?.records || []).map((item: any) => ({
text: item.customerName,
value: item.customerId
}))
} catch (e) {
console.error('加载选项失败', e)
}
}
const onSubmit = async () => {
if (!form.value.projectCode) {
showFailToast('请输入项目编码')
return
}
if (!form.value.projectName) {
showFailToast('请输入项目名称')
return
}
if (!form.value.customerId) {
showFailToast('请选择客户')
return
}
if (!form.value.projectType) {
showFailToast('请选择项目类型')
return
}
submitting.value = true
try {
await createProject({
projectCode: form.value.projectCode,
projectName: form.value.projectName,
customerId: form.value.customerId,
customerName: form.value.customerName,
projectType: form.value.projectType,
budgetAmount: form.value.budgetAmount ? parseFloat(form.value.budgetAmount) : null,
startDate: form.value.startDate || null,
endDate: form.value.endDate || null,
projectManager: form.value.projectManager || null,
remark: form.value.remark || null
})
showSuccessToast('提交成功')
router.back()
} catch (e: any) {
showFailToast(e.message || '提交失败')
} finally {
submitting.value = false
}
}
onMounted(() => {
loadOptions()
})
</script>
<style scoped>
.project-add {
background: #f5f5f5;
min-height: 100vh;
}
.van-cell-group {
margin: 12px;
}
.submit-btn {
margin: 24px 16px;
}
</style>

View File

@ -31,10 +31,6 @@
</div> </div>
</div> </div>
<div class="project-amount"> <div class="project-amount">
<div class="amount-item">
<span class="label">合同金额</span>
<span class="value">{{ formatMoney(item.contractAmount) }}</span>
</div>
<div class="amount-item"> <div class="amount-item">
<span class="label">预算金额</span> <span class="label">预算金额</span>
<span class="value">{{ formatMoney(item.budgetAmount) }}</span> <span class="value">{{ formatMoney(item.budgetAmount) }}</span>
@ -47,7 +43,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { getProjectList } from '@/api' import { getProjectList } from '@/api'
@ -92,7 +88,7 @@ const loadData = async () => {
const res: any = await getProjectList({ const res: any = await getProjectList({
pageNum: pageNum.value, pageNum: pageNum.value,
pageSize, pageSize,
projectName: searchText.value || undefined keyword: searchText.value || undefined
}) })
const records = res.data?.records || [] const records = res.data?.records || []
if (pageNum.value === 1) { if (pageNum.value === 1) {
@ -111,6 +107,8 @@ const loadData = async () => {
} }
const onLoad = () => { const onLoad = () => {
if (loading.value) return
loading.value = true
pageNum.value++ pageNum.value++
loadData() loadData()
} }
@ -125,12 +123,18 @@ const handleSearch = () => {
pageNum.value = 1 pageNum.value = 1
finished.value = false finished.value = false
list.value = [] list.value = []
loading.value = true
loadData() loadData()
} }
const goDetail = (item: any) => { const goDetail = (item: any) => {
router.push(`/project/${item.projectId}`) router.push(`/project/${item.projectId}`)
} }
onMounted(() => {
loading.value = true
loadData()
})
</script> </script>
<style scoped> <style scoped>

View File

@ -0,0 +1,221 @@
<template>
<div class="page receivable-add">
<van-nav-bar title="新增应收款" left-arrow @click-left="$router.back()" />
<van-form @submit="onSubmit">
<van-cell-group inset>
<van-field
v-model="form.customerName"
is-link
readonly
name="customer"
label="关联客户"
placeholder="请选择客户"
:rules="[{ required: true, message: '请选择客户' }]"
required
@click="showCustomerPicker = true"
/>
<van-field
v-model="form.projectName"
is-link
readonly
name="project"
label="关联项目"
placeholder="请选择项目"
@click="showProjectPicker = true"
/>
<van-field
v-model="form.receivableAmount"
name="amount"
label="应收金额"
type="number"
placeholder="请输入应收金额"
:rules="[{ required: true, message: '请输入应收金额' }]"
required
>
<template #button>
<span></span>
</template>
</van-field>
<van-field
v-model="form.receivableDate"
is-link
readonly
name="date"
label="应收日期"
placeholder="请选择日期"
:rules="[{ required: true, message: '请选择应收日期' }]"
required
@click="showDatePicker = true"
/>
<van-field
v-model="form.description"
name="description"
label="备注"
type="textarea"
rows="2"
autosize
placeholder="请输入备注"
/>
</van-cell-group>
<div class="submit-btn">
<van-button round block type="primary" native-type="submit" :loading="submitting">
提交
</van-button>
</div>
</van-form>
<!-- 日期选择器 -->
<van-popup v-model:show="showDatePicker" position="bottom" round>
<van-date-picker
v-model="selectedDate"
@confirm="onDateConfirm"
@cancel="showDatePicker = false"
/>
</van-popup>
<!-- 客户选择器 -->
<van-popup v-model:show="showCustomerPicker" position="bottom" round>
<van-picker
:columns="customerOptions"
@confirm="onCustomerConfirm"
@cancel="showCustomerPicker = false"
/>
</van-popup>
<!-- 项目选择器 -->
<van-popup v-model:show="showProjectPicker" position="bottom" round>
<van-picker
:columns="projectOptions"
@confirm="onProjectConfirm"
@cancel="showProjectPicker = false"
/>
</van-popup>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { showFailToast, showSuccessToast } from 'vant'
import { createReceivable, getCustomerList, getProjectList } from '@/api'
const router = useRouter()
const submitting = ref(false)
const showDatePicker = ref(false)
const showCustomerPicker = ref(false)
const showProjectPicker = ref(false)
const form = ref({
customerId: null as number | null,
customerName: '',
projectId: null as number | null,
projectName: '',
receivableAmount: '',
receivableDate: '',
description: ''
})
const selectedDate = ref([
new Date().getFullYear().toString(),
(new Date().getMonth() + 1).toString().padStart(2, '0'),
new Date().getDate().toString().padStart(2, '0')
])
const customerOptions = ref<{ text: string; value: number }[]>([])
const projectOptions = ref<{ text: string; value: number }[]>([])
const onDateConfirm = ({ selectedValues }: any) => {
form.value.receivableDate = selectedValues.join('-')
showDatePicker.value = false
}
const onCustomerConfirm = ({ selectedOptions }: any) => {
form.value.customerId = selectedOptions[0].value
form.value.customerName = selectedOptions[0].text
showCustomerPicker.value = false
}
const onProjectConfirm = ({ selectedOptions }: any) => {
form.value.projectId = selectedOptions[0].value
form.value.projectName = selectedOptions[0].text
showProjectPicker.value = false
}
const loadOptions = async () => {
try {
const [customerRes, projectRes] = await Promise.all([
getCustomerList({ pageNum: 1, pageSize: 100 }),
getProjectList({ pageNum: 1, pageSize: 100 })
])
customerOptions.value = ((customerRes as any).data?.records || []).map((item: any) => ({
text: item.customerName,
value: item.customerId
}))
projectOptions.value = ((projectRes as any).data?.records || []).map((item: any) => ({
text: item.projectName,
value: item.projectId
}))
} catch (e) {
console.error('加载选项失败', e)
}
}
const onSubmit = async () => {
if (!form.value.customerId) {
showFailToast('请选择客户')
return
}
if (!form.value.receivableAmount) {
showFailToast('请输入应收金额')
return
}
if (!form.value.receivableDate) {
showFailToast('请选择应收日期')
return
}
submitting.value = true
try {
await createReceivable({
customerId: form.value.customerId,
projectId: form.value.projectId,
receivableAmount: parseFloat(form.value.receivableAmount),
receivableDate: form.value.receivableDate,
description: form.value.description
})
showSuccessToast('提交成功')
router.back()
} catch (e: any) {
showFailToast(e.message || '提交失败')
} finally {
submitting.value = false
}
}
onMounted(() => {
loadOptions()
})
</script>
<style scoped>
.receivable-add {
background: #f5f5f5;
min-height: 100vh;
}
.van-cell-group {
margin: 12px;
}
.submit-btn {
margin: 24px 16px;
}
</style>

View File

@ -4,10 +4,15 @@
<div class="back-btn" @click="$router.back()"> <div class="back-btn" @click="$router.back()">
<van-icon name="arrow-left" /> <van-icon name="arrow-left" />
</div> </div>
<span class="header-title">应收款列表</span> <span class="header-title">应收款管理</span>
<div class="placeholder"></div> <div class="placeholder"></div>
</div> </div>
<!-- 搜索栏 -->
<div class="search-bar">
<van-search v-model="searchText" placeholder="搜索客户名称" @search="handleSearch" />
</div>
<van-pull-refresh v-model="refreshing" @refresh="onRefresh"> <van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad"> <van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad">
<div class="list-container"> <div class="list-container">
@ -37,14 +42,20 @@
</div> </div>
</van-list> </van-list>
</van-pull-refresh> </van-pull-refresh>
<!-- 新增按钮 -->
<div class="add-btn" @click="$router.push('/receivable/add')">
<van-icon name="plus" />
</div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { showToast } from 'vant' import { showToast } from 'vant'
import request from '@/api/request' import { getReceivableList } from '@/api'
const searchText = ref('')
const loading = ref(false) const loading = ref(false)
const refreshing = ref(false) const refreshing = ref(false)
const finished = ref(false) const finished = ref(false)
@ -72,35 +83,54 @@ const getStatusName = (status: string) => {
return map[status] || status return map[status] || status
} }
const onLoad = async () => { const loadData = async () => {
try { try {
const res: any = await request.get('/receipt/api/v1/receipt/receivable/page', { const res: any = await getReceivableList({
params: { pageNum: pageNum.value, pageSize } pageNum: pageNum.value,
pageSize,
keyword: searchText.value || undefined
}) })
const records = res.data?.records || [] const records = res.data?.records || []
list.value.push(...records) if (pageNum.value === 1) {
loading.value = false list.value = records
if (records.length < pageSize) {
finished.value = true
} else { } else {
pageNum.value++ list.value.push(...records)
} }
finished.value = records.length < pageSize
loading.value = false
} catch (e: any) { } catch (e: any) {
showToast(e.message || '加载失败') showToast(e.message || '加载失败')
loading.value = false loading.value = false
finished.value = true
} }
} }
const onLoad = () => {
if (loading.value) return
loading.value = true
pageNum.value++
loadData()
}
const onRefresh = () => { const onRefresh = () => {
list.value = [] list.value = []
pageNum.value = 1 pageNum.value = 1
finished.value = false finished.value = false
refreshing.value = false refreshing.value = false
onLoad() loadData()
}
const handleSearch = () => {
pageNum.value = 1
finished.value = false
list.value = []
loading.value = true
loadData()
} }
onMounted(() => { onMounted(() => {
onLoad() loading.value = true
loadData()
}) })
</script> </script>
@ -114,7 +144,7 @@ onMounted(() => {
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 16px 20px; padding: 16px 20px;
margin: 12px -16px 16px; margin: 12px -16px 0;
border-radius: 0; border-radius: 0;
position: sticky; position: sticky;
top: 0; top: 0;
@ -124,6 +154,12 @@ onMounted(() => {
-webkit-backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
} }
.search-bar {
background: #fff;
margin: 0 -16px 12px;
padding: 8px 16px;
}
.back-btn { .back-btn {
width: 36px; width: 36px;
height: 36px; height: 36px;
@ -222,4 +258,27 @@ onMounted(() => {
font-size: 12px; font-size: 12px;
color: var(--mac-text-secondary); color: var(--mac-text-secondary);
} }
.add-btn {
position: fixed;
bottom: 80px;
right: 24px;
width: 56px;
height: 56px;
border-radius: 50%;
background: linear-gradient(135deg, #34C759, #30D158);
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 28px;
box-shadow: 0 4px 12px rgba(52, 199, 89, 0.4);
cursor: pointer;
transition: transform 0.2s ease;
z-index: 100;
}
.add-btn:active {
transform: scale(0.95);
}
</style> </style>

View File

@ -0,0 +1,224 @@
<template>
<div class="page requirement-add">
<van-nav-bar title="新增需求工单" left-arrow @click-left="$router.back()" />
<van-form @submit="onSubmit">
<van-cell-group inset>
<van-field
v-model="form.requirementCode"
name="requirementCode"
label="需求编号"
placeholder="请输入需求编号"
:rules="[{ required: true, message: '请输入需求编号' }]"
required
/>
<van-field
v-model="form.requirementName"
name="requirementName"
label="需求名称"
placeholder="请输入需求名称"
:rules="[{ required: true, message: '请输入需求名称' }]"
required
/>
<van-field
v-model="form.customerName"
is-link
readonly
name="customer"
label="关联客户"
placeholder="请选择客户"
@click="showCustomerPicker = true"
/>
<van-field
v-model="form.projectName"
is-link
readonly
name="project"
label="关联项目"
placeholder="请选择项目"
@click="showProjectPicker = true"
/>
<van-field
v-model="priorityText"
is-link
readonly
name="priority"
label="优先级"
placeholder="请选择优先级"
@click="showPriorityPicker = true"
/>
<van-field
v-model="form.description"
name="description"
label="需求描述"
type="textarea"
rows="3"
autosize
placeholder="请输入需求描述"
/>
</van-cell-group>
<div class="submit-btn">
<van-button round block type="primary" native-type="submit" :loading="submitting">
提交
</van-button>
</div>
</van-form>
<!-- 优先级选择器 -->
<van-popup v-model:show="showPriorityPicker" position="bottom" round>
<van-picker
:columns="priorityOptions"
@confirm="onPriorityConfirm"
@cancel="showPriorityPicker = false"
/>
</van-popup>
<!-- 客户选择器 -->
<van-popup v-model:show="showCustomerPicker" position="bottom" round>
<van-picker
:columns="customerOptions"
@confirm="onCustomerConfirm"
@cancel="showCustomerPicker = false"
/>
</van-popup>
<!-- 项目选择器 -->
<van-popup v-model:show="showProjectPicker" position="bottom" round>
<van-picker
:columns="projectOptions"
@confirm="onProjectConfirm"
@cancel="showProjectPicker = false"
/>
</van-popup>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { showFailToast, showSuccessToast } from 'vant'
import { createRequirement, getCustomerList, getProjectList } from '@/api'
const router = useRouter()
const submitting = ref(false)
const showPriorityPicker = ref(false)
const showCustomerPicker = ref(false)
const showProjectPicker = ref(false)
const form = ref({
requirementCode: '',
requirementName: '',
customerId: null as number | null,
customerName: '',
projectId: null as number | null,
projectName: '',
priority: 'medium',
description: ''
})
const priorityOptions = [
{ text: '高', value: 'high' },
{ text: '中', value: 'medium' },
{ text: '低', value: 'low' }
]
const priorityText = computed(() => {
const item = priorityOptions.find(p => p.value === form.value.priority)
return item?.text || ''
})
const customerOptions = ref<{ text: string; value: number }[]>([])
const projectOptions = ref<{ text: string; value: number }[]>([])
const onPriorityConfirm = ({ selectedOptions }: any) => {
form.value.priority = selectedOptions[0].value
showPriorityPicker.value = false
}
const onCustomerConfirm = ({ selectedOptions }: any) => {
form.value.customerId = selectedOptions[0].value
form.value.customerName = selectedOptions[0].text
showCustomerPicker.value = false
}
const onProjectConfirm = ({ selectedOptions }: any) => {
form.value.projectId = selectedOptions[0].value
form.value.projectName = selectedOptions[0].text
showProjectPicker.value = false
}
const loadOptions = async () => {
try {
const [customerRes, projectRes] = await Promise.all([
getCustomerList({ pageNum: 1, pageSize: 100 }),
getProjectList({ pageNum: 1, pageSize: 100 })
])
customerOptions.value = ((customerRes as any).data?.records || []).map((item: any) => ({
text: item.customerName,
value: item.customerId
}))
projectOptions.value = ((projectRes as any).data?.records || []).map((item: any) => ({
text: item.projectName,
value: item.projectId
}))
} catch (e) {
console.error('加载选项失败', e)
}
}
const onSubmit = async () => {
if (!form.value.requirementCode) {
showFailToast('请输入需求编号')
return
}
if (!form.value.requirementName) {
showFailToast('请输入需求名称')
return
}
submitting.value = true
try {
await createRequirement({
requirementCode: form.value.requirementCode,
requirementName: form.value.requirementName,
customerId: form.value.customerId,
projectId: form.value.projectId,
priority: form.value.priority,
description: form.value.description
})
showSuccessToast('提交成功')
router.back()
} catch (e: any) {
showFailToast(e.message || '提交失败')
} finally {
submitting.value = false
}
}
onMounted(() => {
loadOptions()
})
</script>
<style scoped>
.requirement-add {
background: #f5f5f5;
min-height: 100vh;
}
.van-cell-group {
margin: 12px;
}
.submit-btn {
margin: 24px 16px;
}
</style>

View File

@ -0,0 +1,247 @@
<template>
<div class="page requirement-list">
<van-nav-bar title="需求工单" left-arrow @click-left="$router.back()" />
<!-- 搜索栏 -->
<div class="search-bar">
<van-search v-model="searchText" placeholder="搜索需求标题" @search="handleSearch" />
</div>
<!-- 需求列表 -->
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
<van-list
v-model:loading="loading"
:finished="finished"
finished-text="没有更多了"
@load="onLoad"
>
<div class="requirement-card" v-for="item in list" :key="item.requirementId">
<div class="requirement-header">
<span class="requirement-title">{{ item.title }}</span>
<van-tag :type="getStatusType(item.status)">{{ getStatusText(item.status) }}</van-tag>
</div>
<div class="requirement-info">
<div class="info-item">
<van-icon name="user-o" />
<span>{{ item.customerName || '-' }}</span>
</div>
<div class="info-item">
<van-icon name="todo-list-o" />
<span>{{ item.projectName || '-' }}</span>
</div>
</div>
<div class="requirement-detail">
<div class="detail-item">
<span class="label">优先级</span>
<van-tag :type="getPriorityType(item.priority)">{{ getPriorityText(item.priority) }}</van-tag>
</div>
<div class="detail-item">
<span class="label">创建时间</span>
<span class="value">{{ item.createTime }}</span>
</div>
</div>
</div>
</van-list>
</van-pull-refresh>
<!-- 新增按钮 -->
<div class="add-btn" @click="$router.push('/requirement/add')">
<van-icon name="plus" />
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { getRequirementList } from '@/api'
const searchText = ref('')
const loading = ref(false)
const finished = ref(false)
const refreshing = ref(false)
const list = ref<any[]>([])
const pageNum = ref(1)
const pageSize = 10
const getStatusType = (status: string): 'primary' | 'success' | 'warning' | 'danger' | 'default' => {
const map: Record<string, 'primary' | 'success' | 'warning' | 'danger' | 'default'> = {
'pending': 'warning',
'processing': 'primary',
'completed': 'success',
'cancelled': 'default'
}
return map[status] || 'default'
}
const getStatusText = (status: string) => {
const map: Record<string, string> = {
'pending': '待处理',
'processing': '处理中',
'completed': '已完成',
'cancelled': '已取消'
}
return map[status] || status
}
const getPriorityType = (priority: string): 'primary' | 'success' | 'warning' | 'danger' | 'default' => {
const map: Record<string, 'primary' | 'success' | 'warning' | 'danger' | 'default'> = {
'high': 'danger',
'medium': 'warning',
'low': 'success'
}
return map[priority] || 'default'
}
const getPriorityText = (priority: string) => {
const map: Record<string, string> = {
'high': '高',
'medium': '中',
'low': '低'
}
return map[priority] || priority
}
const loadData = async () => {
try {
const res: any = await getRequirementList({
pageNum: pageNum.value,
pageSize,
keyword: searchText.value || undefined
})
const records = res.data?.records || []
if (pageNum.value === 1) {
list.value = records
} else {
list.value.push(...records)
}
finished.value = records.length < pageSize
} catch (e) {
console.error(e)
finished.value = true
} finally {
loading.value = false
refreshing.value = false
}
}
const onLoad = () => {
if (loading.value) return
loading.value = true
pageNum.value++
loadData()
}
const onRefresh = () => {
pageNum.value = 1
finished.value = false
loadData()
}
const handleSearch = () => {
pageNum.value = 1
finished.value = false
list.value = []
loading.value = true
loadData()
}
onMounted(() => {
loading.value = true
loadData()
})
</script>
<style scoped>
.requirement-list {
background: #f5f5f5;
min-height: 100vh;
}
.search-bar {
background: #fff;
padding: 8px 0;
}
.requirement-card {
background: #fff;
margin: 12px;
padding: 16px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.requirement-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
}
.requirement-title {
font-size: 16px;
font-weight: 600;
color: #333;
flex: 1;
margin-right: 8px;
line-height: 1.4;
}
.requirement-info {
display: flex;
gap: 16px;
margin-bottom: 12px;
}
.info-item {
display: flex;
align-items: center;
gap: 4px;
font-size: 13px;
color: #666;
}
.requirement-detail {
display: flex;
justify-content: space-between;
padding-top: 12px;
border-top: 1px solid #f0f0f0;
}
.detail-item {
display: flex;
align-items: center;
gap: 6px;
}
.detail-item .label {
font-size: 12px;
color: #999;
}
.detail-item .value {
font-size: 12px;
color: #666;
}
.add-btn {
position: fixed;
bottom: 80px;
right: 24px;
width: 56px;
height: 56px;
border-radius: 50%;
background: linear-gradient(135deg, #5856D6, #AF52DE);
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 28px;
box-shadow: 0 4px 12px rgba(88, 86, 214, 0.4);
cursor: pointer;
transition: transform 0.2s ease;
}
.add-btn:active {
transform: scale(0.95);
}
</style>

10
fund-mobile/src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_BASE: string
readonly VITE_API_BASE_URL: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}

View File

@ -1,47 +1,55 @@
import { defineConfig } from 'vite' import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
import Components from 'unplugin-vue-components/vite' import Components from 'unplugin-vue-components/vite'
import { VantResolver } from '@vant/auto-import-resolver' import { VantResolver } from '@vant/auto-import-resolver'
import { fileURLToPath, URL } from 'node:url' import { fileURLToPath, URL } from 'node:url'
export default defineConfig({ export default defineConfig(({ mode }) => {
plugins: [ // 加载环境变量
vue(), const env = loadEnv(mode, process.cwd())
Components({ const base = env.VITE_BASE || '/'
resolvers: [VantResolver()]
}) return {
], // 部署路径前缀
resolve: { base,
alias: { plugins: [
'@': fileURLToPath(new URL('./src', import.meta.url)) vue(),
} Components({
}, resolvers: [VantResolver()]
server: { })
port: 8080, ],
proxy: { resolve: {
'/sys': { alias: {
target: 'http://localhost:8000', '@': fileURLToPath(new URL('./src', import.meta.url))
changeOrigin: true }
}, },
'/cust': { server: {
target: 'http://localhost:8000', port: 8080,
changeOrigin: true proxy: {
}, '/sys': {
'/proj': { target: 'http://localhost:8000',
target: 'http://localhost:8000', changeOrigin: true
changeOrigin: true },
}, '/cust': {
'/exp': { target: 'http://localhost:8000',
target: 'http://localhost:8000', changeOrigin: true
changeOrigin: true },
}, '/proj': {
'/receipt': { target: 'http://localhost:8000',
target: 'http://localhost:8000', changeOrigin: true
changeOrigin: true },
}, '/exp': {
'/file': { target: 'http://localhost:8000',
target: 'http://localhost:8000', changeOrigin: true
changeOrigin: true },
'/receipt': {
target: 'http://localhost:8000',
changeOrigin: true
},
'/file': {
target: 'http://localhost:8000',
changeOrigin: true
}
} }
} }
} }

View File

@ -25,13 +25,13 @@ public class RequirementController {
@GetMapping("/page") @GetMapping("/page")
public Result<Page<RequirementVO>> page( public Result<Page<RequirementVO>> page(
@RequestHeader(value = "X-Tenant-Id", required = false) Long tenantId, @RequestHeader(value = "X-Tenant-Id", required = false) Long tenantId,
@RequestParam(required = false) String requirementTitle, @RequestParam(required = false) String requirementName,
@RequestParam(required = false) String projectName, @RequestParam(required = false) String projectName,
@RequestParam(required = false) String requirementStatus, @RequestParam(required = false) String requirementStatus,
@RequestParam(required = false) String priority, @RequestParam(required = false) String priority,
@RequestParam(defaultValue = "1") int pageNum, @RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "10") int pageSize) { @RequestParam(defaultValue = "10") int pageSize) {
return requirementService.page(tenantId, requirementTitle, requirementStatus, null, null, pageNum, pageSize); return requirementService.page(tenantId, requirementName, requirementStatus, null, null, pageNum, pageSize);
} }
/** /**

View File

@ -15,6 +15,7 @@ public class Project extends BaseEntity {
private String projectCode; private String projectCode;
private String projectName; private String projectName;
private Long customerId; private Long customerId;
private String customerName;
private String projectType; private String projectType;
private BigDecimal budgetAmount; private BigDecimal budgetAmount;
private LocalDate startDate; private LocalDate startDate;
@ -46,6 +47,14 @@ public class Project extends BaseEntity {
this.customerId = customerId; this.customerId = customerId;
} }
public String getCustomerName() {
return customerName;
}
public void setCustomerName(String customerName) {
this.customerName = customerName;
}
public String getProjectType() { public String getProjectType() {
return projectType; return projectType;
} }

View File

@ -20,6 +20,8 @@ public class ProjectCreateDTO {
@NotNull(message = "客户ID不能为空") @NotNull(message = "客户ID不能为空")
private Long customerId; private Long customerId;
private String customerName;
@NotBlank(message = "项目类型不能为空") @NotBlank(message = "项目类型不能为空")
private String projectType; private String projectType;
@ -53,6 +55,14 @@ public class ProjectCreateDTO {
this.customerId = customerId; this.customerId = customerId;
} }
public String getCustomerName() {
return customerName;
}
public void setCustomerName(String customerName) {
this.customerName = customerName;
}
public String getProjectType() { public String getProjectType() {
return projectType; return projectType;
} }

View File

@ -45,6 +45,7 @@ public class ProjectServiceImpl implements ProjectService {
project.setProjectCode(dto.getProjectCode()); project.setProjectCode(dto.getProjectCode());
project.setProjectName(dto.getProjectName()); project.setProjectName(dto.getProjectName());
project.setCustomerId(dto.getCustomerId()); project.setCustomerId(dto.getCustomerId());
project.setCustomerName(dto.getCustomerName());
project.setProjectType(dto.getProjectType()); project.setProjectType(dto.getProjectType());
project.setBudgetAmount(dto.getBudgetAmount()); project.setBudgetAmount(dto.getBudgetAmount());
project.setStartDate(dto.getStartDate()); project.setStartDate(dto.getStartDate());
@ -157,10 +158,11 @@ public class ProjectServiceImpl implements ProjectService {
private ProjectVO convertToVO(Project project) { private ProjectVO convertToVO(Project project) {
ProjectVO vo = new ProjectVO(); ProjectVO vo = new ProjectVO();
vo.setId(project.getId()); vo.setProjectId(project.getId());
vo.setProjectCode(project.getProjectCode()); vo.setProjectCode(project.getProjectCode());
vo.setProjectName(project.getProjectName()); vo.setProjectName(project.getProjectName());
vo.setCustomerId(project.getCustomerId()); vo.setCustomerId(project.getCustomerId());
vo.setCustomerName(project.getCustomerName());
vo.setProjectType(project.getProjectType()); vo.setProjectType(project.getProjectType());
vo.setBudgetAmount(project.getBudgetAmount()); vo.setBudgetAmount(project.getBudgetAmount());
vo.setStartDate(project.getStartDate()); vo.setStartDate(project.getStartDate());

View File

@ -8,10 +8,11 @@ import java.time.LocalDate;
*/ */
public class ProjectVO { public class ProjectVO {
private Long id; private Long projectId;
private String projectCode; private String projectCode;
private String projectName; private String projectName;
private Long customerId; private Long customerId;
private String customerName;
private String projectType; private String projectType;
private BigDecimal budgetAmount; private BigDecimal budgetAmount;
private LocalDate startDate; private LocalDate startDate;
@ -20,12 +21,12 @@ public class ProjectVO {
private Integer status; private Integer status;
private String remark; private String remark;
public Long getId() { public Long getProjectId() {
return id; return projectId;
} }
public void setId(Long id) { public void setProjectId(Long projectId) {
this.id = id; this.projectId = projectId;
} }
public String getProjectCode() { public String getProjectCode() {
@ -52,6 +53,14 @@ public class ProjectVO {
this.customerId = customerId; this.customerId = customerId;
} }
public String getCustomerName() {
return customerName;
}
public void setCustomerName(String customerName) {
this.customerName = customerName;
}
public String getProjectType() { public String getProjectType() {
return projectType; return projectType;
} }

69
scripts/deploy/deploy-admin.sh Executable file
View File

@ -0,0 +1,69 @@
#!/bin/bash
# ============================================
# 管理后台部署脚本
# 用法: ./deploy-admin.sh [本地zip文件路径]
# ============================================
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
source "$SCRIPT_DIR/deploy-config.sh"
# 获取本地zip文件路径
if [ -n "$1" ]; then
LOCAL_ZIP="$1"
else
# 默认使用deploy目录下的fund-admin.zip
LOCAL_ZIP="$LOCAL_DEPLOY_DIR/$LOCAL_ADMIN_ZIP"
fi
log_info "============================================"
log_info "管理后台部署开始"
log_info "============================================"
log_info "本地文件: $LOCAL_ZIP"
log_info "远程路径: $ADMIN_DEPLOY_PATH"
log_info ""
# 检查sshpass
check_sshpass
# 检查本地文件是否存在
if [ ! -f "$LOCAL_ZIP" ]; then
log_error "本地zip文件不存在: $LOCAL_ZIP"
exit 1
fi
# 显示文件大小
FILE_SIZE=$(ls -lh "$LOCAL_ZIP" | awk '{print $5}')
log_info "文件大小: $FILE_SIZE"
# 1. 备份旧版本
log_info "备份旧版本..."
remote_exec "if [ -d '$ADMIN_DEPLOY_PATH' ]; then \
mkdir -p '$ADMIN_DEPLOY_PATH/../backup'; \
BACKUP_NAME='fadmin_backup_\$(date +%Y%m%d_%H%M%S)'; \
mv '$ADMIN_DEPLOY_PATH' '$ADMIN_DEPLOY_PATH/../backup/\$BACKUP_NAME' 2>/dev/null || true; \
echo '备份完成: '\$BACKUP_NAME; \
fi"
# 2. 创建新目录
log_info "创建新目录..."
remote_exec "mkdir -p '$ADMIN_DEPLOY_PATH'"
# 3. 上传zip文件
log_info "上传zip文件..."
TEMP_REMOTE_PATH="/tmp/fund-admin-$$.zip"
upload_file "$LOCAL_ZIP" "$TEMP_REMOTE_PATH"
log_info "上传完成"
# 4. 解压文件
log_info "解压文件..."
remote_exec "cd '$ADMIN_DEPLOY_PATH' && unzip -o '$TEMP_REMOTE_PATH' && rm -f '$TEMP_REMOTE_PATH'"
log_info "解压完成"
# 5. 验证部署
log_info "验证部署..."
FILE_COUNT=$(remote_exec "find '$ADMIN_DEPLOY_PATH' -type f | wc -l")
log_info "部署文件数: $FILE_COUNT"
log_info "============================================"
log_info "管理后台部署完成!"
log_info "============================================"

94
scripts/deploy/deploy-all.sh Executable file
View File

@ -0,0 +1,94 @@
#!/bin/bash
# ============================================
# 全量部署脚本
# 用法: ./deploy-all.sh [mobile|admin|services|all]
# 示例: ./deploy-all.sh mobile # 只部署移动端
# ./deploy-all.sh admin # 只部署管理后台
# ./deploy-all.sh services # 只部署所有服务
# ./deploy-all.sh all # 部署全部
# ============================================
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
source "$SCRIPT_DIR/deploy-config.sh"
DEPLOY_TYPE="${1:-all}"
log_info "============================================"
log_info "全量部署脚本"
log_info "============================================"
log_info "部署类型: $DEPLOY_TYPE"
log_info ""
# 检查sshpass
check_sshpass
# 部署移动端
deploy_mobile() {
log_info ">>> 开始部署移动端..."
"$SCRIPT_DIR/deploy-mobile.sh"
}
# 部署管理后台
deploy_admin() {
log_info ">>> 开始部署管理后台..."
"$SCRIPT_DIR/deploy-admin.sh"
}
# 部署单个服务
deploy_service() {
local service_name="$1"
log_info ">>> 开始部署服务: $service_name"
"$SCRIPT_DIR/deploy-service.sh" "$service_name"
}
# 部署所有服务
deploy_all_services() {
for service in "${SERVICES[@]}"; do
deploy_service "$service"
echo ""
done
}
# 根据参数执行部署
case "$DEPLOY_TYPE" in
mobile)
deploy_mobile
;;
admin)
deploy_admin
;;
services)
deploy_all_services
;;
all)
deploy_mobile
echo ""
deploy_admin
echo ""
deploy_all_services
;;
*)
# 检查是否是服务名
FOUND=0
for service in "${SERVICES[@]}"; do
if [ "$service" = "$DEPLOY_TYPE" ]; then
deploy_service "$DEPLOY_TYPE"
FOUND=1
break
fi
done
if [ $FOUND -eq 0 ]; then
log_error "未知参数: $DEPLOY_TYPE"
echo "用法: $0 [mobile|admin|services|all|服务名]"
echo "可用服务:"
for svc in "${SERVICES[@]}"; do
echo " - $svc"
done
exit 1
fi
;;
esac
log_info "============================================"
log_info "部署完成!"
log_info "============================================"

75
scripts/deploy/deploy-config.sh Executable file
View File

@ -0,0 +1,75 @@
#!/bin/bash
# ============================================
# 部署配置文件
# ============================================
# 生产环境SSH配置
PROD_HOST="82.156.159.46"
PROD_USER="fundsp"
PROD_PASSWORD="fdsp@Ywj\$107P#KM"
# 部署路径配置
MOBILE_DEPLOY_PATH="/home/fundsp/portal/fmobile"
ADMIN_DEPLOY_PATH="/home/fundsp/portal/fadmin"
SERVICE_DEPLOY_BASE="/home/fundsp/app"
# 本地打包路径
# 获取项目根目录(脚本目录的上两级)
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
LOCAL_DEPLOY_DIR="$PROJECT_ROOT/deploy"
LOCAL_MOBILE_ZIP="fund-mobile.zip"
LOCAL_ADMIN_ZIP="fund-admin.zip"
# 服务列表
SERVICES=(
"fund-gateway"
"fund-sys"
"fund-cust"
"fund-proj"
"fund-exp"
"fund-receipt"
"fund-report"
"fund-req"
"fund-file"
)
# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# 日志函数
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# 检查sshpass是否安装
check_sshpass() {
if ! command -v sshpass &> /dev/null; then
log_error "sshpass 未安装,请先安装: sudo apt install sshpass"
exit 1
fi
}
# 执行远程命令(加载环境变量)
remote_exec() {
local cmd="$1"
# 使用 bash -l 加载登录 shell 环境变量(包括 .bash_profile
sshpass -p "$PROD_PASSWORD" ssh -o StrictHostKeyChecking=no "$PROD_USER@$PROD_HOST" "bash -l -c '$cmd'"
}
# 上传文件
upload_file() {
local local_path="$1"
local remote_path="$2"
sshpass -p "$PROD_PASSWORD" scp -o StrictHostKeyChecking=no "$local_path" "$PROD_USER@$PROD_HOST:$remote_path"
}

69
scripts/deploy/deploy-mobile.sh Executable file
View File

@ -0,0 +1,69 @@
#!/bin/bash
# ============================================
# 移动端部署脚本
# 用法: ./deploy-mobile.sh [本地zip文件路径]
# ============================================
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
source "$SCRIPT_DIR/deploy-config.sh"
# 获取本地zip文件路径
if [ -n "$1" ]; then
LOCAL_ZIP="$1"
else
# 默认使用deploy目录下的fund-mobile.zip
LOCAL_ZIP="$LOCAL_DEPLOY_DIR/$LOCAL_MOBILE_ZIP"
fi
log_info "============================================"
log_info "移动端部署开始"
log_info "============================================"
log_info "本地文件: $LOCAL_ZIP"
log_info "远程路径: $MOBILE_DEPLOY_PATH"
log_info ""
# 检查sshpass
check_sshpass
# 检查本地文件是否存在
if [ ! -f "$LOCAL_ZIP" ]; then
log_error "本地zip文件不存在: $LOCAL_ZIP"
exit 1
fi
# 显示文件大小
FILE_SIZE=$(ls -lh "$LOCAL_ZIP" | awk '{print $5}')
log_info "文件大小: $FILE_SIZE"
# 1. 备份旧版本
log_info "备份旧版本..."
remote_exec "if [ -d '$MOBILE_DEPLOY_PATH' ]; then \
mkdir -p '$MOBILE_DEPLOY_PATH/../backup'; \
BACKUP_NAME='fmobile_backup_\$(date +%Y%m%d_%H%M%S)'; \
mv '$MOBILE_DEPLOY_PATH' '$MOBILE_DEPLOY_PATH/../backup/\$BACKUP_NAME' 2>/dev/null || true; \
echo '备份完成: '\$BACKUP_NAME; \
fi"
# 2. 创建新目录
log_info "创建新目录..."
remote_exec "mkdir -p '$MOBILE_DEPLOY_PATH'"
# 3. 上传zip文件
log_info "上传zip文件..."
TEMP_REMOTE_PATH="/tmp/fund-mobile-$$.zip"
upload_file "$LOCAL_ZIP" "$TEMP_REMOTE_PATH"
log_info "上传完成"
# 4. 解压文件
log_info "解压文件..."
remote_exec "cd '$MOBILE_DEPLOY_PATH' && unzip -o '$TEMP_REMOTE_PATH' && rm -f '$TEMP_REMOTE_PATH'"
log_info "解压完成"
# 5. 验证部署
log_info "验证部署..."
FILE_COUNT=$(remote_exec "find '$MOBILE_DEPLOY_PATH' -type f | wc -l")
log_info "部署文件数: $FILE_COUNT"
log_info "============================================"
log_info "移动端部署完成!"
log_info "============================================"

160
scripts/deploy/deploy-service.sh Executable file
View File

@ -0,0 +1,160 @@
#!/bin/bash
# ============================================
# 服务部署脚本
# 用法: ./deploy-service.sh <服务名> [本地tar.gz文件路径]
# 示例: ./deploy-service.sh fund-gateway
# ./deploy-service.sh fund-gateway /path/to/fund-gateway.tar.gz
# 注意: 只上传服务jar和fund-common*.jar不上传整个tar.gz
# ============================================
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
source "$SCRIPT_DIR/deploy-config.sh"
# 检查参数
if [ -z "$1" ]; then
log_error "请指定服务名称"
echo "可用服务:"
for svc in "${SERVICES[@]}"; do
echo " - $svc"
done
exit 1
fi
SERVICE_NAME="$1"
# 获取本地tar.gz文件路径
if [ -n "$2" ]; then
LOCAL_TAR="$2"
else
# 默认使用deploy目录下的服务tar.gz文件
LOCAL_TAR="$LOCAL_DEPLOY_DIR/${SERVICE_NAME}.tar.gz"
fi
SERVICE_DEPLOY_PATH="$SERVICE_DEPLOY_BASE/$SERVICE_NAME"
log_info "============================================"
log_info "服务部署开始: $SERVICE_NAME"
log_info "============================================"
log_info "本地文件: $LOCAL_TAR"
log_info "远程路径: $SERVICE_DEPLOY_PATH"
log_info ""
# 检查sshpass
check_sshpass
# 检查本地文件是否存在
if [ ! -f "$LOCAL_TAR" ]; then
log_error "本地tar.gz文件不存在: $LOCAL_TAR"
exit 1
fi
# 显示文件大小
FILE_SIZE=$(ls -lh "$LOCAL_TAR" | awk '{print $5}')
log_info "tar.gz文件大小: $FILE_SIZE"
# 1. 检查远程服务目录是否存在
log_info "检查远程服务目录..."
DIR_EXISTS=$(remote_exec "if [ -d '$SERVICE_DEPLOY_PATH' ]; then echo 'yes'; else echo 'no'; fi")
if [ "$DIR_EXISTS" != "yes" ]; then
log_error "远程服务目录不存在: $SERVICE_DEPLOY_PATH"
log_error "请先完整部署服务"
exit 1
fi
# 2. 本地解压tar.gz提取jar文件
log_info "本地解压并提取jar文件..."
TEMP_LOCAL_DIR="/tmp/${SERVICE_NAME}_extract_$$"
mkdir -p "$TEMP_LOCAL_DIR"
cd "$TEMP_LOCAL_DIR"
tar -xzf "$LOCAL_TAR"
# 查找解压后的目录
EXTRACTED_DIR=$(ls -d */ 2>/dev/null | head -1 | tr -d '/')
if [ -z "$EXTRACTED_DIR" ] || [ ! -d "$EXTRACTED_DIR/lib" ]; then
log_error "解压后未找到lib目录"
rm -rf "$TEMP_LOCAL_DIR"
exit 1
fi
# 查找服务jar
SERVICE_JAR=$(ls $EXTRACTED_DIR/lib/${SERVICE_NAME}*.jar 2>/dev/null | head -1)
COMMON_JAR=$(ls $EXTRACTED_DIR/lib/fund-common*.jar 2>/dev/null | head -1)
if [ -z "$SERVICE_JAR" ]; then
log_error "未找到服务jar: ${SERVICE_NAME}*.jar"
rm -rf "$TEMP_LOCAL_DIR"
exit 1
fi
log_info "服务jar: $(basename $SERVICE_JAR)"
if [ -n "$COMMON_JAR" ]; then
log_info "fund-common jar: $(basename $COMMON_JAR)"
fi
# 3. 备份远程旧版本jar文件
log_info "备份远程旧版本jar文件..."
remote_exec "cd '$SERVICE_DEPLOY_PATH/lib' && \
mkdir -p ../backup && \
BACKUP_NAME='jar_backup_\$(date +%Y%m%d_%H%M%S)' && \
mkdir -p ../backup/\$BACKUP_NAME && \
if ls ${SERVICE_NAME}*.jar 1>/dev/null 2>&1; then \
cp ${SERVICE_NAME}*.jar ../backup/\$BACKUP_NAME/; \
fi && \
if ls fund-common*.jar 1>/dev/null 2>&1; then \
cp fund-common*.jar ../backup/\$BACKUP_NAME/; \
fi && \
echo '备份完成: '\$BACKUP_NAME"
# 4. 停止服务
log_info "停止服务..."
remote_exec "if [ -f '$SERVICE_DEPLOY_PATH/bin/stop.sh' ]; then \
cd '$SERVICE_DEPLOY_PATH/bin' && ./stop.sh; \
sleep 2; \
echo '服务已停止'; \
else \
echo '未找到stop.sh脚本'; \
fi"
# 5. 上传jar文件只上传变更的文件
log_info "上传jar文件..."
# 上传服务jar
SERVICE_JAR_NAME=$(basename "$SERVICE_JAR")
log_info "上传 $SERVICE_JAR_NAME ($(ls -lh "$SERVICE_JAR" | awk '{print $5}'))..."
upload_file "$SERVICE_JAR" "$SERVICE_DEPLOY_PATH/lib/$SERVICE_JAR_NAME"
log_info "服务jar上传完成"
# 上传fund-common jar如果存在
if [ -n "$COMMON_JAR" ]; then
COMMON_JAR_NAME=$(basename "$COMMON_JAR")
log_info "上传 $COMMON_JAR_NAME ($(ls -lh "$COMMON_JAR" | awk '{print $5}'))..."
# 先删除远程旧的fund-common*.jar
remote_exec "rm -f '$SERVICE_DEPLOY_PATH/lib/fund-common'*.jar 2>/dev/null"
upload_file "$COMMON_JAR" "$SERVICE_DEPLOY_PATH/lib/$COMMON_JAR_NAME"
log_info "fund-common jar上传完成"
fi
# 清理本地临时目录
rm -rf "$TEMP_LOCAL_DIR"
# 6. 启动服务
log_info "启动服务..."
remote_exec "if [ -f '$SERVICE_DEPLOY_PATH/bin/start.sh' ]; then \
cd '$SERVICE_DEPLOY_PATH/bin' && ./start.sh; \
echo '服务已启动'; \
else \
echo '未找到start.sh脚本请手动启动'; \
fi"
# 7. 验证部署
log_info "验证部署..."
SERVICE_JAR_COUNT=$(remote_exec "ls '$SERVICE_DEPLOY_PATH/lib/${SERVICE_NAME}*.jar' 2>/dev/null | wc -l")
COMMON_JAR_COUNT=$(remote_exec "ls '$SERVICE_DEPLOY_PATH/lib/fund-common*.jar' 2>/dev/null | wc -l")
log_info "服务jar数: $SERVICE_JAR_COUNT, fund-common jar数: $COMMON_JAR_COUNT"
log_info "============================================"
log_info "服务部署完成: $SERVICE_NAME"
log_info "============================================"