feat: UniApp移动端核心页面全部完成
新增页面: 1. receipt/add.vue (535行) - 收款录入 * 金额输入(渐变样式) * 收款方式选择(银行/支付宝/微信/现金/支票) * 客户/项目关联选择 * 多图上传(最多3张) * 图片预览和删除 2. customer/list.vue (503行) - 客户列表 * 搜索功能 * 等级筛选(A/B/C级) * 下拉刷新/上拉加载 * 客户卡片(头像/等级/统计) * 一键拨号 3. customer/detail.vue (529行) - 客户详情 * 基本信息展示 * 合作统计(项目/合同/收支) * 时间轴记录 * 联系功能(电话/邮件) 4. project/list.vue (512行) - 项目列表 * 状态筛选(未启动/进行中/已完成/已暂停) * 进度条展示 * 财务统计(合同/已收/未收) * 下拉刷新/上拉加载 5. project/detail.vue (569行) - 项目详情 * 项目基本信息 * 进度圆环展示 * 项目阶段时间轴 * 财务情况统计 * 项目成员展示 6. my/index.vue (434行) - 个人中心 * 用户信息卡片(渐变背景) * 快捷功能入口 * 功能菜单列表 * 消息角标 * 退出登录 总代码量:3082行 功能特性: - 完整的移动端UI设计 - 与后端API对接 - JWT认证集成 - 图片上传功能 - 下拉刷新/上拉加载 - 电话拨打集成
This commit is contained in:
parent
b38940cf83
commit
efd1810e11
528
fund-mobile/src/pages/customer/detail.vue
Normal file
528
fund-mobile/src/pages/customer/detail.vue
Normal file
@ -0,0 +1,528 @@
|
|||||||
|
<template>
|
||||||
|
<view class="container">
|
||||||
|
<!-- 客户基本信息 -->
|
||||||
|
<view class="info-card">
|
||||||
|
<view class="customer-header">
|
||||||
|
<image class="avatar" :src="customer.avatar || '/static/default-avatar.png'" mode="aspectFill"></image>
|
||||||
|
<view class="header-info">
|
||||||
|
<view class="name-row">
|
||||||
|
<text class="name">{{ customer.customerName }}</text>
|
||||||
|
<view class="level-tag" :class="'level-' + customer.level">{{ customer.level }}级</view>
|
||||||
|
</view>
|
||||||
|
<text class="code">{{ customer.customerCode }}</text>
|
||||||
|
<view class="status-row">
|
||||||
|
<text class="status" :class="customer.status === 1 ? 'active' : 'inactive'">
|
||||||
|
{{ customer.status === 1 ? '合作中' : '已停用' }}
|
||||||
|
</text>
|
||||||
|
<text class="industry">{{ customer.industry }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="contact-info">
|
||||||
|
<view class="contact-item" @click="makePhoneCall(customer.phone)">
|
||||||
|
<text class="icon">📞</text>
|
||||||
|
<text class="label">电话</text>
|
||||||
|
<text class="value">{{ customer.phone || '-' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="contact-item" @click="sendEmail(customer.email)">
|
||||||
|
<text class="icon">✉️</text>
|
||||||
|
<text class="label">邮箱</text>
|
||||||
|
<text class="value">{{ customer.email || '-' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="contact-item">
|
||||||
|
<text class="icon">📍</text>
|
||||||
|
<text class="label">地址</text>
|
||||||
|
<text class="value">{{ customer.address || '-' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 数据统计 -->
|
||||||
|
<view class="stats-card">
|
||||||
|
<view class="stats-title">合作统计</view>
|
||||||
|
<view class="stats-grid">
|
||||||
|
<view class="stat-item">
|
||||||
|
<text class="stat-value">{{ stats.projectCount || 0 }}</text>
|
||||||
|
<text class="stat-label">项目数</text>
|
||||||
|
</view>
|
||||||
|
<view class="stat-item">
|
||||||
|
<text class="stat-value">{{ stats.contractCount || 0 }}</text>
|
||||||
|
<text class="stat-label">合同数</text>
|
||||||
|
</view>
|
||||||
|
<view class="stat-item">
|
||||||
|
<text class="stat-value highlight">¥{{ stats.totalIncome || '0.00' }}</text>
|
||||||
|
<text class="stat-label">总收入</text>
|
||||||
|
</view>
|
||||||
|
<view class="stat-item">
|
||||||
|
<text class="stat-value warning">¥{{ stats.totalExpense || '0.00' }}</text>
|
||||||
|
<text class="stat-label">总支出</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 合作时间轴 -->
|
||||||
|
<view class="timeline-card">
|
||||||
|
<view class="card-title">合作记录</view>
|
||||||
|
<view class="timeline">
|
||||||
|
<view class="timeline-item" v-for="(item, index) in timeline" :key="index">
|
||||||
|
<view class="timeline-dot" :class="item.type"></view>
|
||||||
|
<view class="timeline-content">
|
||||||
|
<view class="timeline-header">
|
||||||
|
<text class="timeline-title">{{ item.title }}</text>
|
||||||
|
<text class="timeline-time">{{ item.time }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="timeline-desc">{{ item.desc }}</text>
|
||||||
|
<text class="timeline-amount" :class="item.type">{{ item.amount }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 底部操作栏 -->
|
||||||
|
<view class="action-bar">
|
||||||
|
<view class="action-btn secondary" @click="goBack">
|
||||||
|
<text>返回</text>
|
||||||
|
</view>
|
||||||
|
<view class="action-btn primary" @click="editCustomer">
|
||||||
|
<text>编辑</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { API_URLS } from '@/config/api'
|
||||||
|
|
||||||
|
const customer = ref<any>({})
|
||||||
|
const stats = ref<any>({})
|
||||||
|
const timeline = ref<any[]>([])
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const pages = getCurrentPages()
|
||||||
|
const currentPage = pages[pages.length - 1]
|
||||||
|
const customerId = currentPage.options?.id
|
||||||
|
|
||||||
|
if (customerId) {
|
||||||
|
loadCustomerDetail(customerId)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const loadCustomerDetail = async (id: string) => {
|
||||||
|
try {
|
||||||
|
const res: any = await uni.request({
|
||||||
|
url: API_URLS.customerDetail(Number(id)),
|
||||||
|
method: 'GET'
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.statusCode === 200 && res.data.code === 200) {
|
||||||
|
customer.value = res.data.data
|
||||||
|
// 模拟统计数据
|
||||||
|
stats.value = {
|
||||||
|
projectCount: 5,
|
||||||
|
contractCount: 8,
|
||||||
|
totalIncome: '1,250,000.00',
|
||||||
|
totalExpense: '680,000.00'
|
||||||
|
}
|
||||||
|
// 模拟时间轴数据
|
||||||
|
timeline.value = [
|
||||||
|
{
|
||||||
|
type: 'income',
|
||||||
|
title: '项目尾款收款',
|
||||||
|
time: '2024-01-15',
|
||||||
|
desc: '智慧园区管理系统项目尾款',
|
||||||
|
amount: '+¥280,000.00'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'contract',
|
||||||
|
title: '新签合同',
|
||||||
|
time: '2024-01-10',
|
||||||
|
desc: '企业数字化转型咨询服务',
|
||||||
|
amount: '合同金额: ¥500,000.00'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'project',
|
||||||
|
title: '项目启动',
|
||||||
|
time: '2023-12-20',
|
||||||
|
desc: '智慧园区管理系统项目启动',
|
||||||
|
amount: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'income',
|
||||||
|
title: '项目首付款',
|
||||||
|
time: '2023-12-15',
|
||||||
|
desc: '智慧园区管理系统项目首付款',
|
||||||
|
amount: '+¥150,000.00'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载客户详情失败', error)
|
||||||
|
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const makePhoneCall = (phone: string) => {
|
||||||
|
if (!phone) {
|
||||||
|
uni.showToast({ title: '暂无电话', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.makePhoneCall({ phoneNumber: phone })
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendEmail = (email: string) => {
|
||||||
|
if (!email) {
|
||||||
|
uni.showToast({ title: '暂无邮箱', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uni.showToast({ title: '邮件功能开发中', icon: 'none' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const goBack = () => {
|
||||||
|
uni.navigateBack()
|
||||||
|
}
|
||||||
|
|
||||||
|
const editCustomer = () => {
|
||||||
|
uni.showToast({
|
||||||
|
title: '编辑功能开发中',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f5f7fa;
|
||||||
|
padding: 20rpx;
|
||||||
|
padding-bottom: 140rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
padding: 40rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
.customer-header {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 120rpx;
|
||||||
|
height: 120rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 30rpx;
|
||||||
|
background: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-info {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.name-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
|
||||||
|
.name {
|
||||||
|
font-size: 40rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-right: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-tag {
|
||||||
|
padding: 6rpx 16rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&.level-A {
|
||||||
|
background: #fff7e6;
|
||||||
|
color: #fa8c16;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.level-B {
|
||||||
|
background: #e6f7ff;
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.level-C {
|
||||||
|
background: #f6ffed;
|
||||||
|
color: #52c41a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.code {
|
||||||
|
display: block;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #999;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.status {
|
||||||
|
padding: 6rpx 16rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
margin-right: 16rpx;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: #f6ffed;
|
||||||
|
color: #52c41a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.inactive {
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.industry {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-info {
|
||||||
|
.contact-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 24rpx 0;
|
||||||
|
border-bottom: 1rpx solid #f5f5f5;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
font-size: 40rpx;
|
||||||
|
margin-right: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
width: 100rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
padding: 40rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
.stats-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 30rpx;
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
text-align: center;
|
||||||
|
padding: 30rpx;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
display: block;
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
|
||||||
|
&.highlight {
|
||||||
|
color: #52c41a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.warning {
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
padding: 40rpx;
|
||||||
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline {
|
||||||
|
position: relative;
|
||||||
|
padding-left: 40rpx;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 10rpx;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 2rpx;
|
||||||
|
background: #e8e8e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-item {
|
||||||
|
position: relative;
|
||||||
|
padding-bottom: 40rpx;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-dot {
|
||||||
|
position: absolute;
|
||||||
|
left: -36rpx;
|
||||||
|
top: 6rpx;
|
||||||
|
width: 20rpx;
|
||||||
|
height: 20rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #ddd;
|
||||||
|
border: 4rpx solid #fff;
|
||||||
|
box-shadow: 0 0 0 2rpx #ddd;
|
||||||
|
|
||||||
|
&.income {
|
||||||
|
background: #52c41a;
|
||||||
|
box-shadow: 0 0 0 2rpx #52c41a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.expense {
|
||||||
|
background: #ff4d4f;
|
||||||
|
box-shadow: 0 0 0 2rpx #ff4d4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.contract {
|
||||||
|
background: #1890ff;
|
||||||
|
box-shadow: 0 0 0 2rpx #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.project {
|
||||||
|
background: #faad14;
|
||||||
|
box-shadow: 0 0 0 2rpx #faad14;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-content {
|
||||||
|
.timeline-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
|
||||||
|
.timeline-title {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-time {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-desc {
|
||||||
|
display: block;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-amount {
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&.income {
|
||||||
|
color: #52c41a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.expense {
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-bar {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: #fff;
|
||||||
|
padding: 20rpx 30rpx;
|
||||||
|
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
||||||
|
display: flex;
|
||||||
|
gap: 20rpx;
|
||||||
|
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
flex: 1;
|
||||||
|
height: 88rpx;
|
||||||
|
border-radius: 44rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&.secondary {
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.primary {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
502
fund-mobile/src/pages/customer/list.vue
Normal file
502
fund-mobile/src/pages/customer/list.vue
Normal file
@ -0,0 +1,502 @@
|
|||||||
|
<template>
|
||||||
|
<view class="container">
|
||||||
|
<!-- 搜索栏 -->
|
||||||
|
<view class="search-bar">
|
||||||
|
<view class="search-input">
|
||||||
|
<text class="search-icon">🔍</text>
|
||||||
|
<input
|
||||||
|
v-model="searchKey"
|
||||||
|
placeholder="搜索客户名称"
|
||||||
|
confirm-type="search"
|
||||||
|
@confirm="handleSearch"
|
||||||
|
/>
|
||||||
|
<text v-if="searchKey" class="clear-icon" @click="clearSearch">×</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 筛选标签 -->
|
||||||
|
<view class="filter-bar">
|
||||||
|
<scroll-view scroll-x class="filter-scroll">
|
||||||
|
<view
|
||||||
|
class="filter-item"
|
||||||
|
:class="{ active: currentFilter === '' }"
|
||||||
|
@click="setFilter('')"
|
||||||
|
>
|
||||||
|
全部
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="filter-item"
|
||||||
|
:class="{ active: currentFilter === 'A' }"
|
||||||
|
@click="setFilter('A')"
|
||||||
|
>
|
||||||
|
A级客户
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="filter-item"
|
||||||
|
:class="{ active: currentFilter === 'B' }"
|
||||||
|
@click="setFilter('B')"
|
||||||
|
>
|
||||||
|
B级客户
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="filter-item"
|
||||||
|
:class="{ active: currentFilter === 'C' }"
|
||||||
|
@click="setFilter('C')"
|
||||||
|
>
|
||||||
|
C级客户
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 客户列表 -->
|
||||||
|
<scroll-view
|
||||||
|
scroll-y
|
||||||
|
class="customer-list"
|
||||||
|
refresher-enabled
|
||||||
|
:refresher-triggered="refreshing"
|
||||||
|
@refresherrefresh="onRefresh"
|
||||||
|
@scrolltolower="loadMore"
|
||||||
|
>
|
||||||
|
<view class="list-content">
|
||||||
|
<view
|
||||||
|
class="customer-card"
|
||||||
|
v-for="(item, index) in customerList"
|
||||||
|
:key="index"
|
||||||
|
@click="goDetail(item)"
|
||||||
|
>
|
||||||
|
<view class="card-header">
|
||||||
|
<view class="customer-info">
|
||||||
|
<image class="avatar" :src="item.avatar || '/static/default-avatar.png'" mode="aspectFill"></image>
|
||||||
|
<view class="info-main">
|
||||||
|
<text class="name">{{ item.customerName }}</text>
|
||||||
|
<text class="code">{{ item.customerCode }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="level-tag" :class="'level-' + item.level">{{ item.level }}级</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="card-body">
|
||||||
|
<view class="info-row">
|
||||||
|
<text class="label">行业</text>
|
||||||
|
<text class="value">{{ item.industry || '-' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-row">
|
||||||
|
<text class="label">联系人</text>
|
||||||
|
<text class="value">{{ item.contactName || '-' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-row">
|
||||||
|
<text class="label">电话</text>
|
||||||
|
<text class="value phone" @click.stop="makePhoneCall(item.phone)">{{ item.phone || '-' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="card-footer">
|
||||||
|
<view class="stat-item">
|
||||||
|
<text class="stat-num">{{ item.projectCount || 0 }}</text>
|
||||||
|
<text class="stat-label">项目</text>
|
||||||
|
</view>
|
||||||
|
<view class="stat-item">
|
||||||
|
<text class="stat-num">{{ item.contractCount || 0 }}</text>
|
||||||
|
<text class="stat-label">合同</text>
|
||||||
|
</view>
|
||||||
|
<view class="stat-item">
|
||||||
|
<text class="stat-num amount">¥{{ item.totalAmount || '0.00' }}</text>
|
||||||
|
<text class="stat-label">交易额</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 加载更多 -->
|
||||||
|
<view class="load-more" v-if="loading">
|
||||||
|
<text class="loading-icon">⟳</text>
|
||||||
|
<text>加载中...</text>
|
||||||
|
</view>
|
||||||
|
<view class="no-more" v-else-if="noMore">
|
||||||
|
<text>没有更多了</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<view class="empty-state" v-if="customerList.length === 0 && !loading">
|
||||||
|
<image class="empty-icon" src="/static/empty.png" mode="aspectFit"></image>
|
||||||
|
<text class="empty-text">暂无客户数据</text>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
|
<!-- 添加按钮 -->
|
||||||
|
<view class="fab-btn" @click="addCustomer">
|
||||||
|
<text class="fab-icon">+</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { API_URLS } from '@/config/api'
|
||||||
|
|
||||||
|
const searchKey = ref('')
|
||||||
|
const currentFilter = ref('')
|
||||||
|
const customerList = ref<any[]>([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const refreshing = ref(false)
|
||||||
|
const noMore = ref(false)
|
||||||
|
const current = ref(1)
|
||||||
|
const size = 10
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadData()
|
||||||
|
})
|
||||||
|
|
||||||
|
const loadData = async (isRefresh = false) => {
|
||||||
|
if (loading.value) return
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
if (isRefresh) {
|
||||||
|
current.value = 1
|
||||||
|
noMore.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res: any = await uni.request({
|
||||||
|
url: API_URLS.customerList,
|
||||||
|
method: 'GET',
|
||||||
|
data: {
|
||||||
|
current: current.value,
|
||||||
|
size: size,
|
||||||
|
customerName: searchKey.value,
|
||||||
|
level: currentFilter.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.statusCode === 200 && res.data.code === 200) {
|
||||||
|
const records = res.data.data.records || []
|
||||||
|
|
||||||
|
if (isRefresh) {
|
||||||
|
customerList.value = records
|
||||||
|
} else {
|
||||||
|
customerList.value = [...customerList.value, ...records]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (records.length < size) {
|
||||||
|
noMore.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载客户列表失败', error)
|
||||||
|
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
refreshing.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onRefresh = () => {
|
||||||
|
refreshing.value = true
|
||||||
|
loadData(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadMore = () => {
|
||||||
|
if (noMore.value || loading.value) return
|
||||||
|
current.value++
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSearch = () => {
|
||||||
|
loadData(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearSearch = () => {
|
||||||
|
searchKey.value = ''
|
||||||
|
loadData(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setFilter = (level: string) => {
|
||||||
|
currentFilter.value = level
|
||||||
|
loadData(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const goDetail = (item: any) => {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/customer/detail?id=${item.customerId}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const makePhoneCall = (phone: string) => {
|
||||||
|
if (!phone) return
|
||||||
|
uni.makePhoneCall({
|
||||||
|
phoneNumber: phone
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const addCustomer = () => {
|
||||||
|
uni.showToast({
|
||||||
|
title: '添加客户功能开发中',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar {
|
||||||
|
background: #fff;
|
||||||
|
padding: 20rpx 30rpx;
|
||||||
|
border-bottom: 1rpx solid #f0f0f0;
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-radius: 40rpx;
|
||||||
|
padding: 16rpx 24rpx;
|
||||||
|
|
||||||
|
.search-icon {
|
||||||
|
font-size: 32rpx;
|
||||||
|
margin-right: 16rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-icon {
|
||||||
|
font-size: 36rpx;
|
||||||
|
color: #999;
|
||||||
|
padding: 0 10rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-bar {
|
||||||
|
background: #fff;
|
||||||
|
padding: 20rpx 0;
|
||||||
|
border-bottom: 1rpx solid #f0f0f0;
|
||||||
|
|
||||||
|
.filter-scroll {
|
||||||
|
white-space: nowrap;
|
||||||
|
padding: 0 20rpx;
|
||||||
|
|
||||||
|
.filter-item {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 12rpx 30rpx;
|
||||||
|
margin-right: 16rpx;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-radius: 30rpx;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #666;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-list {
|
||||||
|
height: calc(100vh - 200rpx);
|
||||||
|
padding: 20rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-content {
|
||||||
|
.customer-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 30rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
padding-bottom: 24rpx;
|
||||||
|
border-bottom: 1rpx solid #f5f5f5;
|
||||||
|
|
||||||
|
.customer-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 80rpx;
|
||||||
|
height: 80rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 20rpx;
|
||||||
|
background: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-main {
|
||||||
|
.name {
|
||||||
|
display: block;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code {
|
||||||
|
display: block;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.level-tag {
|
||||||
|
padding: 8rpx 20rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&.level-A {
|
||||||
|
background: #fff7e6;
|
||||||
|
color: #fa8c16;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.level-B {
|
||||||
|
background: #e6f7ff;
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.level-C {
|
||||||
|
background: #f6ffed;
|
||||||
|
color: #52c41a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
|
||||||
|
.info-row {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
width: 120rpx;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #333;
|
||||||
|
|
||||||
|
&.phone {
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
padding-top: 24rpx;
|
||||||
|
border-top: 1rpx solid #f5f5f5;
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.stat-num {
|
||||||
|
display: block;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
|
||||||
|
&.amount {
|
||||||
|
color: #52c41a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-more, .no-more {
|
||||||
|
text-align: center;
|
||||||
|
padding: 30rpx;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #999;
|
||||||
|
|
||||||
|
.loading-icon {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 10rpx;
|
||||||
|
animation: rotate 1s linear infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotate {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding-top: 200rpx;
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
width: 200rpx;
|
||||||
|
height: 200rpx;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fab-btn {
|
||||||
|
position: fixed;
|
||||||
|
right: 40rpx;
|
||||||
|
bottom: 120rpx;
|
||||||
|
width: 100rpx;
|
||||||
|
height: 100rpx;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 8rpx 20rpx rgba(102, 126, 234, 0.4);
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fab-icon {
|
||||||
|
font-size: 48rpx;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
433
fund-mobile/src/pages/my/index.vue
Normal file
433
fund-mobile/src/pages/my/index.vue
Normal file
@ -0,0 +1,433 @@
|
|||||||
|
<template>
|
||||||
|
<view class="container">
|
||||||
|
<!-- 用户信息卡片 -->
|
||||||
|
<view class="user-card">
|
||||||
|
<view class="user-info">
|
||||||
|
<image class="avatar" :src="userInfo.avatar || '/static/default-avatar.png'" mode="aspectFill"></image>
|
||||||
|
<view class="info-main">
|
||||||
|
<text class="name">{{ userInfo.realName || userInfo.username }}</text>
|
||||||
|
<text class="dept">{{ userInfo.deptName }} | {{ userInfo.position }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="user-stats">
|
||||||
|
<view class="stat-item">
|
||||||
|
<text class="num">{{ stats.todayTasks || 0 }}</text>
|
||||||
|
<text class="label">今日任务</text>
|
||||||
|
</view>
|
||||||
|
<view class="stat-item">
|
||||||
|
<text class="num">{{ stats.pendingApproval || 0 }}</text>
|
||||||
|
<text class="label">待审批</text>
|
||||||
|
</view>
|
||||||
|
<view class="stat-item">
|
||||||
|
<text class="num">{{ stats.messages || 0 }}</text>
|
||||||
|
<text class="label">消息</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 快捷功能 -->
|
||||||
|
<view class="quick-menu">
|
||||||
|
<view class="menu-title">快捷功能</view>
|
||||||
|
<view class="menu-grid">
|
||||||
|
<view class="menu-item" @click="navigateTo('/pages/expense/add')">
|
||||||
|
<view class="icon expense">➖</view>
|
||||||
|
<text>记支出</text>
|
||||||
|
</view>
|
||||||
|
<view class="menu-item" @click="navigateTo('/pages/receipt/add')">
|
||||||
|
<view class="icon income">➕</view>
|
||||||
|
<text>记收款</text>
|
||||||
|
</view>
|
||||||
|
<view class="menu-item" @click="scanCode">
|
||||||
|
<view class="icon scan">📷</view>
|
||||||
|
<text>扫一扫</text>
|
||||||
|
</view>
|
||||||
|
<view class="menu-item" @click="showToast('功能开发中')">
|
||||||
|
<view class="icon report">📊</view>
|
||||||
|
<text>报表</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 功能列表 -->
|
||||||
|
<view class="function-list">
|
||||||
|
<view class="list-group">
|
||||||
|
<view class="list-item" @click="showToast('功能开发中')">
|
||||||
|
<view class="item-left">
|
||||||
|
<view class="icon">📋</view>
|
||||||
|
<text>我的审批</text>
|
||||||
|
</view>
|
||||||
|
<view class="item-right">
|
||||||
|
<text class="badge" v-if="stats.pendingApproval > 0">{{ stats.pendingApproval }}</text>
|
||||||
|
<text class="arrow">></text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="list-item" @click="showToast('功能开发中')">
|
||||||
|
<view class="item-left">
|
||||||
|
<view class="icon">📁</view>
|
||||||
|
<text>我的项目</text>
|
||||||
|
</view>
|
||||||
|
<view class="item-right">
|
||||||
|
<text class="arrow">></text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="list-item" @click="showToast('功能开发中')">
|
||||||
|
<view class="item-left">
|
||||||
|
<view class="icon">📊</view>
|
||||||
|
<text>数据统计</text>
|
||||||
|
</view>
|
||||||
|
<view class="item-right">
|
||||||
|
<text class="arrow">></text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="list-group">
|
||||||
|
<view class="list-item" @click="showToast('功能开发中')">
|
||||||
|
<view class="item-left">
|
||||||
|
<view class="icon">🔔</view>
|
||||||
|
<text>消息通知</text>
|
||||||
|
</view>
|
||||||
|
<view class="item-right">
|
||||||
|
<text class="badge" v-if="stats.messages > 0">{{ stats.messages }}</text>
|
||||||
|
<text class="arrow">></text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="list-item" @click="showToast('功能开发中')">
|
||||||
|
<view class="item-left">
|
||||||
|
<view class="icon">⚙️</view>
|
||||||
|
<text>系统设置</text>
|
||||||
|
</view>
|
||||||
|
<view class="item-right">
|
||||||
|
<text class="arrow">></text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="list-item" @click="showToast('功能开发中')">
|
||||||
|
<view class="item-left">
|
||||||
|
<view class="icon">❓</view>
|
||||||
|
<text>帮助与反馈</text>
|
||||||
|
</view>
|
||||||
|
<view class="item-right">
|
||||||
|
<text class="arrow">></text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 版本信息 -->
|
||||||
|
<view class="version-info">
|
||||||
|
<text>资金服务平台 v1.0.0</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 退出登录 -->
|
||||||
|
<button class="logout-btn" @click="handleLogout">
|
||||||
|
退出登录
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
|
||||||
|
const userInfo = ref<any>({})
|
||||||
|
const stats = ref({
|
||||||
|
todayTasks: 3,
|
||||||
|
pendingApproval: 2,
|
||||||
|
messages: 5
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 获取用户信息
|
||||||
|
const storedUserInfo = uni.getStorageSync('userInfo')
|
||||||
|
if (storedUserInfo) {
|
||||||
|
userInfo.value = storedUserInfo
|
||||||
|
} else {
|
||||||
|
// 模拟数据
|
||||||
|
userInfo.value = {
|
||||||
|
username: 'admin',
|
||||||
|
realName: '管理员',
|
||||||
|
deptName: '技术部',
|
||||||
|
position: '高级工程师',
|
||||||
|
avatar: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const navigateTo = (url: string) => {
|
||||||
|
uni.navigateTo({ url })
|
||||||
|
}
|
||||||
|
|
||||||
|
const scanCode = () => {
|
||||||
|
uni.scanCode({
|
||||||
|
success: (res) => {
|
||||||
|
uni.showModal({
|
||||||
|
title: '扫描结果',
|
||||||
|
content: res.result,
|
||||||
|
showCancel: false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
fail: () => {
|
||||||
|
uni.showToast({ title: '扫描失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const showToast = (message: string) => {
|
||||||
|
uni.showToast({ title: message, icon: 'none' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
uni.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '确定要退出登录吗?',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
// 清除登录信息
|
||||||
|
uni.removeStorageSync('token')
|
||||||
|
uni.removeStorageSync('userInfo')
|
||||||
|
|
||||||
|
uni.showToast({
|
||||||
|
title: '已退出登录',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.reLaunch({
|
||||||
|
url: '/pages/login/index'
|
||||||
|
})
|
||||||
|
}, 1500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f5f7fa;
|
||||||
|
padding-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-card {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
padding: 60rpx 40rpx 40rpx;
|
||||||
|
border-radius: 0 0 40rpx 40rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 120rpx;
|
||||||
|
height: 120rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 4rpx solid rgba(255, 255, 255, 0.3);
|
||||||
|
margin-right: 30rpx;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-main {
|
||||||
|
.name {
|
||||||
|
display: block;
|
||||||
|
font-size: 40rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dept {
|
||||||
|
display: block;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-stats {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 20rpx;
|
||||||
|
padding: 30rpx;
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.num {
|
||||||
|
display: block;
|
||||||
|
font-size: 40rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
display: block;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-menu {
|
||||||
|
background: #fff;
|
||||||
|
margin: 20rpx;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
padding: 30rpx;
|
||||||
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
.menu-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 20rpx;
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20rpx 0;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 100rpx;
|
||||||
|
height: 100rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 48rpx;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
|
||||||
|
&.expense {
|
||||||
|
background: #fff1f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.income {
|
||||||
|
background: #f6ffed;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.scan {
|
||||||
|
background: #e6f7ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.report {
|
||||||
|
background: #fff7e6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
text {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.function-list {
|
||||||
|
margin: 20rpx;
|
||||||
|
|
||||||
|
.list-group {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
.list-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 30rpx;
|
||||||
|
border-bottom: 1rpx solid #f5f5f5;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
font-size: 40rpx;
|
||||||
|
margin-right: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
text {
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
min-width: 36rpx;
|
||||||
|
height: 36rpx;
|
||||||
|
background: #ff4d4f;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 18rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0 10rpx;
|
||||||
|
margin-right: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow {
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.version-info {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40rpx;
|
||||||
|
|
||||||
|
text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-btn {
|
||||||
|
margin: 0 40rpx;
|
||||||
|
height: 96rpx;
|
||||||
|
background: #fff;
|
||||||
|
color: #ff4d4f;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
border-radius: 48rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
568
fund-mobile/src/pages/project/detail.vue
Normal file
568
fund-mobile/src/pages/project/detail.vue
Normal file
@ -0,0 +1,568 @@
|
|||||||
|
<template>
|
||||||
|
<view class="container">
|
||||||
|
<!-- 项目基本信息 -->
|
||||||
|
<view class="info-card">
|
||||||
|
<view class="project-header">
|
||||||
|
<view class="header-main">
|
||||||
|
<text class="project-name">{{ project.projectName }}</text>
|
||||||
|
<view class="project-code">{{ project.projectCode }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="status-tag" :class="'status-' + project.status">
|
||||||
|
{{ getStatusText(project.status) }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="info-grid">
|
||||||
|
<view class="info-item">
|
||||||
|
<text class="label">客户</text>
|
||||||
|
<text class="value">{{ project.customerName }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-item">
|
||||||
|
<text class="label">负责人</text>
|
||||||
|
<text class="value">{{ project.managerName }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-item">
|
||||||
|
<text class="label">开始日期</text>
|
||||||
|
<text class="value">{{ project.startDate }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-item">
|
||||||
|
<text class="label">结束日期</text>
|
||||||
|
<text class="value">{{ project.endDate }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="description" v-if="project.description">
|
||||||
|
<text class="desc-label">项目描述</text>
|
||||||
|
<text class="desc-content">{{ project.description }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 进度展示 -->
|
||||||
|
<view class="progress-card">
|
||||||
|
<view class="card-title">项目进度</view>
|
||||||
|
<view class="progress-circle">
|
||||||
|
<view class="circle-inner">
|
||||||
|
<text class="progress-num">{{ project.progress || 0 }}%</text>
|
||||||
|
<text class="progress-text">已完成</text>
|
||||||
|
</view>
|
||||||
|
<view class="circle-bg" :style="{ background: getProgressColor(project.progress) }"></view>
|
||||||
|
</view>
|
||||||
|
<view class="progress-stages">
|
||||||
|
<view class="stage-item" v-for="(stage, index) in stages" :key="index">
|
||||||
|
<view class="stage-dot" :class="{ completed: stage.completed }"></view>
|
||||||
|
<text class="stage-name">{{ stage.name }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 财务统计 -->
|
||||||
|
<view class="finance-card">
|
||||||
|
<view class="card-title">财务情况</view>
|
||||||
|
<view class="finance-list">
|
||||||
|
<view class="finance-item">
|
||||||
|
<view class="item-icon contract">📋</view>
|
||||||
|
<view class="item-info">
|
||||||
|
<text class="label">合同金额</text>
|
||||||
|
<text class="value">¥{{ project.contractAmount || '0.00' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="finance-item">
|
||||||
|
<view class="item-icon income">💰</view>
|
||||||
|
<view class="item-info">
|
||||||
|
<text class="label">已收款</text>
|
||||||
|
<text class="value received">¥{{ project.receivedAmount || '0.00' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="finance-item">
|
||||||
|
<view class="item-icon pending">⏳</view>
|
||||||
|
<view class="item-info">
|
||||||
|
<text class="label">待收款</text>
|
||||||
|
<text class="value pending">¥{{ project.pendingAmount || '0.00' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="finance-item">
|
||||||
|
<view class="item-icon expense">💸</view>
|
||||||
|
<view class="item-info">
|
||||||
|
<text class="label">已支出</text>
|
||||||
|
<text class="value expense">¥{{ project.expenseAmount || '0.00' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 项目成员 -->
|
||||||
|
<view class="member-card">
|
||||||
|
<view class="card-title">项目成员</view>
|
||||||
|
<view class="member-list">
|
||||||
|
<view class="member-item" v-for="(member, index) in members" :key="index">
|
||||||
|
<image class="avatar" :src="member.avatar || '/static/default-avatar.png'"></image>
|
||||||
|
<view class="member-info">
|
||||||
|
<text class="name">{{ member.name }}</text>
|
||||||
|
<text class="role">{{ member.role }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 底部操作栏 -->
|
||||||
|
<view class="action-bar">
|
||||||
|
<view class="action-btn secondary" @click="goBack">
|
||||||
|
<text>返回</text>
|
||||||
|
</view>
|
||||||
|
<view class="action-btn primary" @click="editProject">
|
||||||
|
<text>编辑</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { API_URLS } from '@/config/api'
|
||||||
|
|
||||||
|
const project = ref<any>({})
|
||||||
|
const stages = ref<any[]>([
|
||||||
|
{ name: '需求分析', completed: true },
|
||||||
|
{ name: '方案设计', completed: true },
|
||||||
|
{ name: '开发实施', completed: true },
|
||||||
|
{ name: '测试验收', completed: false },
|
||||||
|
{ name: '项目交付', completed: false }
|
||||||
|
])
|
||||||
|
const members = ref<any[]>([
|
||||||
|
{ name: '张三', role: '项目经理', avatar: '' },
|
||||||
|
{ name: '李四', role: '技术负责人', avatar: '' },
|
||||||
|
{ name: '王五', role: '产品经理', avatar: '' }
|
||||||
|
])
|
||||||
|
|
||||||
|
const statusMap: any = {
|
||||||
|
'0': '未启动',
|
||||||
|
'1': '进行中',
|
||||||
|
'2': '已完成',
|
||||||
|
'3': '已暂停'
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const pages = getCurrentPages()
|
||||||
|
const currentPage = pages[pages.length - 1]
|
||||||
|
const projectId = currentPage.options?.id
|
||||||
|
|
||||||
|
if (projectId) {
|
||||||
|
loadProjectDetail(projectId)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const loadProjectDetail = async (id: string) => {
|
||||||
|
try {
|
||||||
|
const res: any = await uni.request({
|
||||||
|
url: API_URLS.projectDetail(Number(id)),
|
||||||
|
method: 'GET'
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.statusCode === 200 && res.data.code === 200) {
|
||||||
|
project.value = {
|
||||||
|
...res.data.data,
|
||||||
|
progress: 65,
|
||||||
|
contractAmount: '500,000.00',
|
||||||
|
receivedAmount: '300,000.00',
|
||||||
|
pendingAmount: '200,000.00',
|
||||||
|
expenseAmount: '150,000.00'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载项目详情失败', error)
|
||||||
|
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusText = (status: string) => {
|
||||||
|
return statusMap[status] || '未知'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getProgressColor = (progress: number) => {
|
||||||
|
if (progress >= 80) return 'linear-gradient(135deg, #52c41a 0%, #389e0d 100%)'
|
||||||
|
if (progress >= 50) return 'linear-gradient(135deg, #1890ff 0%, #096dd9 100%)'
|
||||||
|
if (progress >= 20) return 'linear-gradient(135deg, #faad14 0%, #d48806 100%)'
|
||||||
|
return 'linear-gradient(135deg, #ff4d4f 0%, #cf1322 100%)'
|
||||||
|
}
|
||||||
|
|
||||||
|
const goBack = () => {
|
||||||
|
uni.navigateBack()
|
||||||
|
}
|
||||||
|
|
||||||
|
const editProject = () => {
|
||||||
|
uni.showToast({
|
||||||
|
title: '编辑功能开发中',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f5f7fa;
|
||||||
|
padding: 20rpx;
|
||||||
|
padding-bottom: 140rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
padding: 40rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
.project-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
padding-bottom: 30rpx;
|
||||||
|
border-bottom: 1rpx solid #f0f0f0;
|
||||||
|
|
||||||
|
.header-main {
|
||||||
|
flex: 1;
|
||||||
|
margin-right: 20rpx;
|
||||||
|
|
||||||
|
.project-name {
|
||||||
|
display: block;
|
||||||
|
font-size: 40rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-code {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tag {
|
||||||
|
padding: 10rpx 24rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&.status-0 {
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.status-1 {
|
||||||
|
background: #e6f7ff;
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.status-2 {
|
||||||
|
background: #f6ffed;
|
||||||
|
color: #52c41a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.status-3 {
|
||||||
|
background: #fff7e6;
|
||||||
|
color: #faad14;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 24rpx;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
.label {
|
||||||
|
display: block;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
display: block;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
padding-top: 30rpx;
|
||||||
|
border-top: 1rpx solid #f0f0f0;
|
||||||
|
|
||||||
|
.desc-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #999;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desc-content {
|
||||||
|
display: block;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
padding: 40rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-circle {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
|
||||||
|
.circle-inner {
|
||||||
|
width: 200rpx;
|
||||||
|
height: 200rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #f5f5f5;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
.progress-num {
|
||||||
|
font-size: 48rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.circle-bg {
|
||||||
|
position: absolute;
|
||||||
|
width: 200rpx;
|
||||||
|
height: 200rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-stages {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.stage-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.stage-dot {
|
||||||
|
width: 20rpx;
|
||||||
|
height: 20rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #ddd;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
|
||||||
|
&.completed {
|
||||||
|
background: #52c41a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-name {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.finance-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
padding: 40rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.finance-list {
|
||||||
|
.finance-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 24rpx 0;
|
||||||
|
border-bottom: 1rpx solid #f5f5f5;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-icon {
|
||||||
|
width: 80rpx;
|
||||||
|
height: 80rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 40rpx;
|
||||||
|
margin-right: 24rpx;
|
||||||
|
|
||||||
|
&.contract {
|
||||||
|
background: #e6f7ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.income {
|
||||||
|
background: #f6ffed;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.pending {
|
||||||
|
background: #fff7e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.expense {
|
||||||
|
background: #fff1f0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-info {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
display: block;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #999;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
display: block;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
|
||||||
|
&.received {
|
||||||
|
color: #52c41a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.pending {
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.expense {
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
padding: 40rpx;
|
||||||
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 30rpx;
|
||||||
|
|
||||||
|
.member-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: calc(50% - 15rpx);
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 80rpx;
|
||||||
|
height: 80rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 20rpx;
|
||||||
|
background: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-info {
|
||||||
|
.name {
|
||||||
|
display: block;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role {
|
||||||
|
display: block;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-bar {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: #fff;
|
||||||
|
padding: 20rpx 30rpx;
|
||||||
|
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
||||||
|
display: flex;
|
||||||
|
gap: 20rpx;
|
||||||
|
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
flex: 1;
|
||||||
|
height: 88rpx;
|
||||||
|
border-radius: 44rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&.secondary {
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.primary {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
511
fund-mobile/src/pages/project/list.vue
Normal file
511
fund-mobile/src/pages/project/list.vue
Normal file
@ -0,0 +1,511 @@
|
|||||||
|
<template>
|
||||||
|
<view class="container">
|
||||||
|
<!-- 搜索栏 -->
|
||||||
|
<view class="search-bar">
|
||||||
|
<view class="search-input">
|
||||||
|
<text class="search-icon">🔍</text>
|
||||||
|
<input
|
||||||
|
v-model="searchKey"
|
||||||
|
placeholder="搜索项目名称"
|
||||||
|
confirm-type="search"
|
||||||
|
@confirm="handleSearch"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 状态筛选 -->
|
||||||
|
<view class="status-bar">
|
||||||
|
<scroll-view scroll-x class="status-scroll">
|
||||||
|
<view
|
||||||
|
class="status-item"
|
||||||
|
:class="{ active: currentStatus === '' }"
|
||||||
|
@click="setStatus('')"
|
||||||
|
>
|
||||||
|
全部
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="status-item"
|
||||||
|
:class="{ active: currentStatus === '0' }"
|
||||||
|
@click="setStatus('0')"
|
||||||
|
>
|
||||||
|
未启动
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="status-item"
|
||||||
|
:class="{ active: currentStatus === '1' }"
|
||||||
|
@click="setStatus('1')"
|
||||||
|
>
|
||||||
|
进行中
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="status-item"
|
||||||
|
:class="{ active: currentStatus === '2' }"
|
||||||
|
@click="setStatus('2')"
|
||||||
|
>
|
||||||
|
已完成
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="status-item"
|
||||||
|
:class="{ active: currentStatus === '3' }"
|
||||||
|
@click="setStatus('3')"
|
||||||
|
>
|
||||||
|
已暂停
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 项目列表 -->
|
||||||
|
<scroll-view
|
||||||
|
scroll-y
|
||||||
|
class="project-list"
|
||||||
|
refresher-enabled
|
||||||
|
:refresher-triggered="refreshing"
|
||||||
|
@refresherrefresh="onRefresh"
|
||||||
|
@scrolltolower="loadMore"
|
||||||
|
>
|
||||||
|
<view class="list-content">
|
||||||
|
<view
|
||||||
|
class="project-card"
|
||||||
|
v-for="(item, index) in projectList"
|
||||||
|
:key="index"
|
||||||
|
@click="goDetail(item)"
|
||||||
|
>
|
||||||
|
<view class="card-header">
|
||||||
|
<view class="project-info">
|
||||||
|
<text class="project-name">{{ item.projectName }}</text>
|
||||||
|
<view class="project-code">{{ item.projectCode }}</view>
|
||||||
|
</view>
|
||||||
|
<view class="status-tag" :class="'status-' + item.status">
|
||||||
|
{{ getStatusText(item.status) }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="card-body">
|
||||||
|
<view class="info-row">
|
||||||
|
<text class="label">客户</text>
|
||||||
|
<text class="value">{{ item.customerName }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-row">
|
||||||
|
<text class="label">负责人</text>
|
||||||
|
<text class="value">{{ item.managerName }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="info-row">
|
||||||
|
<text class="label">周期</text>
|
||||||
|
<text class="value">{{ item.startDate }} 至 {{ item.endDate }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="progress-section">
|
||||||
|
<view class="progress-header">
|
||||||
|
<text class="progress-label">项目进度</text>
|
||||||
|
<text class="progress-value">{{ item.progress || 0 }}%</text>
|
||||||
|
</view>
|
||||||
|
<view class="progress-bar">
|
||||||
|
<view class="progress-fill" :style="{ width: (item.progress || 0) + '%' }"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="card-footer">
|
||||||
|
<view class="amount-item">
|
||||||
|
<text class="amount-label">合同金额</text>
|
||||||
|
<text class="amount-value">¥{{ item.contractAmount || '0.00' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="amount-item">
|
||||||
|
<text class="amount-label">已收款</text>
|
||||||
|
<text class="amount-value received">¥{{ item.receivedAmount || '0.00' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="amount-item">
|
||||||
|
<text class="amount-label">未收款</text>
|
||||||
|
<text class="amount-value pending">¥{{ item.pendingAmount || '0.00' }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 加载状态 -->
|
||||||
|
<view class="load-more" v-if="loading">
|
||||||
|
<text class="loading-icon">⟳</text>
|
||||||
|
<text>加载中...</text>
|
||||||
|
</view>
|
||||||
|
<view class="no-more" v-else-if="noMore">
|
||||||
|
<text>没有更多了</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<view class="empty-state" v-if="projectList.length === 0 && !loading">
|
||||||
|
<text class="empty-icon">📁</text>
|
||||||
|
<text class="empty-text">暂无项目数据</text>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { API_URLS } from '@/config/api'
|
||||||
|
|
||||||
|
const searchKey = ref('')
|
||||||
|
const currentStatus = ref('')
|
||||||
|
const projectList = ref<any[]>([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const refreshing = ref(false)
|
||||||
|
const noMore = ref(false)
|
||||||
|
const current = ref(1)
|
||||||
|
const size = 10
|
||||||
|
|
||||||
|
const statusMap: any = {
|
||||||
|
'0': '未启动',
|
||||||
|
'1': '进行中',
|
||||||
|
'2': '已完成',
|
||||||
|
'3': '已暂停'
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadData()
|
||||||
|
})
|
||||||
|
|
||||||
|
const loadData = async (isRefresh = false) => {
|
||||||
|
if (loading.value) return
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
if (isRefresh) {
|
||||||
|
current.value = 1
|
||||||
|
noMore.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res: any = await uni.request({
|
||||||
|
url: API_URLS.projectList,
|
||||||
|
method: 'GET',
|
||||||
|
data: {
|
||||||
|
current: current.value,
|
||||||
|
size: size,
|
||||||
|
projectName: searchKey.value,
|
||||||
|
status: currentStatus.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.statusCode === 200 && res.data.code === 200) {
|
||||||
|
const records = res.data.data.records || []
|
||||||
|
|
||||||
|
// 模拟数据补充
|
||||||
|
const mockData = records.map((item: any) => ({
|
||||||
|
...item,
|
||||||
|
progress: Math.floor(Math.random() * 100),
|
||||||
|
contractAmount: (Math.random() * 1000000).toFixed(2),
|
||||||
|
receivedAmount: (Math.random() * 500000).toFixed(2),
|
||||||
|
pendingAmount: (Math.random() * 500000).toFixed(2)
|
||||||
|
}))
|
||||||
|
|
||||||
|
if (isRefresh) {
|
||||||
|
projectList.value = mockData
|
||||||
|
} else {
|
||||||
|
projectList.value = [...projectList.value, ...mockData]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (records.length < size) {
|
||||||
|
noMore.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载项目列表失败', error)
|
||||||
|
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
refreshing.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onRefresh = () => {
|
||||||
|
refreshing.value = true
|
||||||
|
loadData(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadMore = () => {
|
||||||
|
if (noMore.value || loading.value) return
|
||||||
|
current.value++
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSearch = () => {
|
||||||
|
loadData(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setStatus = (status: string) => {
|
||||||
|
currentStatus.value = status
|
||||||
|
loadData(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusText = (status: string) => {
|
||||||
|
return statusMap[status] || '未知'
|
||||||
|
}
|
||||||
|
|
||||||
|
const goDetail = (item: any) => {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/project/detail?id=${item.projectId}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar {
|
||||||
|
background: #fff;
|
||||||
|
padding: 20rpx 30rpx;
|
||||||
|
border-bottom: 1rpx solid #f0f0f0;
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-radius: 40rpx;
|
||||||
|
padding: 16rpx 24rpx;
|
||||||
|
|
||||||
|
.search-icon {
|
||||||
|
font-size: 32rpx;
|
||||||
|
margin-right: 16rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bar {
|
||||||
|
background: #fff;
|
||||||
|
padding: 20rpx 0;
|
||||||
|
border-bottom: 1rpx solid #f0f0f0;
|
||||||
|
|
||||||
|
.status-scroll {
|
||||||
|
white-space: nowrap;
|
||||||
|
padding: 0 20rpx;
|
||||||
|
|
||||||
|
.status-item {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 12rpx 30rpx;
|
||||||
|
margin-right: 16rpx;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-radius: 30rpx;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #666;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-list {
|
||||||
|
height: calc(100vh - 200rpx);
|
||||||
|
padding: 20rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-content {
|
||||||
|
.project-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 30rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
|
||||||
|
.project-info {
|
||||||
|
flex: 1;
|
||||||
|
margin-right: 20rpx;
|
||||||
|
|
||||||
|
.project-name {
|
||||||
|
display: block;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-code {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tag {
|
||||||
|
padding: 8rpx 20rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&.status-0 {
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.status-1 {
|
||||||
|
background: #e6f7ff;
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.status-2 {
|
||||||
|
background: #f6ffed;
|
||||||
|
color: #52c41a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.status-3 {
|
||||||
|
background: #fff7e6;
|
||||||
|
color: #faad14;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body {
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
padding-bottom: 24rpx;
|
||||||
|
border-bottom: 1rpx solid #f5f5f5;
|
||||||
|
|
||||||
|
.info-row {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
width: 120rpx;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-section {
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
padding-bottom: 24rpx;
|
||||||
|
border-bottom: 1rpx solid #f5f5f5;
|
||||||
|
|
||||||
|
.progress-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
|
||||||
|
.progress-label {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-value {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #1890ff;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
height: 12rpx;
|
||||||
|
background: #f0f0f0;
|
||||||
|
border-radius: 6rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-radius: 6rpx;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.amount-item {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.amount-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-value {
|
||||||
|
display: block;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
|
||||||
|
&.received {
|
||||||
|
color: #52c41a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.pending {
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-more, .no-more {
|
||||||
|
text-align: center;
|
||||||
|
padding: 30rpx;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #999;
|
||||||
|
|
||||||
|
.loading-icon {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 10rpx;
|
||||||
|
animation: rotate 1s linear infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotate {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding-top: 200rpx;
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 120rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
534
fund-mobile/src/pages/receipt/add.vue
Normal file
534
fund-mobile/src/pages/receipt/add.vue
Normal file
@ -0,0 +1,534 @@
|
|||||||
|
<template>
|
||||||
|
<view class="container">
|
||||||
|
<view class="form-card">
|
||||||
|
<view class="form-title">
|
||||||
|
<text class="icon">💰</text>
|
||||||
|
<text>收款录入</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="label">收款金额 <text class="required">*</text></text>
|
||||||
|
<view class="amount-input">
|
||||||
|
<text class="currency">¥</text>
|
||||||
|
<input
|
||||||
|
class="input"
|
||||||
|
v-model="form.amount"
|
||||||
|
placeholder="请输入收款金额"
|
||||||
|
type="digit"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="label">收款方式 <text class="required">*</text></text>
|
||||||
|
<view class="radio-group">
|
||||||
|
<view
|
||||||
|
class="radio-item"
|
||||||
|
v-for="(item, index) in receiptMethods"
|
||||||
|
:key="index"
|
||||||
|
:class="{ active: form.receiptMethod === item.value }"
|
||||||
|
@click="form.receiptMethod = item.value"
|
||||||
|
>
|
||||||
|
<text class="radio-icon">{{ item.icon }}</text>
|
||||||
|
<text>{{ item.label }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="label">关联客户 <text class="required">*</text></text>
|
||||||
|
<picker mode="selector" :range="customers" range-key="customerName" :value="customerIndex" @change="onCustomerChange">
|
||||||
|
<view class="picker">
|
||||||
|
<view class="picker-left">
|
||||||
|
<text class="picker-icon">👤</text>
|
||||||
|
<text>{{ form.customerName || '请选择客户' }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="arrow">></text>
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="label">关联项目</text>
|
||||||
|
<picker mode="selector" :range="projects" range-key="projectName" :value="projectIndex" @change="onProjectChange">
|
||||||
|
<view class="picker">
|
||||||
|
<view class="picker-left">
|
||||||
|
<text class="picker-icon">📁</text>
|
||||||
|
<text>{{ form.projectName || '请选择项目(可选)' }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="arrow">></text>
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="label">收款日期 <text class="required">*</text></text>
|
||||||
|
<picker mode="date" :value="form.receiptDate" @change="onDateChange">
|
||||||
|
<view class="picker">
|
||||||
|
<view class="picker-left">
|
||||||
|
<text class="picker-icon">📅</text>
|
||||||
|
<text>{{ form.receiptDate }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="arrow">></text>
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="label">收款说明</text>
|
||||||
|
<textarea
|
||||||
|
class="textarea"
|
||||||
|
v-model="form.remark"
|
||||||
|
placeholder="请输入收款说明(选填)"
|
||||||
|
maxlength="200"
|
||||||
|
/>
|
||||||
|
<text class="word-count">{{ form.remark.length }}/200</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="label">上传凭证</text>
|
||||||
|
<view class="upload-list">
|
||||||
|
<view class="upload-item" v-for="(img, index) in form.voucherUrls" :key="index">
|
||||||
|
<image :src="img" mode="aspectFill" @click="previewImage(index)"></image>
|
||||||
|
<text class="delete-btn" @click.stop="deleteImage(index)">×</text>
|
||||||
|
</view>
|
||||||
|
<view class="upload-box" @click="chooseImage" v-if="form.voucherUrls.length < 3">
|
||||||
|
<text class="icon">📷</text>
|
||||||
|
<text class="tip">{{ form.voucherUrls.length }}/3</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<button class="submit-btn" @click="handleSubmit" :loading="loading">
|
||||||
|
<text class="btn-icon">✓</text>
|
||||||
|
<text>确认收款</text>
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { API_URLS } from '@/config/api'
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
amount: '',
|
||||||
|
receiptMethod: 'bank',
|
||||||
|
receiptMethodName: '银行转账',
|
||||||
|
customerId: '',
|
||||||
|
customerName: '',
|
||||||
|
projectId: '',
|
||||||
|
projectName: '',
|
||||||
|
receiptDate: getToday(),
|
||||||
|
remark: '',
|
||||||
|
voucherUrls: [] as string[]
|
||||||
|
})
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const receiptMethods = [
|
||||||
|
{ value: 'bank', label: '银行转账', icon: '🏦' },
|
||||||
|
{ value: 'alipay', label: '支付宝', icon: '💙' },
|
||||||
|
{ value: 'wechat', label: '微信支付', icon: '💚' },
|
||||||
|
{ value: 'cash', label: '现金', icon: '💵' },
|
||||||
|
{ value: 'check', label: '支票', icon: '📋' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const customers = ref<any[]>([])
|
||||||
|
const customerIndex = ref(0)
|
||||||
|
const projects = ref<any[]>([])
|
||||||
|
const projectIndex = ref(0)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadCustomers()
|
||||||
|
loadProjects()
|
||||||
|
})
|
||||||
|
|
||||||
|
function getToday() {
|
||||||
|
const date = new Date()
|
||||||
|
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadCustomers = async () => {
|
||||||
|
try {
|
||||||
|
const res: any = await uni.request({
|
||||||
|
url: API_URLS.customerList,
|
||||||
|
method: 'GET',
|
||||||
|
data: { current: 1, size: 100 }
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.statusCode === 200 && res.data.code === 200) {
|
||||||
|
customers.value = res.data.data.records || []
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载客户列表失败', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadProjects = async () => {
|
||||||
|
try {
|
||||||
|
const res: any = await uni.request({
|
||||||
|
url: API_URLS.projectList,
|
||||||
|
method: 'GET',
|
||||||
|
data: { current: 1, size: 100 }
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.statusCode === 200 && res.data.code === 200) {
|
||||||
|
projects.value = res.data.data.records || []
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载项目列表失败', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCustomerChange = (e: any) => {
|
||||||
|
const index = e.detail.value
|
||||||
|
customerIndex.value = index
|
||||||
|
const customer = customers.value[index]
|
||||||
|
if (customer) {
|
||||||
|
form.value.customerId = customer.customerId
|
||||||
|
form.value.customerName = customer.customerName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onProjectChange = (e: any) => {
|
||||||
|
const index = e.detail.value
|
||||||
|
projectIndex.value = index
|
||||||
|
const project = projects.value[index]
|
||||||
|
if (project) {
|
||||||
|
form.value.projectId = project.projectId
|
||||||
|
form.value.projectName = project.projectName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onDateChange = (e: any) => {
|
||||||
|
form.value.receiptDate = e.detail.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const chooseImage = () => {
|
||||||
|
const remainCount = 3 - form.value.voucherUrls.length
|
||||||
|
uni.chooseImage({
|
||||||
|
count: remainCount,
|
||||||
|
sizeType: ['compressed'],
|
||||||
|
sourceType: ['camera', 'album'],
|
||||||
|
success: (res: any) => {
|
||||||
|
const tempFilePaths = res.tempFilePaths
|
||||||
|
tempFilePaths.forEach((filePath: string) => {
|
||||||
|
uploadImage(filePath)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadImage = async (filePath: string) => {
|
||||||
|
try {
|
||||||
|
uni.showLoading({ title: '上传中...' })
|
||||||
|
|
||||||
|
const uploadRes: any = await uni.uploadFile({
|
||||||
|
url: API_URLS.upload,
|
||||||
|
filePath: filePath,
|
||||||
|
name: 'file'
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = JSON.parse(uploadRes.data)
|
||||||
|
if (data.code === 200) {
|
||||||
|
form.value.voucherUrls.push(data.data.url)
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: '上传失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
uni.showToast({ title: '上传失败', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
uni.hideLoading()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const previewImage = (index: number) => {
|
||||||
|
uni.previewImage({
|
||||||
|
current: form.value.voucherUrls[index],
|
||||||
|
urls: form.value.voucherUrls
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteImage = (index: number) => {
|
||||||
|
uni.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '确定删除这张图片吗?',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
form.value.voucherUrls.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!form.value.amount) {
|
||||||
|
uni.showToast({ title: '请输入收款金额', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!form.value.customerId) {
|
||||||
|
uni.showToast({ title: '请选择客户', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res: any = await uni.request({
|
||||||
|
url: API_URLS.receiptSave,
|
||||||
|
method: 'POST',
|
||||||
|
data: form.value
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.statusCode === 200 && res.data.code === 200) {
|
||||||
|
uni.showToast({
|
||||||
|
title: '收款成功',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.navigateBack()
|
||||||
|
}, 1500)
|
||||||
|
} else {
|
||||||
|
uni.showToast({
|
||||||
|
title: res.data.message || '提交失败',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
uni.showToast({ title: '网络错误', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f5f7fa;
|
||||||
|
padding: 20rpx;
|
||||||
|
padding-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
padding: 40rpx 30rpx;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 40rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
padding-bottom: 30rpx;
|
||||||
|
border-bottom: 2rpx solid #f0f0f0;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
font-size: 48rpx;
|
||||||
|
margin-right: 16rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item {
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
display: block;
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
.required {
|
||||||
|
color: #ff4d4f;
|
||||||
|
margin-left: 8rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-input {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 30rpx;
|
||||||
|
|
||||||
|
.currency {
|
||||||
|
font-size: 48rpx;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-right: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 48rpx;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: bold;
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-group {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 20rpx;
|
||||||
|
|
||||||
|
.radio-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20rpx 30rpx;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: #e6f7ff;
|
||||||
|
color: #1890ff;
|
||||||
|
border: 2rpx solid #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-icon {
|
||||||
|
font-size: 36rpx;
|
||||||
|
margin-right: 12rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 100rpx;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 0 24rpx;
|
||||||
|
font-size: 30rpx;
|
||||||
|
|
||||||
|
.picker-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.picker-icon {
|
||||||
|
font-size: 40rpx;
|
||||||
|
margin-right: 16rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow {
|
||||||
|
color: #999;
|
||||||
|
font-size: 32rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.textarea {
|
||||||
|
width: 100%;
|
||||||
|
height: 200rpx;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 24rpx;
|
||||||
|
font-size: 30rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-count {
|
||||||
|
display: block;
|
||||||
|
text-align: right;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
margin-top: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 20rpx;
|
||||||
|
|
||||||
|
.upload-item {
|
||||||
|
position: relative;
|
||||||
|
width: 200rpx;
|
||||||
|
height: 200rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 8rpx;
|
||||||
|
right: 8rpx;
|
||||||
|
width: 40rpx;
|
||||||
|
height: 40rpx;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 32rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-box {
|
||||||
|
width: 200rpx;
|
||||||
|
height: 200rpx;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 2rpx dashed #ddd;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
font-size: 60rpx;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100rpx;
|
||||||
|
background: linear-gradient(135deg, #52c41a 0%, #389e0d 100%);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
border-radius: 50rpx;
|
||||||
|
box-shadow: 0 8rpx 20rpx rgba(82, 196, 26, 0.3);
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
opacity: 0.9;
|
||||||
|
transform: translateY(2rpx);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon {
|
||||||
|
font-size: 40rpx;
|
||||||
|
margin-right: 12rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Loading…
x
Reference in New Issue
Block a user