Compare commits
27 Commits
bd5f8ab468
...
e5d9db10a8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5d9db10a8 | ||
|
|
852af7ee26 | ||
|
|
112a970563 | ||
|
|
2a74f237df | ||
|
|
fe51e87c17 | ||
|
|
1e346c3a2e | ||
|
|
06dfa26514 | ||
|
|
df2f1cdfa2 | ||
|
|
83e8712dfc | ||
|
|
9498201313 | ||
|
|
f87ee0b51d | ||
|
|
ff9f4d05ad | ||
|
|
9b545b3f00 | ||
|
|
011a6bfb3f | ||
|
|
965d98cab5 | ||
|
|
69f437dbb3 | ||
|
|
b5a954f008 | ||
|
|
205af48cb6 | ||
|
|
400b7272d4 | ||
|
|
e7f1b39ac8 | ||
|
|
d3a77c23f1 | ||
|
|
5e782ac8cc | ||
|
|
2e7fb5f5d4 | ||
|
|
610054918a | ||
|
|
f8e0a51314 | ||
|
|
807f894828 | ||
|
|
1a5b583c2f |
63
Agents.md
63
Agents.md
@ -1,8 +1,8 @@
|
||||
# 资金服务平台 (FundPlatform) - 开发规范
|
||||
|
||||
> **文档版本**: v1.1
|
||||
> **文档版本**: v1.3
|
||||
> **创建日期**: 2026-02-20
|
||||
> **更新日期**: 2026-02-13
|
||||
> **更新日期**: 2026-02-23
|
||||
> **适用范围**: 本项目所有开发人员
|
||||
|
||||
---
|
||||
@ -264,6 +264,52 @@ GET /cust/api/v1/customer/page # 分页查询
|
||||
| 404 | 资源不存在 |
|
||||
| 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实例 |
|
||||
| `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 |
|
||||
| 移动端H5 | http://localhost:8080 |
|
||||
| API网关 | http://localhost:8000 |
|
||||
| Nacos控制台 | http://localhost:8048/nacos |
|
||||
|
||||
|
||||
@ -62,6 +62,7 @@ CREATE TABLE IF NOT EXISTS requirement (
|
||||
receivable_date DATE COMMENT '应收款日期',
|
||||
status VARCHAR(20) DEFAULT 'pending' COMMENT '状态:pending-待开发,developing-开发中,delivered-已交付,completed-已完成',
|
||||
progress INT DEFAULT 0 COMMENT '开发进度(0-100)',
|
||||
remark VARCHAR(500) COMMENT '备注',
|
||||
attachment_url VARCHAR(500) COMMENT '附件URL',
|
||||
created_by BIGINT COMMENT '创建人ID',
|
||||
created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
|
||||
@ -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 前端打包
|
||||
|
||||
前端项目采用 Nginx 子路径部署方式:
|
||||
- **管理后台** (fund-admin): 部署路径 `/fadmin/`
|
||||
- **移动端H5** (fund-mobile): 部署路径 `/fmobile/`
|
||||
- **API网关前缀**: `/fund`
|
||||
|
||||
```bash
|
||||
# 使用部署脚本打包(推荐)
|
||||
./scripts/deploy-frontend-nginx.sh admin # 管理后台
|
||||
./scripts/deploy-frontend-nginx.sh mobile # 移动端H5
|
||||
|
||||
# 或手动打包
|
||||
# 管理后台打包
|
||||
cd fund-admin
|
||||
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-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
|
||||
# 上传一键管理脚本到deploy目录(在解压服务包后执行)
|
||||
# 一键管理脚本位于项目根目录scripts目录,需要单独上传
|
||||
@ -494,7 +552,7 @@ cp scripts/restart-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
|
||||
```
|
||||
|
||||
### 4.6 服务管理脚本
|
||||
### 4.7 服务管理脚本
|
||||
|
||||
每个服务的 `bin` 目录下包含以下脚本:
|
||||
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
# 资金服务平台 (FundPlatform) - 架构设计文档
|
||||
|
||||
> **文档版本**: v1.6
|
||||
> **文档版本**: v1.8
|
||||
> **创建日期**: 2026-02-13
|
||||
> **更新日期**: 2026-02-13
|
||||
> **更新日期**: 2026-02-23
|
||||
> **项目名称**: 资金服务平台
|
||||
> **项目代号**: 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
|
||||
version: '3.8'
|
||||
@ -3978,6 +4135,8 @@ public class GlobalExceptionHandler {
|
||||
| v1.4 | 2026-02-13 | 补充统一全局上下文 GlobalContext,统筹 tid/uid/uname 获取和异步传递 | zhangjf |
|
||||
| v1.5 | 2026-02-13 | 补充模块通信与 OpenFeign 参数对象管理策略、分层架构职责说明、MyBatis-Plus 使用规范、Controller 与参数校验规范、事务与测试规范及开发规则总览 | 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 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -158,6 +158,7 @@ npm run dev
|
||||
|
||||
# 4. 访问系统
|
||||
# 管理后台:http://localhost:3000
|
||||
# 移动端H5:http://localhost:8080
|
||||
# 网关地址:http://localhost:8000
|
||||
# Nacos 控制台:http://localhost:8048/nacos
|
||||
# Grafana 监控:http://localhost:3000 (Docker环境) 或 http://localhost:3001 (本地开发)
|
||||
@ -179,8 +180,8 @@ npm run dev
|
||||
│ │
|
||||
│ ┌──────────────┐ │
|
||||
│ │ 前端服务 │ │
|
||||
│ │ fund-admin │ ← 管理后台 (http://localhost:80) │
|
||||
│ │ fund-mobile │ ← 移动端H5 (http://localhost:81) │
|
||||
│ │ fund-admin │ ← 管理后台 (http://localhost/fadmin/) │
|
||||
│ │ fund-mobile │ ← 移动端H5 (http://localhost/fmobile/) │
|
||||
│ └──────┬───────┘ │
|
||||
│ │ │
|
||||
│ ┌──────┴───────┐ │
|
||||
@ -224,8 +225,8 @@ npm run dev
|
||||
|
||||
| 服务 | 端口 | 说明 |
|
||||
|------|------|------|
|
||||
| fund-admin | 80 | 管理后台前端 |
|
||||
| fund-mobile | 81 | 移动端H5 |
|
||||
| fund-admin | 80 | 管理后台前端 (访问路径: /fadmin/) |
|
||||
| fund-mobile | 80 | 移动端H5 (访问路径: /fmobile/) |
|
||||
| gateway | 8000 | API网关 |
|
||||
| fund-sys | 8100 | 系统管理服务 |
|
||||
| fund-sys-vip001 | 8101 | 系统服务VIP专属实例 |
|
||||
@ -480,14 +481,20 @@ server {
|
||||
listen 80;
|
||||
server_name test.fundplatform.com;
|
||||
|
||||
# 前端静态资源
|
||||
location / {
|
||||
root /opt/fundplatform/admin;
|
||||
try_files $uri $uri/ /index.html;
|
||||
# 管理后台前端 (部署路径: /fadmin/)
|
||||
location /fadmin/ {
|
||||
alias /opt/fundplatform/admin/;
|
||||
try_files $uri $uri/ /fadmin/index.html;
|
||||
}
|
||||
|
||||
# API 代理
|
||||
location /api/ {
|
||||
# 移动端H5 (部署路径: /fmobile/)
|
||||
location /fmobile/ {
|
||||
alias /opt/fundplatform/mobile/;
|
||||
try_files $uri $uri/ /fmobile/index.html;
|
||||
}
|
||||
|
||||
# API 代理 (网关前缀: /fund)
|
||||
location /fund/ {
|
||||
proxy_pass http://gateway/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
|
||||
@ -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 })
|
||||
}
|
||||
|
||||
|
||||
@ -31,24 +31,18 @@
|
||||
|
||||
<el-table :data="tableData" v-loading="loading" border stripe>
|
||||
<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="customerType" label="客户类型" width="100">
|
||||
<template #default="{ row }">
|
||||
<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="contact" label="联系人" width="120" />
|
||||
<el-table-column prop="phone" label="联系电话" width="140" />
|
||||
<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="status" label="状态" width="80">
|
||||
<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>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" width="160" />
|
||||
<el-table-column label="操作" width="280" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<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-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="客户名称" prop="customerName">
|
||||
<el-input v-model="form.customerName" placeholder="请输入客户名称" />
|
||||
<el-form-item label="客户编码" prop="customerCode">
|
||||
<el-input v-model="form.customerCode" placeholder="请输入客户编码" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="客户类型" prop="customerType">
|
||||
<el-select v-model="form.customerType" placeholder="请选择" style="width: 100%">
|
||||
<el-option label="企业" value="ENTERPRISE" />
|
||||
<el-option label="个人" value="INDIVIDUAL" />
|
||||
</el-select>
|
||||
<el-form-item label="客户名称" prop="customerName">
|
||||
<el-input v-model="form.customerName" placeholder="请输入客户名称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="联系人" prop="contactPerson">
|
||||
<el-input v-model="form.contactPerson" placeholder="请输入联系人" />
|
||||
<el-form-item label="联系人" prop="contact">
|
||||
<el-input v-model="form.contact" placeholder="请输入联系人" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="联系电话" prop="contactPhone">
|
||||
<el-input v-model="form.contactPhone" placeholder="请输入联系电话" />
|
||||
<el-form-item label="联系电话" prop="phone">
|
||||
<el-input v-model="form.phone" placeholder="请输入联系电话" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@ -117,8 +108,8 @@
|
||||
<el-col :span="12">
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="form.status">
|
||||
<el-radio value="NORMAL">正常</el-radio>
|
||||
<el-radio value="DISABLED">禁用</el-radio>
|
||||
<el-radio :value="1">启用</el-radio>
|
||||
<el-radio :value="0">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@ -128,8 +119,8 @@
|
||||
<el-input v-model="form.address" placeholder="请输入地址" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="备注" prop="remarks">
|
||||
<el-input v-model="form.remarks" type="textarea" :rows="3" placeholder="请输入备注" />
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
@ -143,21 +134,17 @@
|
||||
<el-dialog title="客户详情" v-model="detailVisible" width="700px">
|
||||
<el-descriptions :column="2" border>
|
||||
<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="客户类型">
|
||||
<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-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-descriptions-item>
|
||||
<el-descriptions-item label="联系人">{{ detailData.contactPerson }}</el-descriptions-item>
|
||||
<el-descriptions-item label="联系电话">{{ detailData.contactPhone }}</el-descriptions-item>
|
||||
<el-descriptions-item label="联系人">{{ detailData.contact }}</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.address }}</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" :span="2">{{ detailData.remarks || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间" :span="2">{{ detailData.createTime }}</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" :span="2">{{ detailData.remark || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-dialog>
|
||||
</div>
|
||||
@ -191,21 +178,21 @@ const formRef = ref<FormInstance>()
|
||||
|
||||
const form = reactive({
|
||||
customerId: null as number | null,
|
||||
customerCode: '',
|
||||
customerName: '',
|
||||
customerType: 'ENTERPRISE',
|
||||
contactPerson: '',
|
||||
contactPhone: '',
|
||||
contact: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
address: '',
|
||||
status: 'NORMAL',
|
||||
remarks: ''
|
||||
status: 1,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
customerCode: [{ required: true, message: '请输入客户编码', trigger: 'blur' }],
|
||||
customerName: [{ required: true, message: '请输入客户名称', trigger: 'blur' }],
|
||||
customerType: [{ required: true, message: '请选择客户类型', trigger: 'change' }],
|
||||
contactPerson: [{ required: true, message: '请输入联系人', trigger: 'blur' }],
|
||||
contactPhone: [
|
||||
contact: [{ required: true, message: '请输入联系人', trigger: 'blur' }],
|
||||
phone: [
|
||||
{ required: true, message: '请输入联系电话', trigger: 'blur' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
|
||||
],
|
||||
@ -306,14 +293,14 @@ const handleSubmit = async () => {
|
||||
|
||||
const resetForm = () => {
|
||||
form.customerId = null
|
||||
form.customerCode = ''
|
||||
form.customerName = ''
|
||||
form.customerType = 'ENTERPRISE'
|
||||
form.contactPerson = ''
|
||||
form.contactPhone = ''
|
||||
form.contact = ''
|
||||
form.phone = ''
|
||||
form.email = ''
|
||||
form.address = ''
|
||||
form.status = 'NORMAL'
|
||||
form.remarks = ''
|
||||
form.status = 1
|
||||
form.remark = ''
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
|
||||
@ -53,6 +53,7 @@
|
||||
¥{{ row.amount?.toLocaleString() || '0' }}
|
||||
</template>
|
||||
</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="approvalStatus" label="审批状态" width="100">
|
||||
<template #default="{ row }">
|
||||
@ -145,6 +146,11 @@
|
||||
<el-input-number v-model="form.amount" :precision="2" :min="0" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</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 :gutter="20">
|
||||
@ -199,6 +205,7 @@
|
||||
<el-descriptions :column="1" border>
|
||||
<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.payeeName || '-' }}</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.applicant }}</el-descriptions-item>
|
||||
@ -229,6 +236,7 @@
|
||||
<el-descriptions-item label="支出金额">
|
||||
¥{{ detailData.amount?.toLocaleString() || '0' }}
|
||||
</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="审批状态">
|
||||
<el-tag v-if="detailData.approvalStatus === 'DRAFT'" type="info">草稿</el-tag>
|
||||
@ -312,6 +320,7 @@ const form = reactive({
|
||||
expenseTypeId: null as number | null,
|
||||
projectId: null as number | null,
|
||||
amount: 0,
|
||||
payeeName: '',
|
||||
expenseDate: '',
|
||||
applicant: '',
|
||||
description: '',
|
||||
@ -323,6 +332,7 @@ const rules = reactive<FormRules>({
|
||||
title: [{ required: true, message: '请输入支出标题', trigger: 'blur' }],
|
||||
expenseTypeId: [{ required: true, message: '请选择支出类型', trigger: 'change' }],
|
||||
amount: [{ required: true, message: '请输入支出金额', trigger: 'blur' }],
|
||||
payeeName: [{ required: true, message: '请输入收款单位', trigger: 'blur' }],
|
||||
expenseDate: [{ required: true, message: '请选择支出日期', trigger: 'change' }],
|
||||
applicant: [{ required: true, message: '请输入申请人', trigger: 'blur' }]
|
||||
})
|
||||
@ -552,6 +562,7 @@ const resetForm = () => {
|
||||
form.expenseTypeId = null
|
||||
form.projectId = null
|
||||
form.amount = 0
|
||||
form.payeeName = ''
|
||||
form.expenseDate = ''
|
||||
form.applicant = ''
|
||||
form.description = ''
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
<el-card shadow="never" class="search-card">
|
||||
<el-form :inline="true" :model="queryParams">
|
||||
<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 label="项目名称">
|
||||
<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-column prop="requirementId" label="需求ID" width="80" />
|
||||
<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="requirementType" label="需求类型" width="100">
|
||||
<template #default="{ row }">
|
||||
@ -113,8 +113,8 @@
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="需求标题" prop="requirementTitle">
|
||||
<el-input v-model="form.requirementTitle" placeholder="请输入需求标题" />
|
||||
<el-form-item label="需求名称" prop="requirementName">
|
||||
<el-input v-model="form.requirementName" placeholder="请输入需求名称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@ -132,6 +132,21 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</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-form-item label="需求类型" prop="requirementType">
|
||||
<el-select v-model="form.requirementType" placeholder="请选择" style="width: 100%">
|
||||
@ -249,8 +264,9 @@
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="需求ID">{{ detailData.requirementId }}</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.customerName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="需求类型">
|
||||
<el-tag v-if="detailData.requirementType === 'FEATURE'" type="primary">功能</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 { getRequirementList, createRequirement, updateRequirement, deleteRequirement } from '@/api/project'
|
||||
import { getProjectList } from '@/api/project'
|
||||
import { getCustomerList } from '@/api/customer'
|
||||
|
||||
// 文件上传相关
|
||||
const uploadUrl = '/file/api/v1/file/upload'
|
||||
@ -307,11 +324,12 @@ const submitLoading = ref(false)
|
||||
const tableData = ref<any[]>([])
|
||||
const total = ref(0)
|
||||
const projectList = ref<any[]>([])
|
||||
const customerList = ref<any[]>([])
|
||||
|
||||
const queryParams = reactive({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
requirementTitle: '',
|
||||
requirementName: '',
|
||||
projectName: '',
|
||||
requirementStatus: '',
|
||||
priority: ''
|
||||
@ -324,8 +342,9 @@ const formRef = ref<FormInstance>()
|
||||
const form = reactive({
|
||||
requirementId: null as number | null,
|
||||
requirementCode: '',
|
||||
requirementTitle: '',
|
||||
requirementName: '',
|
||||
projectId: null as number | null,
|
||||
customerId: null as number | null,
|
||||
requirementType: 'FEATURE',
|
||||
priority: 'MEDIUM',
|
||||
requirementStatus: 'PENDING',
|
||||
@ -338,8 +357,9 @@ const form = reactive({
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
requirementCode: [{ required: true, message: '请输入需求编号', trigger: 'blur' }],
|
||||
requirementTitle: [{ required: true, message: '请输入需求标题', trigger: 'blur' }],
|
||||
requirementName: [{ required: true, message: '请输入需求名称', trigger: 'blur' }],
|
||||
projectId: [{ required: true, message: '请选择项目', trigger: 'change' }],
|
||||
customerId: [{ required: true, message: '请选择客户', trigger: 'change' }],
|
||||
requirementType: [{ 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}`
|
||||
}
|
||||
|
||||
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 () => {
|
||||
loading.value = true
|
||||
try {
|
||||
@ -417,7 +446,7 @@ const handleSearch = () => {
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
queryParams.requirementTitle = ''
|
||||
queryParams.requirementName = ''
|
||||
queryParams.projectName = ''
|
||||
queryParams.requirementStatus = ''
|
||||
queryParams.priority = ''
|
||||
@ -520,8 +549,9 @@ const handleSubmit = async () => {
|
||||
const resetForm = () => {
|
||||
form.requirementId = null
|
||||
form.requirementCode = ''
|
||||
form.requirementTitle = ''
|
||||
form.requirementName = ''
|
||||
form.projectId = null
|
||||
form.customerId = null
|
||||
form.requirementType = 'FEATURE'
|
||||
form.priority = 'MEDIUM'
|
||||
form.requirementStatus = 'PENDING'
|
||||
@ -537,6 +567,7 @@ const resetForm = () => {
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
fetchProjects()
|
||||
fetchCustomers()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@ import java.util.List;
|
||||
* <li>pageNum:当前页码(从 1 开始);</li>
|
||||
* <li>pageSize:每页条数;</li>
|
||||
* <li>total:总记录数;</li>
|
||||
* <li>list:当前页数据列表。</li>
|
||||
* <li>records:当前页数据列表。</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param <T> 列表元素类型
|
||||
@ -31,17 +31,17 @@ public class PageResult<T> implements Serializable {
|
||||
private long total;
|
||||
|
||||
/** 当前页数据 */
|
||||
private List<T> list;
|
||||
private List<T> records;
|
||||
|
||||
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.pageSize = pageSize;
|
||||
this.total = total;
|
||||
this.list = list != null ? list : Collections.emptyList();
|
||||
this.records = records != null ? records : Collections.emptyList();
|
||||
}
|
||||
|
||||
public long getPageNum() {
|
||||
@ -68,11 +68,11 @@ public class PageResult<T> implements Serializable {
|
||||
this.total = total;
|
||||
}
|
||||
|
||||
public List<T> getList() {
|
||||
return list;
|
||||
public List<T> getRecords() {
|
||||
return records;
|
||||
}
|
||||
|
||||
public void setList(List<T> list) {
|
||||
this.list = list;
|
||||
public void setRecords(List<T> records) {
|
||||
this.records = records;
|
||||
}
|
||||
}
|
||||
|
||||
@ -122,7 +122,7 @@ public class CustomerServiceImpl implements CustomerService {
|
||||
|
||||
private CustomerVO convertToVO(Customer customer) {
|
||||
CustomerVO vo = new CustomerVO();
|
||||
vo.setId(customer.getId());
|
||||
vo.setCustomerId(customer.getId());
|
||||
vo.setCustomerCode(customer.getCustomerCode());
|
||||
vo.setCustomerName(customer.getCustomerName());
|
||||
vo.setContact(customer.getContact());
|
||||
|
||||
@ -5,7 +5,7 @@ package com.fundplatform.cust.vo;
|
||||
*/
|
||||
public class CustomerVO {
|
||||
|
||||
private Long id;
|
||||
private Long customerId;
|
||||
private String customerCode;
|
||||
private String customerName;
|
||||
private String contact;
|
||||
@ -15,12 +15,12 @@ public class CustomerVO {
|
||||
private Integer status;
|
||||
private String remark;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
public Long getCustomerId() {
|
||||
return customerId;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
public void setCustomerId(Long customerId) {
|
||||
this.customerId = customerId;
|
||||
}
|
||||
|
||||
public String getCustomerCode() {
|
||||
|
||||
6
fund-mobile/.env.development
Normal file
6
fund-mobile/.env.development
Normal file
@ -0,0 +1,6 @@
|
||||
# 开发环境配置
|
||||
# 开发模式无部署前缀
|
||||
VITE_BASE=/
|
||||
|
||||
# API基础路径(开发模式使用代理)
|
||||
VITE_API_BASE_URL=
|
||||
6
fund-mobile/.env.production
Normal file
6
fund-mobile/.env.production
Normal file
@ -0,0 +1,6 @@
|
||||
# 生产环境配置
|
||||
# 部署路径前缀(Nginx路由使用)
|
||||
VITE_BASE=/fmobile/
|
||||
|
||||
# API基础路径
|
||||
VITE_API_BASE_URL=/fund
|
||||
4
fund-mobile/components.d.ts
vendored
4
fund-mobile/components.d.ts
vendored
@ -11,7 +11,11 @@ declare module 'vue' {
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
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']
|
||||
VanField: typeof import('vant/es')['Field']
|
||||
VanForm: typeof import('vant/es')['Form']
|
||||
VanIcon: typeof import('vant/es')['Icon']
|
||||
VanList: typeof import('vant/es')['List']
|
||||
VanNavBar: typeof import('vant/es')['NavBar']
|
||||
|
||||
@ -31,6 +31,7 @@ body {
|
||||
|
||||
.app-container {
|
||||
min-height: 100vh;
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
|
||||
/* 页面切换动画 */
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import request from './request'
|
||||
|
||||
// 用户认证
|
||||
// ===================== 用户认证 =====================
|
||||
|
||||
export function login(data: { username: string; password: string }) {
|
||||
return request.post('/auth/login', data)
|
||||
}
|
||||
@ -13,8 +14,13 @@ export function logout() {
|
||||
return request.post('/auth/logout')
|
||||
}
|
||||
|
||||
// 项目管理
|
||||
export function getProjectList(params?: { pageNum: number; pageSize: number; projectName?: string }) {
|
||||
export function updatePassword(data: { oldPassword: string; newPassword: string; confirmPassword: string }) {
|
||||
return request.put('/sys/profile/password', data)
|
||||
}
|
||||
|
||||
// ===================== 项目管理 =====================
|
||||
|
||||
export function getProjectList(params?: { pageNum: number; pageSize: number; keyword?: string }) {
|
||||
return request.get('/project/page', { params })
|
||||
}
|
||||
|
||||
@ -22,38 +28,78 @@ export function getProjectById(id: number) {
|
||||
return request.get(`/project/${id}`)
|
||||
}
|
||||
|
||||
// 客户管理
|
||||
export function getCustomerList(params?: { pageNum: number; pageSize: number; customerName?: string }) {
|
||||
export function createProject(data: any) {
|
||||
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 })
|
||||
}
|
||||
|
||||
// 支出管理
|
||||
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) {
|
||||
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 })
|
||||
}
|
||||
|
||||
// 应收款管理
|
||||
export function getReceivableList(params: { pageNum: number; pageSize: number; status?: string }) {
|
||||
export function getExpenseTypeTree() {
|
||||
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 })
|
||||
}
|
||||
|
||||
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) {
|
||||
return request.get(`/receipt/receivable/upcoming-due?daysWithin=${daysWithin}`)
|
||||
}
|
||||
|
||||
// 统计数据
|
||||
export function getTodayIncome() {
|
||||
return request.get('/receipt/receivable/stats/today-income')
|
||||
}
|
||||
|
||||
export function getTodayExpense() {
|
||||
return request.get('/exp/expense/stats/today-expense')
|
||||
}
|
||||
|
||||
export function getUnpaidAmount() {
|
||||
return request.get('/receipt/receivable/stats/unpaid-amount')
|
||||
}
|
||||
@ -61,8 +107,3 @@ export function getUnpaidAmount() {
|
||||
export function getOverdueCount() {
|
||||
return request.get('/receipt/receivable/stats/overdue-count')
|
||||
}
|
||||
|
||||
// 支出类型
|
||||
export function getExpenseTypeTree() {
|
||||
return request.get('/exp/expense-type/tree')
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import axios from 'axios'
|
||||
|
||||
const request = axios.create({
|
||||
baseURL: '/fund',
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL || '/fund',
|
||||
timeout: 15000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
|
||||
@ -2,7 +2,7 @@ import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { useTenantStore } from '@/stores/tenant'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
@ -10,36 +10,85 @@ const router = createRouter({
|
||||
component: () => import('@/views/Home.vue'),
|
||||
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',
|
||||
name: 'ExpenseAdd',
|
||||
component: () => import('@/views/expense/Add.vue'),
|
||||
meta: { title: '新增支出', requiresAuth: true }
|
||||
},
|
||||
// 应收款管理
|
||||
{
|
||||
path: '/receivable',
|
||||
name: 'ReceivableList',
|
||||
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',
|
||||
name: 'ProjectList',
|
||||
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',
|
||||
name: 'CustomerList',
|
||||
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',
|
||||
name: 'My',
|
||||
component: () => import('@/views/my/Index.vue'),
|
||||
meta: { title: '我的', requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/my/change-password',
|
||||
name: 'ChangePassword',
|
||||
component: () => import('@/views/my/ChangePassword.vue'),
|
||||
meta: { title: '修改密码', requiresAuth: true }
|
||||
},
|
||||
// 登录
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
|
||||
@ -1,11 +1,5 @@
|
||||
<template>
|
||||
<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="card-header">
|
||||
@ -43,41 +37,100 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 快捷入口卡片 -->
|
||||
<!-- 快捷操作卡片 -->
|
||||
<div class="quick-card mac-card fade-in-up delay-1">
|
||||
<div class="card-header">
|
||||
<span class="card-title">快捷操作</span>
|
||||
</div>
|
||||
<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-icon">
|
||||
<van-icon name="plus" />
|
||||
<div class="quick-icon expense">
|
||||
<van-icon name="gold-coin-o" />
|
||||
</div>
|
||||
<div class="quick-text">新增支出</div>
|
||||
</div>
|
||||
<div class="quick-item" @click="$router.push('/receivable')">
|
||||
<div class="quick-icon">
|
||||
<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">
|
||||
<div class="quick-item" @click="$router.push('/project/add')">
|
||||
<div class="quick-icon project">
|
||||
<van-icon name="todo-list-o" />
|
||||
</div>
|
||||
<div class="quick-text">项目</div>
|
||||
<div class="quick-text">新增项目</div>
|
||||
</div>
|
||||
<div class="quick-item" @click="$router.push('/customer')">
|
||||
<div class="quick-icon">
|
||||
<div class="quick-item" @click="$router.push('/customer/add')">
|
||||
<div class="quick-icon customer">
|
||||
<van-icon name="friends-o" />
|
||||
</div>
|
||||
<div class="quick-text">客户</div>
|
||||
<div class="quick-text">新增客户</div>
|
||||
</div>
|
||||
<div class="quick-item" @click="$router.push('/my')">
|
||||
<div class="quick-icon">
|
||||
<van-icon name="user-o" />
|
||||
</div>
|
||||
<div class="quick-text">我的</div>
|
||||
</div>
|
||||
|
||||
<!-- 业务服务卡片 -->
|
||||
<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 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>
|
||||
@ -86,7 +139,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import request from '@/api/request'
|
||||
import { getTodayIncome, getTodayExpense, getUnpaidAmount } from '@/api'
|
||||
|
||||
const summary = ref({
|
||||
todayIncome: 0,
|
||||
@ -114,9 +167,9 @@ const formatMoney = (value: number) => {
|
||||
const loadSummary = async () => {
|
||||
try {
|
||||
const [incomeRes, expenseRes, unpaidRes] = await Promise.all([
|
||||
request.get('/receipt/api/v1/receipt/receivable/stats/today-income'),
|
||||
request.get('/exp/api/v1/exp/expense/stats/today-expense'),
|
||||
request.get('/receipt/api/v1/receipt/receivable/stats/unpaid-amount')
|
||||
getTodayIncome(),
|
||||
getTodayExpense(),
|
||||
getUnpaidAmount()
|
||||
])
|
||||
|
||||
summary.value.todayIncome = (incomeRes as any).data || 0
|
||||
@ -135,26 +188,7 @@ onMounted(() => {
|
||||
<style scoped>
|
||||
.home {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.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;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
@ -240,25 +274,29 @@ onMounted(() => {
|
||||
color: var(--mac-text-secondary);
|
||||
}
|
||||
|
||||
/* 快捷操作样式 */
|
||||
.quick-card {
|
||||
margin-bottom: 16px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.quick-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
gap: 12px;
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
.quick-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 16px 8px;
|
||||
padding: 14px 16px;
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
border-radius: 16px;
|
||||
border-radius: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
min-width: 90px;
|
||||
}
|
||||
|
||||
.quick-item:active {
|
||||
@ -267,22 +305,121 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.quick-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(135deg, var(--mac-primary), #5AC8FA);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 22px;
|
||||
color: #fff;
|
||||
margin-bottom: 10px;
|
||||
box-shadow: 0 4px 12px rgba(0, 122, 255, 0.25);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.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 {
|
||||
font-size: 13px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
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>
|
||||
|
||||
@ -51,7 +51,7 @@
|
||||
import { ref, reactive } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { showToast, showSuccessToast } from 'vant'
|
||||
import request from '@/api/request'
|
||||
import { login } from '@/api'
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
@ -74,7 +74,7 @@ const handleLogin = async () => {
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await request.post('/sys/api/v1/auth/login', form)
|
||||
const res: any = await login(form)
|
||||
const data = res.data
|
||||
localStorage.setItem('token', data.token)
|
||||
localStorage.setItem('userInfo', JSON.stringify({
|
||||
|
||||
144
fund-mobile/src/views/customer/Add.vue
Normal file
144
fund-mobile/src/views/customer/Add.vue
Normal 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>
|
||||
@ -20,19 +20,19 @@
|
||||
<div class="customer-avatar">{{ item.customerName?.charAt(0) || 'C' }}</div>
|
||||
<div class="customer-info">
|
||||
<div class="customer-name">{{ item.customerName }}</div>
|
||||
<div class="customer-short">{{ item.customerShort || '-' }}</div>
|
||||
<div class="customer-code">{{ item.customerCode || '-' }}</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 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">
|
||||
<van-icon name="phone-o" />
|
||||
<span>{{ item.phone }}</span>
|
||||
</div>
|
||||
<div class="detail-item" v-if="item.industry">
|
||||
<van-icon name="cluster-o" />
|
||||
<span>{{ item.industry }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</van-list>
|
||||
@ -41,7 +41,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { getCustomerList } from '@/api'
|
||||
|
||||
const searchText = ref('')
|
||||
@ -52,24 +52,12 @@ const list = ref<any[]>([])
|
||||
const pageNum = ref(1)
|
||||
const pageSize = 10
|
||||
|
||||
const getLevelType = (level: string): 'primary' | 'success' | 'warning' | 'danger' | 'default' => {
|
||||
const map: Record<string, 'primary' | 'success' | 'warning' | 'danger' | 'default'> = {
|
||||
'A': 'primary',
|
||||
'B': 'success',
|
||||
'C': 'warning',
|
||||
'D': 'danger'
|
||||
}
|
||||
return map[level] || 'default'
|
||||
const getStatusType = (status: number): 'primary' | 'success' | 'warning' | 'danger' | 'default' => {
|
||||
return status === 1 ? 'success' : 'default'
|
||||
}
|
||||
|
||||
const getLevelText = (level: string) => {
|
||||
const map: Record<string, string> = {
|
||||
'A': 'A类',
|
||||
'B': 'B类',
|
||||
'C': 'C类',
|
||||
'D': 'D类'
|
||||
}
|
||||
return map[level] || '普通'
|
||||
const getStatusText = (status: number) => {
|
||||
return status === 1 ? '启用' : '禁用'
|
||||
}
|
||||
|
||||
const loadData = async () => {
|
||||
@ -77,7 +65,7 @@ const loadData = async () => {
|
||||
const res: any = await getCustomerList({
|
||||
pageNum: pageNum.value,
|
||||
pageSize,
|
||||
customerName: searchText.value || undefined
|
||||
keyword: searchText.value || undefined
|
||||
})
|
||||
const records = res.data?.records || []
|
||||
if (pageNum.value === 1) {
|
||||
@ -96,6 +84,8 @@ const loadData = async () => {
|
||||
}
|
||||
|
||||
const onLoad = () => {
|
||||
if (loading.value) return
|
||||
loading.value = true
|
||||
pageNum.value++
|
||||
loadData()
|
||||
}
|
||||
@ -110,8 +100,14 @@ const handleSearch = () => {
|
||||
pageNum.value = 1
|
||||
finished.value = false
|
||||
list.value = []
|
||||
loading.value = true
|
||||
loadData()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loading.value = true
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -162,7 +158,7 @@ const handleSearch = () => {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.customer-short {
|
||||
.customer-code {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
margin-top: 2px;
|
||||
|
||||
@ -10,12 +10,12 @@
|
||||
|
||||
<div class="form-card mac-card fade-in-up">
|
||||
<div class="form-group">
|
||||
<label>支出标题</label>
|
||||
<label>支出标题 <span class="required">*</span></label>
|
||||
<input v-model="form.title" placeholder="输入标题" class="mac-input" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>支出类型</label>
|
||||
<label>支出类型 <span class="required">*</span></label>
|
||||
<div class="mac-select" @click="showTypePicker = true">
|
||||
<span :class="{ placeholder: !form.expenseTypeName }">
|
||||
{{ form.expenseTypeName || '选择类型' }}
|
||||
@ -25,13 +25,18 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>支出金额</label>
|
||||
<label>支出金额 <span class="required">*</span></label>
|
||||
<div class="amount-input">
|
||||
<span class="currency">¥</span>
|
||||
<input v-model="form.amount" type="number" placeholder="0.00" />
|
||||
</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">
|
||||
<label>支出日期</label>
|
||||
<div class="mac-select" @click="showDatePicker = true">
|
||||
@ -69,8 +74,8 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { showToast, showSuccessToast } from 'vant'
|
||||
import request from '@/api/request'
|
||||
import { showToast, showSuccessToast, showFailToast } from 'vant'
|
||||
import { createExpense, getExpenseTypeTree } from '@/api'
|
||||
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
@ -82,6 +87,7 @@ const form = reactive({
|
||||
expenseTypeId: null as number | null,
|
||||
expenseTypeName: '',
|
||||
amount: '',
|
||||
payeeName: '',
|
||||
expenseDate: '',
|
||||
description: ''
|
||||
})
|
||||
@ -102,15 +108,19 @@ const onDateConfirm = ({ selectedValues }: any) => {
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!form.title) {
|
||||
showToast('请输入支出标题')
|
||||
showFailToast('请输入支出标题')
|
||||
return
|
||||
}
|
||||
if (!form.expenseTypeId) {
|
||||
showToast('请选择支出类型')
|
||||
showFailToast('请选择支出类型')
|
||||
return
|
||||
}
|
||||
if (!form.amount) {
|
||||
showToast('请输入支出金额')
|
||||
showFailToast('请输入支出金额')
|
||||
return
|
||||
}
|
||||
if (!form.payeeName) {
|
||||
showFailToast('请输入收款单位')
|
||||
return
|
||||
}
|
||||
|
||||
@ -124,15 +134,15 @@ const handleSubmit = async () => {
|
||||
amount: parseFloat(form.amount),
|
||||
expenseDate: expenseDateTime,
|
||||
purpose: form.description,
|
||||
payeeName: '待填写'
|
||||
payeeName: form.payeeName
|
||||
}
|
||||
console.log('提交支出数据:', requestData)
|
||||
await request.post('/exp/api/v1/exp/expense', requestData)
|
||||
await createExpense(requestData)
|
||||
showSuccessToast('提交成功')
|
||||
router.back()
|
||||
} catch (e: any) {
|
||||
console.error('提交失败:', e)
|
||||
showToast(e.message || '提交失败')
|
||||
showFailToast(e.message || '提交失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@ -141,7 +151,7 @@ const handleSubmit = async () => {
|
||||
onMounted(async () => {
|
||||
// 加载支出类型
|
||||
try {
|
||||
const res: any = await request.get('/exp/api/v1/exp/expense-type/tree')
|
||||
const res: any = await getExpenseTypeTree()
|
||||
const types = res.data || []
|
||||
typeColumns.value = types.map((t: any) => ({
|
||||
text: t.typeName,
|
||||
@ -219,6 +229,11 @@ onMounted(async () => {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.form-group label .required {
|
||||
color: var(--mac-danger);
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.mac-input {
|
||||
width: 100%;
|
||||
padding: 14px 16px;
|
||||
|
||||
231
fund-mobile/src/views/expense/List.vue
Normal file
231
fund-mobile/src/views/expense/List.vue
Normal 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>
|
||||
145
fund-mobile/src/views/my/ChangePassword.vue
Normal file
145
fund-mobile/src/views/my/ChangePassword.vue
Normal 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>
|
||||
@ -55,7 +55,7 @@ const userInfo = ref({
|
||||
})
|
||||
|
||||
const handleChangePassword = () => {
|
||||
showToast('功能开发中')
|
||||
router.push('/my/change-password')
|
||||
}
|
||||
|
||||
const handleAbout = () => {
|
||||
|
||||
289
fund-mobile/src/views/project/Add.vue
Normal file
289
fund-mobile/src/views/project/Add.vue
Normal 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>
|
||||
@ -31,10 +31,6 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="project-amount">
|
||||
<div class="amount-item">
|
||||
<span class="label">合同金额</span>
|
||||
<span class="value">{{ formatMoney(item.contractAmount) }}</span>
|
||||
</div>
|
||||
<div class="amount-item">
|
||||
<span class="label">预算金额</span>
|
||||
<span class="value">{{ formatMoney(item.budgetAmount) }}</span>
|
||||
@ -47,7 +43,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { getProjectList } from '@/api'
|
||||
|
||||
@ -92,7 +88,7 @@ const loadData = async () => {
|
||||
const res: any = await getProjectList({
|
||||
pageNum: pageNum.value,
|
||||
pageSize,
|
||||
projectName: searchText.value || undefined
|
||||
keyword: searchText.value || undefined
|
||||
})
|
||||
const records = res.data?.records || []
|
||||
if (pageNum.value === 1) {
|
||||
@ -111,6 +107,8 @@ const loadData = async () => {
|
||||
}
|
||||
|
||||
const onLoad = () => {
|
||||
if (loading.value) return
|
||||
loading.value = true
|
||||
pageNum.value++
|
||||
loadData()
|
||||
}
|
||||
@ -125,12 +123,18 @@ const handleSearch = () => {
|
||||
pageNum.value = 1
|
||||
finished.value = false
|
||||
list.value = []
|
||||
loading.value = true
|
||||
loadData()
|
||||
}
|
||||
|
||||
const goDetail = (item: any) => {
|
||||
router.push(`/project/${item.projectId}`)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loading.value = true
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
221
fund-mobile/src/views/receivable/Add.vue
Normal file
221
fund-mobile/src/views/receivable/Add.vue
Normal 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>
|
||||
@ -4,10 +4,15 @@
|
||||
<div class="back-btn" @click="$router.back()">
|
||||
<van-icon name="arrow-left" />
|
||||
</div>
|
||||
<span class="header-title">应收款列表</span>
|
||||
<span class="header-title">应收款管理</span>
|
||||
<div class="placeholder"></div>
|
||||
</div>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<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="list-container">
|
||||
@ -37,14 +42,20 @@
|
||||
</div>
|
||||
</van-list>
|
||||
</van-pull-refresh>
|
||||
|
||||
<!-- 新增按钮 -->
|
||||
<div class="add-btn" @click="$router.push('/receivable/add')">
|
||||
<van-icon name="plus" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { showToast } from 'vant'
|
||||
import request from '@/api/request'
|
||||
import { getReceivableList } from '@/api'
|
||||
|
||||
const searchText = ref('')
|
||||
const loading = ref(false)
|
||||
const refreshing = ref(false)
|
||||
const finished = ref(false)
|
||||
@ -72,35 +83,54 @@ const getStatusName = (status: string) => {
|
||||
return map[status] || status
|
||||
}
|
||||
|
||||
const onLoad = async () => {
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const res: any = await request.get('/receipt/api/v1/receipt/receivable/page', {
|
||||
params: { pageNum: pageNum.value, pageSize }
|
||||
const res: any = await getReceivableList({
|
||||
pageNum: pageNum.value,
|
||||
pageSize,
|
||||
keyword: searchText.value || undefined
|
||||
})
|
||||
const records = res.data?.records || []
|
||||
list.value.push(...records)
|
||||
loading.value = false
|
||||
if (records.length < pageSize) {
|
||||
finished.value = true
|
||||
if (pageNum.value === 1) {
|
||||
list.value = records
|
||||
} else {
|
||||
pageNum.value++
|
||||
list.value.push(...records)
|
||||
}
|
||||
finished.value = records.length < pageSize
|
||||
loading.value = false
|
||||
} catch (e: any) {
|
||||
showToast(e.message || '加载失败')
|
||||
loading.value = false
|
||||
finished.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const onLoad = () => {
|
||||
if (loading.value) return
|
||||
loading.value = true
|
||||
pageNum.value++
|
||||
loadData()
|
||||
}
|
||||
|
||||
const onRefresh = () => {
|
||||
list.value = []
|
||||
pageNum.value = 1
|
||||
finished.value = false
|
||||
refreshing.value = false
|
||||
onLoad()
|
||||
loadData()
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pageNum.value = 1
|
||||
finished.value = false
|
||||
list.value = []
|
||||
loading.value = true
|
||||
loadData()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
onLoad()
|
||||
loading.value = true
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
@ -114,7 +144,7 @@ onMounted(() => {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
margin: 12px -16px 16px;
|
||||
margin: 12px -16px 0;
|
||||
border-radius: 0;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
@ -124,6 +154,12 @@ onMounted(() => {
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
background: #fff;
|
||||
margin: 0 -16px 12px;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
@ -222,4 +258,27 @@ onMounted(() => {
|
||||
font-size: 12px;
|
||||
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>
|
||||
|
||||
224
fund-mobile/src/views/requirement/Add.vue
Normal file
224
fund-mobile/src/views/requirement/Add.vue
Normal 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>
|
||||
247
fund-mobile/src/views/requirement/List.vue
Normal file
247
fund-mobile/src/views/requirement/List.vue
Normal 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
10
fund-mobile/src/vite-env.d.ts
vendored
Normal 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
|
||||
}
|
||||
@ -1,10 +1,17 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import { defineConfig, loadEnv } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import { VantResolver } from '@vant/auto-import-resolver'
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
export default defineConfig({
|
||||
export default defineConfig(({ mode }) => {
|
||||
// 加载环境变量
|
||||
const env = loadEnv(mode, process.cwd())
|
||||
const base = env.VITE_BASE || '/'
|
||||
|
||||
return {
|
||||
// 部署路径前缀
|
||||
base,
|
||||
plugins: [
|
||||
vue(),
|
||||
Components({
|
||||
@ -45,4 +52,5 @@ export default defineConfig({
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@ -25,13 +25,13 @@ public class RequirementController {
|
||||
@GetMapping("/page")
|
||||
public Result<Page<RequirementVO>> page(
|
||||
@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 requirementStatus,
|
||||
@RequestParam(required = false) String priority,
|
||||
@RequestParam(defaultValue = "1") int pageNum,
|
||||
@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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -15,6 +15,7 @@ public class Project extends BaseEntity {
|
||||
private String projectCode;
|
||||
private String projectName;
|
||||
private Long customerId;
|
||||
private String customerName;
|
||||
private String projectType;
|
||||
private BigDecimal budgetAmount;
|
||||
private LocalDate startDate;
|
||||
@ -46,6 +47,14 @@ public class Project extends BaseEntity {
|
||||
this.customerId = customerId;
|
||||
}
|
||||
|
||||
public String getCustomerName() {
|
||||
return customerName;
|
||||
}
|
||||
|
||||
public void setCustomerName(String customerName) {
|
||||
this.customerName = customerName;
|
||||
}
|
||||
|
||||
public String getProjectType() {
|
||||
return projectType;
|
||||
}
|
||||
|
||||
@ -20,6 +20,8 @@ public class ProjectCreateDTO {
|
||||
@NotNull(message = "客户ID不能为空")
|
||||
private Long customerId;
|
||||
|
||||
private String customerName;
|
||||
|
||||
@NotBlank(message = "项目类型不能为空")
|
||||
private String projectType;
|
||||
|
||||
@ -53,6 +55,14 @@ public class ProjectCreateDTO {
|
||||
this.customerId = customerId;
|
||||
}
|
||||
|
||||
public String getCustomerName() {
|
||||
return customerName;
|
||||
}
|
||||
|
||||
public void setCustomerName(String customerName) {
|
||||
this.customerName = customerName;
|
||||
}
|
||||
|
||||
public String getProjectType() {
|
||||
return projectType;
|
||||
}
|
||||
|
||||
@ -45,6 +45,7 @@ public class ProjectServiceImpl implements ProjectService {
|
||||
project.setProjectCode(dto.getProjectCode());
|
||||
project.setProjectName(dto.getProjectName());
|
||||
project.setCustomerId(dto.getCustomerId());
|
||||
project.setCustomerName(dto.getCustomerName());
|
||||
project.setProjectType(dto.getProjectType());
|
||||
project.setBudgetAmount(dto.getBudgetAmount());
|
||||
project.setStartDate(dto.getStartDate());
|
||||
@ -157,10 +158,11 @@ public class ProjectServiceImpl implements ProjectService {
|
||||
|
||||
private ProjectVO convertToVO(Project project) {
|
||||
ProjectVO vo = new ProjectVO();
|
||||
vo.setId(project.getId());
|
||||
vo.setProjectId(project.getId());
|
||||
vo.setProjectCode(project.getProjectCode());
|
||||
vo.setProjectName(project.getProjectName());
|
||||
vo.setCustomerId(project.getCustomerId());
|
||||
vo.setCustomerName(project.getCustomerName());
|
||||
vo.setProjectType(project.getProjectType());
|
||||
vo.setBudgetAmount(project.getBudgetAmount());
|
||||
vo.setStartDate(project.getStartDate());
|
||||
|
||||
@ -8,10 +8,11 @@ import java.time.LocalDate;
|
||||
*/
|
||||
public class ProjectVO {
|
||||
|
||||
private Long id;
|
||||
private Long projectId;
|
||||
private String projectCode;
|
||||
private String projectName;
|
||||
private Long customerId;
|
||||
private String customerName;
|
||||
private String projectType;
|
||||
private BigDecimal budgetAmount;
|
||||
private LocalDate startDate;
|
||||
@ -20,12 +21,12 @@ public class ProjectVO {
|
||||
private Integer status;
|
||||
private String remark;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
public Long getProjectId() {
|
||||
return projectId;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
public void setProjectId(Long projectId) {
|
||||
this.projectId = projectId;
|
||||
}
|
||||
|
||||
public String getProjectCode() {
|
||||
@ -52,6 +53,14 @@ public class ProjectVO {
|
||||
this.customerId = customerId;
|
||||
}
|
||||
|
||||
public String getCustomerName() {
|
||||
return customerName;
|
||||
}
|
||||
|
||||
public void setCustomerName(String customerName) {
|
||||
this.customerName = customerName;
|
||||
}
|
||||
|
||||
public String getProjectType() {
|
||||
return projectType;
|
||||
}
|
||||
|
||||
69
scripts/deploy/deploy-admin.sh
Executable file
69
scripts/deploy/deploy-admin.sh
Executable 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
94
scripts/deploy/deploy-all.sh
Executable 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
75
scripts/deploy/deploy-config.sh
Executable 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
69
scripts/deploy/deploy-mobile.sh
Executable 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
160
scripts/deploy/deploy-service.sh
Executable 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 "============================================"
|
||||
Loading…
x
Reference in New Issue
Block a user