Compare commits
No commits in common. "e5d9db10a8fd8042b82579e3199fdab5e034cb06" and "bd5f8ab468aeb6c300ea0066482ab9ead17cea75" have entirely different histories.
e5d9db10a8
...
bd5f8ab468
63
Agents.md
63
Agents.md
@ -1,8 +1,8 @@
|
|||||||
# 资金服务平台 (FundPlatform) - 开发规范
|
# 资金服务平台 (FundPlatform) - 开发规范
|
||||||
|
|
||||||
> **文档版本**: v1.3
|
> **文档版本**: v1.1
|
||||||
> **创建日期**: 2026-02-20
|
> **创建日期**: 2026-02-20
|
||||||
> **更新日期**: 2026-02-23
|
> **更新日期**: 2026-02-13
|
||||||
> **适用范围**: 本项目所有开发人员
|
> **适用范围**: 本项目所有开发人员
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -264,52 +264,6 @@ GET /cust/api/v1/customer/page # 分页查询
|
|||||||
| 404 | 资源不存在 |
|
| 404 | 资源不存在 |
|
||||||
| 500 | 服务器内部错误 |
|
| 500 | 服务器内部错误 |
|
||||||
|
|
||||||
### 5.6 前端API集中管理规范
|
|
||||||
|
|
||||||
前端项目必须采用**独立目录或文件集中管理**对后台API的请求,便于调整和优化。
|
|
||||||
|
|
||||||
**目录结构**:
|
|
||||||
|
|
||||||
```
|
|
||||||
fund-admin/src/api/ 或 fund-mobile/src/api/
|
|
||||||
├── index.ts # API集中定义入口,统一导出
|
|
||||||
├── request.ts # Axios实例配置、拦截器
|
|
||||||
└── modules/ # 按业务模块拆分(可选)
|
|
||||||
├── auth.ts # 用户认证API
|
|
||||||
├── customer.ts # 客户管理API
|
|
||||||
└── expense.ts # 支出管理API
|
|
||||||
```
|
|
||||||
|
|
||||||
**规范要求**:
|
|
||||||
|
|
||||||
| 要求 | 说明 |
|
|
||||||
|------|------|
|
|
||||||
| 禁止硬编码 | Vue组件中禁止直接使用 `request.get('/xxx/xxx')` 形式 |
|
|
||||||
| 统一入口 | 所有API函数从 `@/api` 统一导出,按模块分组 |
|
|
||||||
| 路径简化 | API函数内部使用简化路径,由 baseURL 统一添加前缀 |
|
|
||||||
| 便于维护 | 后端接口变更时,只需修改 api/index.ts 一处 |
|
|
||||||
|
|
||||||
**正确示例**:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// api/index.ts - 集中定义
|
|
||||||
export function getReceivableList(params: { pageNum: number; pageSize: number }) {
|
|
||||||
return request.get('/receipt/receivable/page', { params })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vue组件 - 使用集中定义
|
|
||||||
import { getReceivableList } from '@/api'
|
|
||||||
const res = await getReceivableList({ pageNum: 1, pageSize: 10 })
|
|
||||||
```
|
|
||||||
|
|
||||||
**错误示例**:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Vue组件 - 硬编码URL(禁止)
|
|
||||||
import request from '@/api/request'
|
|
||||||
const res = await request.get('/receipt/api/v1/receipt/receivable/page')
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 六、多租户规范
|
## 六、多租户规范
|
||||||
@ -957,22 +911,11 @@ public class TokenAuthFilter implements GlobalFilter {
|
|||||||
| `TENANT_ID` | 租户标识 | 空值=共享实例,有值=VIP实例 |
|
| `TENANT_ID` | 租户标识 | 空值=共享实例,有值=VIP实例 |
|
||||||
| `SERVER_PORT` | 服务端口 | 可选,覆盖application.yml |
|
| `SERVER_PORT` | 服务端口 | 可选,覆盖application.yml |
|
||||||
|
|
||||||
### 15.2 前端部署路径
|
### 15.2 访问地址
|
||||||
|
|
||||||
前端项目采用 **Nginx 子路径部署** 模式:
|
|
||||||
|
|
||||||
| 前端项目 | 部署路径 | 访问地址 | 说明 |
|
|
||||||
|----------|----------|----------|------|
|
|
||||||
| fund-admin | `/fadmin/` | `http://host/fadmin/` | 管理后台 |
|
|
||||||
| fund-mobile | `/fmobile/` | `http://host/fmobile/` | 移动端H5 |
|
|
||||||
| API网关 | `/fund/` | `http://host/fund/` | 后端API统一前缀 |
|
|
||||||
|
|
||||||
**开发环境访问地址**:
|
|
||||||
|
|
||||||
| 服务 | 地址 |
|
| 服务 | 地址 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| 管理后台 | http://localhost:3000 |
|
| 管理后台 | http://localhost:3000 |
|
||||||
| 移动端H5 | http://localhost:8080 |
|
|
||||||
| API网关 | http://localhost:8000 |
|
| API网关 | http://localhost:8000 |
|
||||||
| Nacos控制台 | http://localhost:8048/nacos |
|
| Nacos控制台 | http://localhost:8048/nacos |
|
||||||
|
|
||||||
|
|||||||
@ -62,7 +62,6 @@ CREATE TABLE IF NOT EXISTS requirement (
|
|||||||
receivable_date DATE COMMENT '应收款日期',
|
receivable_date DATE COMMENT '应收款日期',
|
||||||
status VARCHAR(20) DEFAULT 'pending' COMMENT '状态:pending-待开发,developing-开发中,delivered-已交付,completed-已完成',
|
status VARCHAR(20) DEFAULT 'pending' COMMENT '状态:pending-待开发,developing-开发中,delivered-已交付,completed-已完成',
|
||||||
progress INT DEFAULT 0 COMMENT '开发进度(0-100)',
|
progress INT DEFAULT 0 COMMENT '开发进度(0-100)',
|
||||||
remark VARCHAR(500) COMMENT '备注',
|
|
||||||
attachment_url VARCHAR(500) COMMENT '附件URL',
|
attachment_url VARCHAR(500) COMMENT '附件URL',
|
||||||
created_by BIGINT COMMENT '创建人ID',
|
created_by BIGINT COMMENT '创建人ID',
|
||||||
created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
|||||||
@ -17,7 +17,7 @@
|
|||||||
│ │
|
│ │
|
||||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||||
│ │ 前端服务 │ │
|
│ │ 前端服务 │ │
|
||||||
│ │ fund-admin (/fadmin/) fund-mobile (/fmobile/) │ │
|
│ │ fund-admin (Nginx:80) fund-mobile (Nginx:81) │ │
|
||||||
│ └─────────────────────────┬───────────────────────────────┘ │
|
│ └─────────────────────────┬───────────────────────────────┘ │
|
||||||
│ │ │
|
│ │ │
|
||||||
│ ┌─────────────────────────▼───────────────────────────────┐ │
|
│ ┌─────────────────────────▼───────────────────────────────┐ │
|
||||||
@ -431,17 +431,7 @@ fundplatform/
|
|||||||
|
|
||||||
#### 4.2.4 前端打包
|
#### 4.2.4 前端打包
|
||||||
|
|
||||||
前端项目采用 Nginx 子路径部署方式:
|
|
||||||
- **管理后台** (fund-admin): 部署路径 `/fadmin/`
|
|
||||||
- **移动端H5** (fund-mobile): 部署路径 `/fmobile/`
|
|
||||||
- **API网关前缀**: `/fund`
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 使用部署脚本打包(推荐)
|
|
||||||
./scripts/deploy-frontend-nginx.sh admin # 管理后台
|
|
||||||
./scripts/deploy-frontend-nginx.sh mobile # 移动端H5
|
|
||||||
|
|
||||||
# 或手动打包
|
|
||||||
# 管理后台打包
|
# 管理后台打包
|
||||||
cd fund-admin
|
cd fund-admin
|
||||||
npm install
|
npm install
|
||||||
@ -495,54 +485,6 @@ tar -xzf fund-receipt.tar.gz -C /opt/fundplatform/deploy/
|
|||||||
tar -xzf fund-report.tar.gz -C /opt/fundplatform/deploy/
|
tar -xzf fund-report.tar.gz -C /opt/fundplatform/deploy/
|
||||||
tar -xzf fund-file.tar.gz -C /opt/fundplatform/deploy/
|
tar -xzf fund-file.tar.gz -C /opt/fundplatform/deploy/
|
||||||
|
|
||||||
# 解压前端发布包
|
|
||||||
mkdir -p /opt/fundplatform/web
|
|
||||||
unzip fund-admin-nginx.zip -d /opt/fundplatform/web/admin/
|
|
||||||
unzip fund-mobile-nginx.zip -d /opt/fundplatform/web/mobile/
|
|
||||||
|
|
||||||
# 复制Nginx配置
|
|
||||||
cp /opt/fundplatform/web/admin/nginx.conf /etc/nginx/conf.d/fadmin.conf
|
|
||||||
cp /opt/fundplatform/web/mobile/nginx.conf /etc/nginx/conf.d/fmobile.conf
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.5 Nginx 配置
|
|
||||||
|
|
||||||
前端采用子路径部署,Nginx 配置示例:
|
|
||||||
|
|
||||||
```nginx
|
|
||||||
# /etc/nginx/conf.d/fundplatform.conf
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name localhost;
|
|
||||||
|
|
||||||
# 管理后台 (部署路径: /fadmin/)
|
|
||||||
location /fadmin/ {
|
|
||||||
alias /opt/fundplatform/web/admin/;
|
|
||||||
try_files $uri $uri/ /fadmin/index.html;
|
|
||||||
}
|
|
||||||
|
|
||||||
# 移动端H5 (部署路径: /fmobile/)
|
|
||||||
location /fmobile/ {
|
|
||||||
alias /opt/fundplatform/web/mobile/;
|
|
||||||
try_files $uri $uri/ /fmobile/index.html;
|
|
||||||
}
|
|
||||||
|
|
||||||
# API代理 (网关前缀: /fund)
|
|
||||||
location /fund/ {
|
|
||||||
proxy_pass http://127.0.0.1:8000/;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**访问地址**:
|
|
||||||
- 管理后台: `http://服务器IP/fadmin/`
|
|
||||||
- 移动端H5: `http://服务器IP/fmobile/`
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 上传一键管理脚本到deploy目录(在解压服务包后执行)
|
# 上传一键管理脚本到deploy目录(在解压服务包后执行)
|
||||||
# 一键管理脚本位于项目根目录scripts目录,需要单独上传
|
# 一键管理脚本位于项目根目录scripts目录,需要单独上传
|
||||||
@ -552,7 +494,7 @@ cp scripts/restart-all.sh /opt/fundplatform/deploy/
|
|||||||
cp scripts/status-all.sh /opt/fundplatform/deploy/
|
cp scripts/status-all.sh /opt/fundplatform/deploy/
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4.6 配置文件修改
|
### 4.5 配置文件修改
|
||||||
|
|
||||||
各服务配置文件需要根据实际环境修改以下配置:
|
各服务配置文件需要根据实际环境修改以下配置:
|
||||||
|
|
||||||
@ -579,7 +521,7 @@ spring:
|
|||||||
namespace: fund-platform
|
namespace: fund-platform
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4.7 服务管理脚本
|
### 4.6 服务管理脚本
|
||||||
|
|
||||||
每个服务的 `bin` 目录下包含以下脚本:
|
每个服务的 `bin` 目录下包含以下脚本:
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
# 资金服务平台 (FundPlatform) - 架构设计文档
|
# 资金服务平台 (FundPlatform) - 架构设计文档
|
||||||
|
|
||||||
> **文档版本**: v1.8
|
> **文档版本**: v1.6
|
||||||
> **创建日期**: 2026-02-13
|
> **创建日期**: 2026-02-13
|
||||||
> **更新日期**: 2026-02-23
|
> **更新日期**: 2026-02-13
|
||||||
> **项目名称**: 资金服务平台
|
> **项目名称**: 资金服务平台
|
||||||
> **项目代号**: fundplatform
|
> **项目代号**: fundplatform
|
||||||
|
|
||||||
@ -3460,164 +3460,7 @@ 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 生产环境部署
|
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────────────────────────────────────────────────────────┐
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
@ -3668,9 +3511,9 @@ const res = await request.get('/receipt/api/v1/receipt/receivable/page')
|
|||||||
└─────────────────────────────────────────────────────────────────┘
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
### 6.4 容器化部署
|
### 6.2 容器化部署
|
||||||
|
|
||||||
#### 6.4.1 Docker Compose 配置
|
#### 6.2.1 Docker Compose 配置
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
version: '3.8'
|
version: '3.8'
|
||||||
@ -4135,8 +3978,6 @@ public class GlobalExceptionHandler {
|
|||||||
| v1.4 | 2026-02-13 | 补充统一全局上下文 GlobalContext,统筹 tid/uid/uname 获取和异步传递 | zhangjf |
|
| v1.4 | 2026-02-13 | 补充统一全局上下文 GlobalContext,统筹 tid/uid/uname 获取和异步传递 | zhangjf |
|
||||||
| v1.5 | 2026-02-13 | 补充模块通信与 OpenFeign 参数对象管理策略、分层架构职责说明、MyBatis-Plus 使用规范、Controller 与参数校验规范、事务与测试规范及开发规则总览 | zhangjf |
|
| v1.5 | 2026-02-13 | 补充模块通信与 OpenFeign 参数对象管理策略、分层架构职责说明、MyBatis-Plus 使用规范、Controller 与参数校验规范、事务与测试规范及开发规则总览 | zhangjf |
|
||||||
| v1.6 | 2026-02-13 | 补充单机部署配置:配置文件分离架构(env.properties+service.properties)、打包目录结构、多租户部署配置、日志配置集中化、脚本加载逻辑 | zhangjf |
|
| v1.6 | 2026-02-13 | 补充单机部署配置:配置文件分离架构(env.properties+service.properties)、打包目录结构、多租户部署配置、日志配置集中化、脚本加载逻辑 | zhangjf |
|
||||||
| v1.7 | 2026-02-23 | 新增6.1前端部署路径设计:Nginx子路径部署、部署路径规划、Nginx配置示例、前端构建配置、API请求路径规范 | zhangjf |
|
|
||||||
| v1.8 | 2026-02-23 | 新增6.1.5 API集中管理规范:独立目录管理、禁止硬编码、统一入口、路径简化、便于维护 | zhangjf |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -158,7 +158,6 @@ npm run dev
|
|||||||
|
|
||||||
# 4. 访问系统
|
# 4. 访问系统
|
||||||
# 管理后台:http://localhost:3000
|
# 管理后台:http://localhost:3000
|
||||||
# 移动端H5:http://localhost:8080
|
|
||||||
# 网关地址:http://localhost:8000
|
# 网关地址:http://localhost:8000
|
||||||
# Nacos 控制台:http://localhost:8048/nacos
|
# Nacos 控制台:http://localhost:8048/nacos
|
||||||
# Grafana 监控:http://localhost:3000 (Docker环境) 或 http://localhost:3001 (本地开发)
|
# Grafana 监控:http://localhost:3000 (Docker环境) 或 http://localhost:3001 (本地开发)
|
||||||
@ -180,8 +179,8 @@ npm run dev
|
|||||||
│ │
|
│ │
|
||||||
│ ┌──────────────┐ │
|
│ ┌──────────────┐ │
|
||||||
│ │ 前端服务 │ │
|
│ │ 前端服务 │ │
|
||||||
│ │ fund-admin │ ← 管理后台 (http://localhost/fadmin/) │
|
│ │ fund-admin │ ← 管理后台 (http://localhost:80) │
|
||||||
│ │ fund-mobile │ ← 移动端H5 (http://localhost/fmobile/) │
|
│ │ fund-mobile │ ← 移动端H5 (http://localhost:81) │
|
||||||
│ └──────┬───────┘ │
|
│ └──────┬───────┘ │
|
||||||
│ │ │
|
│ │ │
|
||||||
│ ┌──────┴───────┐ │
|
│ ┌──────┴───────┐ │
|
||||||
@ -225,8 +224,8 @@ npm run dev
|
|||||||
|
|
||||||
| 服务 | 端口 | 说明 |
|
| 服务 | 端口 | 说明 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| fund-admin | 80 | 管理后台前端 (访问路径: /fadmin/) |
|
| fund-admin | 80 | 管理后台前端 |
|
||||||
| fund-mobile | 80 | 移动端H5 (访问路径: /fmobile/) |
|
| fund-mobile | 81 | 移动端H5 |
|
||||||
| gateway | 8000 | API网关 |
|
| gateway | 8000 | API网关 |
|
||||||
| fund-sys | 8100 | 系统管理服务 |
|
| fund-sys | 8100 | 系统管理服务 |
|
||||||
| fund-sys-vip001 | 8101 | 系统服务VIP专属实例 |
|
| fund-sys-vip001 | 8101 | 系统服务VIP专属实例 |
|
||||||
@ -481,20 +480,14 @@ server {
|
|||||||
listen 80;
|
listen 80;
|
||||||
server_name test.fundplatform.com;
|
server_name test.fundplatform.com;
|
||||||
|
|
||||||
# 管理后台前端 (部署路径: /fadmin/)
|
# 前端静态资源
|
||||||
location /fadmin/ {
|
location / {
|
||||||
alias /opt/fundplatform/admin/;
|
root /opt/fundplatform/admin;
|
||||||
try_files $uri $uri/ /fadmin/index.html;
|
try_files $uri $uri/ /index.html;
|
||||||
}
|
}
|
||||||
|
|
||||||
# 移动端H5 (部署路径: /fmobile/)
|
# API 代理
|
||||||
location /fmobile/ {
|
location /api/ {
|
||||||
alias /opt/fundplatform/mobile/;
|
|
||||||
try_files $uri $uri/ /fmobile/index.html;
|
|
||||||
}
|
|
||||||
|
|
||||||
# API 代理 (网关前缀: /fund)
|
|
||||||
location /fund/ {
|
|
||||||
proxy_pass http://gateway/;
|
proxy_pass http://gateway/;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
|||||||
@ -21,7 +21,7 @@ export function deleteProject(id: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 需求工单
|
// 需求工单
|
||||||
export function getRequirementList(params: { pageNum: number; pageSize: number; requirementName?: string; projectName?: string; requirementStatus?: string; priority?: string }) {
|
export function getRequirementList(params: { pageNum: number; pageSize: number; requirementName?: string; status?: string }) {
|
||||||
return request.get('/requirement/page', { params })
|
return request.get('/requirement/page', { params })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -31,18 +31,24 @@
|
|||||||
|
|
||||||
<el-table :data="tableData" v-loading="loading" border stripe>
|
<el-table :data="tableData" v-loading="loading" border stripe>
|
||||||
<el-table-column prop="customerId" label="客户ID" width="80" />
|
<el-table-column prop="customerId" label="客户ID" width="80" />
|
||||||
<el-table-column prop="customerCode" label="客户编码" width="120" />
|
|
||||||
<el-table-column prop="customerName" label="客户名称" min-width="150" />
|
<el-table-column prop="customerName" label="客户名称" min-width="150" />
|
||||||
<el-table-column prop="contact" label="联系人" width="120" />
|
<el-table-column prop="customerType" label="客户类型" width="100">
|
||||||
<el-table-column prop="phone" label="联系电话" width="140" />
|
<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="email" label="邮箱" min-width="180" show-overflow-tooltip />
|
<el-table-column prop="email" label="邮箱" min-width="180" show-overflow-tooltip />
|
||||||
<el-table-column prop="address" label="地址" min-width="200" show-overflow-tooltip />
|
<el-table-column prop="address" label="地址" min-width="200" show-overflow-tooltip />
|
||||||
<el-table-column prop="status" label="状态" width="80">
|
<el-table-column prop="status" label="状态" width="80">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag v-if="row.status === 1" type="success">启用</el-tag>
|
<el-tag v-if="row.status === 'NORMAL'" type="success">正常</el-tag>
|
||||||
<el-tag v-else type="danger">禁用</el-tag>
|
<el-tag v-else type="danger">禁用</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
<el-table-column prop="createTime" label="创建时间" width="160" />
|
||||||
<el-table-column label="操作" width="280" fixed="right">
|
<el-table-column label="操作" width="280" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button link type="primary" :icon="View" @click="handleView(row)">详情</el-button>
|
<el-button link type="primary" :icon="View" @click="handleView(row)">详情</el-button>
|
||||||
@ -75,26 +81,29 @@
|
|||||||
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
|
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="客户编码" prop="customerCode">
|
<el-form-item label="客户名称" prop="customerName">
|
||||||
<el-input v-model="form.customerCode" placeholder="请输入客户编码" />
|
<el-input v-model="form.customerName" placeholder="请输入客户名称" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="客户名称" prop="customerName">
|
<el-form-item label="客户类型" prop="customerType">
|
||||||
<el-input v-model="form.customerName" placeholder="请输入客户名称" />
|
<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>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="联系人" prop="contact">
|
<el-form-item label="联系人" prop="contactPerson">
|
||||||
<el-input v-model="form.contact" placeholder="请输入联系人" />
|
<el-input v-model="form.contactPerson" placeholder="请输入联系人" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="联系电话" prop="phone">
|
<el-form-item label="联系电话" prop="contactPhone">
|
||||||
<el-input v-model="form.phone" placeholder="请输入联系电话" />
|
<el-input v-model="form.contactPhone" placeholder="请输入联系电话" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
@ -108,8 +117,8 @@
|
|||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="状态" prop="status">
|
<el-form-item label="状态" prop="status">
|
||||||
<el-radio-group v-model="form.status">
|
<el-radio-group v-model="form.status">
|
||||||
<el-radio :value="1">启用</el-radio>
|
<el-radio value="NORMAL">正常</el-radio>
|
||||||
<el-radio :value="0">禁用</el-radio>
|
<el-radio value="DISABLED">禁用</el-radio>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
@ -119,8 +128,8 @@
|
|||||||
<el-input v-model="form.address" placeholder="请输入地址" />
|
<el-input v-model="form.address" placeholder="请输入地址" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="备注" prop="remark">
|
<el-form-item label="备注" prop="remarks">
|
||||||
<el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入备注" />
|
<el-input v-model="form.remarks" type="textarea" :rows="3" placeholder="请输入备注" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
@ -134,17 +143,21 @@
|
|||||||
<el-dialog title="客户详情" v-model="detailVisible" width="700px">
|
<el-dialog title="客户详情" v-model="detailVisible" width="700px">
|
||||||
<el-descriptions :column="2" border>
|
<el-descriptions :column="2" border>
|
||||||
<el-descriptions-item label="客户ID">{{ detailData.customerId }}</el-descriptions-item>
|
<el-descriptions-item label="客户ID">{{ detailData.customerId }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="客户编码">{{ detailData.customerCode }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="客户名称">{{ detailData.customerName }}</el-descriptions-item>
|
<el-descriptions-item label="客户名称">{{ detailData.customerName }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="客户类型">
|
||||||
|
<el-tag v-if="detailData.customerType === 'ENTERPRISE'" type="primary">企业</el-tag>
|
||||||
|
<el-tag v-else type="success">个人</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="状态">
|
<el-descriptions-item label="状态">
|
||||||
<el-tag v-if="detailData.status === 1" type="success">启用</el-tag>
|
<el-tag v-if="detailData.status === 'NORMAL'" type="success">正常</el-tag>
|
||||||
<el-tag v-else type="danger">禁用</el-tag>
|
<el-tag v-else type="danger">禁用</el-tag>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="联系人">{{ detailData.contact }}</el-descriptions-item>
|
<el-descriptions-item label="联系人">{{ detailData.contactPerson }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="联系电话">{{ detailData.phone }}</el-descriptions-item>
|
<el-descriptions-item label="联系电话">{{ detailData.contactPhone }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="邮箱" :span="2">{{ detailData.email }}</el-descriptions-item>
|
<el-descriptions-item label="邮箱" :span="2">{{ detailData.email }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="地址" :span="2">{{ detailData.address }}</el-descriptions-item>
|
<el-descriptions-item label="地址" :span="2">{{ detailData.address }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="备注" :span="2">{{ detailData.remark || '-' }}</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>
|
</el-descriptions>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
@ -178,21 +191,21 @@ const formRef = ref<FormInstance>()
|
|||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
customerId: null as number | null,
|
customerId: null as number | null,
|
||||||
customerCode: '',
|
|
||||||
customerName: '',
|
customerName: '',
|
||||||
contact: '',
|
customerType: 'ENTERPRISE',
|
||||||
phone: '',
|
contactPerson: '',
|
||||||
|
contactPhone: '',
|
||||||
email: '',
|
email: '',
|
||||||
address: '',
|
address: '',
|
||||||
status: 1,
|
status: 'NORMAL',
|
||||||
remark: ''
|
remarks: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
const rules = reactive<FormRules>({
|
const rules = reactive<FormRules>({
|
||||||
customerCode: [{ required: true, message: '请输入客户编码', trigger: 'blur' }],
|
|
||||||
customerName: [{ required: true, message: '请输入客户名称', trigger: 'blur' }],
|
customerName: [{ required: true, message: '请输入客户名称', trigger: 'blur' }],
|
||||||
contact: [{ required: true, message: '请输入联系人', trigger: 'blur' }],
|
customerType: [{ required: true, message: '请选择客户类型', trigger: 'change' }],
|
||||||
phone: [
|
contactPerson: [{ required: true, message: '请输入联系人', trigger: 'blur' }],
|
||||||
|
contactPhone: [
|
||||||
{ required: true, message: '请输入联系电话', trigger: 'blur' },
|
{ required: true, message: '请输入联系电话', trigger: 'blur' },
|
||||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
|
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
|
||||||
],
|
],
|
||||||
@ -293,14 +306,14 @@ const handleSubmit = async () => {
|
|||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
form.customerId = null
|
form.customerId = null
|
||||||
form.customerCode = ''
|
|
||||||
form.customerName = ''
|
form.customerName = ''
|
||||||
form.contact = ''
|
form.customerType = 'ENTERPRISE'
|
||||||
form.phone = ''
|
form.contactPerson = ''
|
||||||
|
form.contactPhone = ''
|
||||||
form.email = ''
|
form.email = ''
|
||||||
form.address = ''
|
form.address = ''
|
||||||
form.status = 1
|
form.status = 'NORMAL'
|
||||||
form.remark = ''
|
form.remarks = ''
|
||||||
formRef.value?.clearValidate()
|
formRef.value?.clearValidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -53,7 +53,6 @@
|
|||||||
¥{{ row.amount?.toLocaleString() || '0' }}
|
¥{{ row.amount?.toLocaleString() || '0' }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="payeeName" label="收款单位" width="140" show-overflow-tooltip />
|
|
||||||
<el-table-column prop="expenseDate" label="支出日期" width="120" />
|
<el-table-column prop="expenseDate" label="支出日期" width="120" />
|
||||||
<el-table-column prop="approvalStatus" label="审批状态" width="100">
|
<el-table-column prop="approvalStatus" label="审批状态" width="100">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
@ -146,11 +145,6 @@
|
|||||||
<el-input-number v-model="form.amount" :precision="2" :min="0" style="width: 100%" />
|
<el-input-number v-model="form.amount" :precision="2" :min="0" style="width: 100%" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
|
||||||
<el-form-item label="收款单位" prop="payeeName">
|
|
||||||
<el-input v-model="form.payeeName" placeholder="请输入收款单位" />
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<el-row :gutter="20">
|
<el-row :gutter="20">
|
||||||
@ -205,7 +199,6 @@
|
|||||||
<el-descriptions :column="1" border>
|
<el-descriptions :column="1" border>
|
||||||
<el-descriptions-item label="支出标题">{{ approvalData.title }}</el-descriptions-item>
|
<el-descriptions-item label="支出标题">{{ approvalData.title }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="支出金额">¥{{ approvalData.amount?.toLocaleString() || '0' }}</el-descriptions-item>
|
<el-descriptions-item label="支出金额">¥{{ approvalData.amount?.toLocaleString() || '0' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="收款单位">{{ approvalData.payeeName || '-' }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="支出类型">{{ approvalData.expenseTypeName }}</el-descriptions-item>
|
<el-descriptions-item label="支出类型">{{ approvalData.expenseTypeName }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="关联项目">{{ approvalData.projectName || '-' }}</el-descriptions-item>
|
<el-descriptions-item label="关联项目">{{ approvalData.projectName || '-' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="申请人">{{ approvalData.applicant }}</el-descriptions-item>
|
<el-descriptions-item label="申请人">{{ approvalData.applicant }}</el-descriptions-item>
|
||||||
@ -236,7 +229,6 @@
|
|||||||
<el-descriptions-item label="支出金额">
|
<el-descriptions-item label="支出金额">
|
||||||
¥{{ detailData.amount?.toLocaleString() || '0' }}
|
¥{{ detailData.amount?.toLocaleString() || '0' }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="收款单位">{{ detailData.payeeName || '-' }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="支出日期">{{ detailData.expenseDate }}</el-descriptions-item>
|
<el-descriptions-item label="支出日期">{{ detailData.expenseDate }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="审批状态">
|
<el-descriptions-item label="审批状态">
|
||||||
<el-tag v-if="detailData.approvalStatus === 'DRAFT'" type="info">草稿</el-tag>
|
<el-tag v-if="detailData.approvalStatus === 'DRAFT'" type="info">草稿</el-tag>
|
||||||
@ -320,7 +312,6 @@ const form = reactive({
|
|||||||
expenseTypeId: null as number | null,
|
expenseTypeId: null as number | null,
|
||||||
projectId: null as number | null,
|
projectId: null as number | null,
|
||||||
amount: 0,
|
amount: 0,
|
||||||
payeeName: '',
|
|
||||||
expenseDate: '',
|
expenseDate: '',
|
||||||
applicant: '',
|
applicant: '',
|
||||||
description: '',
|
description: '',
|
||||||
@ -332,7 +323,6 @@ const rules = reactive<FormRules>({
|
|||||||
title: [{ required: true, message: '请输入支出标题', trigger: 'blur' }],
|
title: [{ required: true, message: '请输入支出标题', trigger: 'blur' }],
|
||||||
expenseTypeId: [{ required: true, message: '请选择支出类型', trigger: 'change' }],
|
expenseTypeId: [{ required: true, message: '请选择支出类型', trigger: 'change' }],
|
||||||
amount: [{ required: true, message: '请输入支出金额', trigger: 'blur' }],
|
amount: [{ required: true, message: '请输入支出金额', trigger: 'blur' }],
|
||||||
payeeName: [{ required: true, message: '请输入收款单位', trigger: 'blur' }],
|
|
||||||
expenseDate: [{ required: true, message: '请选择支出日期', trigger: 'change' }],
|
expenseDate: [{ required: true, message: '请选择支出日期', trigger: 'change' }],
|
||||||
applicant: [{ required: true, message: '请输入申请人', trigger: 'blur' }]
|
applicant: [{ required: true, message: '请输入申请人', trigger: 'blur' }]
|
||||||
})
|
})
|
||||||
@ -562,7 +552,6 @@ const resetForm = () => {
|
|||||||
form.expenseTypeId = null
|
form.expenseTypeId = null
|
||||||
form.projectId = null
|
form.projectId = null
|
||||||
form.amount = 0
|
form.amount = 0
|
||||||
form.payeeName = ''
|
|
||||||
form.expenseDate = ''
|
form.expenseDate = ''
|
||||||
form.applicant = ''
|
form.applicant = ''
|
||||||
form.description = ''
|
form.description = ''
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
<el-card shadow="never" class="search-card">
|
<el-card shadow="never" class="search-card">
|
||||||
<el-form :inline="true" :model="queryParams">
|
<el-form :inline="true" :model="queryParams">
|
||||||
<el-form-item label="需求标题">
|
<el-form-item label="需求标题">
|
||||||
<el-input v-model="queryParams.requirementName" placeholder="请输入需求标题" clearable style="width: 200px" />
|
<el-input v-model="queryParams.requirementTitle" placeholder="请输入需求标题" clearable style="width: 200px" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="项目名称">
|
<el-form-item label="项目名称">
|
||||||
<el-input v-model="queryParams.projectName" placeholder="请输入项目名称" clearable style="width: 180px" />
|
<el-input v-model="queryParams.projectName" placeholder="请输入项目名称" clearable style="width: 180px" />
|
||||||
@ -40,7 +40,7 @@
|
|||||||
<el-table :data="tableData" v-loading="loading" border stripe>
|
<el-table :data="tableData" v-loading="loading" border stripe>
|
||||||
<el-table-column prop="requirementId" label="需求ID" width="80" />
|
<el-table-column prop="requirementId" label="需求ID" width="80" />
|
||||||
<el-table-column prop="requirementCode" label="需求编号" width="140" />
|
<el-table-column prop="requirementCode" label="需求编号" width="140" />
|
||||||
<el-table-column prop="requirementName" label="需求名称" min-width="200" show-overflow-tooltip />
|
<el-table-column prop="requirementTitle" label="需求标题" min-width="200" show-overflow-tooltip />
|
||||||
<el-table-column prop="projectName" label="项目名称" width="150" show-overflow-tooltip />
|
<el-table-column prop="projectName" label="项目名称" width="150" show-overflow-tooltip />
|
||||||
<el-table-column prop="requirementType" label="需求类型" width="100">
|
<el-table-column prop="requirementType" label="需求类型" width="100">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
@ -113,8 +113,8 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="需求名称" prop="requirementName">
|
<el-form-item label="需求标题" prop="requirementTitle">
|
||||||
<el-input v-model="form.requirementName" placeholder="请输入需求名称" />
|
<el-input v-model="form.requirementTitle" placeholder="请输入需求标题" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
@ -132,21 +132,6 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
|
||||||
<el-form-item label="客户" prop="customerId">
|
|
||||||
<el-select v-model="form.customerId" placeholder="请选择客户" filterable style="width: 100%">
|
|
||||||
<el-option
|
|
||||||
v-for="item in customerList"
|
|
||||||
:key="item.customerId"
|
|
||||||
:label="item.customerName"
|
|
||||||
:value="item.customerId"
|
|
||||||
/>
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
|
|
||||||
<el-row :gutter="20">
|
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="需求类型" prop="requirementType">
|
<el-form-item label="需求类型" prop="requirementType">
|
||||||
<el-select v-model="form.requirementType" placeholder="请选择" style="width: 100%">
|
<el-select v-model="form.requirementType" placeholder="请选择" style="width: 100%">
|
||||||
@ -264,9 +249,8 @@
|
|||||||
<el-descriptions :column="2" border>
|
<el-descriptions :column="2" border>
|
||||||
<el-descriptions-item label="需求ID">{{ detailData.requirementId }}</el-descriptions-item>
|
<el-descriptions-item label="需求ID">{{ detailData.requirementId }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="需求编号">{{ detailData.requirementCode }}</el-descriptions-item>
|
<el-descriptions-item label="需求编号">{{ detailData.requirementCode }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="需求名称" :span="2">{{ detailData.requirementName }}</el-descriptions-item>
|
<el-descriptions-item label="需求标题" :span="2">{{ detailData.requirementTitle }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="项目名称">{{ detailData.projectName }}</el-descriptions-item>
|
<el-descriptions-item label="项目名称">{{ detailData.projectName }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="客户名称">{{ detailData.customerName }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="需求类型">
|
<el-descriptions-item label="需求类型">
|
||||||
<el-tag v-if="detailData.requirementType === 'FEATURE'" type="primary">功能</el-tag>
|
<el-tag v-if="detailData.requirementType === 'FEATURE'" type="primary">功能</el-tag>
|
||||||
<el-tag v-else-if="detailData.requirementType === 'BUG'" type="danger">缺陷</el-tag>
|
<el-tag v-else-if="detailData.requirementType === 'BUG'" type="danger">缺陷</el-tag>
|
||||||
@ -309,7 +293,6 @@ import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'elem
|
|||||||
import { Search, Refresh, Plus, Edit, Delete, View, Check, Document } from '@element-plus/icons-vue'
|
import { Search, Refresh, Plus, Edit, Delete, View, Check, Document } from '@element-plus/icons-vue'
|
||||||
import { getRequirementList, createRequirement, updateRequirement, deleteRequirement } from '@/api/project'
|
import { getRequirementList, createRequirement, updateRequirement, deleteRequirement } from '@/api/project'
|
||||||
import { getProjectList } from '@/api/project'
|
import { getProjectList } from '@/api/project'
|
||||||
import { getCustomerList } from '@/api/customer'
|
|
||||||
|
|
||||||
// 文件上传相关
|
// 文件上传相关
|
||||||
const uploadUrl = '/file/api/v1/file/upload'
|
const uploadUrl = '/file/api/v1/file/upload'
|
||||||
@ -324,12 +307,11 @@ const submitLoading = ref(false)
|
|||||||
const tableData = ref<any[]>([])
|
const tableData = ref<any[]>([])
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
const projectList = ref<any[]>([])
|
const projectList = ref<any[]>([])
|
||||||
const customerList = ref<any[]>([])
|
|
||||||
|
|
||||||
const queryParams = reactive({
|
const queryParams = reactive({
|
||||||
pageNum: 1,
|
pageNum: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
requirementName: '',
|
requirementTitle: '',
|
||||||
projectName: '',
|
projectName: '',
|
||||||
requirementStatus: '',
|
requirementStatus: '',
|
||||||
priority: ''
|
priority: ''
|
||||||
@ -342,9 +324,8 @@ const formRef = ref<FormInstance>()
|
|||||||
const form = reactive({
|
const form = reactive({
|
||||||
requirementId: null as number | null,
|
requirementId: null as number | null,
|
||||||
requirementCode: '',
|
requirementCode: '',
|
||||||
requirementName: '',
|
requirementTitle: '',
|
||||||
projectId: null as number | null,
|
projectId: null as number | null,
|
||||||
customerId: null as number | null,
|
|
||||||
requirementType: 'FEATURE',
|
requirementType: 'FEATURE',
|
||||||
priority: 'MEDIUM',
|
priority: 'MEDIUM',
|
||||||
requirementStatus: 'PENDING',
|
requirementStatus: 'PENDING',
|
||||||
@ -357,9 +338,8 @@ const form = reactive({
|
|||||||
|
|
||||||
const rules = reactive<FormRules>({
|
const rules = reactive<FormRules>({
|
||||||
requirementCode: [{ required: true, message: '请输入需求编号', trigger: 'blur' }],
|
requirementCode: [{ required: true, message: '请输入需求编号', trigger: 'blur' }],
|
||||||
requirementName: [{ required: true, message: '请输入需求名称', trigger: 'blur' }],
|
requirementTitle: [{ required: true, message: '请输入需求标题', trigger: 'blur' }],
|
||||||
projectId: [{ required: true, message: '请选择项目', trigger: 'change' }],
|
projectId: [{ required: true, message: '请选择项目', trigger: 'change' }],
|
||||||
customerId: [{ required: true, message: '请选择客户', trigger: 'change' }],
|
|
||||||
requirementType: [{ required: true, message: '请选择需求类型', trigger: 'change' }],
|
requirementType: [{ required: true, message: '请选择需求类型', trigger: 'change' }],
|
||||||
priority: [{ required: true, message: '请选择优先级', trigger: 'change' }]
|
priority: [{ required: true, message: '请选择优先级', trigger: 'change' }]
|
||||||
})
|
})
|
||||||
@ -409,15 +389,6 @@ const getFileUrl = (path: string) => {
|
|||||||
return `/file/api/v1/file/download/${path}`
|
return `/file/api/v1/file/download/${path}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchCustomers = async () => {
|
|
||||||
try {
|
|
||||||
const res: any = await getCustomerList({ pageNum: 1, pageSize: 1000 })
|
|
||||||
customerList.value = res.data?.records || []
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
@ -446,7 +417,7 @@ const handleSearch = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
queryParams.requirementName = ''
|
queryParams.requirementTitle = ''
|
||||||
queryParams.projectName = ''
|
queryParams.projectName = ''
|
||||||
queryParams.requirementStatus = ''
|
queryParams.requirementStatus = ''
|
||||||
queryParams.priority = ''
|
queryParams.priority = ''
|
||||||
@ -549,9 +520,8 @@ const handleSubmit = async () => {
|
|||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
form.requirementId = null
|
form.requirementId = null
|
||||||
form.requirementCode = ''
|
form.requirementCode = ''
|
||||||
form.requirementName = ''
|
form.requirementTitle = ''
|
||||||
form.projectId = null
|
form.projectId = null
|
||||||
form.customerId = null
|
|
||||||
form.requirementType = 'FEATURE'
|
form.requirementType = 'FEATURE'
|
||||||
form.priority = 'MEDIUM'
|
form.priority = 'MEDIUM'
|
||||||
form.requirementStatus = 'PENDING'
|
form.requirementStatus = 'PENDING'
|
||||||
@ -567,7 +537,6 @@ const resetForm = () => {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchData()
|
fetchData()
|
||||||
fetchProjects()
|
fetchProjects()
|
||||||
fetchCustomers()
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import java.util.List;
|
|||||||
* <li>pageNum:当前页码(从 1 开始);</li>
|
* <li>pageNum:当前页码(从 1 开始);</li>
|
||||||
* <li>pageSize:每页条数;</li>
|
* <li>pageSize:每页条数;</li>
|
||||||
* <li>total:总记录数;</li>
|
* <li>total:总记录数;</li>
|
||||||
* <li>records:当前页数据列表。</li>
|
* <li>list:当前页数据列表。</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
*
|
*
|
||||||
* @param <T> 列表元素类型
|
* @param <T> 列表元素类型
|
||||||
@ -31,17 +31,17 @@ public class PageResult<T> implements Serializable {
|
|||||||
private long total;
|
private long total;
|
||||||
|
|
||||||
/** 当前页数据 */
|
/** 当前页数据 */
|
||||||
private List<T> records;
|
private List<T> list;
|
||||||
|
|
||||||
public PageResult() {
|
public PageResult() {
|
||||||
this.records = Collections.emptyList();
|
this.list = Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public PageResult(long pageNum, long pageSize, long total, List<T> records) {
|
public PageResult(long pageNum, long pageSize, long total, List<T> list) {
|
||||||
this.pageNum = pageNum;
|
this.pageNum = pageNum;
|
||||||
this.pageSize = pageSize;
|
this.pageSize = pageSize;
|
||||||
this.total = total;
|
this.total = total;
|
||||||
this.records = records != null ? records : Collections.emptyList();
|
this.list = list != null ? list : Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getPageNum() {
|
public long getPageNum() {
|
||||||
@ -68,11 +68,11 @@ public class PageResult<T> implements Serializable {
|
|||||||
this.total = total;
|
this.total = total;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<T> getRecords() {
|
public List<T> getList() {
|
||||||
return records;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setRecords(List<T> records) {
|
public void setList(List<T> list) {
|
||||||
this.records = records;
|
this.list = list;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -122,7 +122,7 @@ public class CustomerServiceImpl implements CustomerService {
|
|||||||
|
|
||||||
private CustomerVO convertToVO(Customer customer) {
|
private CustomerVO convertToVO(Customer customer) {
|
||||||
CustomerVO vo = new CustomerVO();
|
CustomerVO vo = new CustomerVO();
|
||||||
vo.setCustomerId(customer.getId());
|
vo.setId(customer.getId());
|
||||||
vo.setCustomerCode(customer.getCustomerCode());
|
vo.setCustomerCode(customer.getCustomerCode());
|
||||||
vo.setCustomerName(customer.getCustomerName());
|
vo.setCustomerName(customer.getCustomerName());
|
||||||
vo.setContact(customer.getContact());
|
vo.setContact(customer.getContact());
|
||||||
|
|||||||
@ -5,7 +5,7 @@ package com.fundplatform.cust.vo;
|
|||||||
*/
|
*/
|
||||||
public class CustomerVO {
|
public class CustomerVO {
|
||||||
|
|
||||||
private Long customerId;
|
private Long id;
|
||||||
private String customerCode;
|
private String customerCode;
|
||||||
private String customerName;
|
private String customerName;
|
||||||
private String contact;
|
private String contact;
|
||||||
@ -15,12 +15,12 @@ public class CustomerVO {
|
|||||||
private Integer status;
|
private Integer status;
|
||||||
private String remark;
|
private String remark;
|
||||||
|
|
||||||
public Long getCustomerId() {
|
public Long getId() {
|
||||||
return customerId;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setCustomerId(Long customerId) {
|
public void setId(Long id) {
|
||||||
this.customerId = customerId;
|
this.id = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getCustomerCode() {
|
public String getCustomerCode() {
|
||||||
|
|||||||
@ -1,6 +0,0 @@
|
|||||||
# 开发环境配置
|
|
||||||
# 开发模式无部署前缀
|
|
||||||
VITE_BASE=/
|
|
||||||
|
|
||||||
# API基础路径(开发模式使用代理)
|
|
||||||
VITE_API_BASE_URL=
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
# 生产环境配置
|
|
||||||
# 部署路径前缀(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,11 +11,7 @@ declare module 'vue' {
|
|||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
Tabbar: typeof import('./src/components/Tabbar.vue')['default']
|
Tabbar: typeof import('./src/components/Tabbar.vue')['default']
|
||||||
VanButton: typeof import('vant/es')['Button']
|
|
||||||
VanCellGroup: typeof import('vant/es')['CellGroup']
|
|
||||||
VanDatePicker: typeof import('vant/es')['DatePicker']
|
VanDatePicker: typeof import('vant/es')['DatePicker']
|
||||||
VanField: typeof import('vant/es')['Field']
|
|
||||||
VanForm: typeof import('vant/es')['Form']
|
|
||||||
VanIcon: typeof import('vant/es')['Icon']
|
VanIcon: typeof import('vant/es')['Icon']
|
||||||
VanList: typeof import('vant/es')['List']
|
VanList: typeof import('vant/es')['List']
|
||||||
VanNavBar: typeof import('vant/es')['NavBar']
|
VanNavBar: typeof import('vant/es')['NavBar']
|
||||||
|
|||||||
@ -31,7 +31,6 @@ body {
|
|||||||
|
|
||||||
.app-container {
|
.app-container {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
padding-bottom: 80px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 页面切换动画 */
|
/* 页面切换动画 */
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import request from './request'
|
import request from './request'
|
||||||
|
|
||||||
// ===================== 用户认证 =====================
|
// 用户认证
|
||||||
|
|
||||||
export function login(data: { username: string; password: string }) {
|
export function login(data: { username: string; password: string }) {
|
||||||
return request.post('/auth/login', data)
|
return request.post('/auth/login', data)
|
||||||
}
|
}
|
||||||
@ -14,13 +13,8 @@ export function logout() {
|
|||||||
return request.post('/auth/logout')
|
return request.post('/auth/logout')
|
||||||
}
|
}
|
||||||
|
|
||||||
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; projectName?: string }) {
|
||||||
}
|
|
||||||
|
|
||||||
// ===================== 项目管理 =====================
|
|
||||||
|
|
||||||
export function getProjectList(params?: { pageNum: number; pageSize: number; keyword?: string }) {
|
|
||||||
return request.get('/project/page', { params })
|
return request.get('/project/page', { params })
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,78 +22,38 @@ export function getProjectById(id: number) {
|
|||||||
return request.get(`/project/${id}`)
|
return request.get(`/project/${id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createProject(data: any) {
|
// 客户管理
|
||||||
return request.post('/project', data)
|
export function getCustomerList(params?: { pageNum: number; pageSize: number; customerName?: string }) {
|
||||||
}
|
|
||||||
|
|
||||||
// ===================== 需求工单管理 =====================
|
|
||||||
|
|
||||||
export function getRequirementList(params?: { pageNum: number; pageSize: number; keyword?: string }) {
|
|
||||||
return request.get('/requirement/page', { params })
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getRequirementById(id: number) {
|
|
||||||
return request.get(`/requirement/${id}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createRequirement(data: any) {
|
|
||||||
return request.post('/requirement', data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===================== 客户管理 =====================
|
|
||||||
|
|
||||||
export function getCustomerList(params?: { pageNum: number; pageSize: number; keyword?: string }) {
|
|
||||||
return request.get('/customer/page', { params })
|
return request.get('/customer/page', { params })
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCustomerById(id: number) {
|
// 支出管理
|
||||||
return request.get(`/customer/${id}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createCustomer(data: any) {
|
|
||||||
return request.post('/customer', data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===================== 支出管理 =====================
|
|
||||||
|
|
||||||
export function createExpense(data: any) {
|
export function createExpense(data: any) {
|
||||||
return request.post('/exp/expense', data)
|
return request.post('/exp/expense', data)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getExpenseList(params?: { pageNum: number; pageSize: number; title?: string }) {
|
export function getExpenseList(params: { pageNum: number; pageSize: number }) {
|
||||||
return request.get('/exp/expense/page', { params })
|
return request.get('/exp/expense/page', { params })
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getExpenseTypeTree() {
|
// 应收款管理
|
||||||
return request.get('/exp/expense-type/tree')
|
export function getReceivableList(params: { pageNum: number; pageSize: number; status?: string }) {
|
||||||
}
|
|
||||||
|
|
||||||
export function getTodayExpense() {
|
|
||||||
return request.get('/exp/expense/stats/today-expense')
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===================== 应收款管理 =====================
|
|
||||||
|
|
||||||
export function getReceivableList(params?: { pageNum: number; pageSize: number; status?: string; keyword?: string }) {
|
|
||||||
return request.get('/receipt/receivable/page', { params })
|
return request.get('/receipt/receivable/page', { params })
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getReceivableById(id: number) {
|
|
||||||
return request.get(`/receipt/receivable/${id}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createReceivable(data: any) {
|
|
||||||
return request.post('/receipt/receivable', data)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getUpcomingDueList(daysWithin: number = 7) {
|
export function getUpcomingDueList(daysWithin: number = 7) {
|
||||||
return request.get(`/receipt/receivable/upcoming-due?daysWithin=${daysWithin}`)
|
return request.get(`/receipt/receivable/upcoming-due?daysWithin=${daysWithin}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 统计数据
|
||||||
export function getTodayIncome() {
|
export function getTodayIncome() {
|
||||||
return request.get('/receipt/receivable/stats/today-income')
|
return request.get('/receipt/receivable/stats/today-income')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getTodayExpense() {
|
||||||
|
return request.get('/exp/expense/stats/today-expense')
|
||||||
|
}
|
||||||
|
|
||||||
export function getUnpaidAmount() {
|
export function getUnpaidAmount() {
|
||||||
return request.get('/receipt/receivable/stats/unpaid-amount')
|
return request.get('/receipt/receivable/stats/unpaid-amount')
|
||||||
}
|
}
|
||||||
@ -107,3 +61,8 @@ export function getUnpaidAmount() {
|
|||||||
export function getOverdueCount() {
|
export function getOverdueCount() {
|
||||||
return request.get('/receipt/receivable/stats/overdue-count')
|
return request.get('/receipt/receivable/stats/overdue-count')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 支出类型
|
||||||
|
export function getExpenseTypeTree() {
|
||||||
|
return request.get('/exp/expense-type/tree')
|
||||||
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
const request = axios.create({
|
const request = axios.create({
|
||||||
baseURL: import.meta.env.VITE_API_BASE_URL || '/fund',
|
baseURL: '/fund',
|
||||||
timeout: 15000,
|
timeout: 15000,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { createRouter, createWebHistory } from 'vue-router'
|
|||||||
import { useTenantStore } from '@/stores/tenant'
|
import { useTenantStore } from '@/stores/tenant'
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHistory(),
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
@ -10,85 +10,36 @@ const router = createRouter({
|
|||||||
component: () => import('@/views/Home.vue'),
|
component: () => import('@/views/Home.vue'),
|
||||||
meta: { title: '首页', requiresAuth: true }
|
meta: { title: '首页', requiresAuth: true }
|
||||||
},
|
},
|
||||||
// 需求工单
|
|
||||||
{
|
|
||||||
path: '/requirement',
|
|
||||||
name: 'RequirementList',
|
|
||||||
component: () => import('@/views/requirement/List.vue'),
|
|
||||||
meta: { title: '需求工单', requiresAuth: true }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/requirement/add',
|
|
||||||
name: 'RequirementAdd',
|
|
||||||
component: () => import('@/views/requirement/Add.vue'),
|
|
||||||
meta: { title: '新增需求工单', requiresAuth: true }
|
|
||||||
},
|
|
||||||
// 支出管理
|
|
||||||
{
|
|
||||||
path: '/expense',
|
|
||||||
name: 'ExpenseList',
|
|
||||||
component: () => import('@/views/expense/List.vue'),
|
|
||||||
meta: { title: '支出管理', requiresAuth: true }
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/expense/add',
|
path: '/expense/add',
|
||||||
name: 'ExpenseAdd',
|
name: 'ExpenseAdd',
|
||||||
component: () => import('@/views/expense/Add.vue'),
|
component: () => import('@/views/expense/Add.vue'),
|
||||||
meta: { title: '新增支出', requiresAuth: true }
|
meta: { title: '新增支出', requiresAuth: true }
|
||||||
},
|
},
|
||||||
// 应收款管理
|
|
||||||
{
|
{
|
||||||
path: '/receivable',
|
path: '/receivable',
|
||||||
name: 'ReceivableList',
|
name: 'ReceivableList',
|
||||||
component: () => import('@/views/receivable/List.vue'),
|
component: () => import('@/views/receivable/List.vue'),
|
||||||
meta: { title: '应收款管理', requiresAuth: true }
|
meta: { title: '应收款列表', requiresAuth: true }
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/receivable/add',
|
|
||||||
name: 'ReceivableAdd',
|
|
||||||
component: () => import('@/views/receivable/Add.vue'),
|
|
||||||
meta: { title: '新增应收款', requiresAuth: true }
|
|
||||||
},
|
|
||||||
// 项目管理
|
|
||||||
{
|
{
|
||||||
path: '/project',
|
path: '/project',
|
||||||
name: 'ProjectList',
|
name: 'ProjectList',
|
||||||
component: () => import('@/views/project/List.vue'),
|
component: () => import('@/views/project/List.vue'),
|
||||||
meta: { title: '项目管理', requiresAuth: true }
|
meta: { title: '项目列表', requiresAuth: true }
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/project/add',
|
|
||||||
name: 'ProjectAdd',
|
|
||||||
component: () => import('@/views/project/Add.vue'),
|
|
||||||
meta: { title: '新增项目', requiresAuth: true }
|
|
||||||
},
|
|
||||||
// 客户管理
|
|
||||||
{
|
{
|
||||||
path: '/customer',
|
path: '/customer',
|
||||||
name: 'CustomerList',
|
name: 'CustomerList',
|
||||||
component: () => import('@/views/customer/List.vue'),
|
component: () => import('@/views/customer/List.vue'),
|
||||||
meta: { title: '客户管理', requiresAuth: true }
|
meta: { title: '客户列表', requiresAuth: true }
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/customer/add',
|
|
||||||
name: 'CustomerAdd',
|
|
||||||
component: () => import('@/views/customer/Add.vue'),
|
|
||||||
meta: { title: '新增客户', requiresAuth: true }
|
|
||||||
},
|
|
||||||
// 我的
|
|
||||||
{
|
{
|
||||||
path: '/my',
|
path: '/my',
|
||||||
name: 'My',
|
name: 'My',
|
||||||
component: () => import('@/views/my/Index.vue'),
|
component: () => import('@/views/my/Index.vue'),
|
||||||
meta: { title: '我的', requiresAuth: true }
|
meta: { title: '我的', requiresAuth: true }
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/my/change-password',
|
|
||||||
name: 'ChangePassword',
|
|
||||||
component: () => import('@/views/my/ChangePassword.vue'),
|
|
||||||
meta: { title: '修改密码', requiresAuth: true }
|
|
||||||
},
|
|
||||||
// 登录
|
|
||||||
{
|
{
|
||||||
path: '/login',
|
path: '/login',
|
||||||
name: 'Login',
|
name: 'Login',
|
||||||
|
|||||||
@ -1,5 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page home">
|
<div class="page home">
|
||||||
|
<!-- 顶部标题 -->
|
||||||
|
<div class="header">
|
||||||
|
<div class="header-title">资金服务平台</div>
|
||||||
|
<div class="header-subtitle">Financial Platform</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 数据概览卡片 -->
|
<!-- 数据概览卡片 -->
|
||||||
<div class="summary-card mac-card fade-in-up">
|
<div class="summary-card mac-card fade-in-up">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
@ -37,100 +43,41 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 快捷操作卡片 -->
|
<!-- 快捷入口卡片 -->
|
||||||
<div class="quick-card mac-card fade-in-up delay-1">
|
<div class="quick-card mac-card fade-in-up delay-1">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<span class="card-title">快捷操作</span>
|
<span class="card-title">快捷操作</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="quick-grid">
|
<div class="quick-grid">
|
||||||
<div class="quick-item" @click="$router.push('/requirement/add')">
|
|
||||||
<div class="quick-icon requirement">
|
|
||||||
<van-icon name="description" />
|
|
||||||
</div>
|
|
||||||
<div class="quick-text">新增需求</div>
|
|
||||||
</div>
|
|
||||||
<div class="quick-item" @click="$router.push('/receivable/add')">
|
|
||||||
<div class="quick-icon receivable">
|
|
||||||
<van-icon name="balance-list-o" />
|
|
||||||
</div>
|
|
||||||
<div class="quick-text">新增应收款</div>
|
|
||||||
</div>
|
|
||||||
<div class="quick-item" @click="$router.push('/expense/add')">
|
<div class="quick-item" @click="$router.push('/expense/add')">
|
||||||
<div class="quick-icon expense">
|
<div class="quick-icon">
|
||||||
<van-icon name="gold-coin-o" />
|
<van-icon name="plus" />
|
||||||
</div>
|
</div>
|
||||||
<div class="quick-text">新增支出</div>
|
<div class="quick-text">新增支出</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="quick-item" @click="$router.push('/project/add')">
|
<div class="quick-item" @click="$router.push('/receivable')">
|
||||||
<div class="quick-icon project">
|
<div class="quick-icon">
|
||||||
<van-icon name="todo-list-o" />
|
|
||||||
</div>
|
|
||||||
<div class="quick-text">新增项目</div>
|
|
||||||
</div>
|
|
||||||
<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>
|
|
||||||
</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" />
|
<van-icon name="balance-list-o" />
|
||||||
</div>
|
</div>
|
||||||
<div class="service-content">
|
<div class="quick-text">应收款</div>
|
||||||
<div class="service-name">应收款管理</div>
|
|
||||||
<div class="service-desc">跟踪应收款项</div>
|
|
||||||
</div>
|
|
||||||
<van-icon name="arrow" class="service-arrow" />
|
|
||||||
</div>
|
</div>
|
||||||
<div class="service-item" @click="$router.push('/expense')">
|
<div class="quick-item" @click="$router.push('/project')">
|
||||||
<div class="service-icon expense">
|
<div class="quick-icon">
|
||||||
<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" />
|
<van-icon name="todo-list-o" />
|
||||||
</div>
|
</div>
|
||||||
<div class="service-content">
|
<div class="quick-text">项目</div>
|
||||||
<div class="service-name">项目管理</div>
|
|
||||||
<div class="service-desc">项目信息管理</div>
|
|
||||||
</div>
|
|
||||||
<van-icon name="arrow" class="service-arrow" />
|
|
||||||
</div>
|
</div>
|
||||||
<div class="service-item" @click="$router.push('/customer')">
|
<div class="quick-item" @click="$router.push('/customer')">
|
||||||
<div class="service-icon customer">
|
<div class="quick-icon">
|
||||||
<van-icon name="friends-o" />
|
<van-icon name="friends-o" />
|
||||||
</div>
|
</div>
|
||||||
<div class="service-content">
|
<div class="quick-text">客户</div>
|
||||||
<div class="service-name">客户管理</div>
|
</div>
|
||||||
<div class="service-desc">客户信息管理</div>
|
<div class="quick-item" @click="$router.push('/my')">
|
||||||
|
<div class="quick-icon">
|
||||||
|
<van-icon name="user-o" />
|
||||||
</div>
|
</div>
|
||||||
<van-icon name="arrow" class="service-arrow" />
|
<div class="quick-text">我的</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -139,7 +86,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { getTodayIncome, getTodayExpense, getUnpaidAmount } from '@/api'
|
import request from '@/api/request'
|
||||||
|
|
||||||
const summary = ref({
|
const summary = ref({
|
||||||
todayIncome: 0,
|
todayIncome: 0,
|
||||||
@ -167,9 +114,9 @@ const formatMoney = (value: number) => {
|
|||||||
const loadSummary = async () => {
|
const loadSummary = async () => {
|
||||||
try {
|
try {
|
||||||
const [incomeRes, expenseRes, unpaidRes] = await Promise.all([
|
const [incomeRes, expenseRes, unpaidRes] = await Promise.all([
|
||||||
getTodayIncome(),
|
request.get('/receipt/api/v1/receipt/receivable/stats/today-income'),
|
||||||
getTodayExpense(),
|
request.get('/exp/api/v1/exp/expense/stats/today-expense'),
|
||||||
getUnpaidAmount()
|
request.get('/receipt/api/v1/receipt/receivable/stats/unpaid-amount')
|
||||||
])
|
])
|
||||||
|
|
||||||
summary.value.todayIncome = (incomeRes as any).data || 0
|
summary.value.todayIncome = (incomeRes as any).data || 0
|
||||||
@ -188,7 +135,26 @@ onMounted(() => {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.home {
|
.home {
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
padding-top: 20px;
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: 60px 0 24px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--mac-text);
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-subtitle {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--mac-text-secondary);
|
||||||
|
margin-top: 6px;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary-card {
|
.summary-card {
|
||||||
@ -274,29 +240,25 @@ onMounted(() => {
|
|||||||
color: var(--mac-text-secondary);
|
color: var(--mac-text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 快捷操作样式 */
|
|
||||||
.quick-card {
|
.quick-card {
|
||||||
margin-bottom: 16px;
|
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quick-grid {
|
.quick-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(3, 1fr);
|
||||||
gap: 12px;
|
gap: 16px;
|
||||||
justify-items: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.quick-item {
|
.quick-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 14px 16px;
|
padding: 16px 8px;
|
||||||
background: rgba(0, 0, 0, 0.02);
|
background: rgba(0, 0, 0, 0.02);
|
||||||
border-radius: 14px;
|
border-radius: 16px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
min-width: 90px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.quick-item:active {
|
.quick-item:active {
|
||||||
@ -305,121 +267,22 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.quick-icon {
|
.quick-icon {
|
||||||
width: 44px;
|
width: 48px;
|
||||||
height: 44px;
|
height: 48px;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
|
background: linear-gradient(135deg, var(--mac-primary), #5AC8FA);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 10px;
|
||||||
}
|
box-shadow: 0 4px 12px rgba(0, 122, 255, 0.25);
|
||||||
|
|
||||||
.quick-icon.requirement {
|
|
||||||
background: linear-gradient(135deg, #5856D6, #AF52DE);
|
|
||||||
}
|
|
||||||
|
|
||||||
.quick-icon.receivable {
|
|
||||||
background: linear-gradient(135deg, #34C759, #30D158);
|
|
||||||
}
|
|
||||||
|
|
||||||
.quick-icon.expense {
|
|
||||||
background: linear-gradient(135deg, #FF3B30, #FF453A);
|
|
||||||
}
|
|
||||||
|
|
||||||
.quick-icon.project {
|
|
||||||
background: linear-gradient(135deg, #007AFF, #5AC8FA);
|
|
||||||
}
|
|
||||||
|
|
||||||
.quick-icon.customer {
|
|
||||||
background: linear-gradient(135deg, #FF9500, #FF9F0A);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.quick-text {
|
.quick-text {
|
||||||
font-size: 12px;
|
font-size: 13px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: var(--mac-text);
|
color: var(--mac-text);
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 业务服务样式 */
|
|
||||||
.service-card {
|
|
||||||
padding: 20px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 14px;
|
|
||||||
background: rgba(0, 0, 0, 0.02);
|
|
||||||
border-radius: 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-item:active {
|
|
||||||
background: rgba(0, 122, 255, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-icon {
|
|
||||||
width: 44px;
|
|
||||||
height: 44px;
|
|
||||||
border-radius: 12px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 22px;
|
|
||||||
color: #fff;
|
|
||||||
margin-right: 14px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-icon.requirement {
|
|
||||||
background: linear-gradient(135deg, #5856D6, #AF52DE);
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-icon.receivable {
|
|
||||||
background: linear-gradient(135deg, #34C759, #30D158);
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-icon.expense {
|
|
||||||
background: linear-gradient(135deg, #FF3B30, #FF453A);
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-icon.project {
|
|
||||||
background: linear-gradient(135deg, #007AFF, #5AC8FA);
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-icon.customer {
|
|
||||||
background: linear-gradient(135deg, #FF9500, #FF9F0A);
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-content {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-name {
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--mac-text);
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-desc {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--mac-text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.service-arrow {
|
|
||||||
color: var(--mac-text-secondary);
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -51,7 +51,7 @@
|
|||||||
import { ref, reactive } from 'vue'
|
import { ref, reactive } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { showToast, showSuccessToast } from 'vant'
|
import { showToast, showSuccessToast } from 'vant'
|
||||||
import { login } from '@/api'
|
import request from '@/api/request'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@ -74,7 +74,7 @@ const handleLogin = async () => {
|
|||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const res: any = await login(form)
|
const res: any = await request.post('/sys/api/v1/auth/login', form)
|
||||||
const data = res.data
|
const data = res.data
|
||||||
localStorage.setItem('token', data.token)
|
localStorage.setItem('token', data.token)
|
||||||
localStorage.setItem('userInfo', JSON.stringify({
|
localStorage.setItem('userInfo', JSON.stringify({
|
||||||
|
|||||||
@ -1,144 +0,0 @@
|
|||||||
<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-avatar">{{ item.customerName?.charAt(0) || 'C' }}</div>
|
||||||
<div class="customer-info">
|
<div class="customer-info">
|
||||||
<div class="customer-name">{{ item.customerName }}</div>
|
<div class="customer-name">{{ item.customerName }}</div>
|
||||||
<div class="customer-code">{{ item.customerCode || '-' }}</div>
|
<div class="customer-short">{{ item.customerShort || '-' }}</div>
|
||||||
</div>
|
</div>
|
||||||
<van-tag :type="getStatusType(item.status)">{{ getStatusText(item.status) }}</van-tag>
|
<van-tag :type="getLevelType(item.level)">{{ getLevelText(item.level) }}</van-tag>
|
||||||
</div>
|
</div>
|
||||||
<div class="customer-detail">
|
<div class="customer-detail">
|
||||||
<div class="detail-item" v-if="item.contact">
|
|
||||||
<van-icon name="user-o" />
|
|
||||||
<span>{{ item.contact }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="detail-item" v-if="item.phone">
|
<div class="detail-item" v-if="item.phone">
|
||||||
<van-icon name="phone-o" />
|
<van-icon name="phone-o" />
|
||||||
<span>{{ item.phone }}</span>
|
<span>{{ item.phone }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="detail-item" v-if="item.industry">
|
||||||
|
<van-icon name="cluster-o" />
|
||||||
|
<span>{{ item.industry }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</van-list>
|
</van-list>
|
||||||
@ -41,7 +41,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { getCustomerList } from '@/api'
|
import { getCustomerList } from '@/api'
|
||||||
|
|
||||||
const searchText = ref('')
|
const searchText = ref('')
|
||||||
@ -52,12 +52,24 @@ const list = ref<any[]>([])
|
|||||||
const pageNum = ref(1)
|
const pageNum = ref(1)
|
||||||
const pageSize = 10
|
const pageSize = 10
|
||||||
|
|
||||||
const getStatusType = (status: number): 'primary' | 'success' | 'warning' | 'danger' | 'default' => {
|
const getLevelType = (level: string): 'primary' | 'success' | 'warning' | 'danger' | 'default' => {
|
||||||
return status === 1 ? 'success' : 'default'
|
const map: Record<string, 'primary' | 'success' | 'warning' | 'danger' | 'default'> = {
|
||||||
|
'A': 'primary',
|
||||||
|
'B': 'success',
|
||||||
|
'C': 'warning',
|
||||||
|
'D': 'danger'
|
||||||
|
}
|
||||||
|
return map[level] || 'default'
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStatusText = (status: number) => {
|
const getLevelText = (level: string) => {
|
||||||
return status === 1 ? '启用' : '禁用'
|
const map: Record<string, string> = {
|
||||||
|
'A': 'A类',
|
||||||
|
'B': 'B类',
|
||||||
|
'C': 'C类',
|
||||||
|
'D': 'D类'
|
||||||
|
}
|
||||||
|
return map[level] || '普通'
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
@ -65,7 +77,7 @@ const loadData = async () => {
|
|||||||
const res: any = await getCustomerList({
|
const res: any = await getCustomerList({
|
||||||
pageNum: pageNum.value,
|
pageNum: pageNum.value,
|
||||||
pageSize,
|
pageSize,
|
||||||
keyword: searchText.value || undefined
|
customerName: searchText.value || undefined
|
||||||
})
|
})
|
||||||
const records = res.data?.records || []
|
const records = res.data?.records || []
|
||||||
if (pageNum.value === 1) {
|
if (pageNum.value === 1) {
|
||||||
@ -84,8 +96,6 @@ const loadData = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onLoad = () => {
|
const onLoad = () => {
|
||||||
if (loading.value) return
|
|
||||||
loading.value = true
|
|
||||||
pageNum.value++
|
pageNum.value++
|
||||||
loadData()
|
loadData()
|
||||||
}
|
}
|
||||||
@ -100,14 +110,8 @@ const handleSearch = () => {
|
|||||||
pageNum.value = 1
|
pageNum.value = 1
|
||||||
finished.value = false
|
finished.value = false
|
||||||
list.value = []
|
list.value = []
|
||||||
loading.value = true
|
|
||||||
loadData()
|
loadData()
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
loading.value = true
|
|
||||||
loadData()
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@ -158,7 +162,7 @@ onMounted(() => {
|
|||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.customer-code {
|
.customer-short {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #999;
|
color: #999;
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
|
|||||||
@ -10,12 +10,12 @@
|
|||||||
|
|
||||||
<div class="form-card mac-card fade-in-up">
|
<div class="form-card mac-card fade-in-up">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>支出标题 <span class="required">*</span></label>
|
<label>支出标题</label>
|
||||||
<input v-model="form.title" placeholder="输入标题" class="mac-input" />
|
<input v-model="form.title" placeholder="输入标题" class="mac-input" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>支出类型 <span class="required">*</span></label>
|
<label>支出类型</label>
|
||||||
<div class="mac-select" @click="showTypePicker = true">
|
<div class="mac-select" @click="showTypePicker = true">
|
||||||
<span :class="{ placeholder: !form.expenseTypeName }">
|
<span :class="{ placeholder: !form.expenseTypeName }">
|
||||||
{{ form.expenseTypeName || '选择类型' }}
|
{{ form.expenseTypeName || '选择类型' }}
|
||||||
@ -25,18 +25,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>支出金额 <span class="required">*</span></label>
|
<label>支出金额</label>
|
||||||
<div class="amount-input">
|
<div class="amount-input">
|
||||||
<span class="currency">¥</span>
|
<span class="currency">¥</span>
|
||||||
<input v-model="form.amount" type="number" placeholder="0.00" />
|
<input v-model="form.amount" type="number" placeholder="0.00" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label>收款单位 <span class="required">*</span></label>
|
|
||||||
<input v-model="form.payeeName" placeholder="输入收款单位名称" class="mac-input" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>支出日期</label>
|
<label>支出日期</label>
|
||||||
<div class="mac-select" @click="showDatePicker = true">
|
<div class="mac-select" @click="showDatePicker = true">
|
||||||
@ -74,8 +69,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { showToast, showSuccessToast, showFailToast } from 'vant'
|
import { showToast, showSuccessToast } from 'vant'
|
||||||
import { createExpense, getExpenseTypeTree } from '@/api'
|
import request from '@/api/request'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@ -87,7 +82,6 @@ const form = reactive({
|
|||||||
expenseTypeId: null as number | null,
|
expenseTypeId: null as number | null,
|
||||||
expenseTypeName: '',
|
expenseTypeName: '',
|
||||||
amount: '',
|
amount: '',
|
||||||
payeeName: '',
|
|
||||||
expenseDate: '',
|
expenseDate: '',
|
||||||
description: ''
|
description: ''
|
||||||
})
|
})
|
||||||
@ -108,19 +102,15 @@ const onDateConfirm = ({ selectedValues }: any) => {
|
|||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!form.title) {
|
if (!form.title) {
|
||||||
showFailToast('请输入支出标题')
|
showToast('请输入支出标题')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!form.expenseTypeId) {
|
if (!form.expenseTypeId) {
|
||||||
showFailToast('请选择支出类型')
|
showToast('请选择支出类型')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!form.amount) {
|
if (!form.amount) {
|
||||||
showFailToast('请输入支出金额')
|
showToast('请输入支出金额')
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!form.payeeName) {
|
|
||||||
showFailToast('请输入收款单位')
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,15 +124,15 @@ const handleSubmit = async () => {
|
|||||||
amount: parseFloat(form.amount),
|
amount: parseFloat(form.amount),
|
||||||
expenseDate: expenseDateTime,
|
expenseDate: expenseDateTime,
|
||||||
purpose: form.description,
|
purpose: form.description,
|
||||||
payeeName: form.payeeName
|
payeeName: '待填写'
|
||||||
}
|
}
|
||||||
console.log('提交支出数据:', requestData)
|
console.log('提交支出数据:', requestData)
|
||||||
await createExpense(requestData)
|
await request.post('/exp/api/v1/exp/expense', requestData)
|
||||||
showSuccessToast('提交成功')
|
showSuccessToast('提交成功')
|
||||||
router.back()
|
router.back()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('提交失败:', e)
|
console.error('提交失败:', e)
|
||||||
showFailToast(e.message || '提交失败')
|
showToast(e.message || '提交失败')
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@ -151,7 +141,7 @@ const handleSubmit = async () => {
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// 加载支出类型
|
// 加载支出类型
|
||||||
try {
|
try {
|
||||||
const res: any = await getExpenseTypeTree()
|
const res: any = await request.get('/exp/api/v1/exp/expense-type/tree')
|
||||||
const types = res.data || []
|
const types = res.data || []
|
||||||
typeColumns.value = types.map((t: any) => ({
|
typeColumns.value = types.map((t: any) => ({
|
||||||
text: t.typeName,
|
text: t.typeName,
|
||||||
@ -229,11 +219,6 @@ onMounted(async () => {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group label .required {
|
|
||||||
color: var(--mac-danger);
|
|
||||||
margin-left: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mac-input {
|
.mac-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 14px 16px;
|
padding: 14px 16px;
|
||||||
|
|||||||
@ -1,231 +0,0 @@
|
|||||||
<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>
|
|
||||||
@ -1,145 +0,0 @@
|
|||||||
<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 = () => {
|
const handleChangePassword = () => {
|
||||||
router.push('/my/change-password')
|
showToast('功能开发中')
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleAbout = () => {
|
const handleAbout = () => {
|
||||||
|
|||||||
@ -1,289 +0,0 @@
|
|||||||
<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,6 +31,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="project-amount">
|
<div class="project-amount">
|
||||||
|
<div class="amount-item">
|
||||||
|
<span class="label">合同金额</span>
|
||||||
|
<span class="value">{{ formatMoney(item.contractAmount) }}</span>
|
||||||
|
</div>
|
||||||
<div class="amount-item">
|
<div class="amount-item">
|
||||||
<span class="label">预算金额</span>
|
<span class="label">预算金额</span>
|
||||||
<span class="value">{{ formatMoney(item.budgetAmount) }}</span>
|
<span class="value">{{ formatMoney(item.budgetAmount) }}</span>
|
||||||
@ -43,7 +47,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { getProjectList } from '@/api'
|
import { getProjectList } from '@/api'
|
||||||
|
|
||||||
@ -88,7 +92,7 @@ const loadData = async () => {
|
|||||||
const res: any = await getProjectList({
|
const res: any = await getProjectList({
|
||||||
pageNum: pageNum.value,
|
pageNum: pageNum.value,
|
||||||
pageSize,
|
pageSize,
|
||||||
keyword: searchText.value || undefined
|
projectName: searchText.value || undefined
|
||||||
})
|
})
|
||||||
const records = res.data?.records || []
|
const records = res.data?.records || []
|
||||||
if (pageNum.value === 1) {
|
if (pageNum.value === 1) {
|
||||||
@ -107,8 +111,6 @@ const loadData = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onLoad = () => {
|
const onLoad = () => {
|
||||||
if (loading.value) return
|
|
||||||
loading.value = true
|
|
||||||
pageNum.value++
|
pageNum.value++
|
||||||
loadData()
|
loadData()
|
||||||
}
|
}
|
||||||
@ -123,18 +125,12 @@ const handleSearch = () => {
|
|||||||
pageNum.value = 1
|
pageNum.value = 1
|
||||||
finished.value = false
|
finished.value = false
|
||||||
list.value = []
|
list.value = []
|
||||||
loading.value = true
|
|
||||||
loadData()
|
loadData()
|
||||||
}
|
}
|
||||||
|
|
||||||
const goDetail = (item: any) => {
|
const goDetail = (item: any) => {
|
||||||
router.push(`/project/${item.projectId}`)
|
router.push(`/project/${item.projectId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
loading.value = true
|
|
||||||
loadData()
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@ -1,221 +0,0 @@
|
|||||||
<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,15 +4,10 @@
|
|||||||
<div class="back-btn" @click="$router.back()">
|
<div class="back-btn" @click="$router.back()">
|
||||||
<van-icon name="arrow-left" />
|
<van-icon name="arrow-left" />
|
||||||
</div>
|
</div>
|
||||||
<span class="header-title">应收款管理</span>
|
<span class="header-title">应收款列表</span>
|
||||||
<div class="placeholder"></div>
|
<div class="placeholder"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 搜索栏 -->
|
|
||||||
<div class="search-bar">
|
|
||||||
<van-search v-model="searchText" placeholder="搜索客户名称" @search="handleSearch" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
|
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
|
||||||
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad">
|
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad">
|
||||||
<div class="list-container">
|
<div class="list-container">
|
||||||
@ -42,20 +37,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</van-list>
|
</van-list>
|
||||||
</van-pull-refresh>
|
</van-pull-refresh>
|
||||||
|
|
||||||
<!-- 新增按钮 -->
|
|
||||||
<div class="add-btn" @click="$router.push('/receivable/add')">
|
|
||||||
<van-icon name="plus" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { showToast } from 'vant'
|
import { showToast } from 'vant'
|
||||||
import { getReceivableList } from '@/api'
|
import request from '@/api/request'
|
||||||
|
|
||||||
const searchText = ref('')
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const refreshing = ref(false)
|
const refreshing = ref(false)
|
||||||
const finished = ref(false)
|
const finished = ref(false)
|
||||||
@ -83,54 +72,35 @@ const getStatusName = (status: string) => {
|
|||||||
return map[status] || status
|
return map[status] || status
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadData = async () => {
|
const onLoad = async () => {
|
||||||
try {
|
try {
|
||||||
const res: any = await getReceivableList({
|
const res: any = await request.get('/receipt/api/v1/receipt/receivable/page', {
|
||||||
pageNum: pageNum.value,
|
params: { pageNum: pageNum.value, pageSize }
|
||||||
pageSize,
|
|
||||||
keyword: searchText.value || undefined
|
|
||||||
})
|
})
|
||||||
const records = res.data?.records || []
|
const records = res.data?.records || []
|
||||||
if (pageNum.value === 1) {
|
list.value.push(...records)
|
||||||
list.value = records
|
|
||||||
} else {
|
|
||||||
list.value.push(...records)
|
|
||||||
}
|
|
||||||
finished.value = records.length < pageSize
|
|
||||||
loading.value = false
|
loading.value = false
|
||||||
|
if (records.length < pageSize) {
|
||||||
|
finished.value = true
|
||||||
|
} else {
|
||||||
|
pageNum.value++
|
||||||
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
showToast(e.message || '加载失败')
|
showToast(e.message || '加载失败')
|
||||||
loading.value = false
|
loading.value = false
|
||||||
finished.value = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onLoad = () => {
|
|
||||||
if (loading.value) return
|
|
||||||
loading.value = true
|
|
||||||
pageNum.value++
|
|
||||||
loadData()
|
|
||||||
}
|
|
||||||
|
|
||||||
const onRefresh = () => {
|
const onRefresh = () => {
|
||||||
list.value = []
|
list.value = []
|
||||||
pageNum.value = 1
|
pageNum.value = 1
|
||||||
finished.value = false
|
finished.value = false
|
||||||
refreshing.value = false
|
refreshing.value = false
|
||||||
loadData()
|
onLoad()
|
||||||
}
|
|
||||||
|
|
||||||
const handleSearch = () => {
|
|
||||||
pageNum.value = 1
|
|
||||||
finished.value = false
|
|
||||||
list.value = []
|
|
||||||
loading.value = true
|
|
||||||
loadData()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loading.value = true
|
onLoad()
|
||||||
loadData()
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -144,7 +114,7 @@ onMounted(() => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 16px 20px;
|
padding: 16px 20px;
|
||||||
margin: 12px -16px 0;
|
margin: 12px -16px 16px;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
@ -154,12 +124,6 @@ onMounted(() => {
|
|||||||
-webkit-backdrop-filter: blur(20px);
|
-webkit-backdrop-filter: blur(20px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-bar {
|
|
||||||
background: #fff;
|
|
||||||
margin: 0 -16px 12px;
|
|
||||||
padding: 8px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.back-btn {
|
.back-btn {
|
||||||
width: 36px;
|
width: 36px;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
@ -258,27 +222,4 @@ onMounted(() => {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--mac-text-secondary);
|
color: var(--mac-text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-btn {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 80px;
|
|
||||||
right: 24px;
|
|
||||||
width: 56px;
|
|
||||||
height: 56px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: linear-gradient(135deg, #34C759, #30D158);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: #fff;
|
|
||||||
font-size: 28px;
|
|
||||||
box-shadow: 0 4px 12px rgba(52, 199, 89, 0.4);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: transform 0.2s ease;
|
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-btn:active {
|
|
||||||
transform: scale(0.95);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,224 +0,0 @@
|
|||||||
<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>
|
|
||||||
@ -1,247 +0,0 @@
|
|||||||
<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
10
fund-mobile/src/vite-env.d.ts
vendored
@ -1,10 +0,0 @@
|
|||||||
/// <reference types="vite/client" />
|
|
||||||
|
|
||||||
interface ImportMetaEnv {
|
|
||||||
readonly VITE_BASE: string
|
|
||||||
readonly VITE_API_BASE_URL: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ImportMeta {
|
|
||||||
readonly env: ImportMetaEnv
|
|
||||||
}
|
|
||||||
@ -1,55 +1,47 @@
|
|||||||
import { defineConfig, loadEnv } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
import Components from 'unplugin-vue-components/vite'
|
import Components from 'unplugin-vue-components/vite'
|
||||||
import { VantResolver } from '@vant/auto-import-resolver'
|
import { VantResolver } from '@vant/auto-import-resolver'
|
||||||
import { fileURLToPath, URL } from 'node:url'
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
|
|
||||||
export default defineConfig(({ mode }) => {
|
export default defineConfig({
|
||||||
// 加载环境变量
|
plugins: [
|
||||||
const env = loadEnv(mode, process.cwd())
|
vue(),
|
||||||
const base = env.VITE_BASE || '/'
|
Components({
|
||||||
|
resolvers: [VantResolver()]
|
||||||
return {
|
})
|
||||||
// 部署路径前缀
|
],
|
||||||
base,
|
resolve: {
|
||||||
plugins: [
|
alias: {
|
||||||
vue(),
|
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||||
Components({
|
}
|
||||||
resolvers: [VantResolver()]
|
},
|
||||||
})
|
server: {
|
||||||
],
|
port: 8080,
|
||||||
resolve: {
|
proxy: {
|
||||||
alias: {
|
'/sys': {
|
||||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
target: 'http://localhost:8000',
|
||||||
}
|
changeOrigin: true
|
||||||
},
|
},
|
||||||
server: {
|
'/cust': {
|
||||||
port: 8080,
|
target: 'http://localhost:8000',
|
||||||
proxy: {
|
changeOrigin: true
|
||||||
'/sys': {
|
},
|
||||||
target: 'http://localhost:8000',
|
'/proj': {
|
||||||
changeOrigin: true
|
target: 'http://localhost:8000',
|
||||||
},
|
changeOrigin: true
|
||||||
'/cust': {
|
},
|
||||||
target: 'http://localhost:8000',
|
'/exp': {
|
||||||
changeOrigin: true
|
target: 'http://localhost:8000',
|
||||||
},
|
changeOrigin: true
|
||||||
'/proj': {
|
},
|
||||||
target: 'http://localhost:8000',
|
'/receipt': {
|
||||||
changeOrigin: true
|
target: 'http://localhost:8000',
|
||||||
},
|
changeOrigin: true
|
||||||
'/exp': {
|
},
|
||||||
target: 'http://localhost:8000',
|
'/file': {
|
||||||
changeOrigin: true
|
target: 'http://localhost:8000',
|
||||||
},
|
changeOrigin: true
|
||||||
'/receipt': {
|
|
||||||
target: 'http://localhost:8000',
|
|
||||||
changeOrigin: true
|
|
||||||
},
|
|
||||||
'/file': {
|
|
||||||
target: 'http://localhost:8000',
|
|
||||||
changeOrigin: true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,13 +25,13 @@ public class RequirementController {
|
|||||||
@GetMapping("/page")
|
@GetMapping("/page")
|
||||||
public Result<Page<RequirementVO>> page(
|
public Result<Page<RequirementVO>> page(
|
||||||
@RequestHeader(value = "X-Tenant-Id", required = false) Long tenantId,
|
@RequestHeader(value = "X-Tenant-Id", required = false) Long tenantId,
|
||||||
@RequestParam(required = false) String requirementName,
|
@RequestParam(required = false) String requirementTitle,
|
||||||
@RequestParam(required = false) String projectName,
|
@RequestParam(required = false) String projectName,
|
||||||
@RequestParam(required = false) String requirementStatus,
|
@RequestParam(required = false) String requirementStatus,
|
||||||
@RequestParam(required = false) String priority,
|
@RequestParam(required = false) String priority,
|
||||||
@RequestParam(defaultValue = "1") int pageNum,
|
@RequestParam(defaultValue = "1") int pageNum,
|
||||||
@RequestParam(defaultValue = "10") int pageSize) {
|
@RequestParam(defaultValue = "10") int pageSize) {
|
||||||
return requirementService.page(tenantId, requirementName, requirementStatus, null, null, pageNum, pageSize);
|
return requirementService.page(tenantId, requirementTitle, requirementStatus, null, null, pageNum, pageSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -15,7 +15,6 @@ public class Project extends BaseEntity {
|
|||||||
private String projectCode;
|
private String projectCode;
|
||||||
private String projectName;
|
private String projectName;
|
||||||
private Long customerId;
|
private Long customerId;
|
||||||
private String customerName;
|
|
||||||
private String projectType;
|
private String projectType;
|
||||||
private BigDecimal budgetAmount;
|
private BigDecimal budgetAmount;
|
||||||
private LocalDate startDate;
|
private LocalDate startDate;
|
||||||
@ -47,14 +46,6 @@ public class Project extends BaseEntity {
|
|||||||
this.customerId = customerId;
|
this.customerId = customerId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getCustomerName() {
|
|
||||||
return customerName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCustomerName(String customerName) {
|
|
||||||
this.customerName = customerName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getProjectType() {
|
public String getProjectType() {
|
||||||
return projectType;
|
return projectType;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,8 +20,6 @@ public class ProjectCreateDTO {
|
|||||||
@NotNull(message = "客户ID不能为空")
|
@NotNull(message = "客户ID不能为空")
|
||||||
private Long customerId;
|
private Long customerId;
|
||||||
|
|
||||||
private String customerName;
|
|
||||||
|
|
||||||
@NotBlank(message = "项目类型不能为空")
|
@NotBlank(message = "项目类型不能为空")
|
||||||
private String projectType;
|
private String projectType;
|
||||||
|
|
||||||
@ -55,14 +53,6 @@ public class ProjectCreateDTO {
|
|||||||
this.customerId = customerId;
|
this.customerId = customerId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getCustomerName() {
|
|
||||||
return customerName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCustomerName(String customerName) {
|
|
||||||
this.customerName = customerName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getProjectType() {
|
public String getProjectType() {
|
||||||
return projectType;
|
return projectType;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -45,7 +45,6 @@ public class ProjectServiceImpl implements ProjectService {
|
|||||||
project.setProjectCode(dto.getProjectCode());
|
project.setProjectCode(dto.getProjectCode());
|
||||||
project.setProjectName(dto.getProjectName());
|
project.setProjectName(dto.getProjectName());
|
||||||
project.setCustomerId(dto.getCustomerId());
|
project.setCustomerId(dto.getCustomerId());
|
||||||
project.setCustomerName(dto.getCustomerName());
|
|
||||||
project.setProjectType(dto.getProjectType());
|
project.setProjectType(dto.getProjectType());
|
||||||
project.setBudgetAmount(dto.getBudgetAmount());
|
project.setBudgetAmount(dto.getBudgetAmount());
|
||||||
project.setStartDate(dto.getStartDate());
|
project.setStartDate(dto.getStartDate());
|
||||||
@ -158,11 +157,10 @@ public class ProjectServiceImpl implements ProjectService {
|
|||||||
|
|
||||||
private ProjectVO convertToVO(Project project) {
|
private ProjectVO convertToVO(Project project) {
|
||||||
ProjectVO vo = new ProjectVO();
|
ProjectVO vo = new ProjectVO();
|
||||||
vo.setProjectId(project.getId());
|
vo.setId(project.getId());
|
||||||
vo.setProjectCode(project.getProjectCode());
|
vo.setProjectCode(project.getProjectCode());
|
||||||
vo.setProjectName(project.getProjectName());
|
vo.setProjectName(project.getProjectName());
|
||||||
vo.setCustomerId(project.getCustomerId());
|
vo.setCustomerId(project.getCustomerId());
|
||||||
vo.setCustomerName(project.getCustomerName());
|
|
||||||
vo.setProjectType(project.getProjectType());
|
vo.setProjectType(project.getProjectType());
|
||||||
vo.setBudgetAmount(project.getBudgetAmount());
|
vo.setBudgetAmount(project.getBudgetAmount());
|
||||||
vo.setStartDate(project.getStartDate());
|
vo.setStartDate(project.getStartDate());
|
||||||
|
|||||||
@ -8,11 +8,10 @@ import java.time.LocalDate;
|
|||||||
*/
|
*/
|
||||||
public class ProjectVO {
|
public class ProjectVO {
|
||||||
|
|
||||||
private Long projectId;
|
private Long id;
|
||||||
private String projectCode;
|
private String projectCode;
|
||||||
private String projectName;
|
private String projectName;
|
||||||
private Long customerId;
|
private Long customerId;
|
||||||
private String customerName;
|
|
||||||
private String projectType;
|
private String projectType;
|
||||||
private BigDecimal budgetAmount;
|
private BigDecimal budgetAmount;
|
||||||
private LocalDate startDate;
|
private LocalDate startDate;
|
||||||
@ -21,12 +20,12 @@ public class ProjectVO {
|
|||||||
private Integer status;
|
private Integer status;
|
||||||
private String remark;
|
private String remark;
|
||||||
|
|
||||||
public Long getProjectId() {
|
public Long getId() {
|
||||||
return projectId;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setProjectId(Long projectId) {
|
public void setId(Long id) {
|
||||||
this.projectId = projectId;
|
this.id = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getProjectCode() {
|
public String getProjectCode() {
|
||||||
@ -53,14 +52,6 @@ public class ProjectVO {
|
|||||||
this.customerId = customerId;
|
this.customerId = customerId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getCustomerName() {
|
|
||||||
return customerName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCustomerName(String customerName) {
|
|
||||||
this.customerName = customerName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getProjectType() {
|
public String getProjectType() {
|
||||||
return projectType;
|
return projectType;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,69 +0,0 @@
|
|||||||
#!/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 "============================================"
|
|
||||||
@ -1,94 +0,0 @@
|
|||||||
#!/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 "============================================"
|
|
||||||
@ -1,75 +0,0 @@
|
|||||||
#!/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"
|
|
||||||
}
|
|
||||||
@ -1,69 +0,0 @@
|
|||||||
#!/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 "============================================"
|
|
||||||
@ -1,160 +0,0 @@
|
|||||||
#!/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