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