feat: 移动端首页重构与业务模块完善
1. 首页布局调整 - 保留今日概览板块 - 快捷操作板块:新增需求工单、新增应收款、新增支出、新增项目、新增客户 - 新增业务服务板块:需求工单、应收款管理、支出管理、项目管理、客户管理入口 2. 新增页面 - 需求工单:列表页(支持搜索)、新增页 - 支出管理:列表页(支持搜索)、保留新增页 - 应收款:新增页、列表页添加搜索功能 - 项目:新增页、列表页优化搜索参数 - 客户:新增页、列表页优化搜索参数 3. API更新 - 新增需求工单相关API(getRequirementList、getRequirementById、createRequirement) - 新增项目新增API(createProject) - 新增客户新增API(createCustomer) - 新增应收款新增API(createReceivable) - 更新搜索参数为统一的keyword格式 4. 路由更新 - 新增需求工单列表/新增路由 - 新增支出管理列表路由 - 新增应收款新增路由 - 新增项目新增路由 - 新增客户新增路由
This commit is contained in:
parent
5e782ac8cc
commit
d3a77c23f1
4
fund-mobile/components.d.ts
vendored
4
fund-mobile/components.d.ts
vendored
@ -11,7 +11,11 @@ declare module 'vue' {
|
|||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
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']
|
||||||
|
|||||||
@ -16,7 +16,7 @@ export function logout() {
|
|||||||
|
|
||||||
// ===================== 项目管理 =====================
|
// ===================== 项目管理 =====================
|
||||||
|
|
||||||
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 })
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,19 +24,45 @@ 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 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; customerName?: string }) {
|
export function getCustomerList(params?: { pageNum: number; pageSize: number; keyword?: string }) {
|
||||||
return request.get('/customer/page', { params })
|
return request.get('/customer/page', { params })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getCustomerById(id: number) {
|
||||||
|
return request.get(`/customer/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createCustomer(data: any) {
|
||||||
|
return request.post('/customer', data)
|
||||||
|
}
|
||||||
|
|
||||||
// ===================== 支出管理 =====================
|
// ===================== 支出管理 =====================
|
||||||
|
|
||||||
export function createExpense(data: any) {
|
export function createExpense(data: any) {
|
||||||
return request.post('/exp/expense', data)
|
return request.post('/exp/expense', data)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getExpenseList(params: { pageNum: number; pageSize: number }) {
|
export function getExpenseList(params?: { pageNum: number; pageSize: number; title?: string }) {
|
||||||
return request.get('/exp/expense/page', { params })
|
return request.get('/exp/expense/page', { params })
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,10 +76,18 @@ export function getTodayExpense() {
|
|||||||
|
|
||||||
// ===================== 应收款管理 =====================
|
// ===================== 应收款管理 =====================
|
||||||
|
|
||||||
export function getReceivableList(params: { pageNum: number; pageSize: number; status?: string }) {
|
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}`)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,36 +10,79 @@ 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: '/login',
|
path: '/login',
|
||||||
name: 'Login',
|
name: 'Login',
|
||||||
|
|||||||
@ -43,41 +43,100 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 快捷入口卡片 -->
|
<!-- 快捷操作卡片 -->
|
||||||
<div class="quick-card mac-card fade-in-up delay-1">
|
<div class="quick-card mac-card fade-in-up delay-1">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<span class="card-title">快捷操作</span>
|
<span class="card-title">快捷操作</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="quick-grid">
|
<div class="quick-grid">
|
||||||
|
<div class="quick-item" @click="$router.push('/requirement/add')">
|
||||||
|
<div class="quick-icon requirement">
|
||||||
|
<van-icon name="description" />
|
||||||
|
</div>
|
||||||
|
<div class="quick-text">新增需求</div>
|
||||||
|
</div>
|
||||||
|
<div class="quick-item" @click="$router.push('/receivable/add')">
|
||||||
|
<div class="quick-icon receivable">
|
||||||
|
<van-icon name="balance-list-o" />
|
||||||
|
</div>
|
||||||
|
<div class="quick-text">新增应收款</div>
|
||||||
|
</div>
|
||||||
<div class="quick-item" @click="$router.push('/expense/add')">
|
<div class="quick-item" @click="$router.push('/expense/add')">
|
||||||
<div class="quick-icon">
|
<div class="quick-icon expense">
|
||||||
<van-icon name="plus" />
|
<van-icon name="gold-coin-o" />
|
||||||
</div>
|
</div>
|
||||||
<div class="quick-text">新增支出</div>
|
<div class="quick-text">新增支出</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="quick-item" @click="$router.push('/receivable')">
|
<div class="quick-item" @click="$router.push('/project/add')">
|
||||||
<div class="quick-icon">
|
<div class="quick-icon project">
|
||||||
<van-icon name="balance-list-o" />
|
|
||||||
</div>
|
|
||||||
<div class="quick-text">应收款</div>
|
|
||||||
</div>
|
|
||||||
<div class="quick-item" @click="$router.push('/project')">
|
|
||||||
<div class="quick-icon">
|
|
||||||
<van-icon name="todo-list-o" />
|
<van-icon name="todo-list-o" />
|
||||||
</div>
|
</div>
|
||||||
<div class="quick-text">项目</div>
|
<div class="quick-text">新增项目</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="quick-item" @click="$router.push('/customer')">
|
<div class="quick-item" @click="$router.push('/customer/add')">
|
||||||
<div class="quick-icon">
|
<div class="quick-icon customer">
|
||||||
<van-icon name="friends-o" />
|
<van-icon name="friends-o" />
|
||||||
</div>
|
</div>
|
||||||
<div class="quick-text">客户</div>
|
<div class="quick-text">新增客户</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="quick-item" @click="$router.push('/my')">
|
</div>
|
||||||
<div class="quick-icon">
|
</div>
|
||||||
<van-icon name="user-o" />
|
|
||||||
|
<!-- 业务服务卡片 -->
|
||||||
|
<div class="service-card mac-card fade-in-up delay-2">
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="card-title">业务服务</span>
|
||||||
|
</div>
|
||||||
|
<div class="service-list">
|
||||||
|
<div class="service-item" @click="$router.push('/requirement')">
|
||||||
|
<div class="service-icon requirement">
|
||||||
|
<van-icon name="description" />
|
||||||
</div>
|
</div>
|
||||||
<div class="quick-text">我的</div>
|
<div class="service-content">
|
||||||
|
<div class="service-name">需求工单</div>
|
||||||
|
<div class="service-desc">管理需求工单流程</div>
|
||||||
|
</div>
|
||||||
|
<van-icon name="arrow" class="service-arrow" />
|
||||||
|
</div>
|
||||||
|
<div class="service-item" @click="$router.push('/receivable')">
|
||||||
|
<div class="service-icon receivable">
|
||||||
|
<van-icon name="balance-list-o" />
|
||||||
|
</div>
|
||||||
|
<div class="service-content">
|
||||||
|
<div class="service-name">应收款管理</div>
|
||||||
|
<div class="service-desc">跟踪应收款项</div>
|
||||||
|
</div>
|
||||||
|
<van-icon name="arrow" class="service-arrow" />
|
||||||
|
</div>
|
||||||
|
<div class="service-item" @click="$router.push('/expense')">
|
||||||
|
<div class="service-icon expense">
|
||||||
|
<van-icon name="gold-coin-o" />
|
||||||
|
</div>
|
||||||
|
<div class="service-content">
|
||||||
|
<div class="service-name">支出管理</div>
|
||||||
|
<div class="service-desc">管理支出审批流程</div>
|
||||||
|
</div>
|
||||||
|
<van-icon name="arrow" class="service-arrow" />
|
||||||
|
</div>
|
||||||
|
<div class="service-item" @click="$router.push('/project')">
|
||||||
|
<div class="service-icon project">
|
||||||
|
<van-icon name="todo-list-o" />
|
||||||
|
</div>
|
||||||
|
<div class="service-content">
|
||||||
|
<div class="service-name">项目管理</div>
|
||||||
|
<div class="service-desc">项目信息管理</div>
|
||||||
|
</div>
|
||||||
|
<van-icon name="arrow" class="service-arrow" />
|
||||||
|
</div>
|
||||||
|
<div class="service-item" @click="$router.push('/customer')">
|
||||||
|
<div class="service-icon customer">
|
||||||
|
<van-icon name="friends-o" />
|
||||||
|
</div>
|
||||||
|
<div class="service-content">
|
||||||
|
<div class="service-name">客户管理</div>
|
||||||
|
<div class="service-desc">客户信息管理</div>
|
||||||
|
</div>
|
||||||
|
<van-icon name="arrow" class="service-arrow" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -240,23 +299,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(5, 1fr);
|
||||||
gap: 16px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quick-item {
|
.quick-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 16px 8px;
|
padding: 12px 4px;
|
||||||
background: rgba(0, 0, 0, 0.02);
|
background: rgba(0, 0, 0, 0.02);
|
||||||
border-radius: 16px;
|
border-radius: 12px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
@ -267,22 +328,121 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.quick-icon {
|
.quick-icon {
|
||||||
width: 48px;
|
width: 40px;
|
||||||
height: 48px;
|
height: 40px;
|
||||||
border-radius: 14px;
|
border-radius: 12px;
|
||||||
background: linear-gradient(135deg, var(--mac-primary), #5AC8FA);
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 20px;
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-icon.requirement {
|
||||||
|
background: linear-gradient(135deg, #5856D6, #AF52DE);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-icon.receivable {
|
||||||
|
background: linear-gradient(135deg, #34C759, #30D158);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-icon.expense {
|
||||||
|
background: linear-gradient(135deg, #FF3B30, #FF453A);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-icon.project {
|
||||||
|
background: linear-gradient(135deg, #007AFF, #5AC8FA);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-icon.customer {
|
||||||
|
background: linear-gradient(135deg, #FF9500, #FF9F0A);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-text {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--mac-text);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 业务服务样式 */
|
||||||
|
.service-card {
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 14px;
|
||||||
|
background: rgba(0, 0, 0, 0.02);
|
||||||
|
border-radius: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-item:active {
|
||||||
|
background: rgba(0, 122, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-icon {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
border-radius: 12px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
margin-bottom: 10px;
|
margin-right: 14px;
|
||||||
box-shadow: 0 4px 12px rgba(0, 122, 255, 0.25);
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quick-text {
|
.service-icon.requirement {
|
||||||
font-size: 13px;
|
background: linear-gradient(135deg, #5856D6, #AF52DE);
|
||||||
font-weight: 500;
|
}
|
||||||
|
|
||||||
|
.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);
|
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>
|
||||||
|
|||||||
171
fund-mobile/src/views/customer/Add.vue
Normal file
171
fund-mobile/src/views/customer/Add.vue
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
<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.customerName"
|
||||||
|
name="customerName"
|
||||||
|
label="客户名称"
|
||||||
|
placeholder="请输入客户名称"
|
||||||
|
:rules="[{ required: true, message: '请输入客户名称' }]"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<van-field
|
||||||
|
v-model="form.customerShort"
|
||||||
|
name="customerShort"
|
||||||
|
label="简称"
|
||||||
|
placeholder="请输入客户简称"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<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="levelText"
|
||||||
|
is-link
|
||||||
|
readonly
|
||||||
|
name="level"
|
||||||
|
label="客户等级"
|
||||||
|
placeholder="请选择等级"
|
||||||
|
@click="showLevelPicker = true"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<van-field
|
||||||
|
v-model="form.industry"
|
||||||
|
name="industry"
|
||||||
|
label="所属行业"
|
||||||
|
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>
|
||||||
|
|
||||||
|
<!-- 等级选择器 -->
|
||||||
|
<van-popup v-model:show="showLevelPicker" position="bottom" round>
|
||||||
|
<van-picker
|
||||||
|
:columns="levelOptions"
|
||||||
|
@confirm="onLevelConfirm"
|
||||||
|
@cancel="showLevelPicker = false"
|
||||||
|
/>
|
||||||
|
</van-popup>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { showToast, showSuccessToast } from 'vant'
|
||||||
|
import { createCustomer } from '@/api'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const submitting = ref(false)
|
||||||
|
const showLevelPicker = ref(false)
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
customerName: '',
|
||||||
|
customerShort: '',
|
||||||
|
phone: '',
|
||||||
|
email: '',
|
||||||
|
level: 'C',
|
||||||
|
industry: '',
|
||||||
|
address: '',
|
||||||
|
remark: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const levelOptions = [
|
||||||
|
{ text: 'A类(重点客户)', value: 'A' },
|
||||||
|
{ text: 'B类(一般客户)', value: 'B' },
|
||||||
|
{ text: 'C类(普通客户)', value: 'C' },
|
||||||
|
{ text: 'D类(潜在客户)', value: 'D' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const levelText = computed(() => {
|
||||||
|
const item = levelOptions.find(l => l.value === form.value.level)
|
||||||
|
return item?.text || ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const onLevelConfirm = ({ selectedOptions }: any) => {
|
||||||
|
form.value.level = selectedOptions[0].value
|
||||||
|
showLevelPicker.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSubmit = async () => {
|
||||||
|
if (!form.value.customerName) {
|
||||||
|
showToast('请输入客户名称')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
await createCustomer({
|
||||||
|
customerName: form.value.customerName,
|
||||||
|
customerShort: form.value.customerShort,
|
||||||
|
phone: form.value.phone,
|
||||||
|
email: form.value.email,
|
||||||
|
level: form.value.level,
|
||||||
|
industry: form.value.industry,
|
||||||
|
address: form.value.address,
|
||||||
|
remark: form.value.remark
|
||||||
|
})
|
||||||
|
showSuccessToast('提交成功')
|
||||||
|
router.back()
|
||||||
|
} catch (e: any) {
|
||||||
|
showToast(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>
|
||||||
@ -77,7 +77,7 @@ const loadData = async () => {
|
|||||||
const res: any = await getCustomerList({
|
const res: any = await getCustomerList({
|
||||||
pageNum: pageNum.value,
|
pageNum: pageNum.value,
|
||||||
pageSize,
|
pageSize,
|
||||||
customerName: searchText.value || undefined
|
keyword: searchText.value || undefined
|
||||||
})
|
})
|
||||||
const records = res.data?.records || []
|
const records = res.data?.records || []
|
||||||
if (pageNum.value === 1) {
|
if (pageNum.value === 1) {
|
||||||
|
|||||||
224
fund-mobile/src/views/expense/List.vue
Normal file
224
fund-mobile/src/views/expense/List.vue
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
<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 } 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 = () => {
|
||||||
|
pageNum.value++
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const onRefresh = () => {
|
||||||
|
pageNum.value = 1
|
||||||
|
finished.value = false
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSearch = () => {
|
||||||
|
pageNum.value = 1
|
||||||
|
finished.value = false
|
||||||
|
list.value = []
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.expense-list {
|
||||||
|
background: #f5f5f5;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding-bottom: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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: 24px;
|
||||||
|
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>
|
||||||
233
fund-mobile/src/views/project/Add.vue
Normal file
233
fund-mobile/src/views/project/Add.vue
Normal file
@ -0,0 +1,233 @@
|
|||||||
|
<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.projectName"
|
||||||
|
name="projectName"
|
||||||
|
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.contractAmount"
|
||||||
|
name="contractAmount"
|
||||||
|
label="合同金额"
|
||||||
|
type="number"
|
||||||
|
placeholder="请输入合同金额"
|
||||||
|
>
|
||||||
|
<template #button>
|
||||||
|
<span>元</span>
|
||||||
|
</template>
|
||||||
|
</van-field>
|
||||||
|
|
||||||
|
<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="statusText"
|
||||||
|
is-link
|
||||||
|
readonly
|
||||||
|
name="status"
|
||||||
|
label="项目状态"
|
||||||
|
placeholder="请选择状态"
|
||||||
|
@click="showStatusPicker = 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="showStartPicker" position="bottom" round>
|
||||||
|
<van-date-picker
|
||||||
|
v-model="selectedStartDate"
|
||||||
|
@confirm="onStartDateConfirm"
|
||||||
|
@cancel="showStartPicker = false"
|
||||||
|
/>
|
||||||
|
</van-popup>
|
||||||
|
|
||||||
|
<!-- 状态选择器 -->
|
||||||
|
<van-popup v-model:show="showStatusPicker" position="bottom" round>
|
||||||
|
<van-picker
|
||||||
|
:columns="statusOptions"
|
||||||
|
@confirm="onStatusConfirm"
|
||||||
|
@cancel="showStatusPicker = 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 { showToast, showSuccessToast } from 'vant'
|
||||||
|
import { createProject, getCustomerList } from '@/api'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const submitting = ref(false)
|
||||||
|
const showStartPicker = ref(false)
|
||||||
|
const showStatusPicker = ref(false)
|
||||||
|
const showCustomerPicker = ref(false)
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
projectName: '',
|
||||||
|
customerId: null as number | null,
|
||||||
|
customerName: '',
|
||||||
|
contractAmount: '',
|
||||||
|
budgetAmount: '',
|
||||||
|
startDate: '',
|
||||||
|
status: 'preparing',
|
||||||
|
description: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedStartDate = ref([
|
||||||
|
new Date().getFullYear().toString(),
|
||||||
|
(new Date().getMonth() + 1).toString().padStart(2, '0'),
|
||||||
|
new Date().getDate().toString().padStart(2, '0')
|
||||||
|
])
|
||||||
|
|
||||||
|
const statusOptions = [
|
||||||
|
{ text: '筹备中', value: 'preparing' },
|
||||||
|
{ text: '进行中', value: 'ongoing' },
|
||||||
|
{ text: '已完成', value: 'completed' },
|
||||||
|
{ text: '已归档', value: 'archived' },
|
||||||
|
{ text: '已取消', value: 'cancelled' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const statusText = computed(() => {
|
||||||
|
const item = statusOptions.find(s => s.value === form.value.status)
|
||||||
|
return item?.text || ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const customerOptions = ref<{ text: string; value: number }[]>([])
|
||||||
|
|
||||||
|
const onStartDateConfirm = ({ selectedValues }: any) => {
|
||||||
|
form.value.startDate = selectedValues.join('-')
|
||||||
|
showStartPicker.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const onStatusConfirm = ({ selectedOptions }: any) => {
|
||||||
|
form.value.status = selectedOptions[0].value
|
||||||
|
showStatusPicker.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.projectName) {
|
||||||
|
showToast('请输入项目名称')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
await createProject({
|
||||||
|
projectName: form.value.projectName,
|
||||||
|
customerId: form.value.customerId,
|
||||||
|
contractAmount: form.value.contractAmount ? parseFloat(form.value.contractAmount) : null,
|
||||||
|
budgetAmount: form.value.budgetAmount ? parseFloat(form.value.budgetAmount) : null,
|
||||||
|
startDate: form.value.startDate || null,
|
||||||
|
status: form.value.status,
|
||||||
|
description: form.value.description
|
||||||
|
})
|
||||||
|
showSuccessToast('提交成功')
|
||||||
|
router.back()
|
||||||
|
} catch (e: any) {
|
||||||
|
showToast(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>
|
||||||
@ -92,7 +92,7 @@ const loadData = async () => {
|
|||||||
const res: any = await getProjectList({
|
const res: any = await getProjectList({
|
||||||
pageNum: pageNum.value,
|
pageNum: pageNum.value,
|
||||||
pageSize,
|
pageSize,
|
||||||
projectName: searchText.value || undefined
|
keyword: searchText.value || undefined
|
||||||
})
|
})
|
||||||
const records = res.data?.records || []
|
const records = res.data?.records || []
|
||||||
if (pageNum.value === 1) {
|
if (pageNum.value === 1) {
|
||||||
|
|||||||
221
fund-mobile/src/views/receivable/Add.vue
Normal file
221
fund-mobile/src/views/receivable/Add.vue
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page receivable-add">
|
||||||
|
<van-nav-bar title="新增应收款" left-arrow @click-left="$router.back()" />
|
||||||
|
|
||||||
|
<van-form @submit="onSubmit">
|
||||||
|
<van-cell-group inset>
|
||||||
|
<van-field
|
||||||
|
v-model="form.customerName"
|
||||||
|
is-link
|
||||||
|
readonly
|
||||||
|
name="customer"
|
||||||
|
label="关联客户"
|
||||||
|
placeholder="请选择客户"
|
||||||
|
:rules="[{ required: true, message: '请选择客户' }]"
|
||||||
|
required
|
||||||
|
@click="showCustomerPicker = true"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<van-field
|
||||||
|
v-model="form.projectName"
|
||||||
|
is-link
|
||||||
|
readonly
|
||||||
|
name="project"
|
||||||
|
label="关联项目"
|
||||||
|
placeholder="请选择项目"
|
||||||
|
@click="showProjectPicker = true"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<van-field
|
||||||
|
v-model="form.receivableAmount"
|
||||||
|
name="amount"
|
||||||
|
label="应收金额"
|
||||||
|
type="number"
|
||||||
|
placeholder="请输入应收金额"
|
||||||
|
:rules="[{ required: true, message: '请输入应收金额' }]"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<template #button>
|
||||||
|
<span>元</span>
|
||||||
|
</template>
|
||||||
|
</van-field>
|
||||||
|
|
||||||
|
<van-field
|
||||||
|
v-model="form.receivableDate"
|
||||||
|
is-link
|
||||||
|
readonly
|
||||||
|
name="date"
|
||||||
|
label="应收日期"
|
||||||
|
placeholder="请选择日期"
|
||||||
|
:rules="[{ required: true, message: '请选择应收日期' }]"
|
||||||
|
required
|
||||||
|
@click="showDatePicker = true"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<van-field
|
||||||
|
v-model="form.description"
|
||||||
|
name="description"
|
||||||
|
label="备注"
|
||||||
|
type="textarea"
|
||||||
|
rows="2"
|
||||||
|
autosize
|
||||||
|
placeholder="请输入备注"
|
||||||
|
/>
|
||||||
|
</van-cell-group>
|
||||||
|
|
||||||
|
<div class="submit-btn">
|
||||||
|
<van-button round block type="primary" native-type="submit" :loading="submitting">
|
||||||
|
提交
|
||||||
|
</van-button>
|
||||||
|
</div>
|
||||||
|
</van-form>
|
||||||
|
|
||||||
|
<!-- 日期选择器 -->
|
||||||
|
<van-popup v-model:show="showDatePicker" position="bottom" round>
|
||||||
|
<van-date-picker
|
||||||
|
v-model="selectedDate"
|
||||||
|
@confirm="onDateConfirm"
|
||||||
|
@cancel="showDatePicker = false"
|
||||||
|
/>
|
||||||
|
</van-popup>
|
||||||
|
|
||||||
|
<!-- 客户选择器 -->
|
||||||
|
<van-popup v-model:show="showCustomerPicker" position="bottom" round>
|
||||||
|
<van-picker
|
||||||
|
:columns="customerOptions"
|
||||||
|
@confirm="onCustomerConfirm"
|
||||||
|
@cancel="showCustomerPicker = false"
|
||||||
|
/>
|
||||||
|
</van-popup>
|
||||||
|
|
||||||
|
<!-- 项目选择器 -->
|
||||||
|
<van-popup v-model:show="showProjectPicker" position="bottom" round>
|
||||||
|
<van-picker
|
||||||
|
:columns="projectOptions"
|
||||||
|
@confirm="onProjectConfirm"
|
||||||
|
@cancel="showProjectPicker = false"
|
||||||
|
/>
|
||||||
|
</van-popup>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { showToast, 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) {
|
||||||
|
showToast('请选择客户')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!form.value.receivableAmount) {
|
||||||
|
showToast('请输入应收金额')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!form.value.receivableDate) {
|
||||||
|
showToast('请选择应收日期')
|
||||||
|
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) {
|
||||||
|
showToast(e.message || '提交失败')
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadOptions()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.receivable-add {
|
||||||
|
background: #f5f5f5;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.van-cell-group {
|
||||||
|
margin: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn {
|
||||||
|
margin: 24px 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -4,10 +4,15 @@
|
|||||||
<div class="back-btn" @click="$router.back()">
|
<div class="back-btn" @click="$router.back()">
|
||||||
<van-icon name="arrow-left" />
|
<van-icon name="arrow-left" />
|
||||||
</div>
|
</div>
|
||||||
<span class="header-title">应收款列表</span>
|
<span class="header-title">应收款管理</span>
|
||||||
<div class="placeholder"></div>
|
<div class="placeholder"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 搜索栏 -->
|
||||||
|
<div class="search-bar">
|
||||||
|
<van-search v-model="searchText" placeholder="搜索客户名称" @search="handleSearch" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
|
<van-pull-refresh v-model="refreshing" @refresh="onRefresh">
|
||||||
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad">
|
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad">
|
||||||
<div class="list-container">
|
<div class="list-container">
|
||||||
@ -37,6 +42,11 @@
|
|||||||
</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>
|
||||||
|
|
||||||
@ -45,6 +55,7 @@ import { ref, onMounted } from 'vue'
|
|||||||
import { showToast } from 'vant'
|
import { showToast } from 'vant'
|
||||||
import { getReceivableList } from '@/api'
|
import { getReceivableList } from '@/api'
|
||||||
|
|
||||||
|
const searchText = ref('')
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const refreshing = ref(false)
|
const refreshing = ref(false)
|
||||||
const finished = ref(false)
|
const finished = ref(false)
|
||||||
@ -72,20 +83,34 @@ const getStatusName = (status: string) => {
|
|||||||
return map[status] || status
|
return map[status] || status
|
||||||
}
|
}
|
||||||
|
|
||||||
const onLoad = async () => {
|
const loadData = async () => {
|
||||||
try {
|
try {
|
||||||
const res: any = await getReceivableList({ pageNum: pageNum.value, pageSize })
|
const res: any = await getReceivableList({
|
||||||
|
pageNum: pageNum.value,
|
||||||
|
pageSize,
|
||||||
|
keyword: searchText.value || undefined
|
||||||
|
})
|
||||||
const records = res.data?.records || []
|
const records = res.data?.records || []
|
||||||
list.value.push(...records)
|
if (pageNum.value === 1) {
|
||||||
loading.value = false
|
list.value = records
|
||||||
if (records.length < pageSize) {
|
|
||||||
finished.value = true
|
|
||||||
} else {
|
} else {
|
||||||
pageNum.value++
|
list.value.push(...records)
|
||||||
}
|
}
|
||||||
|
finished.value = records.length < pageSize
|
||||||
|
loading.value = false
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
showToast(e.message || '加载失败')
|
showToast(e.message || '加载失败')
|
||||||
loading.value = false
|
loading.value = false
|
||||||
|
finished.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onLoad = () => {
|
||||||
|
if (pageNum.value === 1 && list.value.length === 0) {
|
||||||
|
loadData()
|
||||||
|
} else {
|
||||||
|
pageNum.value++
|
||||||
|
loadData()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,17 +119,25 @@ const onRefresh = () => {
|
|||||||
pageNum.value = 1
|
pageNum.value = 1
|
||||||
finished.value = false
|
finished.value = false
|
||||||
refreshing.value = false
|
refreshing.value = false
|
||||||
onLoad()
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSearch = () => {
|
||||||
|
pageNum.value = 1
|
||||||
|
finished.value = false
|
||||||
|
list.value = []
|
||||||
|
loadData()
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
onLoad()
|
loadData()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.receivable-list {
|
.receivable-list {
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
|
padding-bottom: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-bar {
|
.header-bar {
|
||||||
@ -112,7 +145,7 @@ onMounted(() => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 16px 20px;
|
padding: 16px 20px;
|
||||||
margin: 12px -16px 16px;
|
margin: 12px -16px 0;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
@ -122,6 +155,12 @@ onMounted(() => {
|
|||||||
-webkit-backdrop-filter: blur(20px);
|
-webkit-backdrop-filter: blur(20px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search-bar {
|
||||||
|
background: #fff;
|
||||||
|
margin: 0 -16px 12px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.back-btn {
|
.back-btn {
|
||||||
width: 36px;
|
width: 36px;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
@ -220,4 +259,27 @@ onMounted(() => {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--mac-text-secondary);
|
color: var(--mac-text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.add-btn {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 24px;
|
||||||
|
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>
|
||||||
|
|||||||
209
fund-mobile/src/views/requirement/Add.vue
Normal file
209
fund-mobile/src/views/requirement/Add.vue
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
<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.title"
|
||||||
|
name="title"
|
||||||
|
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 { showToast, 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({
|
||||||
|
title: '',
|
||||||
|
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.title) {
|
||||||
|
showToast('请输入需求标题')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
await createRequirement({
|
||||||
|
title: form.value.title,
|
||||||
|
customerId: form.value.customerId,
|
||||||
|
projectId: form.value.projectId,
|
||||||
|
priority: form.value.priority,
|
||||||
|
description: form.value.description
|
||||||
|
})
|
||||||
|
showSuccessToast('提交成功')
|
||||||
|
router.back()
|
||||||
|
} catch (e: any) {
|
||||||
|
showToast(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>
|
||||||
240
fund-mobile/src/views/requirement/List.vue
Normal file
240
fund-mobile/src/views/requirement/List.vue
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
<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 } 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 = () => {
|
||||||
|
pageNum.value++
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const onRefresh = () => {
|
||||||
|
pageNum.value = 1
|
||||||
|
finished.value = false
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSearch = () => {
|
||||||
|
pageNum.value = 1
|
||||||
|
finished.value = false
|
||||||
|
list.value = []
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.requirement-list {
|
||||||
|
background: #f5f5f5;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding-bottom: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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: 24px;
|
||||||
|
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>
|
||||||
Loading…
x
Reference in New Issue
Block a user