修改政府端前端,银行端小程序和后端接口
This commit is contained in:
@@ -1,6 +1,13 @@
|
||||
{
|
||||
"pages": [
|
||||
"pages/index/index",
|
||||
"pages/warning/warning",
|
||||
"pages/projects/projects",
|
||||
"pages/business/business",
|
||||
"pages/business/loan-products/loan-products",
|
||||
"pages/business/loan-applications/loan-applications",
|
||||
"pages/business/loan-contracts/loan-contracts",
|
||||
"pages/business/loan-releases/loan-releases",
|
||||
"pages/profile/profile",
|
||||
"pages/login/login",
|
||||
"pages/dashboard/dashboard",
|
||||
"pages/customers/customers",
|
||||
@@ -11,8 +18,7 @@
|
||||
"pages/assets/monitor",
|
||||
"pages/risk/risk",
|
||||
"pages/reports/reports",
|
||||
"pages/reports/dashboard",
|
||||
"pages/profile/profile"
|
||||
"pages/reports/dashboard"
|
||||
],
|
||||
"window": {
|
||||
"backgroundTextStyle": "light",
|
||||
@@ -28,33 +34,19 @@
|
||||
"backgroundColor": "#ffffff",
|
||||
"list": [
|
||||
{
|
||||
"pagePath": "pages/index/index",
|
||||
"iconPath": "images/home.png",
|
||||
"selectedIconPath": "images/home-active.png",
|
||||
"text": "首页"
|
||||
"pagePath": "pages/warning/warning",
|
||||
"text": "日检预警"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/dashboard/dashboard",
|
||||
"iconPath": "images/dashboard.png",
|
||||
"selectedIconPath": "images/dashboard-active.png",
|
||||
"text": "看板"
|
||||
"pagePath": "pages/projects/projects",
|
||||
"text": "项目清单"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/customers/customers",
|
||||
"iconPath": "images/customers.png",
|
||||
"selectedIconPath": "images/customers-active.png",
|
||||
"text": "客户"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/transactions/transactions",
|
||||
"iconPath": "images/transactions.png",
|
||||
"selectedIconPath": "images/transactions-active.png",
|
||||
"text": "交易"
|
||||
"pagePath": "pages/business/business",
|
||||
"text": "业务管理"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/profile/profile",
|
||||
"iconPath": "images/profile.png",
|
||||
"selectedIconPath": "images/profile-active.png",
|
||||
"text": "我的"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,54 +1,66 @@
|
||||
// pages/assets/assets.js
|
||||
const bankService = require('../../services/bankService.js')
|
||||
|
||||
Page({
|
||||
|
||||
/**
|
||||
* 页面的初始数据
|
||||
*/
|
||||
data: {
|
||||
searchKeyword: '',
|
||||
loading: false,
|
||||
assetsList: []
|
||||
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.loadAssetsData()
|
||||
/**
|
||||
* 生命周期函数--监听页面加载
|
||||
*/
|
||||
onLoad(options) {
|
||||
|
||||
},
|
||||
|
||||
loadAssetsData() {
|
||||
// 模拟数据
|
||||
this.setData({
|
||||
assetsList: [
|
||||
{
|
||||
id: 1,
|
||||
type: 'savings',
|
||||
name: '储蓄卡',
|
||||
bankName: '中国银行',
|
||||
balance: '125,680.50',
|
||||
cardNumber: '**** **** **** 1234',
|
||||
status: 'active',
|
||||
statusText: '正常'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '信用卡',
|
||||
bankName: '招商银行',
|
||||
balance: '-2,450.00',
|
||||
cardNumber: '**** **** **** 5678',
|
||||
status: 'active',
|
||||
statusText: '正常'
|
||||
}
|
||||
]
|
||||
})
|
||||
/**
|
||||
* 生命周期函数--监听页面初次渲染完成
|
||||
*/
|
||||
onReady() {
|
||||
|
||||
},
|
||||
|
||||
onSearchInput(e) {
|
||||
this.setData({
|
||||
searchKeyword: e.detail.value
|
||||
})
|
||||
/**
|
||||
* 生命周期函数--监听页面显示
|
||||
*/
|
||||
onShow() {
|
||||
|
||||
},
|
||||
|
||||
handleAssetTap(e) {
|
||||
const { assetId } = e.currentTarget.dataset
|
||||
wx.navigateTo({
|
||||
url: `/pages/assets/detail?id=${assetId}`
|
||||
})
|
||||
/**
|
||||
* 生命周期函数--监听页面隐藏
|
||||
*/
|
||||
onHide() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面卸载
|
||||
*/
|
||||
onUnload() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面相关事件处理函数--监听用户下拉动作
|
||||
*/
|
||||
onPullDownRefresh() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面上拉触底事件的处理函数
|
||||
*/
|
||||
onReachBottom() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 用户点击右上角分享
|
||||
*/
|
||||
onShareAppMessage() {
|
||||
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,52 +1,2 @@
|
||||
<!--pages/assets/assets.wxml-->
|
||||
<view class="assets-container">
|
||||
<!-- 搜索栏 -->
|
||||
<view class="search-section">
|
||||
<view class="search-bar">
|
||||
<input
|
||||
value="{{searchKeyword}}"
|
||||
type="text"
|
||||
placeholder="搜索资产..."
|
||||
class="search-input"
|
||||
bindinput="onSearchInput"
|
||||
/>
|
||||
<view class="search-icon">🔍</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 资产列表 -->
|
||||
<view class="assets-list">
|
||||
<view class="list-header">
|
||||
<view class="list-title">资产管理</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{loading}}" class="loading">
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<view wx:elif="{{assetsList.length === 0}}" class="empty">
|
||||
<view class="empty-icon">💰</view>
|
||||
<view class="empty-text">暂无资产记录</view>
|
||||
</view>
|
||||
|
||||
<view wx:else class="list-content">
|
||||
<view
|
||||
wx:for="{{assetsList}}"
|
||||
wx:key="id"
|
||||
class="asset-item bank-card"
|
||||
data-asset-id="{{item.id}}"
|
||||
bindtap="handleAssetTap"
|
||||
>
|
||||
<view class="bank-card-header">
|
||||
<view class="bank-name">{{item.bankName}}</view>
|
||||
<view class="card-type">{{item.name}}</view>
|
||||
</view>
|
||||
<view class="bank-card-number">{{item.cardNumber}}</view>
|
||||
<view class="bank-card-info">
|
||||
<view class="bank-card-balance">{{item.balance}}</view>
|
||||
<view class="bank-card-type">余额</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<text>pages/assets/assets.wxml</text>
|
||||
@@ -1,90 +0,0 @@
|
||||
/* pages/assets/assets.wxss */
|
||||
.assets-container {
|
||||
min-height: 100vh;
|
||||
background: #f6f6f6;
|
||||
}
|
||||
|
||||
.search-section {
|
||||
padding: 20rpx;
|
||||
background: #fff;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
height: 72rpx;
|
||||
background: #f8f9fa;
|
||||
border: none;
|
||||
border-radius: 36rpx;
|
||||
padding: 0 60rpx 0 30rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
right: 30rpx;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.assets-list {
|
||||
background: #fff;
|
||||
margin: 0 20rpx;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.list-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 30rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.list-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.loading,
|
||||
.empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 80rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.loading-text,
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 80rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.list-content {
|
||||
space-y: 20rpx;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.asset-item {
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.asset-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
96
bank_mini_program/pages/business/business.js
Normal file
96
bank_mini_program/pages/business/business.js
Normal file
@@ -0,0 +1,96 @@
|
||||
// pages/business/business.js
|
||||
const { apiService } = require('../../services/apiService');
|
||||
|
||||
Page({
|
||||
data: {
|
||||
businessStats: {
|
||||
loanProducts: 0,
|
||||
applications: 0,
|
||||
contracts: 0,
|
||||
releases: 0
|
||||
}
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.loadBusinessStats();
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.loadBusinessStats();
|
||||
},
|
||||
|
||||
// 加载业务统计
|
||||
async loadBusinessStats() {
|
||||
try {
|
||||
// 并行获取各项统计数据
|
||||
const [productsRes, applicationsRes, contractsRes, releasesRes] = await Promise.allSettled([
|
||||
apiService.loanProducts.getStats(),
|
||||
apiService.loanApplications.getStats(),
|
||||
apiService.loanContracts.getStats(),
|
||||
apiService.loanReleases.getStats()
|
||||
]);
|
||||
|
||||
const stats = {
|
||||
loanProducts: productsRes.status === 'fulfilled' ? productsRes.value.data?.total || 0 : 0,
|
||||
applications: applicationsRes.status === 'fulfilled' ? applicationsRes.value.data?.total || 0 : 0,
|
||||
contracts: contractsRes.status === 'fulfilled' ? contractsRes.value.data?.total || 0 : 0,
|
||||
releases: releasesRes.status === 'fulfilled' ? releasesRes.value.data?.total || 0 : 0
|
||||
};
|
||||
|
||||
this.setData({
|
||||
businessStats: stats
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('加载业务统计失败:', error);
|
||||
// 使用默认数据
|
||||
this.setData({
|
||||
businessStats: {
|
||||
loanProducts: 0,
|
||||
applications: 0,
|
||||
contracts: 0,
|
||||
releases: 0
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 导航到对应页面
|
||||
navigateToPage(e) {
|
||||
const type = e.currentTarget.dataset.type;
|
||||
|
||||
switch (type) {
|
||||
case 'loan-products':
|
||||
wx.navigateTo({
|
||||
url: '/pages/business/loan-products/loan-products'
|
||||
});
|
||||
break;
|
||||
case 'loan-applications':
|
||||
wx.navigateTo({
|
||||
url: '/pages/business/loan-applications/loan-applications'
|
||||
});
|
||||
break;
|
||||
case 'loan-contracts':
|
||||
wx.navigateTo({
|
||||
url: '/pages/business/loan-contracts/loan-contracts'
|
||||
});
|
||||
break;
|
||||
case 'loan-releases':
|
||||
wx.navigateTo({
|
||||
url: '/pages/business/loan-releases/loan-releases'
|
||||
});
|
||||
break;
|
||||
default:
|
||||
wx.showToast({
|
||||
title: '功能开发中',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 下拉刷新
|
||||
onPullDownRefresh() {
|
||||
this.loadBusinessStats().finally(() => {
|
||||
wx.stopPullDownRefresh();
|
||||
});
|
||||
}
|
||||
});
|
||||
3
bank_mini_program/pages/business/business.json
Normal file
3
bank_mini_program/pages/business/business.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"usingComponents": {}
|
||||
}
|
||||
41
bank_mini_program/pages/business/business.wxml
Normal file
41
bank_mini_program/pages/business/business.wxml
Normal file
@@ -0,0 +1,41 @@
|
||||
<!--pages/business/business.wxml-->
|
||||
<view class="business-container">
|
||||
<!-- 业务菜单列表 -->
|
||||
<view class="menu-list">
|
||||
<view
|
||||
class="menu-item"
|
||||
data-type="loan-products"
|
||||
bindtap="navigateToPage"
|
||||
>
|
||||
<text class="menu-text">普惠商品</text>
|
||||
<text class="menu-arrow">></text>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="menu-item"
|
||||
data-type="loan-applications"
|
||||
bindtap="navigateToPage"
|
||||
>
|
||||
<text class="menu-text">业务申请进度</text>
|
||||
<text class="menu-arrow">></text>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="menu-item"
|
||||
data-type="loan-contracts"
|
||||
bindtap="navigateToPage"
|
||||
>
|
||||
<text class="menu-text">业务合同</text>
|
||||
<text class="menu-arrow">></text>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="menu-item"
|
||||
data-type="loan-releases"
|
||||
bindtap="navigateToPage"
|
||||
>
|
||||
<text class="menu-text">业务解押</text>
|
||||
<text class="menu-arrow">></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
42
bank_mini_program/pages/business/business.wxss
Normal file
42
bank_mini_program/pages/business/business.wxss
Normal file
@@ -0,0 +1,42 @@
|
||||
/* pages/business/business.wxss */
|
||||
.business-container {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
padding-bottom: 100rpx;
|
||||
}
|
||||
|
||||
/* 业务菜单列表 */
|
||||
.menu-list {
|
||||
background-color: #fff;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 30rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
background-color: #fff;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.menu-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.menu-item:active {
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
|
||||
.menu-text {
|
||||
font-size: 32rpx;
|
||||
color: #333;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.menu-arrow {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
font-weight: 300;
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
// pages/business/loan-applications/loan-applications.js
|
||||
const { apiService } = require('../../../services/apiService');
|
||||
|
||||
Page({
|
||||
data: {
|
||||
applications: [],
|
||||
loading: false,
|
||||
searchKeyword: '',
|
||||
filterStatus: 'all',
|
||||
typeMap: {
|
||||
'personal': '个人贷款',
|
||||
'mortgage': '住房贷款',
|
||||
'business': '企业贷款',
|
||||
'agricultural': '农业贷款'
|
||||
},
|
||||
statusMap: {
|
||||
'pending': '待审核',
|
||||
'processing': '审核中',
|
||||
'approved': '已通过',
|
||||
'rejected': '已拒绝'
|
||||
}
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.loadApplications();
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.loadApplications();
|
||||
},
|
||||
|
||||
// 加载申请数据
|
||||
async loadApplications() {
|
||||
this.setData({ loading: true });
|
||||
|
||||
try {
|
||||
const params = {
|
||||
page: 1,
|
||||
limit: 20,
|
||||
search: this.data.searchKeyword,
|
||||
status: this.data.filterStatus === 'all' ? '' : this.data.filterStatus
|
||||
};
|
||||
|
||||
const response = await apiService.loanApplications.getList(params);
|
||||
|
||||
if (response.success) {
|
||||
const applications = response.data.applications.map(application => ({
|
||||
...application,
|
||||
typeText: this.data.typeMap[application.type] || application.type,
|
||||
statusText: this.data.statusMap[application.status] || application.status,
|
||||
applicationTime: this.formatDate(application.applicationTime)
|
||||
}));
|
||||
|
||||
this.setData({
|
||||
applications,
|
||||
loading: false
|
||||
});
|
||||
} else {
|
||||
throw new Error(response.message || '获取申请列表失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载申请失败:', error);
|
||||
|
||||
// 使用模拟数据作为降级处理
|
||||
const mockApplications = [
|
||||
{
|
||||
id: 1,
|
||||
applicationNumber: 'APP-202401180001',
|
||||
applicantName: '张三',
|
||||
type: 'personal',
|
||||
typeText: '个人贷款',
|
||||
status: 'pending',
|
||||
statusText: '待审核',
|
||||
amount: 200000,
|
||||
term: 24,
|
||||
interestRate: 6.5,
|
||||
applicationTime: '2024-01-18 09:30',
|
||||
phone: '13800138000',
|
||||
purpose: '个人消费'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
applicationNumber: 'APP-202401180002',
|
||||
applicantName: '李四',
|
||||
type: 'mortgage',
|
||||
typeText: '住房贷款',
|
||||
status: 'approved',
|
||||
statusText: '已通过',
|
||||
amount: 500000,
|
||||
term: 240,
|
||||
interestRate: 4.5,
|
||||
applicationTime: '2024-01-18 10:15',
|
||||
phone: '13800138001',
|
||||
purpose: '购买住房'
|
||||
}
|
||||
];
|
||||
|
||||
this.setData({
|
||||
applications: mockApplications,
|
||||
loading: false
|
||||
});
|
||||
|
||||
wx.showToast({
|
||||
title: '使用模拟数据',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 格式化日期
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return '';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
},
|
||||
|
||||
// 搜索输入
|
||||
onSearchInput(e) {
|
||||
this.setData({
|
||||
searchKeyword: e.detail.value
|
||||
});
|
||||
this.loadApplications();
|
||||
},
|
||||
|
||||
// 筛选状态
|
||||
onFilterChange(e) {
|
||||
const status = e.detail.value;
|
||||
this.setData({
|
||||
filterStatus: status
|
||||
});
|
||||
this.loadApplications();
|
||||
},
|
||||
|
||||
// 查看申请详情
|
||||
viewApplicationDetail(e) {
|
||||
const applicationId = e.currentTarget.dataset.applicationId;
|
||||
wx.navigateTo({
|
||||
url: `/pages/business/loan-applications/detail?applicationId=${applicationId}`
|
||||
});
|
||||
},
|
||||
|
||||
// 下拉刷新
|
||||
onPullDownRefresh() {
|
||||
this.loadApplications().finally(() => {
|
||||
wx.stopPullDownRefresh();
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,101 @@
|
||||
<!--pages/business/loan-applications/loan-applications.wxml-->
|
||||
<view class="loan-applications-container">
|
||||
<!-- 搜索栏 -->
|
||||
<view class="search-bar">
|
||||
<view class="search-input">
|
||||
<text class="search-icon">🔍</text>
|
||||
<input
|
||||
placeholder="搜索申请人姓名或申请单号"
|
||||
value="{{searchKeyword}}"
|
||||
bindinput="onSearchInput"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 筛选栏 -->
|
||||
<view class="filter-bar">
|
||||
<picker
|
||||
bindchange="onFilterChange"
|
||||
value="{{filterStatus}}"
|
||||
range="{{['全部', '待审核', '审核中', '已通过', '已拒绝']}}"
|
||||
range-key=""
|
||||
>
|
||||
<view class="filter-item">
|
||||
<text>状态筛选</text>
|
||||
<text class="filter-value">{{filterStatus === 'all' ? '全部' : filterStatus}}</text>
|
||||
<text class="arrow">></text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<!-- 申请列表 -->
|
||||
<view class="application-list">
|
||||
<view
|
||||
class="application-item"
|
||||
wx:for="{{applications}}"
|
||||
wx:key="id"
|
||||
data-application-id="{{item.id}}"
|
||||
bindtap="viewApplicationDetail"
|
||||
>
|
||||
<view class="application-header">
|
||||
<text class="application-number">{{item.applicationNumber}}</text>
|
||||
<view class="application-status {{item.status}}">
|
||||
<text>{{item.statusText}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="application-info">
|
||||
<view class="info-row">
|
||||
<text class="info-label">申请人:</text>
|
||||
<text class="info-value">{{item.applicantName}}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-row">
|
||||
<text class="info-label">申请类型:</text>
|
||||
<text class="info-value">{{item.typeText}}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-row">
|
||||
<text class="info-label">申请金额:</text>
|
||||
<text class="info-value">{{item.amount}}元</text>
|
||||
</view>
|
||||
|
||||
<view class="info-row">
|
||||
<text class="info-label">申请期限:</text>
|
||||
<text class="info-value">{{item.term}}个月</text>
|
||||
</view>
|
||||
|
||||
<view class="info-row">
|
||||
<text class="info-label">利率:</text>
|
||||
<text class="info-value">{{item.interestRate}}%</text>
|
||||
</view>
|
||||
|
||||
<view class="info-row">
|
||||
<text class="info-label">申请时间:</text>
|
||||
<text class="info-value">{{item.applicationTime}}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-row">
|
||||
<text class="info-label">联系电话:</text>
|
||||
<text class="info-value">{{item.phone}}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-row">
|
||||
<text class="info-label">申请用途:</text>
|
||||
<text class="info-value">{{item.purpose}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view class="loading" wx:if="{{loading}}">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" wx:if="{{!loading && applications.length === 0}}">
|
||||
<text class="empty-icon">📋</text>
|
||||
<text class="empty-text">暂无申请数据</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,157 @@
|
||||
/* pages/business/loan-applications/loan-applications.wxss */
|
||||
.loan-applications-container {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
padding-bottom: 100rpx;
|
||||
}
|
||||
|
||||
/* 搜索栏 */
|
||||
.search-bar {
|
||||
padding: 20rpx 30rpx;
|
||||
background-color: #fff;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 25rpx;
|
||||
padding: 0 20rpx;
|
||||
height: 70rpx;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
margin-right: 15rpx;
|
||||
}
|
||||
|
||||
.search-input input {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 筛选栏 */
|
||||
.filter-bar {
|
||||
background-color: #fff;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 30rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.filter-value {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 申请列表 */
|
||||
.application-list {
|
||||
padding: 20rpx 30rpx;
|
||||
}
|
||||
|
||||
.application-item {
|
||||
background-color: #fff;
|
||||
margin-bottom: 20rpx;
|
||||
padding: 30rpx;
|
||||
border-radius: 8rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.application-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.application-number {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.application-status {
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 20rpx;
|
||||
font-size: 24rpx;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.application-status.pending {
|
||||
background-color: #faad14;
|
||||
}
|
||||
|
||||
.application-status.processing {
|
||||
background-color: #1890ff;
|
||||
}
|
||||
|
||||
.application-status.approved {
|
||||
background-color: #52c41a;
|
||||
}
|
||||
|
||||
.application-status.rejected {
|
||||
background-color: #f5222d;
|
||||
}
|
||||
|
||||
.application-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
color: #666;
|
||||
min-width: 140rpx;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 60rpx 0;
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 100rpx 0;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 80rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
// pages/business/loan-contracts/loan-contracts.js
|
||||
const { apiService } = require('../../../services/apiService');
|
||||
|
||||
Page({
|
||||
data: {
|
||||
contracts: [],
|
||||
loading: false,
|
||||
searchKeyword: '',
|
||||
filterStatus: 'all',
|
||||
typeMap: {
|
||||
'personal': '个人贷款',
|
||||
'mortgage': '住房贷款',
|
||||
'business': '企业贷款',
|
||||
'agricultural': '农业贷款'
|
||||
},
|
||||
statusMap: {
|
||||
'active': '生效中',
|
||||
'expired': '已到期',
|
||||
'terminated': '已终止'
|
||||
}
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.loadContracts();
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.loadContracts();
|
||||
},
|
||||
|
||||
// 加载合同数据
|
||||
async loadContracts() {
|
||||
this.setData({ loading: true });
|
||||
|
||||
try {
|
||||
const params = {
|
||||
page: 1,
|
||||
limit: 20,
|
||||
search: this.data.searchKeyword,
|
||||
status: this.data.filterStatus === 'all' ? '' : this.data.filterStatus
|
||||
};
|
||||
|
||||
const response = await apiService.loanContracts.getList(params);
|
||||
|
||||
if (response.success) {
|
||||
const contracts = response.data.contracts.map(contract => ({
|
||||
...contract,
|
||||
typeText: this.data.typeMap[contract.type] || contract.type,
|
||||
statusText: this.data.statusMap[contract.status] || contract.status,
|
||||
signDate: this.formatDate(contract.signDate),
|
||||
expiryDate: this.formatDate(contract.expiryDate)
|
||||
}));
|
||||
|
||||
this.setData({
|
||||
contracts,
|
||||
loading: false
|
||||
});
|
||||
} else {
|
||||
throw new Error(response.message || '获取合同列表失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载合同失败:', error);
|
||||
|
||||
// 使用模拟数据作为降级处理
|
||||
const mockContracts = [
|
||||
{
|
||||
id: 1,
|
||||
contractNumber: 'CONTRACT-202401180001',
|
||||
customerName: '张三',
|
||||
type: 'personal',
|
||||
typeText: '个人贷款',
|
||||
status: 'active',
|
||||
statusText: '生效中',
|
||||
amount: 200000,
|
||||
term: 24,
|
||||
interestRate: 6.5,
|
||||
signDate: '2024-01-18',
|
||||
expiryDate: '2026-01-18',
|
||||
phone: '13800138000'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
contractNumber: 'CONTRACT-202401180002',
|
||||
customerName: '李四',
|
||||
type: 'mortgage',
|
||||
typeText: '住房贷款',
|
||||
status: 'active',
|
||||
statusText: '生效中',
|
||||
amount: 500000,
|
||||
term: 240,
|
||||
interestRate: 4.5,
|
||||
signDate: '2024-01-18',
|
||||
expiryDate: '2044-01-18',
|
||||
phone: '13800138001'
|
||||
}
|
||||
];
|
||||
|
||||
this.setData({
|
||||
contracts: mockContracts,
|
||||
loading: false
|
||||
});
|
||||
|
||||
wx.showToast({
|
||||
title: '使用模拟数据',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 格式化日期
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return '';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('zh-CN');
|
||||
},
|
||||
|
||||
// 搜索输入
|
||||
onSearchInput(e) {
|
||||
this.setData({
|
||||
searchKeyword: e.detail.value
|
||||
});
|
||||
this.loadContracts();
|
||||
},
|
||||
|
||||
// 筛选状态
|
||||
onFilterChange(e) {
|
||||
const status = e.detail.value;
|
||||
this.setData({
|
||||
filterStatus: status
|
||||
});
|
||||
this.loadContracts();
|
||||
},
|
||||
|
||||
// 查看合同详情
|
||||
viewContractDetail(e) {
|
||||
const contractId = e.currentTarget.dataset.contractId;
|
||||
wx.navigateTo({
|
||||
url: `/pages/business/loan-contracts/detail?contractId=${contractId}`
|
||||
});
|
||||
},
|
||||
|
||||
// 下拉刷新
|
||||
onPullDownRefresh() {
|
||||
this.loadContracts().finally(() => {
|
||||
wx.stopPullDownRefresh();
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,101 @@
|
||||
<!--pages/business/loan-contracts/loan-contracts.wxml-->
|
||||
<view class="loan-contracts-container">
|
||||
<!-- 搜索栏 -->
|
||||
<view class="search-bar">
|
||||
<view class="search-input">
|
||||
<text class="search-icon">🔍</text>
|
||||
<input
|
||||
placeholder="搜索合同编号或客户姓名"
|
||||
value="{{searchKeyword}}"
|
||||
bindinput="onSearchInput"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 筛选栏 -->
|
||||
<view class="filter-bar">
|
||||
<picker
|
||||
bindchange="onFilterChange"
|
||||
value="{{filterStatus}}"
|
||||
range="{{['全部', '生效中', '已到期', '已终止']}}"
|
||||
range-key=""
|
||||
>
|
||||
<view class="filter-item">
|
||||
<text>状态筛选</text>
|
||||
<text class="filter-value">{{filterStatus === 'all' ? '全部' : filterStatus}}</text>
|
||||
<text class="arrow">></text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<!-- 合同列表 -->
|
||||
<view class="contract-list">
|
||||
<view
|
||||
class="contract-item"
|
||||
wx:for="{{contracts}}"
|
||||
wx:key="id"
|
||||
data-contract-id="{{item.id}}"
|
||||
bindtap="viewContractDetail"
|
||||
>
|
||||
<view class="contract-header">
|
||||
<text class="contract-number">{{item.contractNumber}}</text>
|
||||
<view class="contract-status {{item.status}}">
|
||||
<text>{{item.statusText}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="contract-info">
|
||||
<view class="info-row">
|
||||
<text class="info-label">客户姓名:</text>
|
||||
<text class="info-value">{{item.customerName}}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-row">
|
||||
<text class="info-label">合同类型:</text>
|
||||
<text class="info-value">{{item.typeText}}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-row">
|
||||
<text class="info-label">合同金额:</text>
|
||||
<text class="info-value">{{item.amount}}元</text>
|
||||
</view>
|
||||
|
||||
<view class="info-row">
|
||||
<text class="info-label">合同期限:</text>
|
||||
<text class="info-value">{{item.term}}个月</text>
|
||||
</view>
|
||||
|
||||
<view class="info-row">
|
||||
<text class="info-label">利率:</text>
|
||||
<text class="info-value">{{item.interestRate}}%</text>
|
||||
</view>
|
||||
|
||||
<view class="info-row">
|
||||
<text class="info-label">签订时间:</text>
|
||||
<text class="info-value">{{item.signDate}}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-row">
|
||||
<text class="info-label">到期时间:</text>
|
||||
<text class="info-value">{{item.expiryDate}}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-row">
|
||||
<text class="info-label">联系电话:</text>
|
||||
<text class="info-value">{{item.phone}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view class="loading" wx:if="{{loading}}">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" wx:if="{{!loading && contracts.length === 0}}">
|
||||
<text class="empty-icon">📄</text>
|
||||
<text class="empty-text">暂无合同数据</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,153 @@
|
||||
/* pages/business/loan-contracts/loan-contracts.wxss */
|
||||
.loan-contracts-container {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
padding-bottom: 100rpx;
|
||||
}
|
||||
|
||||
/* 搜索栏 */
|
||||
.search-bar {
|
||||
padding: 20rpx 30rpx;
|
||||
background-color: #fff;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 25rpx;
|
||||
padding: 0 20rpx;
|
||||
height: 70rpx;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
margin-right: 15rpx;
|
||||
}
|
||||
|
||||
.search-input input {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 筛选栏 */
|
||||
.filter-bar {
|
||||
background-color: #fff;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 30rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.filter-value {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 合同列表 */
|
||||
.contract-list {
|
||||
padding: 20rpx 30rpx;
|
||||
}
|
||||
|
||||
.contract-item {
|
||||
background-color: #fff;
|
||||
margin-bottom: 20rpx;
|
||||
padding: 30rpx;
|
||||
border-radius: 8rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.contract-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.contract-number {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.contract-status {
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 20rpx;
|
||||
font-size: 24rpx;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.contract-status.active {
|
||||
background-color: #52c41a;
|
||||
}
|
||||
|
||||
.contract-status.expired {
|
||||
background-color: #faad14;
|
||||
}
|
||||
|
||||
.contract-status.terminated {
|
||||
background-color: #f5222d;
|
||||
}
|
||||
|
||||
.contract-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
color: #666;
|
||||
min-width: 140rpx;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 60rpx 0;
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 100rpx 0;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 80rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
136
bank_mini_program/pages/business/loan-products/loan-products.js
Normal file
136
bank_mini_program/pages/business/loan-products/loan-products.js
Normal file
@@ -0,0 +1,136 @@
|
||||
// pages/business/loan-products/loan-products.js
|
||||
const { apiService } = require('../../../services/apiService');
|
||||
|
||||
Page({
|
||||
data: {
|
||||
products: [],
|
||||
loading: false,
|
||||
searchKeyword: '',
|
||||
filterStatus: 'all',
|
||||
typeMap: {
|
||||
'personal': '个人贷款',
|
||||
'mortgage': '住房贷款',
|
||||
'business': '企业贷款',
|
||||
'agricultural': '农业贷款'
|
||||
}
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.loadProducts();
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.loadProducts();
|
||||
},
|
||||
|
||||
// 加载商品数据
|
||||
async loadProducts() {
|
||||
this.setData({ loading: true });
|
||||
|
||||
try {
|
||||
const params = {
|
||||
page: 1,
|
||||
limit: 20,
|
||||
search: this.data.searchKeyword,
|
||||
status: this.data.filterStatus === 'all' ? '' : this.data.filterStatus
|
||||
};
|
||||
|
||||
const response = await apiService.loanProducts.getList(params);
|
||||
|
||||
if (response.success) {
|
||||
const products = response.data.products.map(product => ({
|
||||
...product,
|
||||
statusText: product.status === 'active' ? '在售' : '停售',
|
||||
typeText: this.data.typeMap[product.type] || product.type
|
||||
}));
|
||||
|
||||
this.setData({
|
||||
products,
|
||||
loading: false
|
||||
});
|
||||
} else {
|
||||
throw new Error(response.message || '获取商品列表失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载商品失败:', error);
|
||||
|
||||
// 使用模拟数据作为降级处理
|
||||
const mockProducts = [
|
||||
{
|
||||
id: 1,
|
||||
name: '个人住房贷款',
|
||||
code: 'LOAN-001',
|
||||
type: 'mortgage',
|
||||
typeText: '住房贷款',
|
||||
status: 'active',
|
||||
statusText: '在售',
|
||||
minAmount: 100000,
|
||||
maxAmount: 5000000,
|
||||
minTerm: 12,
|
||||
maxTerm: 360,
|
||||
interestRate: 4.5,
|
||||
maxInterestRate: 6.5,
|
||||
description: '专为个人购房提供的住房抵押贷款产品'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '个人消费贷款',
|
||||
code: 'LOAN-002',
|
||||
type: 'personal',
|
||||
typeText: '个人贷款',
|
||||
status: 'active',
|
||||
statusText: '在售',
|
||||
minAmount: 10000,
|
||||
maxAmount: 500000,
|
||||
minTerm: 6,
|
||||
maxTerm: 60,
|
||||
interestRate: 6.8,
|
||||
maxInterestRate: 12.5,
|
||||
description: '用于个人消费支出的信用贷款产品'
|
||||
}
|
||||
];
|
||||
|
||||
this.setData({
|
||||
products: mockProducts,
|
||||
loading: false
|
||||
});
|
||||
|
||||
wx.showToast({
|
||||
title: '使用模拟数据',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 搜索输入
|
||||
onSearchInput(e) {
|
||||
this.setData({
|
||||
searchKeyword: e.detail.value
|
||||
});
|
||||
this.loadProducts();
|
||||
},
|
||||
|
||||
// 筛选状态
|
||||
onFilterChange(e) {
|
||||
const status = e.detail.value;
|
||||
this.setData({
|
||||
filterStatus: status
|
||||
});
|
||||
this.loadProducts();
|
||||
},
|
||||
|
||||
// 查看商品详情
|
||||
viewProductDetail(e) {
|
||||
const productId = e.currentTarget.dataset.productId;
|
||||
wx.navigateTo({
|
||||
url: `/pages/business/loan-products/detail?productId=${productId}`
|
||||
});
|
||||
},
|
||||
|
||||
// 下拉刷新
|
||||
onPullDownRefresh() {
|
||||
this.loadProducts().finally(() => {
|
||||
wx.stopPullDownRefresh();
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
<!--pages/business/loan-products/loan-products.wxml-->
|
||||
<view class="loan-products-container">
|
||||
<!-- 搜索栏 -->
|
||||
<view class="search-bar">
|
||||
<view class="search-input">
|
||||
<text class="search-icon">🔍</text>
|
||||
<input
|
||||
placeholder="搜索商品名称"
|
||||
value="{{searchKeyword}}"
|
||||
bindinput="onSearchInput"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 筛选栏 -->
|
||||
<view class="filter-bar">
|
||||
<picker
|
||||
bindchange="onFilterChange"
|
||||
value="{{filterStatus}}"
|
||||
range="{{['全部', '在售', '停售']}}"
|
||||
range-key=""
|
||||
>
|
||||
<view class="filter-item">
|
||||
<text>状态筛选</text>
|
||||
<text class="filter-value">{{filterStatus === 'all' ? '全部' : filterStatus}}</text>
|
||||
<text class="arrow">></text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<!-- 商品列表 -->
|
||||
<view class="product-list">
|
||||
<view
|
||||
class="product-item"
|
||||
wx:for="{{products}}"
|
||||
wx:key="id"
|
||||
data-product-id="{{item.id}}"
|
||||
bindtap="viewProductDetail"
|
||||
>
|
||||
<view class="product-header">
|
||||
<text class="product-name">{{item.name}}</text>
|
||||
<view class="product-status {{item.status}}">
|
||||
<text>{{item.statusText}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="product-info">
|
||||
<view class="info-row">
|
||||
<text class="info-label">产品代码:</text>
|
||||
<text class="info-value">{{item.code}}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-row">
|
||||
<text class="info-label">产品类型:</text>
|
||||
<text class="info-value">{{item.typeText}}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-row">
|
||||
<text class="info-label">贷款额度:</text>
|
||||
<text class="info-value">{{item.minAmount}} - {{item.maxAmount}}元</text>
|
||||
</view>
|
||||
|
||||
<view class="info-row">
|
||||
<text class="info-label">贷款期限:</text>
|
||||
<text class="info-value">{{item.minTerm}} - {{item.maxTerm}}个月</text>
|
||||
</view>
|
||||
|
||||
<view class="info-row">
|
||||
<text class="info-label">利率范围:</text>
|
||||
<text class="info-value">{{item.interestRate}}% - {{item.maxInterestRate}}%</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="product-desc">
|
||||
<text class="desc-text">{{item.description}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view class="loading" wx:if="{{loading}}">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" wx:if="{{!loading && products.length === 0}}">
|
||||
<text class="empty-icon">📦</text>
|
||||
<text class="empty-text">暂无商品数据</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,159 @@
|
||||
/* pages/business/loan-products/loan-products.wxss */
|
||||
.loan-products-container {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
padding-bottom: 100rpx;
|
||||
}
|
||||
|
||||
/* 搜索栏 */
|
||||
.search-bar {
|
||||
padding: 20rpx 30rpx;
|
||||
background-color: #fff;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 25rpx;
|
||||
padding: 0 20rpx;
|
||||
height: 70rpx;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
margin-right: 15rpx;
|
||||
}
|
||||
|
||||
.search-input input {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 筛选栏 */
|
||||
.filter-bar {
|
||||
background-color: #fff;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 30rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.filter-value {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 商品列表 */
|
||||
.product-list {
|
||||
padding: 20rpx 30rpx;
|
||||
}
|
||||
|
||||
.product-item {
|
||||
background-color: #fff;
|
||||
margin-bottom: 20rpx;
|
||||
padding: 30rpx;
|
||||
border-radius: 8rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.product-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.product-status {
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 20rpx;
|
||||
font-size: 24rpx;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.product-status.active {
|
||||
background-color: #52c41a;
|
||||
}
|
||||
|
||||
.product-status.inactive {
|
||||
background-color: #faad14;
|
||||
}
|
||||
|
||||
.product-info {
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
color: #666;
|
||||
min-width: 140rpx;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.product-desc {
|
||||
padding-top: 20rpx;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.desc-text {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 60rpx 0;
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 100rpx 0;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 80rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
147
bank_mini_program/pages/business/loan-releases/loan-releases.js
Normal file
147
bank_mini_program/pages/business/loan-releases/loan-releases.js
Normal file
@@ -0,0 +1,147 @@
|
||||
// pages/business/loan-releases/loan-releases.js
|
||||
const { apiService } = require('../../../services/apiService');
|
||||
|
||||
Page({
|
||||
data: {
|
||||
releases: [],
|
||||
loading: false,
|
||||
searchKeyword: '',
|
||||
filterStatus: 'all',
|
||||
statusMap: {
|
||||
'pending': '待处理',
|
||||
'processing': '处理中',
|
||||
'approved': '已通过',
|
||||
'rejected': '已拒绝',
|
||||
'completed': '已完成',
|
||||
'released': '已解押'
|
||||
}
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.loadReleases();
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.loadReleases();
|
||||
},
|
||||
|
||||
// 加载解押申请数据
|
||||
async loadReleases() {
|
||||
this.setData({ loading: true });
|
||||
|
||||
try {
|
||||
const params = {
|
||||
page: 1,
|
||||
limit: 20,
|
||||
searchValue: this.data.searchKeyword,
|
||||
status: this.data.filterStatus === 'all' ? '' : this.data.filterStatus
|
||||
};
|
||||
|
||||
const response = await apiService.loanReleases.getList(params);
|
||||
|
||||
if (response.success) {
|
||||
const releases = response.data.releases.map(release => ({
|
||||
...release,
|
||||
statusText: this.data.statusMap[release.status] || release.status,
|
||||
applicationTime: this.formatDate(release.applicationTime)
|
||||
}));
|
||||
|
||||
this.setData({
|
||||
releases,
|
||||
loading: false
|
||||
});
|
||||
} else {
|
||||
throw new Error(response.message || '获取解押申请列表失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载解押申请失败:', error);
|
||||
|
||||
// 使用模拟数据作为降级处理
|
||||
const mockReleases = [
|
||||
{
|
||||
id: 1,
|
||||
applicationNumber: '20240227145555918',
|
||||
customerName: '刘超',
|
||||
productName: '中国工商银行扎旗支行"畜禽活体抵押"',
|
||||
assetType: '牛',
|
||||
releaseQuantity: '10头',
|
||||
releaseAmount: 10000,
|
||||
status: 'released',
|
||||
statusText: '已解押',
|
||||
applicationTime: '2024-02-27 06:55',
|
||||
applicantPhone: '13800138000',
|
||||
processComment: '审核通过,同意解押'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
applicationNumber: '20240227145555919',
|
||||
customerName: '张三',
|
||||
productName: '个人住房贷款',
|
||||
assetType: '房产',
|
||||
releaseQuantity: '1套',
|
||||
releaseAmount: 50000,
|
||||
status: 'pending',
|
||||
statusText: '待处理',
|
||||
applicationTime: '2024-02-27 10:30',
|
||||
applicantPhone: '13800138001',
|
||||
processComment: ''
|
||||
}
|
||||
];
|
||||
|
||||
this.setData({
|
||||
releases: mockReleases,
|
||||
loading: false
|
||||
});
|
||||
|
||||
wx.showToast({
|
||||
title: '使用模拟数据',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 格式化日期
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return '';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
},
|
||||
|
||||
// 搜索输入
|
||||
onSearchInput(e) {
|
||||
this.setData({
|
||||
searchKeyword: e.detail.value
|
||||
});
|
||||
this.loadReleases();
|
||||
},
|
||||
|
||||
// 筛选状态
|
||||
onFilterChange(e) {
|
||||
const status = e.detail.value;
|
||||
this.setData({
|
||||
filterStatus: status
|
||||
});
|
||||
this.loadReleases();
|
||||
},
|
||||
|
||||
// 查看解押申请详情
|
||||
viewReleaseDetail(e) {
|
||||
const releaseId = e.currentTarget.dataset.releaseId;
|
||||
wx.navigateTo({
|
||||
url: `/pages/business/loan-releases/detail?releaseId=${releaseId}`
|
||||
});
|
||||
},
|
||||
|
||||
// 下拉刷新
|
||||
onPullDownRefresh() {
|
||||
this.loadReleases().finally(() => {
|
||||
wx.stopPullDownRefresh();
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,101 @@
|
||||
<!--pages/business/loan-releases/loan-releases.wxml-->
|
||||
<view class="loan-releases-container">
|
||||
<!-- 搜索栏 -->
|
||||
<view class="search-bar">
|
||||
<view class="search-input">
|
||||
<text class="search-icon">🔍</text>
|
||||
<input
|
||||
placeholder="搜索申请单号或客户姓名"
|
||||
value="{{searchKeyword}}"
|
||||
bindinput="onSearchInput"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 筛选栏 -->
|
||||
<view class="filter-bar">
|
||||
<picker
|
||||
bindchange="onFilterChange"
|
||||
value="{{filterStatus}}"
|
||||
range="{{['全部', '待处理', '处理中', '已通过', '已拒绝', '已完成', '已解押']}}"
|
||||
range-key=""
|
||||
>
|
||||
<view class="filter-item">
|
||||
<text>状态筛选</text>
|
||||
<text class="filter-value">{{filterStatus === 'all' ? '全部' : filterStatus}}</text>
|
||||
<text class="arrow">></text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<!-- 解押申请列表 -->
|
||||
<view class="release-list">
|
||||
<view
|
||||
class="release-item"
|
||||
wx:for="{{releases}}"
|
||||
wx:key="id"
|
||||
data-release-id="{{item.id}}"
|
||||
bindtap="viewReleaseDetail"
|
||||
>
|
||||
<view class="release-header">
|
||||
<text class="release-number">{{item.applicationNumber}}</text>
|
||||
<view class="release-status {{item.status}}">
|
||||
<text>{{item.statusText}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="release-info">
|
||||
<view class="info-row">
|
||||
<text class="info-label">客户姓名:</text>
|
||||
<text class="info-value">{{item.customerName}}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-row">
|
||||
<text class="info-label">产品名称:</text>
|
||||
<text class="info-value">{{item.productName}}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-row">
|
||||
<text class="info-label">抵押物类型:</text>
|
||||
<text class="info-value">{{item.assetType}}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-row">
|
||||
<text class="info-label">解押数量:</text>
|
||||
<text class="info-value">{{item.releaseQuantity}}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-row">
|
||||
<text class="info-label">解押金额:</text>
|
||||
<text class="info-value">{{item.releaseAmount}}元</text>
|
||||
</view>
|
||||
|
||||
<view class="info-row">
|
||||
<text class="info-label">申请时间:</text>
|
||||
<text class="info-value">{{item.applicationTime}}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-row">
|
||||
<text class="info-label">联系电话:</text>
|
||||
<text class="info-value">{{item.applicantPhone}}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-row" wx:if="{{item.processComment}}">
|
||||
<text class="info-label">处理意见:</text>
|
||||
<text class="info-value">{{item.processComment}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view class="loading" wx:if="{{loading}}">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" wx:if="{{!loading && releases.length === 0}}">
|
||||
<text class="empty-icon">🔓</text>
|
||||
<text class="empty-text">暂无解押申请数据</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,165 @@
|
||||
/* pages/business/loan-releases/loan-releases.wxss */
|
||||
.loan-releases-container {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
padding-bottom: 100rpx;
|
||||
}
|
||||
|
||||
/* 搜索栏 */
|
||||
.search-bar {
|
||||
padding: 20rpx 30rpx;
|
||||
background-color: #fff;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 25rpx;
|
||||
padding: 0 20rpx;
|
||||
height: 70rpx;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
margin-right: 15rpx;
|
||||
}
|
||||
|
||||
.search-input input {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 筛选栏 */
|
||||
.filter-bar {
|
||||
background-color: #fff;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 30rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.filter-value {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 解押申请列表 */
|
||||
.release-list {
|
||||
padding: 20rpx 30rpx;
|
||||
}
|
||||
|
||||
.release-item {
|
||||
background-color: #fff;
|
||||
margin-bottom: 20rpx;
|
||||
padding: 30rpx;
|
||||
border-radius: 8rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.release-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.release-number {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.release-status {
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 20rpx;
|
||||
font-size: 24rpx;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.release-status.pending {
|
||||
background-color: #faad14;
|
||||
}
|
||||
|
||||
.release-status.processing {
|
||||
background-color: #1890ff;
|
||||
}
|
||||
|
||||
.release-status.approved {
|
||||
background-color: #52c41a;
|
||||
}
|
||||
|
||||
.release-status.rejected {
|
||||
background-color: #f5222d;
|
||||
}
|
||||
|
||||
.release-status.completed {
|
||||
background-color: #13c2c2;
|
||||
}
|
||||
|
||||
.release-status.released {
|
||||
background-color: #722ed1;
|
||||
}
|
||||
|
||||
.release-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
color: #666;
|
||||
min-width: 140rpx;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 60rpx 0;
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 100rpx 0;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 80rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
@@ -1,69 +1,66 @@
|
||||
// pages/customers/customers.js
|
||||
const bankService = require('../../services/bankService.js')
|
||||
|
||||
Page({
|
||||
|
||||
/**
|
||||
* 页面的初始数据
|
||||
*/
|
||||
data: {
|
||||
searchKeyword: '',
|
||||
loading: false,
|
||||
customersList: []
|
||||
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.loadCustomersData()
|
||||
/**
|
||||
* 生命周期函数--监听页面加载
|
||||
*/
|
||||
onLoad(options) {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面初次渲染完成
|
||||
*/
|
||||
onReady() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面显示
|
||||
*/
|
||||
onShow() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面隐藏
|
||||
*/
|
||||
onHide() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面卸载
|
||||
*/
|
||||
onUnload() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面相关事件处理函数--监听用户下拉动作
|
||||
*/
|
||||
onPullDownRefresh() {
|
||||
this.loadCustomersData()
|
||||
setTimeout(() => {
|
||||
wx.stopPullDownRefresh()
|
||||
}, 1000)
|
||||
|
||||
},
|
||||
|
||||
loadCustomersData() {
|
||||
// 模拟数据
|
||||
this.setData({
|
||||
customersList: [
|
||||
{
|
||||
id: 1,
|
||||
name: '张三',
|
||||
phone: '13800138000',
|
||||
email: 'zhangsan@example.com',
|
||||
creditScore: 850,
|
||||
totalAssets: '¥125,680.50',
|
||||
status: 'active',
|
||||
statusText: '活跃'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '李四',
|
||||
phone: '13800138001',
|
||||
email: 'lisi@example.com',
|
||||
creditScore: 720,
|
||||
totalAssets: '¥89,450.00',
|
||||
status: 'active',
|
||||
statusText: '活跃'
|
||||
}
|
||||
]
|
||||
})
|
||||
/**
|
||||
* 页面上拉触底事件的处理函数
|
||||
*/
|
||||
onReachBottom() {
|
||||
|
||||
},
|
||||
|
||||
onSearchInput(e) {
|
||||
this.setData({
|
||||
searchKeyword: e.detail.value
|
||||
})
|
||||
},
|
||||
/**
|
||||
* 用户点击右上角分享
|
||||
*/
|
||||
onShareAppMessage() {
|
||||
|
||||
handleAdd() {
|
||||
wx.showToast({
|
||||
title: '新增功能待实现',
|
||||
icon: 'none'
|
||||
})
|
||||
},
|
||||
|
||||
handleCustomerTap(e) {
|
||||
const { customerId } = e.currentTarget.dataset
|
||||
wx.navigateTo({
|
||||
url: `/pages/customers/detail?id=${customerId}`
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,64 +1,2 @@
|
||||
<!--pages/customers/customers.wxml-->
|
||||
<view class="customers-container">
|
||||
<!-- 搜索栏 -->
|
||||
<view class="search-section">
|
||||
<view class="search-bar">
|
||||
<input
|
||||
value="{{searchKeyword}}"
|
||||
type="text"
|
||||
placeholder="搜索客户..."
|
||||
class="search-input"
|
||||
bindinput="onSearchInput"
|
||||
/>
|
||||
<view class="search-icon">🔍</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 客户列表 -->
|
||||
<view class="customers-list">
|
||||
<view class="list-header">
|
||||
<view class="list-title">客户管理</view>
|
||||
<view class="add-btn" bindtap="handleAdd">
|
||||
<text class="add-text">新增</text>
|
||||
<view class="add-icon">+</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{loading}}" class="loading">
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<view wx:elif="{{customersList.length === 0}}" class="empty">
|
||||
<view class="empty-icon">👥</view>
|
||||
<view class="empty-text">暂无客户记录</view>
|
||||
</view>
|
||||
|
||||
<view wx:else class="list-content">
|
||||
<view
|
||||
wx:for="{{customersList}}"
|
||||
wx:key="id"
|
||||
class="customer-item"
|
||||
data-customer-id="{{item.id}}"
|
||||
bindtap="handleCustomerTap"
|
||||
>
|
||||
<view class="item-avatar">
|
||||
<image src="/images/avatar.png" class="avatar-img" />
|
||||
</view>
|
||||
<view class="item-content">
|
||||
<view class="item-header">
|
||||
<view class="item-name">{{item.name}}</view>
|
||||
<view class="item-status {{item.status}}">
|
||||
{{item.statusText}}
|
||||
</view>
|
||||
</view>
|
||||
<view class="item-info">
|
||||
<view class="item-phone">{{item.phone}}</view>
|
||||
<view class="item-email">{{item.email}}</view>
|
||||
<view class="item-assets">总资产: {{item.totalAssets}}</view>
|
||||
<view class="item-credit">信用评分: {{item.creditScore}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<text>pages/customers/customers.wxml</text>
|
||||
@@ -1,166 +0,0 @@
|
||||
/* pages/customers/customers.wxss */
|
||||
.customers-container {
|
||||
min-height: 100vh;
|
||||
background: #f6f6f6;
|
||||
}
|
||||
|
||||
.search-section {
|
||||
padding: 20rpx;
|
||||
background: #fff;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
height: 72rpx;
|
||||
background: #f8f9fa;
|
||||
border: none;
|
||||
border-radius: 36rpx;
|
||||
padding: 0 60rpx 0 30rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
right: 30rpx;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.customers-list {
|
||||
background: #fff;
|
||||
margin: 0 20rpx;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.list-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 30rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.list-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16rpx 24rpx;
|
||||
background: #1890ff;
|
||||
border-radius: 24rpx;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.add-text {
|
||||
font-size: 24rpx;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.add-icon {
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.loading,
|
||||
.empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 80rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.loading-text,
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 80rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.list-content {
|
||||
space-y: 0;
|
||||
}
|
||||
|
||||
.customer-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 30rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.customer-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.item-avatar {
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.avatar-img {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 40rpx;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.item-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.item-status {
|
||||
font-size: 20rpx;
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 20rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.item-status.active {
|
||||
background: #f6ffed;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.item-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
space-y: 8rpx;
|
||||
}
|
||||
|
||||
.item-phone,
|
||||
.item-email,
|
||||
.item-assets,
|
||||
.item-credit {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
@@ -1,87 +1,66 @@
|
||||
// pages/dashboard/dashboard.js
|
||||
const bankService = require('../../services/bankService.js')
|
||||
|
||||
Page({
|
||||
|
||||
/**
|
||||
* 页面的初始数据
|
||||
*/
|
||||
data: {
|
||||
overviewCards: [
|
||||
{
|
||||
key: 'totalAssets',
|
||||
icon: '💰',
|
||||
label: '总资产',
|
||||
value: '¥1,234,567.89',
|
||||
trend: 'up',
|
||||
trendText: '+2.5%',
|
||||
type: 'primary'
|
||||
},
|
||||
{
|
||||
key: 'monthlyIncome',
|
||||
icon: '📈',
|
||||
label: '月收入',
|
||||
value: '¥85,000.00',
|
||||
trend: 'up',
|
||||
trendText: '+5.2%',
|
||||
type: 'success'
|
||||
},
|
||||
{
|
||||
key: 'activeCustomers',
|
||||
icon: '👥',
|
||||
label: '活跃客户',
|
||||
value: '1,234',
|
||||
trend: 'up',
|
||||
trendText: '+12',
|
||||
type: 'warning'
|
||||
},
|
||||
{
|
||||
key: 'riskLevel',
|
||||
icon: '⚠️',
|
||||
label: '风险等级',
|
||||
value: '低',
|
||||
trend: 'down',
|
||||
trendText: '-0.5%',
|
||||
type: 'danger'
|
||||
}
|
||||
],
|
||||
chartData: {
|
||||
labels: ['1月', '2月', '3月', '4月', '5月', '6月'],
|
||||
income: [65000, 72000, 68000, 75000, 82000, 85000],
|
||||
expense: [45000, 48000, 52000, 49000, 55000, 58000]
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.loadDashboardData()
|
||||
/**
|
||||
* 生命周期函数--监听页面加载
|
||||
*/
|
||||
onLoad(options) {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面初次渲染完成
|
||||
*/
|
||||
onReady() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面显示
|
||||
*/
|
||||
onShow() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面隐藏
|
||||
*/
|
||||
onHide() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面卸载
|
||||
*/
|
||||
onUnload() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面相关事件处理函数--监听用户下拉动作
|
||||
*/
|
||||
onPullDownRefresh() {
|
||||
this.loadDashboardData()
|
||||
setTimeout(() => {
|
||||
wx.stopPullDownRefresh()
|
||||
}, 1000)
|
||||
|
||||
},
|
||||
|
||||
async loadDashboardData() {
|
||||
try {
|
||||
const data = await bankService.getDashboardData()
|
||||
if (data) {
|
||||
this.updateOverviewCards(data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载仪表板数据失败:', error)
|
||||
wx.showToast({
|
||||
title: '加载数据失败',
|
||||
icon: 'error'
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 页面上拉触底事件的处理函数
|
||||
*/
|
||||
onReachBottom() {
|
||||
|
||||
},
|
||||
|
||||
updateOverviewCards(data) {
|
||||
const overviewCards = this.data.overviewCards.map(card => {
|
||||
const newValue = data[card.key] || card.value
|
||||
return {
|
||||
...card,
|
||||
value: newValue
|
||||
}
|
||||
})
|
||||
this.setData({ overviewCards })
|
||||
|
||||
/**
|
||||
* 用户点击右上角分享
|
||||
*/
|
||||
onShareAppMessage() {
|
||||
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,66 +1,2 @@
|
||||
<!--pages/dashboard/dashboard.wxml-->
|
||||
<view class="dashboard-container">
|
||||
<!-- 数据概览卡片 -->
|
||||
<view class="overview-cards">
|
||||
<view
|
||||
wx:for="{{overviewCards}}"
|
||||
wx:key="key"
|
||||
class="overview-card {{item.type}}"
|
||||
>
|
||||
<view class="card-icon">{{item.icon}}</view>
|
||||
<view class="card-content">
|
||||
<view class="card-value">{{item.value}}</view>
|
||||
<view class="card-label">{{item.label}}</view>
|
||||
<view class="card-trend {{item.trend}}">
|
||||
{{item.trendText}}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 图表区域 -->
|
||||
<view class="charts-section">
|
||||
<view class="section-title">收入支出趋势</view>
|
||||
<view class="chart-container">
|
||||
<view class="chart-placeholder">
|
||||
<view class="chart-icon">📊</view>
|
||||
<view class="chart-text">图表数据加载中...</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 快速统计 -->
|
||||
<view class="quick-stats">
|
||||
<view class="section-title">快速统计</view>
|
||||
<view class="stats-grid">
|
||||
<view class="stat-item">
|
||||
<view class="stat-icon">💳</view>
|
||||
<view class="stat-info">
|
||||
<view class="stat-value">1,234</view>
|
||||
<view class="stat-label">今日交易</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<view class="stat-icon">👥</view>
|
||||
<view class="stat-info">
|
||||
<view class="stat-value">89</view>
|
||||
<view class="stat-label">新增客户</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<view class="stat-icon">💰</view>
|
||||
<view class="stat-info">
|
||||
<view class="stat-value">¥2.5M</view>
|
||||
<view class="stat-label">今日流水</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<view class="stat-icon">📈</view>
|
||||
<view class="stat-info">
|
||||
<view class="stat-value">+15%</view>
|
||||
<view class="stat-label">增长率</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<text>pages/dashboard/dashboard.wxml</text>
|
||||
@@ -1,147 +0,0 @@
|
||||
/* pages/dashboard/dashboard.wxss */
|
||||
.dashboard-container {
|
||||
min-height: 100vh;
|
||||
background: #f6f6f6;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.overview-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 20rpx;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.overview-card {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.overview-card.primary {
|
||||
border-left: 8rpx solid #1890ff;
|
||||
}
|
||||
|
||||
.overview-card.success {
|
||||
border-left: 8rpx solid #52c41a;
|
||||
}
|
||||
|
||||
.overview-card.warning {
|
||||
border-left: 8rpx solid #faad14;
|
||||
}
|
||||
|
||||
.overview-card.danger {
|
||||
border-left: 8rpx solid #ff4d4f;
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
font-size: 48rpx;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.card-value {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.card-label {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.card-trend {
|
||||
font-size: 20rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.card-trend.up {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.card-trend.down {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.charts-section,
|
||||
.quick-stats {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 400rpx;
|
||||
background: #f8f9fa;
|
||||
border-radius: 12rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.chart-placeholder {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.chart-icon {
|
||||
font-size: 80rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.chart-text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx;
|
||||
background: #f8f9fa;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
font-size: 32rpx;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.stat-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 22rpx;
|
||||
color: #666;
|
||||
}
|
||||
@@ -1,88 +1,66 @@
|
||||
// pages/profile/profile.js
|
||||
const auth = require('../../utils/auth.js')
|
||||
|
||||
Page({
|
||||
|
||||
/**
|
||||
* 页面的初始数据
|
||||
*/
|
||||
data: {
|
||||
userInfo: {},
|
||||
menuItems: [
|
||||
{
|
||||
key: 'settings',
|
||||
title: '设置',
|
||||
icon: '⚙️',
|
||||
path: ''
|
||||
},
|
||||
{
|
||||
key: 'about',
|
||||
title: '关于',
|
||||
icon: 'ℹ️',
|
||||
path: ''
|
||||
},
|
||||
{
|
||||
key: 'help',
|
||||
title: '帮助',
|
||||
icon: '❓',
|
||||
path: ''
|
||||
},
|
||||
{
|
||||
key: 'feedback',
|
||||
title: '反馈',
|
||||
icon: '💬',
|
||||
path: ''
|
||||
}
|
||||
]
|
||||
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.loadUserInfo()
|
||||
/**
|
||||
* 生命周期函数--监听页面加载
|
||||
*/
|
||||
onLoad(options) {
|
||||
|
||||
},
|
||||
|
||||
loadUserInfo() {
|
||||
const userInfo = auth.getUser()
|
||||
this.setData({
|
||||
userInfo: userInfo || {}
|
||||
})
|
||||
/**
|
||||
* 生命周期函数--监听页面初次渲染完成
|
||||
*/
|
||||
onReady() {
|
||||
|
||||
},
|
||||
|
||||
handleMenuTap(e) {
|
||||
const { key } = e.currentTarget.dataset
|
||||
|
||||
switch (key) {
|
||||
case 'settings':
|
||||
wx.showToast({
|
||||
title: '设置功能待实现',
|
||||
icon: 'none'
|
||||
})
|
||||
break
|
||||
case 'about':
|
||||
wx.showToast({
|
||||
title: '关于功能待实现',
|
||||
icon: 'none'
|
||||
})
|
||||
break
|
||||
case 'help':
|
||||
wx.showToast({
|
||||
title: '帮助功能待实现',
|
||||
icon: 'none'
|
||||
})
|
||||
break
|
||||
case 'feedback':
|
||||
wx.showToast({
|
||||
title: '反馈功能待实现',
|
||||
icon: 'none'
|
||||
})
|
||||
break
|
||||
}
|
||||
/**
|
||||
* 生命周期函数--监听页面显示
|
||||
*/
|
||||
onShow() {
|
||||
|
||||
},
|
||||
|
||||
handleLogout() {
|
||||
wx.showModal({
|
||||
title: '确认退出',
|
||||
content: '确定要退出登录吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
auth.logout()
|
||||
}
|
||||
}
|
||||
})
|
||||
/**
|
||||
* 生命周期函数--监听页面隐藏
|
||||
*/
|
||||
onHide() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面卸载
|
||||
*/
|
||||
onUnload() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面相关事件处理函数--监听用户下拉动作
|
||||
*/
|
||||
onPullDownRefresh() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面上拉触底事件的处理函数
|
||||
*/
|
||||
onReachBottom() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 用户点击右上角分享
|
||||
*/
|
||||
onShareAppMessage() {
|
||||
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,36 +1,2 @@
|
||||
<!--pages/profile/profile.wxml-->
|
||||
<view class="profile-container">
|
||||
<!-- 用户信息区域 -->
|
||||
<view class="user-section">
|
||||
<view class="user-avatar">
|
||||
<image src="/images/avatar.png" class="avatar-img" />
|
||||
</view>
|
||||
<view class="user-info">
|
||||
<view class="username">{{userInfo.name || '银行管理员'}}</view>
|
||||
<view class="user-role">{{userInfo.role || '客户经理'}}</view>
|
||||
<view class="user-phone">{{userInfo.phone || '13800138000'}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 功能菜单 -->
|
||||
<view class="menu-section">
|
||||
<view
|
||||
wx:for="{{menuItems}}"
|
||||
wx:key="key"
|
||||
class="menu-item"
|
||||
data-key="{{item.key}}"
|
||||
bindtap="handleMenuTap"
|
||||
>
|
||||
<view class="menu-icon">{{item.icon}}</view>
|
||||
<view class="menu-title">{{item.title}}</view>
|
||||
<view class="menu-arrow">></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 退出登录 -->
|
||||
<view class="logout-section">
|
||||
<button class="logout-btn" bindtap="handleLogout">
|
||||
退出登录
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
<text>pages/profile/profile.wxml</text>
|
||||
@@ -1,103 +0,0 @@
|
||||
/* pages/profile/profile.wxss */
|
||||
.profile-container {
|
||||
min-height: 100vh;
|
||||
background: #f6f6f6;
|
||||
}
|
||||
|
||||
.user-section {
|
||||
background: #fff;
|
||||
padding: 60rpx 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
margin-right: 30rpx;
|
||||
}
|
||||
|
||||
.avatar-img {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 60rpx;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.user-role {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.user-phone {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.menu-section {
|
||||
background: #fff;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 30rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.menu-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.menu-item:active {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
font-size: 32rpx;
|
||||
margin-right: 24rpx;
|
||||
width: 40rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.menu-title {
|
||||
flex: 1;
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.menu-arrow {
|
||||
font-size: 24rpx;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.logout-section {
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
background: #ff4d4f;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 44rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
133
bank_mini_program/pages/projects/projects.js
Normal file
133
bank_mini_program/pages/projects/projects.js
Normal file
@@ -0,0 +1,133 @@
|
||||
// pages/projects/projects.js
|
||||
const { apiService } = require('../../services/apiService');
|
||||
|
||||
Page({
|
||||
data: {
|
||||
projects: [],
|
||||
activeTab: 'all',
|
||||
loading: false,
|
||||
statusMap: {
|
||||
'supervision': '监管中',
|
||||
'completed': '已结项',
|
||||
'pending': '待监管'
|
||||
}
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.loadProjects();
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.loadProjects();
|
||||
},
|
||||
|
||||
// 加载项目数据
|
||||
async loadProjects() {
|
||||
this.setData({ loading: true });
|
||||
|
||||
try {
|
||||
const params = {
|
||||
page: 1,
|
||||
limit: 20,
|
||||
status: this.data.activeTab === 'all' ? '' : this.data.activeTab
|
||||
};
|
||||
|
||||
const response = await apiService.projects.getList(params);
|
||||
|
||||
if (response.success) {
|
||||
const projects = response.data.projects.map(project => ({
|
||||
...project,
|
||||
statusText: this.data.statusMap[project.status] || project.status,
|
||||
showDeviceInfo: project.status === 'completed' // 只有已结项的项目显示设备信息
|
||||
}));
|
||||
|
||||
this.setData({
|
||||
projects,
|
||||
loading: false
|
||||
});
|
||||
} else {
|
||||
throw new Error(response.message || '获取项目列表失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载项目失败:', error);
|
||||
|
||||
// 使用模拟数据作为降级处理
|
||||
const mockProjects = [
|
||||
{
|
||||
id: 1,
|
||||
farmName: '大数据中心',
|
||||
loanOfficer: '1',
|
||||
status: 'completed',
|
||||
statusText: '已结项',
|
||||
supervisionObject: '牛',
|
||||
supervisionQuantity: 10,
|
||||
supervisionPeriod: '23天',
|
||||
supervisionAmount: '10000.00',
|
||||
startTime: '2024-02-21',
|
||||
endTime: '2024-03-15',
|
||||
earTag: 0,
|
||||
collar: 0,
|
||||
host: 0,
|
||||
showDeviceInfo: true
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
farmName: '大数据中心',
|
||||
loanOfficer: '1',
|
||||
status: 'completed',
|
||||
statusText: '已结项',
|
||||
supervisionObject: '牛',
|
||||
supervisionQuantity: 0,
|
||||
supervisionPeriod: '0天',
|
||||
supervisionAmount: '0.00',
|
||||
startTime: '2024-02-26',
|
||||
endTime: '2024-02-27',
|
||||
earTag: 0,
|
||||
collar: 0,
|
||||
host: 0,
|
||||
showDeviceInfo: false
|
||||
}
|
||||
];
|
||||
|
||||
this.setData({
|
||||
projects: mockProjects,
|
||||
loading: false
|
||||
});
|
||||
|
||||
wx.showToast({
|
||||
title: '使用模拟数据',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 切换标签
|
||||
onTabChange(e) {
|
||||
const tab = e.currentTarget.dataset.tab;
|
||||
this.setData({
|
||||
activeTab: tab
|
||||
});
|
||||
this.loadProjects();
|
||||
},
|
||||
|
||||
// 查看项目详情
|
||||
viewProjectDetail(e) {
|
||||
const projectId = e.currentTarget.dataset.projectId;
|
||||
wx.navigateTo({
|
||||
url: `/pages/projects/detail?projectId=${projectId}`
|
||||
});
|
||||
},
|
||||
|
||||
// 下拉刷新
|
||||
onPullDownRefresh() {
|
||||
this.loadProjects().finally(() => {
|
||||
wx.stopPullDownRefresh();
|
||||
});
|
||||
},
|
||||
|
||||
// 上拉加载更多
|
||||
onReachBottom() {
|
||||
// 这里可以实现分页加载
|
||||
console.log('加载更多项目');
|
||||
}
|
||||
});
|
||||
3
bank_mini_program/pages/projects/projects.json
Normal file
3
bank_mini_program/pages/projects/projects.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"usingComponents": {}
|
||||
}
|
||||
131
bank_mini_program/pages/projects/projects.wxml
Normal file
131
bank_mini_program/pages/projects/projects.wxml
Normal file
@@ -0,0 +1,131 @@
|
||||
<!--pages/projects/projects.wxml-->
|
||||
<view class="projects-container">
|
||||
|
||||
|
||||
<!-- 标题栏 -->
|
||||
<view class="header">
|
||||
<text class="title">项目清单</text>
|
||||
|
||||
</view>
|
||||
|
||||
<!-- 筛选标签 -->
|
||||
<view class="filter-tabs">
|
||||
<view
|
||||
class="tab-item {{activeTab === 'all' ? 'active' : ''}}"
|
||||
data-tab="all"
|
||||
bindtap="onTabChange"
|
||||
>
|
||||
<text>全部</text>
|
||||
</view>
|
||||
<view
|
||||
class="tab-item {{activeTab === 'supervision' ? 'active' : ''}}"
|
||||
data-tab="supervision"
|
||||
bindtap="onTabChange"
|
||||
>
|
||||
<text>监管中</text>
|
||||
</view>
|
||||
<view
|
||||
class="tab-item {{activeTab === 'pending' ? 'active' : ''}}"
|
||||
data-tab="pending"
|
||||
bindtap="onTabChange"
|
||||
>
|
||||
<text>待监管</text>
|
||||
</view>
|
||||
<view
|
||||
class="tab-item {{activeTab === 'completed' ? 'active' : ''}}"
|
||||
data-tab="completed"
|
||||
bindtap="onTabChange"
|
||||
>
|
||||
<text>已结项</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 项目列表 -->
|
||||
<view class="project-list">
|
||||
<view
|
||||
class="project-item"
|
||||
wx:for="{{projects}}"
|
||||
wx:key="id"
|
||||
data-project-id="{{item.id}}"
|
||||
bindtap="viewProjectDetail"
|
||||
>
|
||||
<!-- 项目标题 -->
|
||||
<view class="project-title">{{item.farmName}}</view>
|
||||
|
||||
<!-- 项目信息 -->
|
||||
<view class="project-info">
|
||||
<view class="info-row">
|
||||
<text class="info-label">法人/负责人:</text>
|
||||
<text class="info-value">{{item.loanOfficer || '1'}}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-row">
|
||||
<text class="info-label">当前状态:</text>
|
||||
<view class="status-tag {{item.status}}">
|
||||
<text>{{item.statusText}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="info-row">
|
||||
<text class="info-label">监管对象:</text>
|
||||
<text class="info-value">{{item.supervisionObject}}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-row">
|
||||
<text class="info-label">监管数量:</text>
|
||||
<text class="info-value">{{item.supervisionQuantity}}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-row">
|
||||
<text class="info-label">监管周期:</text>
|
||||
<text class="info-value">{{item.supervisionPeriod}}</text>
|
||||
</view>
|
||||
|
||||
<view class="info-row">
|
||||
<text class="info-label">监管金额:</text>
|
||||
<text class="info-value">{{item.supervisionAmount}}元</text>
|
||||
</view>
|
||||
|
||||
<view class="info-row">
|
||||
<text class="info-label">起止时间:</text>
|
||||
<text class="info-value">{{item.startTime}} 至 {{item.endTime}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 设备信息 -->
|
||||
<view class="device-info" wx:if="{{item.showDeviceInfo}}">
|
||||
<view class="device-row">
|
||||
<text class="device-label">脚环:</text>
|
||||
<text class="device-value">{{item.earTag || 0}}</text>
|
||||
</view>
|
||||
<view class="device-row">
|
||||
<text class="device-label">耳标:</text>
|
||||
<text class="device-value">{{item.earTag || 0}}</text>
|
||||
</view>
|
||||
<view class="device-row">
|
||||
<text class="device-label">项圈:</text>
|
||||
<text class="device-value">{{item.collar || 0}}</text>
|
||||
</view>
|
||||
<view class="device-row">
|
||||
<text class="device-label">主机:</text>
|
||||
<text class="device-value">{{item.host || 0}}</text>
|
||||
</view>
|
||||
<view class="device-row">
|
||||
<text class="device-label">饲喂机:</text>
|
||||
<text class="device-value">0</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view class="loading" wx:if="{{loading}}">
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" wx:if="{{!loading && projects.length === 0}}">
|
||||
<text class="empty-icon">📋</text>
|
||||
<text class="empty-text">暂无项目数据</text>
|
||||
</view>
|
||||
</view>
|
||||
218
bank_mini_program/pages/projects/projects.wxss
Normal file
218
bank_mini_program/pages/projects/projects.wxss
Normal file
@@ -0,0 +1,218 @@
|
||||
/* pages/projects/projects.wxss */
|
||||
.projects-container {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
padding-bottom: 100rpx;
|
||||
}
|
||||
|
||||
/* 状态栏 */
|
||||
.status-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20rpx 30rpx;
|
||||
background-color: #fff;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.status-icons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15rpx;
|
||||
}
|
||||
|
||||
.icon, .signal, .wifi, .network, .signal-bars, .battery {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 标题栏 */
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 30rpx;
|
||||
background-color: #fff;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header-icons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
font-size: 32rpx;
|
||||
color: #666;
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 30rpx;
|
||||
}
|
||||
|
||||
/* 筛选标签 */
|
||||
.filter-tabs {
|
||||
display: flex;
|
||||
background-color: #fff;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
padding: 0 30rpx;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 30rpx 0;
|
||||
position: relative;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.tab-item.active {
|
||||
color: #1890ff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tab-item.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 60rpx;
|
||||
height: 4rpx;
|
||||
background-color: #1890ff;
|
||||
border-radius: 2rpx;
|
||||
}
|
||||
|
||||
/* 项目列表 */
|
||||
.project-list {
|
||||
padding: 20rpx 30rpx;
|
||||
}
|
||||
|
||||
.project-item {
|
||||
background-color: #fff;
|
||||
margin-bottom: 20rpx;
|
||||
padding: 30rpx;
|
||||
border-radius: 8rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.project-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.project-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15rpx;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
color: #666;
|
||||
min-width: 160rpx;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 20rpx;
|
||||
font-size: 24rpx;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.status-tag.supervision {
|
||||
background-color: #1890ff;
|
||||
}
|
||||
|
||||
.status-tag.completed {
|
||||
background-color: #52c41a;
|
||||
}
|
||||
|
||||
.status-tag.pending {
|
||||
background-color: #faad14;
|
||||
}
|
||||
|
||||
/* 设备信息 */
|
||||
.device-info {
|
||||
margin-top: 20rpx;
|
||||
padding-top: 20rpx;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.device-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 24rpx;
|
||||
min-width: 120rpx;
|
||||
}
|
||||
|
||||
.device-label {
|
||||
color: #999;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.device-value {
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 60rpx 0;
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 100rpx 0;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 80rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
@@ -1,51 +1,66 @@
|
||||
// pages/reports/reports.js
|
||||
const bankService = require('../../services/bankService.js')
|
||||
|
||||
Page({
|
||||
|
||||
/**
|
||||
* 页面的初始数据
|
||||
*/
|
||||
data: {
|
||||
searchKeyword: '',
|
||||
loading: false,
|
||||
reportsList: []
|
||||
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.loadReportsData()
|
||||
/**
|
||||
* 生命周期函数--监听页面加载
|
||||
*/
|
||||
onLoad(options) {
|
||||
|
||||
},
|
||||
|
||||
loadReportsData() {
|
||||
// 模拟数据
|
||||
this.setData({
|
||||
reportsList: [
|
||||
{
|
||||
id: 1,
|
||||
title: '月度财务报表',
|
||||
type: 'monthly',
|
||||
createTime: '2024-01-15 10:00',
|
||||
status: 'completed',
|
||||
statusText: '已完成'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '风险分析报告',
|
||||
type: 'risk',
|
||||
createTime: '2024-01-14 15:30',
|
||||
status: 'pending',
|
||||
statusText: '生成中'
|
||||
}
|
||||
]
|
||||
})
|
||||
/**
|
||||
* 生命周期函数--监听页面初次渲染完成
|
||||
*/
|
||||
onReady() {
|
||||
|
||||
},
|
||||
|
||||
onSearchInput(e) {
|
||||
this.setData({
|
||||
searchKeyword: e.detail.value
|
||||
})
|
||||
/**
|
||||
* 生命周期函数--监听页面显示
|
||||
*/
|
||||
onShow() {
|
||||
|
||||
},
|
||||
|
||||
handleReportTap(e) {
|
||||
const { reportId } = e.currentTarget.dataset
|
||||
wx.navigateTo({
|
||||
url: `/pages/reports/detail?id=${reportId}`
|
||||
})
|
||||
/**
|
||||
* 生命周期函数--监听页面隐藏
|
||||
*/
|
||||
onHide() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面卸载
|
||||
*/
|
||||
onUnload() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面相关事件处理函数--监听用户下拉动作
|
||||
*/
|
||||
onPullDownRefresh() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面上拉触底事件的处理函数
|
||||
*/
|
||||
onReachBottom() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 用户点击右上角分享
|
||||
*/
|
||||
onShareAppMessage() {
|
||||
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,55 +1,2 @@
|
||||
<!--pages/reports/reports.wxml-->
|
||||
<view class="reports-container">
|
||||
<!-- 搜索栏 -->
|
||||
<view class="search-section">
|
||||
<view class="search-bar">
|
||||
<input
|
||||
value="{{searchKeyword}}"
|
||||
type="text"
|
||||
placeholder="搜索报表..."
|
||||
class="search-input"
|
||||
bindinput="onSearchInput"
|
||||
/>
|
||||
<view class="search-icon">🔍</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 报表列表 -->
|
||||
<view class="reports-list">
|
||||
<view class="list-header">
|
||||
<view class="list-title">报表分析</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{loading}}" class="loading">
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<view wx:elif="{{reportsList.length === 0}}" class="empty">
|
||||
<view class="empty-icon">📈</view>
|
||||
<view class="empty-text">暂无报表记录</view>
|
||||
</view>
|
||||
|
||||
<view wx:else class="list-content">
|
||||
<view
|
||||
wx:for="{{reportsList}}"
|
||||
wx:key="id"
|
||||
class="report-item"
|
||||
data-report-id="{{item.id}}"
|
||||
bindtap="handleReportTap"
|
||||
>
|
||||
<view class="report-icon">
|
||||
<text wx:if="{{item.type === 'monthly'}}">📊</text>
|
||||
<text wx:elif="{{item.type === 'risk'}}">⚠️</text>
|
||||
<text wx:else>📋</text>
|
||||
</view>
|
||||
<view class="report-content">
|
||||
<view class="report-title">{{item.title}}</view>
|
||||
<view class="report-time">{{item.createTime}}</view>
|
||||
</view>
|
||||
<view class="report-status {{item.status}}">
|
||||
{{item.statusText}}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<text>pages/reports/reports.wxml</text>
|
||||
@@ -1,137 +0,0 @@
|
||||
/* pages/reports/reports.wxss */
|
||||
.reports-container {
|
||||
min-height: 100vh;
|
||||
background: #f6f6f6;
|
||||
}
|
||||
|
||||
.search-section {
|
||||
padding: 20rpx;
|
||||
background: #fff;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
height: 72rpx;
|
||||
background: #f8f9fa;
|
||||
border: none;
|
||||
border-radius: 36rpx;
|
||||
padding: 0 60rpx 0 30rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
right: 30rpx;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.reports-list {
|
||||
background: #fff;
|
||||
margin: 0 20rpx;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.list-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 30rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.list-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.loading,
|
||||
.empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 80rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.loading-text,
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 80rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.list-content {
|
||||
space-y: 0;
|
||||
}
|
||||
|
||||
.report-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 30rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.report-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.report-icon {
|
||||
font-size: 32rpx;
|
||||
margin-right: 20rpx;
|
||||
width: 60rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.report-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.report-title {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.report-time {
|
||||
font-size: 20rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.report-status {
|
||||
font-size: 20rpx;
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 20rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.report-status.completed {
|
||||
background: #f6ffed;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.report-status.pending {
|
||||
background: #fff7e6;
|
||||
color: #faad14;
|
||||
}
|
||||
|
||||
.report-status.failed {
|
||||
background: #fff2f0;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
@@ -1,51 +1,66 @@
|
||||
// pages/risk/risk.js
|
||||
const bankService = require('../../services/bankService.js')
|
||||
|
||||
Page({
|
||||
|
||||
/**
|
||||
* 页面的初始数据
|
||||
*/
|
||||
data: {
|
||||
searchKeyword: '',
|
||||
loading: false,
|
||||
riskData: {
|
||||
overallRisk: '低',
|
||||
riskScore: 85,
|
||||
riskItems: [
|
||||
{
|
||||
id: 1,
|
||||
title: '信用风险',
|
||||
level: '低',
|
||||
score: 90,
|
||||
description: '客户信用状况良好'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '市场风险',
|
||||
level: '中',
|
||||
score: 75,
|
||||
description: '市场波动影响较小'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '操作风险',
|
||||
level: '低',
|
||||
score: 88,
|
||||
description: '操作流程规范'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.loadRiskData()
|
||||
/**
|
||||
* 生命周期函数--监听页面加载
|
||||
*/
|
||||
onLoad(options) {
|
||||
|
||||
},
|
||||
|
||||
loadRiskData() {
|
||||
// 模拟数据加载
|
||||
console.log('风险数据加载完成')
|
||||
/**
|
||||
* 生命周期函数--监听页面初次渲染完成
|
||||
*/
|
||||
onReady() {
|
||||
|
||||
},
|
||||
|
||||
onSearchInput(e) {
|
||||
this.setData({
|
||||
searchKeyword: e.detail.value
|
||||
})
|
||||
/**
|
||||
* 生命周期函数--监听页面显示
|
||||
*/
|
||||
onShow() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面隐藏
|
||||
*/
|
||||
onHide() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面卸载
|
||||
*/
|
||||
onUnload() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面相关事件处理函数--监听用户下拉动作
|
||||
*/
|
||||
onPullDownRefresh() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面上拉触底事件的处理函数
|
||||
*/
|
||||
onReachBottom() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 用户点击右上角分享
|
||||
*/
|
||||
onShareAppMessage() {
|
||||
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,36 +1,2 @@
|
||||
<!--pages/risk/risk.wxml-->
|
||||
<view class="risk-container">
|
||||
<!-- 风险概览 -->
|
||||
<view class="risk-overview">
|
||||
<view class="overview-card">
|
||||
<view class="overview-title">整体风险等级</view>
|
||||
<view class="overview-level {{riskData.overallRisk === '低' ? 'low' : riskData.overallRisk === '中' ? 'medium' : 'high'}}">
|
||||
{{riskData.overallRisk}}
|
||||
</view>
|
||||
<view class="overview-score">风险评分: {{riskData.riskScore}}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 风险项目列表 -->
|
||||
<view class="risk-items">
|
||||
<view class="section-title">风险项目</view>
|
||||
<view class="risk-list">
|
||||
<view
|
||||
wx:for="{{riskData.riskItems}}"
|
||||
wx:key="id"
|
||||
class="risk-item"
|
||||
>
|
||||
<view class="risk-header">
|
||||
<view class="risk-title">{{item.title}}</view>
|
||||
<view class="risk-level {{item.level === '低' ? 'low' : item.level === '中' ? 'medium' : 'high'}}">
|
||||
{{item.level}}
|
||||
</view>
|
||||
</view>
|
||||
<view class="risk-content">
|
||||
<view class="risk-score">评分: {{item.score}}</view>
|
||||
<view class="risk-desc">{{item.description}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<text>pages/risk/risk.wxml</text>
|
||||
@@ -1,127 +0,0 @@
|
||||
/* pages/risk/risk.wxss */
|
||||
.risk-container {
|
||||
min-height: 100vh;
|
||||
background: #f6f6f6;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.risk-overview {
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.overview-card {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 40rpx;
|
||||
text-align: center;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.overview-title {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.overview-level {
|
||||
font-size: 48rpx;
|
||||
font-weight: 700;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.overview-level.low {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.overview-level.medium {
|
||||
color: #faad14;
|
||||
}
|
||||
|
||||
.overview-level.high {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.overview-score {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.risk-items {
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 30rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.risk-list {
|
||||
space-y: 20rpx;
|
||||
}
|
||||
|
||||
.risk-item {
|
||||
padding: 30rpx;
|
||||
background: #f8f9fa;
|
||||
border-radius: 12rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.risk-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.risk-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.risk-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.risk-level {
|
||||
font-size: 20rpx;
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 20rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.risk-level.low {
|
||||
background: #f6ffed;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.risk-level.medium {
|
||||
background: #fff7e6;
|
||||
color: #faad14;
|
||||
}
|
||||
|
||||
.risk-level.high {
|
||||
background: #fff2f0;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.risk-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
space-y: 8rpx;
|
||||
}
|
||||
|
||||
.risk-score {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.risk-desc {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
@@ -1,53 +1,66 @@
|
||||
// pages/transactions/transactions.js
|
||||
const bankService = require('../../services/bankService.js')
|
||||
|
||||
Page({
|
||||
|
||||
/**
|
||||
* 页面的初始数据
|
||||
*/
|
||||
data: {
|
||||
searchKeyword: '',
|
||||
loading: false,
|
||||
transactionsList: []
|
||||
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.loadTransactionsData()
|
||||
/**
|
||||
* 生命周期函数--监听页面加载
|
||||
*/
|
||||
onLoad(options) {
|
||||
|
||||
},
|
||||
|
||||
loadTransactionsData() {
|
||||
// 模拟数据
|
||||
this.setData({
|
||||
transactionsList: [
|
||||
{
|
||||
id: 1,
|
||||
type: 'income',
|
||||
title: '工资收入',
|
||||
amount: '+8,500.00',
|
||||
time: '2024-01-15 09:00',
|
||||
status: 'success',
|
||||
statusText: '成功'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: 'expense',
|
||||
title: '生活缴费',
|
||||
amount: '-1,200.00',
|
||||
time: '2024-01-14 14:30',
|
||||
status: 'success',
|
||||
statusText: '成功'
|
||||
}
|
||||
]
|
||||
})
|
||||
/**
|
||||
* 生命周期函数--监听页面初次渲染完成
|
||||
*/
|
||||
onReady() {
|
||||
|
||||
},
|
||||
|
||||
onSearchInput(e) {
|
||||
this.setData({
|
||||
searchKeyword: e.detail.value
|
||||
})
|
||||
/**
|
||||
* 生命周期函数--监听页面显示
|
||||
*/
|
||||
onShow() {
|
||||
|
||||
},
|
||||
|
||||
handleTransactionTap(e) {
|
||||
const { transactionId } = e.currentTarget.dataset
|
||||
wx.navigateTo({
|
||||
url: `/pages/transactions/detail?id=${transactionId}`
|
||||
})
|
||||
/**
|
||||
* 生命周期函数--监听页面隐藏
|
||||
*/
|
||||
onHide() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面卸载
|
||||
*/
|
||||
onUnload() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面相关事件处理函数--监听用户下拉动作
|
||||
*/
|
||||
onPullDownRefresh() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面上拉触底事件的处理函数
|
||||
*/
|
||||
onReachBottom() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 用户点击右上角分享
|
||||
*/
|
||||
onShareAppMessage() {
|
||||
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,56 +1,2 @@
|
||||
<!--pages/transactions/transactions.wxml-->
|
||||
<view class="transactions-container">
|
||||
<!-- 搜索栏 -->
|
||||
<view class="search-section">
|
||||
<view class="search-bar">
|
||||
<input
|
||||
value="{{searchKeyword}}"
|
||||
type="text"
|
||||
placeholder="搜索交易记录..."
|
||||
class="search-input"
|
||||
bindinput="onSearchInput"
|
||||
/>
|
||||
<view class="search-icon">🔍</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 交易列表 -->
|
||||
<view class="transactions-list">
|
||||
<view class="list-header">
|
||||
<view class="list-title">交易记录</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{loading}}" class="loading">
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<view wx:elif="{{transactionsList.length === 0}}" class="empty">
|
||||
<view class="empty-icon">💳</view>
|
||||
<view class="empty-text">暂无交易记录</view>
|
||||
</view>
|
||||
|
||||
<view wx:else class="list-content">
|
||||
<view
|
||||
wx:for="{{transactionsList}}"
|
||||
wx:key="id"
|
||||
class="transaction-item"
|
||||
data-transaction-id="{{item.id}}"
|
||||
bindtap="handleTransactionTap"
|
||||
>
|
||||
<view class="transaction-icon">
|
||||
<text wx:if="{{item.type === 'income'}}">💰</text>
|
||||
<text wx:elif="{{item.type === 'expense'}}">💸</text>
|
||||
<text wx:else>🔄</text>
|
||||
</view>
|
||||
<view class="transaction-content">
|
||||
<view class="transaction-title">{{item.title}}</view>
|
||||
<view class="transaction-time">{{item.time}}</view>
|
||||
</view>
|
||||
<view class="transaction-amount {{item.type}}">{{item.amount}}</view>
|
||||
<view class="transaction-status {{item.status}}">
|
||||
{{item.statusText}}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<text>pages/transactions/transactions.wxml</text>
|
||||
@@ -1,155 +0,0 @@
|
||||
/* pages/transactions/transactions.wxss */
|
||||
.transactions-container {
|
||||
min-height: 100vh;
|
||||
background: #f6f6f6;
|
||||
}
|
||||
|
||||
.search-section {
|
||||
padding: 20rpx;
|
||||
background: #fff;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
height: 72rpx;
|
||||
background: #f8f9fa;
|
||||
border: none;
|
||||
border-radius: 36rpx;
|
||||
padding: 0 60rpx 0 30rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
right: 30rpx;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.transactions-list {
|
||||
background: #fff;
|
||||
margin: 0 20rpx;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.list-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 30rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.list-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.loading,
|
||||
.empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 80rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.loading-text,
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 80rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.list-content {
|
||||
space-y: 0;
|
||||
}
|
||||
|
||||
.transaction-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 30rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.transaction-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.transaction-icon {
|
||||
font-size: 32rpx;
|
||||
margin-right: 20rpx;
|
||||
width: 60rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.transaction-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.transaction-title {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.transaction-time {
|
||||
font-size: 20rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.transaction-amount {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.transaction-amount.income {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.transaction-amount.expense {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.transaction-amount.transfer {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.transaction-status {
|
||||
font-size: 20rpx;
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 20rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.transaction-status.success {
|
||||
background: #f6ffed;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.transaction-status.pending {
|
||||
background: #fff7e6;
|
||||
color: #faad14;
|
||||
}
|
||||
|
||||
.transaction-status.failed {
|
||||
background: #fff2f0;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
72
bank_mini_program/pages/warning/warning.js
Normal file
72
bank_mini_program/pages/warning/warning.js
Normal file
@@ -0,0 +1,72 @@
|
||||
// pages/warning/warning.js
|
||||
Page({
|
||||
data: {
|
||||
farms: [
|
||||
{
|
||||
id: '13847540178',
|
||||
name: '13847540178_养殖场',
|
||||
warnings: {
|
||||
notInventoried: 0,
|
||||
abnormalSteps: 0,
|
||||
collarCut: 0,
|
||||
hostDisconnected: 0
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '15848532959',
|
||||
name: '15848532959_养殖场',
|
||||
warnings: {
|
||||
notInventoried: 0,
|
||||
abnormalSteps: 0,
|
||||
collarCut: 0,
|
||||
hostDisconnected: 0
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '15848525265',
|
||||
name: '15848525265_养殖场',
|
||||
warnings: {
|
||||
notInventoried: 0,
|
||||
abnormalSteps: 0,
|
||||
collarCut: 0,
|
||||
hostDisconnected: 0
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '15849585844',
|
||||
name: '15849585844_养殖场',
|
||||
warnings: {
|
||||
notInventoried: 0,
|
||||
abnormalSteps: 0,
|
||||
collarCut: 0,
|
||||
hostDisconnected: 0
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
console.log('日检预警页面加载');
|
||||
},
|
||||
|
||||
onShow() {
|
||||
console.log('日检预警页面显示');
|
||||
},
|
||||
|
||||
// 查看今日预警详情
|
||||
viewTodayWarnings() {
|
||||
wx.showToast({
|
||||
title: '今日预警详情',
|
||||
icon: 'none'
|
||||
});
|
||||
},
|
||||
|
||||
// 查看养殖场详情
|
||||
viewFarmDetail(e) {
|
||||
const farmId = e.currentTarget.dataset.farmId;
|
||||
wx.showToast({
|
||||
title: `查看养殖场 ${farmId}`,
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
});
|
||||
3
bank_mini_program/pages/warning/warning.json
Normal file
3
bank_mini_program/pages/warning/warning.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"usingComponents": {}
|
||||
}
|
||||
52
bank_mini_program/pages/warning/warning.wxml
Normal file
52
bank_mini_program/pages/warning/warning.wxml
Normal file
@@ -0,0 +1,52 @@
|
||||
<!--pages/warning/warning.wxml-->
|
||||
<view class="warning-container">
|
||||
|
||||
|
||||
<!-- 标题栏 -->
|
||||
<view class="header">
|
||||
<text class="title">日常预警</text>
|
||||
</view>
|
||||
|
||||
<!-- 提示横幅 -->
|
||||
<view class="banner">
|
||||
<text class="banner-icon">📢</text>
|
||||
<text class="banner-text">每天6:30进行盘点统计</text>
|
||||
</view>
|
||||
|
||||
<!-- 今日预警 -->
|
||||
<view class="today-warnings" bindtap="viewTodayWarnings">
|
||||
<text class="section-title">今日预警</text>
|
||||
<text class="arrow">></text>
|
||||
</view>
|
||||
|
||||
<!-- 养殖场列表 -->
|
||||
<view class="farm-list">
|
||||
<view
|
||||
class="farm-item"
|
||||
wx:for="{{farms}}"
|
||||
wx:key="id"
|
||||
data-farm-id="{{item.id}}"
|
||||
bindtap="viewFarmDetail"
|
||||
>
|
||||
<view class="farm-name">{{item.name}}</view>
|
||||
<view class="warnings-grid">
|
||||
<view class="warning-item red">
|
||||
<text class="warning-count">{{item.warnings.notInventoried}}</text>
|
||||
<text class="warning-label">未盘点</text>
|
||||
</view>
|
||||
<view class="warning-item green">
|
||||
<text class="warning-count">{{item.warnings.abnormalSteps}}</text>
|
||||
<text class="warning-label">步数异常</text>
|
||||
</view>
|
||||
<view class="warning-item blue">
|
||||
<text class="warning-count">{{item.warnings.collarCut}}</text>
|
||||
<text class="warning-label">项圈剪断</text>
|
||||
</view>
|
||||
<view class="warning-item orange">
|
||||
<text class="warning-count">{{item.warnings.hostDisconnected}}</text>
|
||||
<text class="warning-label">主机断网</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
183
bank_mini_program/pages/warning/warning.wxss
Normal file
183
bank_mini_program/pages/warning/warning.wxss
Normal file
@@ -0,0 +1,183 @@
|
||||
/* pages/warning/warning.wxss */
|
||||
.warning-container {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
padding-bottom: 100rpx;
|
||||
}
|
||||
|
||||
/* 状态栏 */
|
||||
.status-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20rpx 30rpx;
|
||||
background-color: #fff;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.status-icons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.icon, .signal, .battery {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 标题栏 */
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 30rpx;
|
||||
background-color: #fff;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.header-icons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
font-size: 32rpx;
|
||||
color: #666;
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 30rpx;
|
||||
}
|
||||
|
||||
/* 提示横幅 */
|
||||
.banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 20rpx 30rpx;
|
||||
padding: 20rpx;
|
||||
background-color: #fff3cd;
|
||||
border-radius: 8rpx;
|
||||
border-left: 6rpx solid #ffc107;
|
||||
}
|
||||
|
||||
.banner-icon {
|
||||
font-size: 28rpx;
|
||||
margin-right: 15rpx;
|
||||
}
|
||||
|
||||
.banner-text {
|
||||
font-size: 28rpx;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
/* 今日预警 */
|
||||
.today-warnings {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 30rpx;
|
||||
background-color: #fff;
|
||||
margin: 20rpx 30rpx 0;
|
||||
border-radius: 8rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
font-size: 32rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 养殖场列表 */
|
||||
.farm-list {
|
||||
margin: 20rpx 30rpx;
|
||||
}
|
||||
|
||||
.farm-item {
|
||||
background-color: #fff;
|
||||
margin-bottom: 20rpx;
|
||||
padding: 30rpx;
|
||||
border-radius: 8rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.farm-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.warnings-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 15rpx;
|
||||
}
|
||||
|
||||
.warning-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20rpx 10rpx;
|
||||
border-radius: 8rpx;
|
||||
min-height: 120rpx;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.warning-item.red {
|
||||
background-color: #ffebee;
|
||||
border: 2rpx solid #f44336;
|
||||
}
|
||||
|
||||
.warning-item.green {
|
||||
background-color: #e8f5e8;
|
||||
border: 2rpx solid #4caf50;
|
||||
}
|
||||
|
||||
.warning-item.blue {
|
||||
background-color: #e3f2fd;
|
||||
border: 2rpx solid #2196f3;
|
||||
}
|
||||
|
||||
.warning-item.orange {
|
||||
background-color: #fff3e0;
|
||||
border: 2rpx solid #ff9800;
|
||||
}
|
||||
|
||||
.warning-count {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.warning-label {
|
||||
font-size: 22rpx;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
}
|
||||
313
bank_mini_program/services/apiService.js
Normal file
313
bank_mini_program/services/apiService.js
Normal file
@@ -0,0 +1,313 @@
|
||||
// 银行端小程序API服务层
|
||||
const API_BASE_URL = 'https://ad.ningmuyun.com';
|
||||
|
||||
// 获取存储的token
|
||||
const getToken = () => {
|
||||
return wx.getStorageSync('bank_token') || '';
|
||||
};
|
||||
|
||||
// 设置token
|
||||
const setToken = (token) => {
|
||||
wx.setStorageSync('bank_token', token);
|
||||
};
|
||||
|
||||
// 清除token
|
||||
const clearToken = () => {
|
||||
wx.removeStorageSync('bank_token');
|
||||
wx.removeStorageSync('bank_user');
|
||||
};
|
||||
|
||||
// 创建请求头
|
||||
const createHeaders = (headers = {}) => {
|
||||
const token = getToken();
|
||||
const defaultHeaders = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
if (token) {
|
||||
defaultHeaders['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
return { ...defaultHeaders, ...headers };
|
||||
};
|
||||
|
||||
// 处理API响应
|
||||
const handleResponse = (response) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (response.statusCode === 401) {
|
||||
clearToken();
|
||||
wx.showToast({
|
||||
title: '登录已过期,请重新登录',
|
||||
icon: 'none'
|
||||
});
|
||||
reject(new Error('认证已过期'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.statusCode >= 400) {
|
||||
wx.showToast({
|
||||
title: response.data?.message || '请求失败',
|
||||
icon: 'none'
|
||||
});
|
||||
reject(new Error(response.data?.message || '请求失败'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.data && !response.data.success) {
|
||||
wx.showToast({
|
||||
title: response.data.message || '操作失败',
|
||||
icon: 'none'
|
||||
});
|
||||
reject(new Error(response.data.message || '操作失败'));
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(response.data);
|
||||
});
|
||||
};
|
||||
|
||||
// 发起请求
|
||||
const request = (options) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: `${API_BASE_URL}${options.url}`,
|
||||
method: options.method || 'GET',
|
||||
data: options.data || {},
|
||||
header: createHeaders(options.headers),
|
||||
success: (response) => {
|
||||
handleResponse(response).then(resolve).catch(reject);
|
||||
},
|
||||
fail: (error) => {
|
||||
wx.showToast({
|
||||
title: '网络请求失败',
|
||||
icon: 'none'
|
||||
});
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// API服务
|
||||
const apiService = {
|
||||
// 认证相关
|
||||
auth: {
|
||||
// 登录
|
||||
login: (username, password) => {
|
||||
return request({
|
||||
url: '/api/auth/login',
|
||||
method: 'POST',
|
||||
data: { username, password }
|
||||
});
|
||||
},
|
||||
|
||||
// 获取当前用户信息
|
||||
getCurrentUser: () => {
|
||||
return request({
|
||||
url: '/api/auth/me',
|
||||
method: 'GET'
|
||||
});
|
||||
},
|
||||
|
||||
// 登出
|
||||
logout: () => {
|
||||
return request({
|
||||
url: '/api/auth/logout',
|
||||
method: 'POST'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 项目相关
|
||||
projects: {
|
||||
// 获取项目列表
|
||||
getList: (params = {}) => {
|
||||
return request({
|
||||
url: '/api/projects',
|
||||
method: 'GET',
|
||||
data: params
|
||||
});
|
||||
},
|
||||
|
||||
// 获取项目详情
|
||||
getById: (id) => {
|
||||
return request({
|
||||
url: `/api/projects/${id}`,
|
||||
method: 'GET'
|
||||
});
|
||||
},
|
||||
|
||||
// 创建项目
|
||||
create: (data) => {
|
||||
return request({
|
||||
url: '/api/projects',
|
||||
method: 'POST',
|
||||
data
|
||||
});
|
||||
},
|
||||
|
||||
// 更新项目
|
||||
update: (id, data) => {
|
||||
return request({
|
||||
url: `/api/projects/${id}`,
|
||||
method: 'PUT',
|
||||
data
|
||||
});
|
||||
},
|
||||
|
||||
// 删除项目
|
||||
delete: (id) => {
|
||||
return request({
|
||||
url: `/api/projects/${id}`,
|
||||
method: 'DELETE'
|
||||
});
|
||||
},
|
||||
|
||||
// 获取项目统计
|
||||
getStats: () => {
|
||||
return request({
|
||||
url: '/api/projects/stats',
|
||||
method: 'GET'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 监管任务相关
|
||||
supervisionTasks: {
|
||||
// 获取监管任务列表
|
||||
getList: (params = {}) => {
|
||||
return request({
|
||||
url: '/api/supervision-tasks',
|
||||
method: 'GET',
|
||||
data: params
|
||||
});
|
||||
},
|
||||
|
||||
// 获取监管任务详情
|
||||
getById: (id) => {
|
||||
return request({
|
||||
url: `/api/supervision-tasks/${id}`,
|
||||
method: 'GET'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 贷款商品相关
|
||||
loanProducts: {
|
||||
// 获取贷款商品列表
|
||||
getList: (params = {}) => {
|
||||
return request({
|
||||
url: '/api/loan-products',
|
||||
method: 'GET',
|
||||
data: params
|
||||
});
|
||||
},
|
||||
|
||||
// 获取贷款商品详情
|
||||
getById: (id) => {
|
||||
return request({
|
||||
url: `/api/loan-products/${id}`,
|
||||
method: 'GET'
|
||||
});
|
||||
},
|
||||
|
||||
// 获取贷款商品统计
|
||||
getStats: () => {
|
||||
return request({
|
||||
url: '/api/loan-products/stats',
|
||||
method: 'GET'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 贷款申请相关
|
||||
loanApplications: {
|
||||
// 获取贷款申请列表
|
||||
getList: (params = {}) => {
|
||||
return request({
|
||||
url: '/api/loan-applications',
|
||||
method: 'GET',
|
||||
data: params
|
||||
});
|
||||
},
|
||||
|
||||
// 获取贷款申请详情
|
||||
getById: (id) => {
|
||||
return request({
|
||||
url: `/api/loan-applications/${id}`,
|
||||
method: 'GET'
|
||||
});
|
||||
},
|
||||
|
||||
// 获取申请统计
|
||||
getStats: () => {
|
||||
return request({
|
||||
url: '/api/loan-applications/stats',
|
||||
method: 'GET'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 贷款合同相关
|
||||
loanContracts: {
|
||||
// 获取贷款合同列表
|
||||
getList: (params = {}) => {
|
||||
return request({
|
||||
url: '/api/loan-contracts',
|
||||
method: 'GET',
|
||||
data: params
|
||||
});
|
||||
},
|
||||
|
||||
// 获取贷款合同详情
|
||||
getById: (id) => {
|
||||
return request({
|
||||
url: `/api/loan-contracts/${id}`,
|
||||
method: 'GET'
|
||||
});
|
||||
},
|
||||
|
||||
// 获取合同统计
|
||||
getStats: () => {
|
||||
return request({
|
||||
url: '/api/loan-contracts/stats',
|
||||
method: 'GET'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 贷款解押相关
|
||||
loanReleases: {
|
||||
// 获取贷款解押列表
|
||||
getList: (params = {}) => {
|
||||
return request({
|
||||
url: '/api/loan-releases',
|
||||
method: 'GET',
|
||||
data: params
|
||||
});
|
||||
},
|
||||
|
||||
// 获取贷款解押详情
|
||||
getById: (id) => {
|
||||
return request({
|
||||
url: `/api/loan-releases/${id}`,
|
||||
method: 'GET'
|
||||
});
|
||||
},
|
||||
|
||||
// 获取解押统计
|
||||
getStats: () => {
|
||||
return request({
|
||||
url: '/api/loan-releases/stats',
|
||||
method: 'GET'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
apiService,
|
||||
setToken,
|
||||
clearToken,
|
||||
getToken
|
||||
};
|
||||
@@ -1,118 +0,0 @@
|
||||
<template>
|
||||
<view id="app">
|
||||
<!-- 小程序页面内容 -->
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'App',
|
||||
onLaunch() {
|
||||
console.log('银行端小程序启动')
|
||||
|
||||
// 检查登录状态
|
||||
this.checkLoginStatus()
|
||||
|
||||
// 初始化应用配置
|
||||
this.initAppConfig()
|
||||
},
|
||||
onShow() {
|
||||
console.log('银行端小程序显示')
|
||||
},
|
||||
onHide() {
|
||||
console.log('银行端小程序隐藏')
|
||||
},
|
||||
methods: {
|
||||
checkLoginStatus() {
|
||||
// 开发环境暂时跳过登录检查
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
return
|
||||
}
|
||||
|
||||
const token = uni.getStorageSync('token')
|
||||
if (!token) {
|
||||
// 跳转到登录页
|
||||
uni.reLaunch({
|
||||
url: '/pages/login/login'
|
||||
})
|
||||
}
|
||||
},
|
||||
initAppConfig() {
|
||||
// 设置全局配置
|
||||
uni.setStorageSync('appConfig', {
|
||||
version: '1.0.0',
|
||||
apiBaseUrl: 'http://localhost:3002'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '@/styles/base.scss';
|
||||
|
||||
#app {
|
||||
font-family: 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* 全局样式 */
|
||||
page {
|
||||
background-color: #f5f7fa;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 通用类 */
|
||||
.container {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 12rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 20rpx;
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #2c5aa0 0%, #1e3a8a 100%);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8rpx;
|
||||
padding: 24rpx 48rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #f8f9fa;
|
||||
color: #2c5aa0;
|
||||
border: 2rpx solid #2c5aa0;
|
||||
border-radius: 8rpx;
|
||||
padding: 22rpx 46rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
color: #2c5aa0;
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.text-warning {
|
||||
color: #faad14;
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"pages": [
|
||||
"pages/index/index",
|
||||
"pages/login/login"
|
||||
],
|
||||
"window": {
|
||||
"navigationBarTitleText": "银行小程序",
|
||||
"navigationBarBackgroundColor": "#1976D2",
|
||||
"navigationBarTextStyle": "white"
|
||||
},
|
||||
"usingComponents": {
|
||||
"van-button": "@vant/weapp/button/index"
|
||||
}
|
||||
}
|
||||
@@ -1,290 +0,0 @@
|
||||
<template>
|
||||
<view v-if="hasAccess">
|
||||
<slot></slot>
|
||||
</view>
|
||||
<view v-else-if="showFallback" class="auth-fallback">
|
||||
<slot name="fallback">
|
||||
<view class="fallback-content">
|
||||
<view class="fallback-icon">
|
||||
<text class="iconfont icon-lock"></text>
|
||||
</view>
|
||||
<text class="fallback-text">{{ fallbackText }}</text>
|
||||
<view class="fallback-actions" v-if="showActions">
|
||||
<button class="fallback-btn" @click="handleLogin" v-if="!isLoggedIn">
|
||||
登录
|
||||
</button>
|
||||
<button class="fallback-btn secondary" @click="handleBack" v-else>
|
||||
返回
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</slot>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { useUserStore } from '@/store'
|
||||
import {
|
||||
isLoggedIn,
|
||||
hasPermission,
|
||||
hasRole,
|
||||
hasAnyPermission,
|
||||
hasAnyRole,
|
||||
redirectToLogin
|
||||
} from '@/utils/auth'
|
||||
|
||||
export default {
|
||||
name: 'AuthGuard',
|
||||
props: {
|
||||
// 是否需要登录
|
||||
requireAuth: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
|
||||
// 必需的权限(字符串或数组)
|
||||
permissions: {
|
||||
type: [String, Array],
|
||||
default: () => []
|
||||
},
|
||||
|
||||
// 必需的角色(字符串或数组)
|
||||
roles: {
|
||||
type: [String, Array],
|
||||
default: () => []
|
||||
},
|
||||
|
||||
// 权限检查模式:'any' 任一权限,'all' 所有权限
|
||||
permissionMode: {
|
||||
type: String,
|
||||
default: 'any',
|
||||
validator: (value) => ['any', 'all'].includes(value)
|
||||
},
|
||||
|
||||
// 角色检查模式:'any' 任一角色,'all' 所有角色
|
||||
roleMode: {
|
||||
type: String,
|
||||
default: 'any',
|
||||
validator: (value) => ['any', 'all'].includes(value)
|
||||
},
|
||||
|
||||
// 是否显示无权限时的后备内容
|
||||
showFallback: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
|
||||
// 后备内容文本
|
||||
fallbackText: {
|
||||
type: String,
|
||||
default: '权限不足'
|
||||
},
|
||||
|
||||
// 是否显示操作按钮
|
||||
showActions: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
|
||||
// 权限不足时的回调
|
||||
onDenied: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
|
||||
// 是否自动重定向到登录页
|
||||
autoRedirect: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
|
||||
emits: ['access-denied', 'access-granted'],
|
||||
|
||||
setup(props, { emit }) {
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 计算是否有访问权限
|
||||
const hasAccess = computed(() => {
|
||||
// 如果不需要认证,直接通过
|
||||
if (!props.requireAuth) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查登录状态
|
||||
if (!isLoggedIn()) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查权限
|
||||
if (props.permissions && props.permissions.length > 0) {
|
||||
const permissions = Array.isArray(props.permissions)
|
||||
? props.permissions
|
||||
: [props.permissions]
|
||||
|
||||
let hasRequiredPermission = false
|
||||
|
||||
if (props.permissionMode === 'all') {
|
||||
hasRequiredPermission = permissions.every(permission =>
|
||||
hasPermission(permission)
|
||||
)
|
||||
} else {
|
||||
hasRequiredPermission = hasAnyPermission(permissions)
|
||||
}
|
||||
|
||||
if (!hasRequiredPermission) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 检查角色
|
||||
if (props.roles && props.roles.length > 0) {
|
||||
const roles = Array.isArray(props.roles)
|
||||
? props.roles
|
||||
: [props.roles]
|
||||
|
||||
let hasRequiredRole = false
|
||||
|
||||
if (props.roleMode === 'all') {
|
||||
hasRequiredRole = roles.every(role => hasRole(role))
|
||||
} else {
|
||||
hasRequiredRole = hasAnyRole(roles)
|
||||
}
|
||||
|
||||
if (!hasRequiredRole) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
// 处理权限检查结果
|
||||
const handleAccessCheck = () => {
|
||||
if (hasAccess.value) {
|
||||
emit('access-granted')
|
||||
} else {
|
||||
emit('access-denied', {
|
||||
isLoggedIn: isLoggedIn(),
|
||||
permissions: props.permissions,
|
||||
roles: props.roles
|
||||
})
|
||||
|
||||
// 执行自定义回调
|
||||
if (typeof props.onDenied === 'function') {
|
||||
props.onDenied({
|
||||
isLoggedIn: isLoggedIn(),
|
||||
permissions: props.permissions,
|
||||
roles: props.roles
|
||||
})
|
||||
}
|
||||
|
||||
// 自动重定向
|
||||
if (props.autoRedirect && !isLoggedIn()) {
|
||||
setTimeout(() => {
|
||||
redirectToLogin()
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理登录按钮点击
|
||||
const handleLogin = () => {
|
||||
redirectToLogin()
|
||||
}
|
||||
|
||||
// 处理返回按钮点击
|
||||
const handleBack = () => {
|
||||
uni.navigateBack({
|
||||
fail: () => {
|
||||
uni.switchTab({
|
||||
url: '/pages/index/index'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
handleAccessCheck()
|
||||
})
|
||||
|
||||
// 监听用户状态变化
|
||||
userStore.$subscribe((mutation, state) => {
|
||||
handleAccessCheck()
|
||||
})
|
||||
|
||||
return {
|
||||
hasAccess,
|
||||
isLoggedIn: isLoggedIn(),
|
||||
handleLogin,
|
||||
handleBack
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/styles/variables.scss';
|
||||
|
||||
.auth-fallback {
|
||||
min-height: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: $spacing-xl;
|
||||
}
|
||||
|
||||
.fallback-content {
|
||||
text-align: center;
|
||||
|
||||
.fallback-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin: 0 auto $spacing-lg;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background-color: rgba($text-color-placeholder, 0.1);
|
||||
|
||||
.iconfont {
|
||||
font-size: 40px;
|
||||
color: $text-color-placeholder;
|
||||
}
|
||||
}
|
||||
|
||||
.fallback-text {
|
||||
display: block;
|
||||
font-size: $font-size-lg;
|
||||
color: $text-color-secondary;
|
||||
margin-bottom: $spacing-lg;
|
||||
}
|
||||
|
||||
.fallback-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: $spacing-md;
|
||||
|
||||
.fallback-btn {
|
||||
padding: $spacing-sm $spacing-lg;
|
||||
border-radius: $border-radius-md;
|
||||
font-size: $font-size-md;
|
||||
border: none;
|
||||
background-color: $primary-color;
|
||||
color: white;
|
||||
|
||||
&.secondary {
|
||||
background-color: $background-color;
|
||||
color: $text-color-secondary;
|
||||
border: 1px solid $border-color;
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,508 +0,0 @@
|
||||
<template>
|
||||
<view class="action-sheet-overlay" :class="{ 'show': visible }" @click="handleOverlayClick">
|
||||
<view class="action-sheet" :class="{ 'show': visible }" @click.stop>
|
||||
<!-- 标题区域 -->
|
||||
<view class="action-sheet-header" v-if="title || description">
|
||||
<text class="action-sheet-title" v-if="title">{{ title }}</text>
|
||||
<text class="action-sheet-description" v-if="description">{{ description }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 操作列表 -->
|
||||
<view class="action-sheet-body">
|
||||
<view
|
||||
class="action-item"
|
||||
:class="{
|
||||
'disabled': item.disabled,
|
||||
'destructive': item.destructive,
|
||||
'loading': item.loading
|
||||
}"
|
||||
v-for="(item, index) in actions"
|
||||
:key="index"
|
||||
@click="handleActionClick(item, index)"
|
||||
>
|
||||
<!-- 图标 -->
|
||||
<text
|
||||
class="iconfont action-icon"
|
||||
:class="item.icon"
|
||||
v-if="item.icon && !item.loading"
|
||||
></text>
|
||||
|
||||
<!-- 加载图标 -->
|
||||
<text
|
||||
class="iconfont action-icon icon-loading"
|
||||
v-if="item.loading"
|
||||
></text>
|
||||
|
||||
<!-- 文本 -->
|
||||
<text class="action-text">{{ item.text }}</text>
|
||||
|
||||
<!-- 描述 -->
|
||||
<text class="action-description" v-if="item.description">{{ item.description }}</text>
|
||||
|
||||
<!-- 右侧内容 -->
|
||||
<view class="action-suffix" v-if="item.suffix">
|
||||
<text class="action-suffix-text">{{ item.suffix }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 右侧图标 -->
|
||||
<text
|
||||
class="iconfont action-suffix-icon"
|
||||
:class="item.suffixIcon"
|
||||
v-if="item.suffixIcon"
|
||||
></text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 取消按钮 -->
|
||||
<view class="action-sheet-footer" v-if="showCancel">
|
||||
<view class="action-item cancel-item" @click="handleCancel">
|
||||
<text class="action-text">{{ cancelText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 安全区域 -->
|
||||
<view class="safe-area-bottom"></view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ActionSheet',
|
||||
props: {
|
||||
// 是否显示
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 标题
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 描述
|
||||
description: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 操作列表
|
||||
actions: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
// 是否显示取消按钮
|
||||
showCancel: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 取消按钮文本
|
||||
cancelText: {
|
||||
type: String,
|
||||
default: '取消'
|
||||
},
|
||||
// 点击遮罩是否关闭
|
||||
closeOnClickOverlay: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 是否显示圆角
|
||||
round: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 安全区域适配
|
||||
safeAreaInsetBottom: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
emits: ['update:visible', 'select', 'cancel', 'close'],
|
||||
setup(props, { emit }) {
|
||||
const handleOverlayClick = () => {
|
||||
if (props.closeOnClickOverlay) {
|
||||
handleClose()
|
||||
}
|
||||
}
|
||||
|
||||
const handleActionClick = (item, index) => {
|
||||
if (item.disabled || item.loading) {
|
||||
return
|
||||
}
|
||||
|
||||
emit('select', { item, index })
|
||||
|
||||
if (!item.keepOpen) {
|
||||
handleClose()
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
emit('cancel')
|
||||
handleClose()
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
emit('update:visible', false)
|
||||
emit('close')
|
||||
}
|
||||
|
||||
return {
|
||||
handleOverlayClick,
|
||||
handleActionClick,
|
||||
handleCancel,
|
||||
handleClose
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/styles/variables.scss';
|
||||
|
||||
.action-sheet-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1000;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.action-sheet {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: white;
|
||||
border-radius: $border-radius-lg $border-radius-lg 0 0;
|
||||
transform: translateY(100%);
|
||||
transition: transform 0.3s ease;
|
||||
max-height: 80vh;
|
||||
overflow: hidden;
|
||||
|
||||
&.show {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.action-sheet-header {
|
||||
padding: $spacing-lg $spacing-lg $spacing-md;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid $border-color-light;
|
||||
|
||||
.action-sheet-title {
|
||||
display: block;
|
||||
font-size: $font-size-lg;
|
||||
font-weight: 600;
|
||||
color: $text-color-primary;
|
||||
line-height: 1.4;
|
||||
margin-bottom: $spacing-xs;
|
||||
}
|
||||
|
||||
.action-sheet-description {
|
||||
display: block;
|
||||
font-size: $font-size-sm;
|
||||
color: $text-color-secondary;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.action-sheet-body {
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
|
||||
.action-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: $spacing-md $spacing-lg;
|
||||
background: white;
|
||||
border-bottom: 1px solid $border-color-light;
|
||||
transition: background-color 0.2s ease;
|
||||
cursor: pointer;
|
||||
min-height: 56px;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:active:not(.disabled):not(.loading) {
|
||||
background: $bg-color-light;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&.destructive {
|
||||
.action-text {
|
||||
color: $error-color;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
color: $error-color;
|
||||
}
|
||||
}
|
||||
|
||||
&.loading {
|
||||
cursor: not-allowed;
|
||||
|
||||
.action-icon.icon-loading {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
font-size: 18px;
|
||||
color: $text-color-secondary;
|
||||
margin-right: $spacing-sm;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.action-text {
|
||||
flex: 1;
|
||||
font-size: $font-size-md;
|
||||
color: $text-color-primary;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.action-description {
|
||||
display: block;
|
||||
font-size: $font-size-sm;
|
||||
color: $text-color-secondary;
|
||||
line-height: 1.3;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.action-suffix {
|
||||
margin-left: $spacing-sm;
|
||||
|
||||
.action-suffix-text {
|
||||
font-size: $font-size-sm;
|
||||
color: $text-color-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
.action-suffix-icon {
|
||||
font-size: 16px;
|
||||
color: $text-color-secondary;
|
||||
margin-left: $spacing-sm;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-sheet-footer {
|
||||
border-top: 8px solid $bg-color-light;
|
||||
|
||||
.cancel-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: $spacing-md $spacing-lg;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
min-height: 56px;
|
||||
|
||||
&:active {
|
||||
background: $bg-color-light;
|
||||
}
|
||||
|
||||
.action-text {
|
||||
font-size: $font-size-md;
|
||||
color: $text-color-secondary;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.safe-area-bottom {
|
||||
height: env(safe-area-inset-bottom);
|
||||
background: white;
|
||||
}
|
||||
|
||||
// 动画
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
// 无圆角样式
|
||||
.action-sheet:not(.round) {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
// 滚动条样式
|
||||
.action-sheet-body::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.action-sheet-body::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.action-sheet-body::-webkit-scrollbar-thumb {
|
||||
background: rgba($text-color-secondary, 0.3);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.action-sheet-body::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba($text-color-secondary, 0.5);
|
||||
}
|
||||
|
||||
// 特殊布局
|
||||
.action-item {
|
||||
&.with-description {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
.action-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
.action-icon {
|
||||
margin-right: $spacing-sm;
|
||||
}
|
||||
|
||||
.action-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.action-suffix,
|
||||
.action-suffix-icon {
|
||||
margin-left: $spacing-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.action-description {
|
||||
margin-top: $spacing-xs;
|
||||
margin-left: 26px; // 图标宽度 + 间距
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式适配
|
||||
@media (max-width: 480px) {
|
||||
.action-sheet {
|
||||
max-height: 85vh;
|
||||
|
||||
.action-sheet-header {
|
||||
padding: $spacing-md $spacing-md $spacing-sm;
|
||||
|
||||
.action-sheet-title {
|
||||
font-size: $font-size-md;
|
||||
}
|
||||
}
|
||||
|
||||
.action-sheet-body {
|
||||
max-height: 65vh;
|
||||
|
||||
.action-item {
|
||||
padding: $spacing-sm $spacing-md;
|
||||
min-height: 48px;
|
||||
|
||||
.action-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.action-text {
|
||||
font-size: $font-size-sm;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-sheet-footer {
|
||||
.cancel-item {
|
||||
padding: $spacing-sm $spacing-md;
|
||||
min-height: 48px;
|
||||
|
||||
.action-text {
|
||||
font-size: $font-size-sm;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 暗色主题适配
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.action-sheet-overlay {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
.action-sheet {
|
||||
background: #1f1f1f;
|
||||
|
||||
.action-sheet-header {
|
||||
border-bottom-color: #333;
|
||||
|
||||
.action-sheet-title {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.action-sheet-description {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.action-sheet-body {
|
||||
.action-item {
|
||||
background: #1f1f1f;
|
||||
border-bottom-color: #333;
|
||||
|
||||
&:active:not(.disabled):not(.loading) {
|
||||
background: #333;
|
||||
}
|
||||
|
||||
.action-text {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.action-description,
|
||||
.action-suffix-text {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.action-icon,
|
||||
.action-suffix-icon {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-sheet-footer {
|
||||
border-top-color: #333;
|
||||
|
||||
.cancel-item {
|
||||
background: #1f1f1f;
|
||||
|
||||
&:active {
|
||||
background: #333;
|
||||
}
|
||||
|
||||
.action-text {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.safe-area-bottom {
|
||||
background: #1f1f1f;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,492 +0,0 @@
|
||||
<template>
|
||||
<view class="empty-state" :class="[size, { 'with-background': withBackground }]">
|
||||
<view class="empty-container">
|
||||
<!-- 图标或图片 -->
|
||||
<view class="empty-icon" v-if="icon || image">
|
||||
<image
|
||||
v-if="image"
|
||||
:src="image"
|
||||
class="empty-image"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<text
|
||||
v-else-if="icon"
|
||||
class="iconfont empty-icon-text"
|
||||
:class="icon"
|
||||
:style="{ color: iconColor, fontSize: iconSize }"
|
||||
></text>
|
||||
</view>
|
||||
|
||||
<!-- 默认图标 -->
|
||||
<view class="empty-icon" v-if="!icon && !image">
|
||||
<view class="default-empty-icon">
|
||||
<view class="icon-circle">
|
||||
<text class="iconfont icon-inbox"></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 标题 -->
|
||||
<text class="empty-title" v-if="title">{{ title }}</text>
|
||||
|
||||
<!-- 描述 -->
|
||||
<text class="empty-description" v-if="description">{{ description }}</text>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<view class="empty-actions" v-if="$slots.actions || actionText">
|
||||
<slot name="actions">
|
||||
<view
|
||||
class="action-button"
|
||||
:class="actionType"
|
||||
@click="handleAction"
|
||||
v-if="actionText"
|
||||
>
|
||||
<text class="iconfont" :class="actionIcon" v-if="actionIcon"></text>
|
||||
<text class="action-text">{{ actionText }}</text>
|
||||
</view>
|
||||
</slot>
|
||||
</view>
|
||||
|
||||
<!-- 自定义内容插槽 -->
|
||||
<view class="empty-custom" v-if="$slots.default">
|
||||
<slot></slot>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'EmptyState',
|
||||
props: {
|
||||
// 图标
|
||||
icon: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 图片
|
||||
image: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 图标颜色
|
||||
iconColor: {
|
||||
type: String,
|
||||
default: '#d9d9d9'
|
||||
},
|
||||
// 图标大小
|
||||
iconSize: {
|
||||
type: String,
|
||||
default: '48px'
|
||||
},
|
||||
// 标题
|
||||
title: {
|
||||
type: String,
|
||||
default: '暂无数据'
|
||||
},
|
||||
// 描述
|
||||
description: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 尺寸
|
||||
size: {
|
||||
type: String,
|
||||
default: 'medium', // small, medium, large
|
||||
validator: (value) => ['small', 'medium', 'large'].includes(value)
|
||||
},
|
||||
// 是否显示背景
|
||||
withBackground: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 操作按钮文本
|
||||
actionText: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 操作按钮图标
|
||||
actionIcon: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 操作按钮类型
|
||||
actionType: {
|
||||
type: String,
|
||||
default: 'primary', // primary, default, text
|
||||
validator: (value) => ['primary', 'default', 'text'].includes(value)
|
||||
}
|
||||
},
|
||||
emits: ['action'],
|
||||
setup(props, { emit }) {
|
||||
const handleAction = () => {
|
||||
emit('action')
|
||||
}
|
||||
|
||||
return {
|
||||
handleAction
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/styles/variables.scss';
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
min-height: 200px;
|
||||
padding: $spacing-lg;
|
||||
|
||||
&.with-background {
|
||||
background: white;
|
||||
border-radius: $border-radius-lg;
|
||||
box-shadow: $shadow-light;
|
||||
}
|
||||
|
||||
&.small {
|
||||
min-height: 120px;
|
||||
padding: $spacing-md;
|
||||
|
||||
.empty-container {
|
||||
.empty-icon {
|
||||
margin-bottom: $spacing-sm;
|
||||
|
||||
.empty-image {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.empty-icon-text {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.default-empty-icon {
|
||||
.icon-circle {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
|
||||
.iconfont {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: $font-size-md;
|
||||
margin-bottom: $spacing-xs;
|
||||
}
|
||||
|
||||
.empty-description {
|
||||
font-size: $font-size-sm;
|
||||
margin-bottom: $spacing-sm;
|
||||
}
|
||||
|
||||
.empty-actions {
|
||||
.action-button {
|
||||
padding: $spacing-xs $spacing-sm;
|
||||
font-size: $font-size-sm;
|
||||
|
||||
.iconfont {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.medium {
|
||||
min-height: 200px;
|
||||
padding: $spacing-lg;
|
||||
|
||||
.empty-container {
|
||||
.empty-icon {
|
||||
margin-bottom: $spacing-md;
|
||||
|
||||
.empty-image {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.empty-icon-text {
|
||||
font-size: 48px;
|
||||
}
|
||||
|
||||
.default-empty-icon {
|
||||
.icon-circle {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
|
||||
.iconfont {
|
||||
font-size: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: $font-size-lg;
|
||||
margin-bottom: $spacing-sm;
|
||||
}
|
||||
|
||||
.empty-description {
|
||||
font-size: $font-size-md;
|
||||
margin-bottom: $spacing-md;
|
||||
}
|
||||
|
||||
.empty-actions {
|
||||
.action-button {
|
||||
padding: $spacing-sm $spacing-md;
|
||||
font-size: $font-size-md;
|
||||
|
||||
.iconfont {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.large {
|
||||
min-height: 300px;
|
||||
padding: $spacing-xl;
|
||||
|
||||
.empty-container {
|
||||
.empty-icon {
|
||||
margin-bottom: $spacing-lg;
|
||||
|
||||
.empty-image {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.empty-icon-text {
|
||||
font-size: 64px;
|
||||
}
|
||||
|
||||
.default-empty-icon {
|
||||
.icon-circle {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
|
||||
.iconfont {
|
||||
font-size: 48px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: $font-size-xl;
|
||||
margin-bottom: $spacing-md;
|
||||
}
|
||||
|
||||
.empty-description {
|
||||
font-size: $font-size-lg;
|
||||
margin-bottom: $spacing-lg;
|
||||
}
|
||||
|
||||
.empty-actions {
|
||||
.action-button {
|
||||
padding: $spacing-md $spacing-lg;
|
||||
font-size: $font-size-lg;
|
||||
|
||||
.iconfont {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.empty-image {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.empty-icon-text {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.default-empty-icon {
|
||||
.icon-circle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: $bg-color-light;
|
||||
color: $text-color-secondary;
|
||||
|
||||
.iconfont {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
color: $text-color-primary;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.empty-description {
|
||||
color: $text-color-secondary;
|
||||
line-height: 1.5;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.empty-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: $spacing-sm;
|
||||
|
||||
.action-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: $spacing-xs;
|
||||
border-radius: $border-radius-sm;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
|
||||
&.primary {
|
||||
background: $primary-color;
|
||||
color: white;
|
||||
border: 1px solid $primary-color;
|
||||
|
||||
&:active {
|
||||
background: $primary-color-dark;
|
||||
border-color: $primary-color-dark;
|
||||
}
|
||||
}
|
||||
|
||||
&.default {
|
||||
background: white;
|
||||
color: $text-color-primary;
|
||||
border: 1px solid $border-color;
|
||||
|
||||
&:active {
|
||||
background: $bg-color-light;
|
||||
border-color: $primary-color;
|
||||
color: $primary-color;
|
||||
}
|
||||
}
|
||||
|
||||
&.text {
|
||||
background: transparent;
|
||||
color: $primary-color;
|
||||
border: none;
|
||||
|
||||
&:active {
|
||||
background: rgba($primary-color, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.action-text {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-custom {
|
||||
margin-top: $spacing-md;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// 预设主题
|
||||
.empty-state {
|
||||
&.theme-no-data {
|
||||
.default-empty-icon .icon-circle {
|
||||
background: rgba($info-color, 0.1);
|
||||
color: $info-color;
|
||||
}
|
||||
}
|
||||
|
||||
&.theme-no-network {
|
||||
.default-empty-icon .icon-circle {
|
||||
background: rgba($warning-color, 0.1);
|
||||
color: $warning-color;
|
||||
}
|
||||
}
|
||||
|
||||
&.theme-error {
|
||||
.default-empty-icon .icon-circle {
|
||||
background: rgba($error-color, 0.1);
|
||||
color: $error-color;
|
||||
}
|
||||
}
|
||||
|
||||
&.theme-success {
|
||||
.default-empty-icon .icon-circle {
|
||||
background: rgba($success-color, 0.1);
|
||||
color: $success-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式适配
|
||||
@media (max-width: 480px) {
|
||||
.empty-state {
|
||||
&.large {
|
||||
min-height: 240px;
|
||||
padding: $spacing-lg;
|
||||
|
||||
.empty-container {
|
||||
.empty-icon {
|
||||
.empty-image {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.empty-icon-text {
|
||||
font-size: 56px;
|
||||
}
|
||||
|
||||
.default-empty-icon {
|
||||
.icon-circle {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
|
||||
.iconfont {
|
||||
font-size: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: $font-size-lg;
|
||||
}
|
||||
|
||||
.empty-description {
|
||||
font-size: $font-size-md;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,381 +0,0 @@
|
||||
<template>
|
||||
<view class="loading-spinner" :class="{ 'full-screen': fullScreen }" v-if="visible">
|
||||
<view class="spinner-container" :class="size">
|
||||
<view class="spinner" :class="type">
|
||||
<view class="spinner-dot" v-if="type === 'dots'"></view>
|
||||
<view class="spinner-dot" v-if="type === 'dots'"></view>
|
||||
<view class="spinner-dot" v-if="type === 'dots'"></view>
|
||||
|
||||
<view class="spinner-circle" v-if="type === 'circle'">
|
||||
<view class="circle-path"></view>
|
||||
</view>
|
||||
|
||||
<view class="spinner-wave" v-if="type === 'wave'">
|
||||
<view class="wave-bar"></view>
|
||||
<view class="wave-bar"></view>
|
||||
<view class="wave-bar"></view>
|
||||
<view class="wave-bar"></view>
|
||||
<view class="wave-bar"></view>
|
||||
</view>
|
||||
|
||||
<view class="spinner-pulse" v-if="type === 'pulse'"></view>
|
||||
</view>
|
||||
|
||||
<text class="loading-text" v-if="text">{{ text }}</text>
|
||||
</view>
|
||||
|
||||
<view class="loading-overlay" v-if="overlay" @click="handleOverlayClick"></view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'LoadingSpinner',
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'circle', // circle, dots, wave, pulse
|
||||
validator: (value) => ['circle', 'dots', 'wave', 'pulse'].includes(value)
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'medium', // small, medium, large
|
||||
validator: (value) => ['small', 'medium', 'large'].includes(value)
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
fullScreen: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
overlay: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
overlayClickable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: '#1890ff'
|
||||
}
|
||||
},
|
||||
emits: ['overlay-click'],
|
||||
setup(props, { emit }) {
|
||||
const handleOverlayClick = () => {
|
||||
if (props.overlayClickable) {
|
||||
emit('overlay-click')
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
handleOverlayClick
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/styles/variables.scss';
|
||||
|
||||
.loading-spinner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&.full-screen {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 9999;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
}
|
||||
|
||||
.spinner-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&.small {
|
||||
.spinner {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: $font-size-sm;
|
||||
margin-top: $spacing-xs;
|
||||
}
|
||||
}
|
||||
|
||||
&.medium {
|
||||
.spinner {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: $font-size-md;
|
||||
margin-top: $spacing-sm;
|
||||
}
|
||||
}
|
||||
|
||||
&.large {
|
||||
.spinner {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: $font-size-lg;
|
||||
margin-top: $spacing-md;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.spinner {
|
||||
position: relative;
|
||||
|
||||
// 圆形加载动画
|
||||
&.circle {
|
||||
.spinner-circle {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
border: 2px solid rgba($primary-color, 0.2);
|
||||
border-top-color: $primary-color;
|
||||
animation: spin 1s linear infinite;
|
||||
|
||||
.circle-path {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
border: 2px solid transparent;
|
||||
border-top-color: $primary-color;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 点状加载动画
|
||||
&.dots {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
|
||||
.spinner-dot {
|
||||
width: 25%;
|
||||
height: 100%;
|
||||
background: $primary-color;
|
||||
border-radius: 50%;
|
||||
animation: dot-bounce 1.4s ease-in-out infinite both;
|
||||
|
||||
&:nth-child(1) {
|
||||
animation-delay: -0.32s;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
animation-delay: -0.16s;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
animation-delay: 0s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 波浪加载动画
|
||||
&.wave {
|
||||
.spinner-wave {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.wave-bar {
|
||||
width: 15%;
|
||||
height: 100%;
|
||||
background: $primary-color;
|
||||
border-radius: 2px;
|
||||
animation: wave-scale 1.2s ease-in-out infinite;
|
||||
|
||||
&:nth-child(1) {
|
||||
animation-delay: -1.2s;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
animation-delay: -1.1s;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
animation-delay: -1.0s;
|
||||
}
|
||||
|
||||
&:nth-child(4) {
|
||||
animation-delay: -0.9s;
|
||||
}
|
||||
|
||||
&:nth-child(5) {
|
||||
animation-delay: -0.8s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 脉冲加载动画
|
||||
&.pulse {
|
||||
.spinner-pulse {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: $primary-color;
|
||||
border-radius: 50%;
|
||||
animation: pulse-scale 1s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
color: $text-color-primary;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: transparent;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
// 动画定义
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dot-bounce {
|
||||
0%, 80%, 100% {
|
||||
transform: scale(0);
|
||||
opacity: 0.5;
|
||||
}
|
||||
40% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes wave-scale {
|
||||
0%, 40%, 100% {
|
||||
transform: scaleY(0.4);
|
||||
}
|
||||
20% {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse-scale {
|
||||
0% {
|
||||
transform: scale(0);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 主题色变量支持
|
||||
.loading-spinner[data-color="success"] {
|
||||
.spinner-dot,
|
||||
.spinner-circle,
|
||||
.circle-path,
|
||||
.wave-bar,
|
||||
.spinner-pulse {
|
||||
border-color: $success-color;
|
||||
background-color: $success-color;
|
||||
}
|
||||
|
||||
.spinner-circle {
|
||||
border-color: rgba($success-color, 0.2);
|
||||
border-top-color: $success-color;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-spinner[data-color="warning"] {
|
||||
.spinner-dot,
|
||||
.spinner-circle,
|
||||
.circle-path,
|
||||
.wave-bar,
|
||||
.spinner-pulse {
|
||||
border-color: $warning-color;
|
||||
background-color: $warning-color;
|
||||
}
|
||||
|
||||
.spinner-circle {
|
||||
border-color: rgba($warning-color, 0.2);
|
||||
border-top-color: $warning-color;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-spinner[data-color="error"] {
|
||||
.spinner-dot,
|
||||
.spinner-circle,
|
||||
.circle-path,
|
||||
.wave-bar,
|
||||
.spinner-pulse {
|
||||
border-color: $error-color;
|
||||
background-color: $error-color;
|
||||
}
|
||||
|
||||
.spinner-circle {
|
||||
border-color: rgba($error-color, 0.2);
|
||||
border-top-color: $error-color;
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式适配
|
||||
@media (max-width: 480px) {
|
||||
.spinner-container {
|
||||
&.small {
|
||||
.spinner {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&.medium {
|
||||
.spinner {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
&.large {
|
||||
.spinner {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,501 +0,0 @@
|
||||
<template>
|
||||
<view
|
||||
class="status-tag"
|
||||
:class="[
|
||||
`status-${status}`,
|
||||
`size-${size}`,
|
||||
`type-${type}`,
|
||||
{
|
||||
'with-icon': showIcon,
|
||||
'with-dot': showDot,
|
||||
'clickable': clickable,
|
||||
'bordered': bordered
|
||||
}
|
||||
]"
|
||||
@click="handleClick"
|
||||
>
|
||||
<!-- 状态点 -->
|
||||
<view class="status-dot" v-if="showDot"></view>
|
||||
|
||||
<!-- 图标 -->
|
||||
<text
|
||||
class="iconfont status-icon"
|
||||
:class="iconClass"
|
||||
v-if="showIcon && iconClass"
|
||||
></text>
|
||||
|
||||
<!-- 文本内容 -->
|
||||
<text class="status-text">{{ text || statusText }}</text>
|
||||
|
||||
<!-- 右侧图标 -->
|
||||
<text
|
||||
class="iconfont status-suffix-icon"
|
||||
:class="suffixIcon"
|
||||
v-if="suffixIcon"
|
||||
></text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'StatusTag',
|
||||
props: {
|
||||
// 状态类型
|
||||
status: {
|
||||
type: String,
|
||||
default: 'default',
|
||||
validator: (value) => [
|
||||
'default', 'primary', 'success', 'warning', 'error', 'info',
|
||||
'pending', 'processing', 'approved', 'rejected', 'cancelled',
|
||||
'active', 'inactive', 'online', 'offline', 'normal', 'abnormal'
|
||||
].includes(value)
|
||||
},
|
||||
// 显示文本
|
||||
text: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 尺寸
|
||||
size: {
|
||||
type: String,
|
||||
default: 'medium',
|
||||
validator: (value) => ['small', 'medium', 'large'].includes(value)
|
||||
},
|
||||
// 类型样式
|
||||
type: {
|
||||
type: String,
|
||||
default: 'filled',
|
||||
validator: (value) => ['filled', 'outlined', 'light'].includes(value)
|
||||
},
|
||||
// 是否显示图标
|
||||
showIcon: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 是否显示状态点
|
||||
showDot: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 自定义图标
|
||||
icon: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 后缀图标
|
||||
suffixIcon: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 是否可点击
|
||||
clickable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 是否显示边框
|
||||
bordered: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
emits: ['click'],
|
||||
computed: {
|
||||
// 状态对应的默认文本
|
||||
statusText() {
|
||||
const statusMap = {
|
||||
default: '默认',
|
||||
primary: '主要',
|
||||
success: '成功',
|
||||
warning: '警告',
|
||||
error: '错误',
|
||||
info: '信息',
|
||||
pending: '待处理',
|
||||
processing: '处理中',
|
||||
approved: '已通过',
|
||||
rejected: '已拒绝',
|
||||
cancelled: '已取消',
|
||||
active: '活跃',
|
||||
inactive: '非活跃',
|
||||
online: '在线',
|
||||
offline: '离线',
|
||||
normal: '正常',
|
||||
abnormal: '异常'
|
||||
}
|
||||
return statusMap[this.status] || this.status
|
||||
},
|
||||
// 状态对应的图标
|
||||
iconClass() {
|
||||
if (this.icon) {
|
||||
return this.icon
|
||||
}
|
||||
|
||||
const iconMap = {
|
||||
success: 'icon-check-circle',
|
||||
warning: 'icon-warning-circle',
|
||||
error: 'icon-close-circle',
|
||||
info: 'icon-info-circle',
|
||||
pending: 'icon-clock-circle',
|
||||
processing: 'icon-loading',
|
||||
approved: 'icon-check',
|
||||
rejected: 'icon-close',
|
||||
cancelled: 'icon-stop',
|
||||
active: 'icon-play-circle',
|
||||
inactive: 'icon-pause-circle',
|
||||
online: 'icon-wifi',
|
||||
offline: 'icon-disconnect',
|
||||
normal: 'icon-check-circle',
|
||||
abnormal: 'icon-warning-circle'
|
||||
}
|
||||
|
||||
return iconMap[this.status] || ''
|
||||
}
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const handleClick = () => {
|
||||
if (props.clickable) {
|
||||
emit('click')
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
handleClick
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/styles/variables.scss';
|
||||
|
||||
.status-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
border-radius: $border-radius-sm;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
|
||||
&.clickable {
|
||||
cursor: pointer;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
// 尺寸样式
|
||||
&.size-small {
|
||||
padding: 2px 6px;
|
||||
font-size: $font-size-xs;
|
||||
min-height: 20px;
|
||||
|
||||
.status-dot {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
.status-icon,
|
||||
.status-suffix-icon {
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&.size-medium {
|
||||
padding: 4px 8px;
|
||||
font-size: $font-size-sm;
|
||||
min-height: 24px;
|
||||
|
||||
.status-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.status-icon,
|
||||
.status-suffix-icon {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
&.size-large {
|
||||
padding: 6px 12px;
|
||||
font-size: $font-size-md;
|
||||
min-height: 32px;
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.status-icon,
|
||||
.status-suffix-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
// 状态颜色 - filled 类型
|
||||
&.type-filled {
|
||||
color: white;
|
||||
|
||||
&.status-default {
|
||||
background: $text-color-secondary;
|
||||
border: 1px solid $text-color-secondary;
|
||||
}
|
||||
|
||||
&.status-primary {
|
||||
background: $primary-color;
|
||||
border: 1px solid $primary-color;
|
||||
}
|
||||
|
||||
&.status-success,
|
||||
&.status-approved,
|
||||
&.status-normal,
|
||||
&.status-online,
|
||||
&.status-active {
|
||||
background: $success-color;
|
||||
border: 1px solid $success-color;
|
||||
}
|
||||
|
||||
&.status-warning,
|
||||
&.status-pending,
|
||||
&.status-abnormal {
|
||||
background: $warning-color;
|
||||
border: 1px solid $warning-color;
|
||||
}
|
||||
|
||||
&.status-error,
|
||||
&.status-rejected,
|
||||
&.status-cancelled,
|
||||
&.status-offline {
|
||||
background: $error-color;
|
||||
border: 1px solid $error-color;
|
||||
}
|
||||
|
||||
&.status-info,
|
||||
&.status-processing {
|
||||
background: $info-color;
|
||||
border: 1px solid $info-color;
|
||||
}
|
||||
|
||||
&.status-inactive {
|
||||
background: $text-color-disabled;
|
||||
border: 1px solid $text-color-disabled;
|
||||
}
|
||||
}
|
||||
|
||||
// 状态颜色 - outlined 类型
|
||||
&.type-outlined {
|
||||
background: white;
|
||||
|
||||
&.status-default {
|
||||
color: $text-color-secondary;
|
||||
border: 1px solid $text-color-secondary;
|
||||
}
|
||||
|
||||
&.status-primary {
|
||||
color: $primary-color;
|
||||
border: 1px solid $primary-color;
|
||||
}
|
||||
|
||||
&.status-success,
|
||||
&.status-approved,
|
||||
&.status-normal,
|
||||
&.status-online,
|
||||
&.status-active {
|
||||
color: $success-color;
|
||||
border: 1px solid $success-color;
|
||||
}
|
||||
|
||||
&.status-warning,
|
||||
&.status-pending,
|
||||
&.status-abnormal {
|
||||
color: $warning-color;
|
||||
border: 1px solid $warning-color;
|
||||
}
|
||||
|
||||
&.status-error,
|
||||
&.status-rejected,
|
||||
&.status-cancelled,
|
||||
&.status-offline {
|
||||
color: $error-color;
|
||||
border: 1px solid $error-color;
|
||||
}
|
||||
|
||||
&.status-info,
|
||||
&.status-processing {
|
||||
color: $info-color;
|
||||
border: 1px solid $info-color;
|
||||
}
|
||||
|
||||
&.status-inactive {
|
||||
color: $text-color-disabled;
|
||||
border: 1px solid $text-color-disabled;
|
||||
}
|
||||
}
|
||||
|
||||
// 状态颜色 - light 类型
|
||||
&.type-light {
|
||||
border: none;
|
||||
|
||||
&.status-default {
|
||||
color: $text-color-secondary;
|
||||
background: rgba($text-color-secondary, 0.1);
|
||||
}
|
||||
|
||||
&.status-primary {
|
||||
color: $primary-color;
|
||||
background: rgba($primary-color, 0.1);
|
||||
}
|
||||
|
||||
&.status-success,
|
||||
&.status-approved,
|
||||
&.status-normal,
|
||||
&.status-online,
|
||||
&.status-active {
|
||||
color: $success-color;
|
||||
background: rgba($success-color, 0.1);
|
||||
}
|
||||
|
||||
&.status-warning,
|
||||
&.status-pending,
|
||||
&.status-abnormal {
|
||||
color: $warning-color;
|
||||
background: rgba($warning-color, 0.1);
|
||||
}
|
||||
|
||||
&.status-error,
|
||||
&.status-rejected,
|
||||
&.status-cancelled,
|
||||
&.status-offline {
|
||||
color: $error-color;
|
||||
background: rgba($error-color, 0.1);
|
||||
}
|
||||
|
||||
&.status-info,
|
||||
&.status-processing {
|
||||
color: $info-color;
|
||||
background: rgba($info-color, 0.1);
|
||||
}
|
||||
|
||||
&.status-inactive {
|
||||
color: $text-color-disabled;
|
||||
background: rgba($text-color-disabled, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
// 无边框样式
|
||||
&:not(.bordered) {
|
||||
border: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-icon,
|
||||
.status-suffix-icon {
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.icon-loading {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.status-text {
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
// 动画
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
// 特殊状态的脉冲动画
|
||||
.status-tag {
|
||||
&.status-processing {
|
||||
&.type-filled,
|
||||
&.type-light {
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
}
|
||||
|
||||
&.status-online {
|
||||
.status-dot {
|
||||
animation: pulse-dot 2s infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse-dot {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.2);
|
||||
opacity: 0.7;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 组合样式
|
||||
.status-tag {
|
||||
&.with-icon.with-dot {
|
||||
.status-dot {
|
||||
margin-right: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
// 悬浮效果
|
||||
&.clickable {
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: $shadow-light;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式适配
|
||||
@media (max-width: 480px) {
|
||||
.status-tag {
|
||||
&.size-large {
|
||||
padding: 4px 10px;
|
||||
font-size: $font-size-sm;
|
||||
min-height: 28px;
|
||||
|
||||
.status-icon,
|
||||
.status-suffix-icon {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,228 +0,0 @@
|
||||
import { describe, it, expect, beforeEach } from '@jest/globals'
|
||||
import { mountComponent, expectElementExists, expectElementText } from '../../../tests/utils/test-utils'
|
||||
import StatusTag from '../StatusTag.vue'
|
||||
|
||||
describe('StatusTag.vue', () => {
|
||||
let wrapper: any
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = null
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (wrapper) {
|
||||
wrapper.unmount()
|
||||
}
|
||||
})
|
||||
|
||||
it('renders with default props', () => {
|
||||
wrapper = mountComponent(StatusTag, {
|
||||
props: {
|
||||
status: 'success',
|
||||
text: '成功'
|
||||
}
|
||||
})
|
||||
|
||||
expectElementExists(wrapper, '.status-tag')
|
||||
expectElementText(wrapper, '.status-tag__text', '成功')
|
||||
expect(wrapper.classes()).toContain('status-tag--success')
|
||||
})
|
||||
|
||||
it('renders different status types correctly', () => {
|
||||
const statusTypes = [
|
||||
{ status: 'success', class: 'status-tag--success' },
|
||||
{ status: 'error', class: 'status-tag--error' },
|
||||
{ status: 'warning', class: 'status-tag--warning' },
|
||||
{ status: 'info', class: 'status-tag--info' },
|
||||
{ status: 'pending', class: 'status-tag--pending' }
|
||||
]
|
||||
|
||||
statusTypes.forEach(({ status, class: expectedClass }) => {
|
||||
wrapper = mountComponent(StatusTag, {
|
||||
props: {
|
||||
status,
|
||||
text: '测试'
|
||||
}
|
||||
})
|
||||
|
||||
expect(wrapper.classes()).toContain(expectedClass)
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
|
||||
it('renders different sizes correctly', () => {
|
||||
const sizes = ['small', 'medium', 'large']
|
||||
|
||||
sizes.forEach(size => {
|
||||
wrapper = mountComponent(StatusTag, {
|
||||
props: {
|
||||
status: 'success',
|
||||
text: '测试',
|
||||
size
|
||||
}
|
||||
})
|
||||
|
||||
expect(wrapper.classes()).toContain(`status-tag--${size}`)
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
|
||||
it('shows dot when showDot is true', () => {
|
||||
wrapper = mountComponent(StatusTag, {
|
||||
props: {
|
||||
status: 'success',
|
||||
text: '测试',
|
||||
showDot: true
|
||||
}
|
||||
})
|
||||
|
||||
expectElementExists(wrapper, '.status-tag__dot')
|
||||
})
|
||||
|
||||
it('shows icon when icon prop is provided', () => {
|
||||
wrapper = mountComponent(StatusTag, {
|
||||
props: {
|
||||
status: 'success',
|
||||
text: '测试',
|
||||
icon: 'check'
|
||||
}
|
||||
})
|
||||
|
||||
expectElementExists(wrapper, '.status-tag__icon')
|
||||
})
|
||||
|
||||
it('shows right icon when rightIcon prop is provided', () => {
|
||||
wrapper = mountComponent(StatusTag, {
|
||||
props: {
|
||||
status: 'success',
|
||||
text: '测试',
|
||||
rightIcon: 'arrow-right'
|
||||
}
|
||||
})
|
||||
|
||||
expectElementExists(wrapper, '.status-tag__right-icon')
|
||||
})
|
||||
|
||||
it('applies custom color when color prop is provided', () => {
|
||||
const customColor = '#ff0000'
|
||||
wrapper = mountComponent(StatusTag, {
|
||||
props: {
|
||||
status: 'success',
|
||||
text: '测试',
|
||||
color: customColor
|
||||
}
|
||||
})
|
||||
|
||||
const element = wrapper.find('.status-tag')
|
||||
expect(element.attributes('style')).toContain(`--status-color: ${customColor}`)
|
||||
})
|
||||
|
||||
it('emits click event when clicked and clickable is true', async () => {
|
||||
wrapper = mountComponent(StatusTag, {
|
||||
props: {
|
||||
status: 'success',
|
||||
text: '测试',
|
||||
clickable: true
|
||||
}
|
||||
})
|
||||
|
||||
await wrapper.trigger('click')
|
||||
expect(wrapper.emitted('click')).toBeTruthy()
|
||||
expect(wrapper.emitted('click')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('does not emit click event when clickable is false', async () => {
|
||||
wrapper = mountComponent(StatusTag, {
|
||||
props: {
|
||||
status: 'success',
|
||||
text: '测试',
|
||||
clickable: false
|
||||
}
|
||||
})
|
||||
|
||||
await wrapper.trigger('click')
|
||||
expect(wrapper.emitted('click')).toBeFalsy()
|
||||
})
|
||||
|
||||
it('applies disabled class when disabled is true', () => {
|
||||
wrapper = mountComponent(StatusTag, {
|
||||
props: {
|
||||
status: 'success',
|
||||
text: '测试',
|
||||
disabled: true
|
||||
}
|
||||
})
|
||||
|
||||
expect(wrapper.classes()).toContain('status-tag--disabled')
|
||||
})
|
||||
|
||||
it('applies round class when round is true', () => {
|
||||
wrapper = mountComponent(StatusTag, {
|
||||
props: {
|
||||
status: 'success',
|
||||
text: '测试',
|
||||
round: true
|
||||
}
|
||||
})
|
||||
|
||||
expect(wrapper.classes()).toContain('status-tag--round')
|
||||
})
|
||||
|
||||
it('applies plain class when plain is true', () => {
|
||||
wrapper = mountComponent(StatusTag, {
|
||||
props: {
|
||||
status: 'success',
|
||||
text: '测试',
|
||||
plain: true
|
||||
}
|
||||
})
|
||||
|
||||
expect(wrapper.classes()).toContain('status-tag--plain')
|
||||
})
|
||||
|
||||
it('computes status text correctly for predefined statuses', () => {
|
||||
const statusTextMap = [
|
||||
{ status: 'active', expectedText: '活跃' },
|
||||
{ status: 'inactive', expectedText: '非活跃' },
|
||||
{ status: 'online', expectedText: '在线' },
|
||||
{ status: 'offline', expectedText: '离线' },
|
||||
{ status: 'approved', expectedText: '已审批' },
|
||||
{ status: 'rejected', expectedText: '已拒绝' },
|
||||
{ status: 'processing', expectedText: '处理中' },
|
||||
{ status: 'completed', expectedText: '已完成' },
|
||||
{ status: 'cancelled', expectedText: '已取消' }
|
||||
]
|
||||
|
||||
statusTextMap.forEach(({ status, expectedText }) => {
|
||||
wrapper = mountComponent(StatusTag, {
|
||||
props: { status }
|
||||
})
|
||||
|
||||
expectElementText(wrapper, '.status-tag__text', expectedText)
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
|
||||
it('uses custom text when provided', () => {
|
||||
const customText = '自定义状态'
|
||||
wrapper = mountComponent(StatusTag, {
|
||||
props: {
|
||||
status: 'success',
|
||||
text: customText
|
||||
}
|
||||
})
|
||||
|
||||
expectElementText(wrapper, '.status-tag__text', customText)
|
||||
})
|
||||
|
||||
it('computes status icon correctly for predefined statuses', () => {
|
||||
wrapper = mountComponent(StatusTag, {
|
||||
props: {
|
||||
status: 'success',
|
||||
showIcon: true
|
||||
}
|
||||
})
|
||||
|
||||
expectElementExists(wrapper, '.status-tag__icon')
|
||||
})
|
||||
})
|
||||
@@ -1,21 +0,0 @@
|
||||
// 银行后端API配置
|
||||
export default {
|
||||
// 基础配置
|
||||
BASE_URL: 'http://localhost:5351', // 银行后端本地运行在5351端口
|
||||
TIMEOUT: 10000,
|
||||
|
||||
// 账户相关接口
|
||||
ACCOUNT: {
|
||||
CREATE: '/api/accounts',
|
||||
LIST: '/api/accounts',
|
||||
DETAIL: '/api/accounts/:id',
|
||||
DEPOSIT: '/api/accounts/:id/deposit',
|
||||
WITHDRAW: '/api/accounts/:id/withdraw'
|
||||
},
|
||||
|
||||
// 用户相关接口
|
||||
USER: {
|
||||
LOGIN: '/api/auth/login',
|
||||
INFO: '/api/users/me'
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { createSSRApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import App from './App.vue'
|
||||
|
||||
export function createApp() {
|
||||
const app = createSSRApp(App)
|
||||
const pinia = createPinia()
|
||||
|
||||
app.use(pinia)
|
||||
|
||||
return {
|
||||
app
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
{
|
||||
"name": "银行监管服务小程序",
|
||||
"appid": "wx1b9c7cd2d0e0bfd3",
|
||||
"description": "专业的银行监管服务平台,提供信贷管理、风险监控、客户服务等功能",
|
||||
"versionName": "1.0.0",
|
||||
"versionCode": "100",
|
||||
"transformPx": false,
|
||||
"app-plus": {
|
||||
"usingComponents": true
|
||||
},
|
||||
"h5": {
|
||||
"title": "银行监管服务",
|
||||
"router": {
|
||||
"mode": "hash",
|
||||
"base": "/"
|
||||
}
|
||||
},
|
||||
"mp-weixin": {
|
||||
"appid": "wx1b9c7cd2d0e0bfd3",
|
||||
"setting": {
|
||||
"urlCheck": false,
|
||||
"es6": true,
|
||||
"minified": true,
|
||||
"postcss": true
|
||||
},
|
||||
"usingComponents": true,
|
||||
"permission": {
|
||||
"scope.userLocation": {
|
||||
"desc": "获取位置信息用于抵押物地理位置监控"
|
||||
},
|
||||
"scope.camera": {
|
||||
"desc": "拍照功能用于现场核查和资产确认"
|
||||
}
|
||||
},
|
||||
"requiredBackgroundModes": ["location"],
|
||||
"plugins": {
|
||||
"WechatSI": {
|
||||
"version": "0.3.3",
|
||||
"provider": "wx069ba97219f66d99"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mp-alipay": {
|
||||
"usingComponents": true
|
||||
},
|
||||
"quickapp": {}
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
{
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/index/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "银行监管",
|
||||
"enablePullDownRefresh": true,
|
||||
"backgroundTextStyle": "dark"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/login/login",
|
||||
"style": {
|
||||
"navigationBarTitleText": "登录",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/dashboard/dashboard",
|
||||
"style": {
|
||||
"navigationBarTitleText": "工作台",
|
||||
"enablePullDownRefresh": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/customers/customers",
|
||||
"style": {
|
||||
"navigationBarTitleText": "客户管理",
|
||||
"enablePullDownRefresh": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/customers/customer-detail",
|
||||
"style": {
|
||||
"navigationBarTitleText": "客户详情"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/assets/assets",
|
||||
"style": {
|
||||
"navigationBarTitleText": "资产监管",
|
||||
"enablePullDownRefresh": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/assets/asset-detail",
|
||||
"style": {
|
||||
"navigationBarTitleText": "资产详情"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/transactions/transactions",
|
||||
"style": {
|
||||
"navigationBarTitleText": "交易管理",
|
||||
"enablePullDownRefresh": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/transactions/transaction-detail",
|
||||
"style": {
|
||||
"navigationBarTitleText": "交易详情"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/risk/risk",
|
||||
"style": {
|
||||
"navigationBarTitleText": "风险监控",
|
||||
"enablePullDownRefresh": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/risk/risk-detail",
|
||||
"style": {
|
||||
"navigationBarTitleText": "风险详情"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/profile/profile",
|
||||
"style": {
|
||||
"navigationBarTitleText": "个人中心",
|
||||
"enablePullDownRefresh": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
"navigationBarTextStyle": "white",
|
||||
"navigationBarTitleText": "银行监管",
|
||||
"navigationBarBackgroundColor": "#2c5aa0",
|
||||
"backgroundColor": "#f5f7fa",
|
||||
"backgroundTextStyle": "dark",
|
||||
"app-plus": {
|
||||
"background": "#f5f7fa"
|
||||
}
|
||||
},
|
||||
"tabBar": {
|
||||
"color": "#666666",
|
||||
"selectedColor": "#2c5aa0",
|
||||
"backgroundColor": "#ffffff",
|
||||
"borderStyle": "white",
|
||||
"height": "50px",
|
||||
"fontSize": "12px",
|
||||
"iconWidth": "24px",
|
||||
"spacing": "3px",
|
||||
"list": [
|
||||
{
|
||||
"pagePath": "pages/index/index",
|
||||
"text": "首页",
|
||||
"iconPath": "static/images/tab-home.png",
|
||||
"selectedIconPath": "static/images/tab-home-active.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/dashboard/dashboard",
|
||||
"text": "工作台",
|
||||
"iconPath": "static/images/tab-dashboard.png",
|
||||
"selectedIconPath": "static/images/tab-dashboard-active.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/customers/customers",
|
||||
"text": "客户",
|
||||
"iconPath": "static/images/tab-customers.png",
|
||||
"selectedIconPath": "static/images/tab-customers-active.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/risk/risk",
|
||||
"text": "风控",
|
||||
"iconPath": "static/images/tab-risk.png",
|
||||
"selectedIconPath": "static/images/tab-risk-active.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/profile/profile",
|
||||
"text": "我的",
|
||||
"iconPath": "static/images/tab-profile.png",
|
||||
"selectedIconPath": "static/images/tab-profile-active.png"
|
||||
}
|
||||
]
|
||||
},
|
||||
"condition": {
|
||||
"current": 0,
|
||||
"list": []
|
||||
}
|
||||
}
|
||||
@@ -1,983 +0,0 @@
|
||||
<template>
|
||||
<view class="assets-page">
|
||||
<!-- 自定义导航栏 -->
|
||||
<view class="custom-navbar">
|
||||
<view class="navbar-content">
|
||||
<text class="navbar-title">资产监管</text>
|
||||
<view class="navbar-actions">
|
||||
<view class="action-btn" @click="showFilter = true">
|
||||
<text class="iconfont icon-filter"></text>
|
||||
</view>
|
||||
<view class="action-btn" @click="refreshData">
|
||||
<text class="iconfont icon-refresh"></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 资产概览 -->
|
||||
<view class="overview-section">
|
||||
<view class="overview-card">
|
||||
<view class="card-header">
|
||||
<text class="card-title">资产概览</text>
|
||||
<text class="update-time">更新时间:{{ updateTime }}</text>
|
||||
</view>
|
||||
<view class="overview-grid">
|
||||
<view class="overview-item" v-for="item in overviewData" :key="item.key">
|
||||
<view class="item-value" :class="item.trend">{{ item.value }}</view>
|
||||
<view class="item-label">{{ item.label }}</view>
|
||||
<view class="item-change" :class="item.trend">
|
||||
<text class="trend-icon">{{ item.trend === 'up' ? '↗' : item.trend === 'down' ? '↘' : '→' }}</text>
|
||||
<text>{{ item.change }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 监管指标 -->
|
||||
<view class="indicators-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">监管指标</text>
|
||||
<view class="header-actions">
|
||||
<picker mode="selector" :value="selectedPeriod" :range="periodOptions" @change="onPeriodChange">
|
||||
<view class="picker-btn">
|
||||
<text>{{ periodOptions[selectedPeriod] }}</text>
|
||||
<text class="iconfont icon-arrow-down"></text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
<view class="indicators-grid">
|
||||
<view class="indicator-card" v-for="indicator in indicators" :key="indicator.id">
|
||||
<view class="indicator-header">
|
||||
<text class="indicator-name">{{ indicator.name }}</text>
|
||||
<view class="indicator-status" :class="indicator.status">
|
||||
{{ indicator.statusText }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="indicator-value">
|
||||
<text class="value">{{ indicator.value }}</text>
|
||||
<text class="unit">{{ indicator.unit }}</text>
|
||||
</view>
|
||||
<view class="indicator-progress">
|
||||
<view class="progress-bar">
|
||||
<view class="progress-fill" :style="{ width: indicator.progress + '%' }"></view>
|
||||
</view>
|
||||
<text class="progress-text">{{ indicator.progress }}%</text>
|
||||
</view>
|
||||
<view class="indicator-target">
|
||||
<text>目标值:{{ indicator.target }}{{ indicator.unit }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 资产列表 -->
|
||||
<view class="assets-list-section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">资产列表</text>
|
||||
<view class="list-stats">
|
||||
<text>共 {{ totalAssets }} 项资产</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="filter-tabs">
|
||||
<view
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === tab.key }"
|
||||
v-for="tab in filterTabs"
|
||||
:key="tab.key"
|
||||
@click="switchTab(tab.key)"
|
||||
>
|
||||
<text>{{ tab.label }}</text>
|
||||
<view class="tab-count">{{ tab.count }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view
|
||||
class="assets-list"
|
||||
scroll-y
|
||||
@scrolltolower="loadMore"
|
||||
:refresher-enabled="true"
|
||||
:refresher-triggered="refreshing"
|
||||
@refresherrefresh="onRefresh"
|
||||
>
|
||||
<view class="asset-item" v-for="asset in assetsList" :key="asset.id" @click="viewAssetDetail(asset)">
|
||||
<view class="asset-header">
|
||||
<view class="asset-info">
|
||||
<text class="asset-name">{{ asset.name }}</text>
|
||||
<text class="asset-code">编号:{{ asset.code }}</text>
|
||||
</view>
|
||||
<view class="asset-status" :class="asset.status">
|
||||
{{ asset.statusText }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="asset-details">
|
||||
<view class="detail-row">
|
||||
<text class="label">资产类型:</text>
|
||||
<text class="value">{{ asset.type }}</text>
|
||||
</view>
|
||||
<view class="detail-row">
|
||||
<text class="label">评估价值:</text>
|
||||
<text class="value amount">¥{{ asset.value }}</text>
|
||||
</view>
|
||||
<view class="detail-row">
|
||||
<text class="label">抵押率:</text>
|
||||
<text class="value">{{ asset.mortgageRate }}%</text>
|
||||
</view>
|
||||
<view class="detail-row">
|
||||
<text class="label">更新时间:</text>
|
||||
<text class="value">{{ asset.updateTime }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="asset-actions">
|
||||
<view class="action-btn primary" @click.stop="monitorAsset(asset)">
|
||||
<text>监控</text>
|
||||
</view>
|
||||
<view class="action-btn" @click.stop="evaluateAsset(asset)">
|
||||
<text>评估</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="load-more" v-if="hasMore">
|
||||
<text>加载更多...</text>
|
||||
</view>
|
||||
<view class="no-more" v-else-if="assetsList.length > 0">
|
||||
<text>没有更多数据了</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 筛选弹窗 -->
|
||||
<uni-popup ref="filterPopup" type="bottom" :mask-click="false">
|
||||
<view class="filter-popup">
|
||||
<view class="popup-header">
|
||||
<text class="popup-title">筛选条件</text>
|
||||
<view class="popup-actions">
|
||||
<text class="action-text" @click="resetFilter">重置</text>
|
||||
<text class="action-text primary" @click="applyFilter">确定</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="filter-content">
|
||||
<view class="filter-group">
|
||||
<text class="group-title">资产类型</text>
|
||||
<view class="checkbox-group">
|
||||
<label class="checkbox-item" v-for="type in assetTypes" :key="type.value">
|
||||
<checkbox :value="type.value" :checked="filterForm.types.includes(type.value)" />
|
||||
<text>{{ type.label }}</text>
|
||||
</label>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="filter-group">
|
||||
<text class="group-title">资产状态</text>
|
||||
<view class="checkbox-group">
|
||||
<label class="checkbox-item" v-for="status in assetStatuses" :key="status.value">
|
||||
<checkbox :value="status.value" :checked="filterForm.statuses.includes(status.value)" />
|
||||
<text>{{ status.label }}</text>
|
||||
</label>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="filter-group">
|
||||
<text class="group-title">价值范围</text>
|
||||
<view class="range-inputs">
|
||||
<input
|
||||
class="range-input"
|
||||
type="number"
|
||||
placeholder="最小值"
|
||||
v-model="filterForm.minValue"
|
||||
/>
|
||||
<text class="range-separator">-</text>
|
||||
<input
|
||||
class="range-input"
|
||||
type="number"
|
||||
placeholder="最大值"
|
||||
v-model="filterForm.maxValue"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</uni-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { request } from '@/utils/request'
|
||||
|
||||
export default {
|
||||
name: 'AssetsPage',
|
||||
setup() {
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const refreshing = ref(false)
|
||||
const showFilter = ref(false)
|
||||
const selectedPeriod = ref(0)
|
||||
const activeTab = ref('all')
|
||||
const updateTime = ref('')
|
||||
const totalAssets = ref(0)
|
||||
const hasMore = ref(true)
|
||||
const currentPage = ref(1)
|
||||
|
||||
// 概览数据
|
||||
const overviewData = ref([
|
||||
{ key: 'total', label: '总资产价值', value: '¥2,580.5万', change: '+12.5%', trend: 'up' },
|
||||
{ key: 'mortgage', label: '抵押资产', value: '¥1,850.2万', change: '+8.3%', trend: 'up' },
|
||||
{ key: 'available', label: '可用资产', value: '¥730.3万', change: '-2.1%', trend: 'down' },
|
||||
{ key: 'risk', label: '风险资产', value: '¥125.6万', change: '+5.2%', trend: 'up' }
|
||||
])
|
||||
|
||||
// 监管指标
|
||||
const indicators = ref([
|
||||
{
|
||||
id: 1,
|
||||
name: '资产覆盖率',
|
||||
value: '125.8',
|
||||
unit: '%',
|
||||
progress: 85,
|
||||
target: '120',
|
||||
status: 'normal',
|
||||
statusText: '正常'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '抵押率',
|
||||
value: '68.5',
|
||||
unit: '%',
|
||||
progress: 68,
|
||||
target: '80',
|
||||
status: 'normal',
|
||||
statusText: '正常'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '风险资产占比',
|
||||
value: '4.9',
|
||||
unit: '%',
|
||||
progress: 49,
|
||||
target: '10',
|
||||
status: 'warning',
|
||||
statusText: '预警'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '资产流动性',
|
||||
value: '28.3',
|
||||
unit: '%',
|
||||
progress: 28,
|
||||
target: '30',
|
||||
status: 'risk',
|
||||
statusText: '风险'
|
||||
}
|
||||
])
|
||||
|
||||
// 资产列表
|
||||
const assetsList = ref([])
|
||||
|
||||
// 筛选相关
|
||||
const periodOptions = ['近7天', '近30天', '近3个月', '近6个月', '近1年']
|
||||
const filterTabs = ref([
|
||||
{ key: 'all', label: '全部', count: 0 },
|
||||
{ key: 'normal', label: '正常', count: 0 },
|
||||
{ key: 'warning', label: '预警', count: 0 },
|
||||
{ key: 'risk', label: '风险', count: 0 }
|
||||
])
|
||||
|
||||
const assetTypes = [
|
||||
{ value: 'livestock', label: '牲畜资产' },
|
||||
{ value: 'equipment', label: '设备资产' },
|
||||
{ value: 'land', label: '土地资产' },
|
||||
{ value: 'building', label: '建筑资产' }
|
||||
]
|
||||
|
||||
const assetStatuses = [
|
||||
{ value: 'normal', label: '正常' },
|
||||
{ value: 'warning', label: '预警' },
|
||||
{ value: 'risk', label: '风险' },
|
||||
{ value: 'frozen', label: '冻结' }
|
||||
]
|
||||
|
||||
const filterForm = reactive({
|
||||
types: [],
|
||||
statuses: [],
|
||||
minValue: '',
|
||||
maxValue: ''
|
||||
})
|
||||
|
||||
// 方法
|
||||
const initPageData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
await Promise.all([
|
||||
getOverviewData(),
|
||||
getIndicators(),
|
||||
getAssetsList()
|
||||
])
|
||||
updateTime.value = new Date().toLocaleString()
|
||||
} catch (error) {
|
||||
console.error('初始化页面数据失败:', error)
|
||||
uni.showToast({
|
||||
title: '数据加载失败',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const getOverviewData = async () => {
|
||||
const response = await request.get('/api/assets/overview')
|
||||
if (response.success) {
|
||||
overviewData.value = response.data
|
||||
}
|
||||
}
|
||||
|
||||
const getIndicators = async () => {
|
||||
const response = await request.get('/api/assets/indicators', {
|
||||
period: periodOptions[selectedPeriod.value]
|
||||
})
|
||||
if (response.success) {
|
||||
indicators.value = response.data
|
||||
}
|
||||
}
|
||||
|
||||
const getAssetsList = async (reset = false) => {
|
||||
if (reset) {
|
||||
currentPage.value = 1
|
||||
assetsList.value = []
|
||||
}
|
||||
|
||||
const response = await request.get('/api/assets/list', {
|
||||
page: currentPage.value,
|
||||
pageSize: 20,
|
||||
status: activeTab.value === 'all' ? '' : activeTab.value,
|
||||
...filterForm
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
const { list, total, hasMore: more } = response.data
|
||||
|
||||
if (reset) {
|
||||
assetsList.value = list
|
||||
} else {
|
||||
assetsList.value.push(...list)
|
||||
}
|
||||
|
||||
totalAssets.value = total
|
||||
hasMore.value = more
|
||||
|
||||
// 更新标签计数
|
||||
updateTabCounts(response.data.statusCounts)
|
||||
}
|
||||
}
|
||||
|
||||
const updateTabCounts = (counts) => {
|
||||
filterTabs.value.forEach(tab => {
|
||||
tab.count = counts[tab.key] || 0
|
||||
})
|
||||
}
|
||||
|
||||
const refreshData = async () => {
|
||||
await initPageData()
|
||||
uni.showToast({
|
||||
title: '刷新成功',
|
||||
icon: 'success'
|
||||
})
|
||||
}
|
||||
|
||||
const onPeriodChange = (e) => {
|
||||
selectedPeriod.value = e.detail.value
|
||||
getIndicators()
|
||||
}
|
||||
|
||||
const switchTab = (tabKey) => {
|
||||
activeTab.value = tabKey
|
||||
getAssetsList(true)
|
||||
}
|
||||
|
||||
const loadMore = () => {
|
||||
if (hasMore.value && !loading.value) {
|
||||
currentPage.value++
|
||||
getAssetsList()
|
||||
}
|
||||
}
|
||||
|
||||
const onRefresh = async () => {
|
||||
refreshing.value = true
|
||||
await getAssetsList(true)
|
||||
refreshing.value = false
|
||||
}
|
||||
|
||||
const viewAssetDetail = (asset) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/assets/detail?id=${asset.id}`
|
||||
})
|
||||
}
|
||||
|
||||
const monitorAsset = (asset) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/assets/monitor?id=${asset.id}`
|
||||
})
|
||||
}
|
||||
|
||||
const evaluateAsset = (asset) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/assets/evaluate?id=${asset.id}`
|
||||
})
|
||||
}
|
||||
|
||||
const applyFilter = () => {
|
||||
showFilter.value = false
|
||||
getAssetsList(true)
|
||||
}
|
||||
|
||||
const resetFilter = () => {
|
||||
Object.assign(filterForm, {
|
||||
types: [],
|
||||
statuses: [],
|
||||
minValue: '',
|
||||
maxValue: ''
|
||||
})
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
initPageData()
|
||||
})
|
||||
|
||||
return {
|
||||
loading,
|
||||
refreshing,
|
||||
showFilter,
|
||||
selectedPeriod,
|
||||
activeTab,
|
||||
updateTime,
|
||||
totalAssets,
|
||||
hasMore,
|
||||
overviewData,
|
||||
indicators,
|
||||
assetsList,
|
||||
periodOptions,
|
||||
filterTabs,
|
||||
assetTypes,
|
||||
assetStatuses,
|
||||
filterForm,
|
||||
initPageData,
|
||||
refreshData,
|
||||
onPeriodChange,
|
||||
switchTab,
|
||||
loadMore,
|
||||
onRefresh,
|
||||
viewAssetDetail,
|
||||
monitorAsset,
|
||||
evaluateAsset,
|
||||
applyFilter,
|
||||
resetFilter
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/styles/variables.scss';
|
||||
|
||||
.assets-page {
|
||||
min-height: 100vh;
|
||||
background-color: $bg-color-light;
|
||||
}
|
||||
|
||||
.custom-navbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
background: linear-gradient(135deg, $primary-color, $primary-color-light);
|
||||
padding-top: var(--status-bar-height);
|
||||
|
||||
.navbar-content {
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 $spacing-md;
|
||||
|
||||
.navbar-title {
|
||||
font-size: $font-size-lg;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.navbar-actions {
|
||||
display: flex;
|
||||
gap: $spacing-sm;
|
||||
|
||||
.action-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: $border-radius-sm;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.overview-section {
|
||||
margin-top: calc(44px + var(--status-bar-height));
|
||||
padding: $spacing-md;
|
||||
|
||||
.overview-card {
|
||||
background: white;
|
||||
border-radius: $border-radius-lg;
|
||||
padding: $spacing-lg;
|
||||
box-shadow: $shadow-light;
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: $spacing-lg;
|
||||
|
||||
.card-title {
|
||||
font-size: $font-size-lg;
|
||||
font-weight: 600;
|
||||
color: $text-color-primary;
|
||||
}
|
||||
|
||||
.update-time {
|
||||
font-size: $font-size-sm;
|
||||
color: $text-color-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
.overview-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: $spacing-lg;
|
||||
|
||||
.overview-item {
|
||||
text-align: center;
|
||||
|
||||
.item-value {
|
||||
font-size: $font-size-xl;
|
||||
font-weight: 600;
|
||||
margin-bottom: $spacing-xs;
|
||||
|
||||
&.up { color: $success-color; }
|
||||
&.down { color: $error-color; }
|
||||
}
|
||||
|
||||
.item-label {
|
||||
font-size: $font-size-sm;
|
||||
color: $text-color-secondary;
|
||||
margin-bottom: $spacing-xs;
|
||||
}
|
||||
|
||||
.item-change {
|
||||
font-size: $font-size-sm;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
|
||||
&.up { color: $success-color; }
|
||||
&.down { color: $error-color; }
|
||||
|
||||
.trend-icon {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.indicators-section {
|
||||
padding: 0 $spacing-md $spacing-md;
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: $spacing-md;
|
||||
|
||||
.section-title {
|
||||
font-size: $font-size-lg;
|
||||
font-weight: 600;
|
||||
color: $text-color-primary;
|
||||
}
|
||||
|
||||
.picker-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $spacing-xs;
|
||||
padding: $spacing-xs $spacing-sm;
|
||||
background: white;
|
||||
border-radius: $border-radius-sm;
|
||||
border: 1px solid $border-color;
|
||||
font-size: $font-size-sm;
|
||||
color: $text-color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.indicators-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: $spacing-md;
|
||||
|
||||
.indicator-card {
|
||||
background: white;
|
||||
border-radius: $border-radius-md;
|
||||
padding: $spacing-md;
|
||||
box-shadow: $shadow-light;
|
||||
|
||||
.indicator-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: $spacing-sm;
|
||||
|
||||
.indicator-name {
|
||||
font-size: $font-size-sm;
|
||||
color: $text-color-secondary;
|
||||
}
|
||||
|
||||
.indicator-status {
|
||||
padding: 2px 6px;
|
||||
border-radius: $border-radius-xs;
|
||||
font-size: 10px;
|
||||
|
||||
&.normal {
|
||||
background: rgba($success-color, 0.1);
|
||||
color: $success-color;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
background: rgba($warning-color, 0.1);
|
||||
color: $warning-color;
|
||||
}
|
||||
|
||||
&.risk {
|
||||
background: rgba($error-color, 0.1);
|
||||
color: $error-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.indicator-value {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 2px;
|
||||
margin-bottom: $spacing-sm;
|
||||
|
||||
.value {
|
||||
font-size: $font-size-xl;
|
||||
font-weight: 600;
|
||||
color: $text-color-primary;
|
||||
}
|
||||
|
||||
.unit {
|
||||
font-size: $font-size-sm;
|
||||
color: $text-color-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
.indicator-progress {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $spacing-xs;
|
||||
margin-bottom: $spacing-xs;
|
||||
|
||||
.progress-bar {
|
||||
flex: 1;
|
||||
height: 4px;
|
||||
background: $bg-color-light;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: $primary-color;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
font-size: 10px;
|
||||
color: $text-color-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
.indicator-target {
|
||||
font-size: 10px;
|
||||
color: $text-color-secondary;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.assets-list-section {
|
||||
padding: 0 $spacing-md;
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: $spacing-md;
|
||||
|
||||
.section-title {
|
||||
font-size: $font-size-lg;
|
||||
font-weight: 600;
|
||||
color: $text-color-primary;
|
||||
}
|
||||
|
||||
.list-stats {
|
||||
font-size: $font-size-sm;
|
||||
color: $text-color-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-tabs {
|
||||
display: flex;
|
||||
background: white;
|
||||
border-radius: $border-radius-md;
|
||||
padding: 4px;
|
||||
margin-bottom: $spacing-md;
|
||||
box-shadow: $shadow-light;
|
||||
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: $spacing-xs;
|
||||
padding: $spacing-sm;
|
||||
border-radius: $border-radius-sm;
|
||||
font-size: $font-size-sm;
|
||||
color: $text-color-secondary;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.active {
|
||||
background: $primary-color;
|
||||
color: white;
|
||||
|
||||
.tab-count {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.tab-count {
|
||||
padding: 2px 6px;
|
||||
border-radius: $border-radius-xs;
|
||||
background: $bg-color-light;
|
||||
font-size: 10px;
|
||||
min-width: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.assets-list {
|
||||
height: calc(100vh - 400px);
|
||||
|
||||
.asset-item {
|
||||
background: white;
|
||||
border-radius: $border-radius-md;
|
||||
padding: $spacing-md;
|
||||
margin-bottom: $spacing-md;
|
||||
box-shadow: $shadow-light;
|
||||
|
||||
.asset-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: $spacing-md;
|
||||
|
||||
.asset-info {
|
||||
.asset-name {
|
||||
font-size: $font-size-md;
|
||||
font-weight: 600;
|
||||
color: $text-color-primary;
|
||||
display: block;
|
||||
margin-bottom: $spacing-xs;
|
||||
}
|
||||
|
||||
.asset-code {
|
||||
font-size: $font-size-sm;
|
||||
color: $text-color-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
.asset-status {
|
||||
padding: 4px 8px;
|
||||
border-radius: $border-radius-xs;
|
||||
font-size: $font-size-xs;
|
||||
|
||||
&.normal {
|
||||
background: rgba($success-color, 0.1);
|
||||
color: $success-color;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
background: rgba($warning-color, 0.1);
|
||||
color: $warning-color;
|
||||
}
|
||||
|
||||
&.risk {
|
||||
background: rgba($error-color, 0.1);
|
||||
color: $error-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.asset-details {
|
||||
margin-bottom: $spacing-md;
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: $spacing-xs;
|
||||
|
||||
.label {
|
||||
font-size: $font-size-sm;
|
||||
color: $text-color-secondary;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: $font-size-sm;
|
||||
color: $text-color-primary;
|
||||
|
||||
&.amount {
|
||||
font-weight: 600;
|
||||
color: $primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.asset-actions {
|
||||
display: flex;
|
||||
gap: $spacing-sm;
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
padding: $spacing-sm;
|
||||
border-radius: $border-radius-sm;
|
||||
text-align: center;
|
||||
font-size: $font-size-sm;
|
||||
border: 1px solid $border-color;
|
||||
color: $text-color-primary;
|
||||
|
||||
&.primary {
|
||||
background: $primary-color;
|
||||
border-color: $primary-color;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.load-more, .no-more {
|
||||
text-align: center;
|
||||
padding: $spacing-lg;
|
||||
font-size: $font-size-sm;
|
||||
color: $text-color-secondary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filter-popup {
|
||||
background: white;
|
||||
border-radius: $border-radius-lg $border-radius-lg 0 0;
|
||||
max-height: 80vh;
|
||||
|
||||
.popup-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: $spacing-lg;
|
||||
border-bottom: 1px solid $border-color;
|
||||
|
||||
.popup-title {
|
||||
font-size: $font-size-lg;
|
||||
font-weight: 600;
|
||||
color: $text-color-primary;
|
||||
}
|
||||
|
||||
.popup-actions {
|
||||
display: flex;
|
||||
gap: $spacing-lg;
|
||||
|
||||
.action-text {
|
||||
font-size: $font-size-md;
|
||||
color: $text-color-secondary;
|
||||
|
||||
&.primary {
|
||||
color: $primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filter-content {
|
||||
padding: $spacing-lg;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
|
||||
.filter-group {
|
||||
margin-bottom: $spacing-xl;
|
||||
|
||||
.group-title {
|
||||
font-size: $font-size-md;
|
||||
font-weight: 600;
|
||||
color: $text-color-primary;
|
||||
margin-bottom: $spacing-md;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: $spacing-md;
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $spacing-xs;
|
||||
font-size: $font-size-sm;
|
||||
color: $text-color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.range-inputs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $spacing-sm;
|
||||
|
||||
.range-input {
|
||||
flex: 1;
|
||||
padding: $spacing-sm;
|
||||
border: 1px solid $border-color;
|
||||
border-radius: $border-radius-sm;
|
||||
font-size: $font-size-sm;
|
||||
}
|
||||
|
||||
.range-separator {
|
||||
color: $text-color-secondary;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,866 +0,0 @@
|
||||
<template>
|
||||
<view class="customers-container">
|
||||
<!-- 自定义导航栏 -->
|
||||
<view class="custom-navbar">
|
||||
<view class="navbar-content">
|
||||
<view class="navbar-left" @click="goBack">
|
||||
<text class="iconfont icon-arrow-left"></text>
|
||||
</view>
|
||||
<text class="navbar-title">客户管理</text>
|
||||
<view class="navbar-right">
|
||||
<view class="action-item" @click="showSearchModal">
|
||||
<text class="iconfont icon-search"></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 页面内容 -->
|
||||
<view class="page-content">
|
||||
<!-- 筛选栏 -->
|
||||
<view class="filter-bar">
|
||||
<scroll-view class="filter-scroll" scroll-x>
|
||||
<view class="filter-list">
|
||||
<view
|
||||
class="filter-item"
|
||||
:class="{ active: activeFilter === item.value }"
|
||||
v-for="(item, index) in filterOptions"
|
||||
:key="index"
|
||||
@click="changeFilter(item.value)"
|
||||
>
|
||||
<text class="filter-text">{{ item.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<view class="stats-bar">
|
||||
<view class="stats-item">
|
||||
<text class="stats-value">{{ totalCount }}</text>
|
||||
<text class="stats-label">总客户数</text>
|
||||
</view>
|
||||
<view class="stats-item">
|
||||
<text class="stats-value">{{ activeCount }}</text>
|
||||
<text class="stats-label">活跃客户</text>
|
||||
</view>
|
||||
<view class="stats-item">
|
||||
<text class="stats-value">{{ riskCount }}</text>
|
||||
<text class="stats-label">风险客户</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 客户列表 -->
|
||||
<scroll-view
|
||||
class="customer-list"
|
||||
scroll-y
|
||||
refresher-enabled
|
||||
:refresher-triggered="refreshing"
|
||||
@refresherrefresh="onRefresh"
|
||||
@scrolltolower="loadMore"
|
||||
>
|
||||
<view
|
||||
class="customer-item"
|
||||
v-for="(customer, index) in customerList"
|
||||
:key="customer.id"
|
||||
@click="viewCustomerDetail(customer)"
|
||||
>
|
||||
<view class="customer-avatar">
|
||||
<image
|
||||
v-if="customer.avatar"
|
||||
:src="customer.avatar"
|
||||
class="avatar-image"
|
||||
/>
|
||||
<view v-else class="avatar-placeholder">
|
||||
<text class="avatar-text">{{ customer.name.charAt(0) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="customer-info">
|
||||
<view class="customer-header">
|
||||
<text class="customer-name">{{ customer.name }}</text>
|
||||
<view class="customer-status" :class="customer.statusClass">
|
||||
<text class="status-text">{{ customer.statusText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="customer-details">
|
||||
<text class="detail-item">{{ customer.phone }}</text>
|
||||
<text class="detail-item">{{ customer.businessType }}</text>
|
||||
</view>
|
||||
|
||||
<view class="customer-metrics">
|
||||
<view class="metric-item">
|
||||
<text class="metric-label">贷款余额</text>
|
||||
<text class="metric-value">¥{{ formatAmount(customer.loanBalance) }}</text>
|
||||
</view>
|
||||
<view class="metric-item">
|
||||
<text class="metric-label">风险等级</text>
|
||||
<text class="metric-value" :class="customer.riskLevelClass">{{ customer.riskLevel }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="customer-actions">
|
||||
<view class="action-btn" @click.stop="callCustomer(customer)">
|
||||
<text class="iconfont icon-phone"></text>
|
||||
</view>
|
||||
<view class="action-btn" @click.stop="messageCustomer(customer)">
|
||||
<text class="iconfont icon-message"></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view class="load-more" v-if="hasMore">
|
||||
<text class="load-text">{{ loading ? '加载中...' : '上拉加载更多' }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 没有更多数据 -->
|
||||
<view class="no-more" v-if="!hasMore && customerList.length > 0">
|
||||
<text class="no-more-text">没有更多数据了</text>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" v-if="!loading && customerList.length === 0">
|
||||
<text class="empty-text">暂无客户数据</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 搜索弹窗 -->
|
||||
<uni-popup ref="searchPopup" type="top">
|
||||
<view class="search-modal">
|
||||
<view class="search-header">
|
||||
<view class="search-input-wrapper">
|
||||
<text class="iconfont icon-search search-icon"></text>
|
||||
<input
|
||||
class="search-input"
|
||||
placeholder="搜索客户姓名、手机号"
|
||||
v-model="searchKeyword"
|
||||
@input="onSearchInput"
|
||||
@confirm="performSearch"
|
||||
/>
|
||||
<text class="search-cancel" @click="hideSearchModal">取消</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 搜索历史 -->
|
||||
<view class="search-history" v-if="searchHistory.length > 0 && !searchKeyword">
|
||||
<view class="history-header">
|
||||
<text class="history-title">搜索历史</text>
|
||||
<text class="history-clear" @click="clearSearchHistory">清空</text>
|
||||
</view>
|
||||
<view class="history-list">
|
||||
<view
|
||||
class="history-item"
|
||||
v-for="(item, index) in searchHistory"
|
||||
:key="index"
|
||||
@click="searchByHistory(item)"
|
||||
>
|
||||
<text class="history-text">{{ item }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 搜索建议 -->
|
||||
<view class="search-suggestions" v-if="searchSuggestions.length > 0 && searchKeyword">
|
||||
<view
|
||||
class="suggestion-item"
|
||||
v-for="(suggestion, index) in searchSuggestions"
|
||||
:key="index"
|
||||
@click="searchBySuggestion(suggestion)"
|
||||
>
|
||||
<text class="iconfont icon-search suggestion-icon"></text>
|
||||
<text class="suggestion-text">{{ suggestion }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</uni-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useUserStore, useAppStore } from '@/store'
|
||||
import { get } from '@/utils/request'
|
||||
|
||||
export default {
|
||||
name: 'Customers',
|
||||
setup() {
|
||||
const userStore = useUserStore()
|
||||
const appStore = useAppStore()
|
||||
|
||||
// 响应式数据
|
||||
const refreshing = ref(false)
|
||||
const loading = ref(false)
|
||||
const hasMore = ref(true)
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(20)
|
||||
|
||||
const activeFilter = ref('all')
|
||||
const searchKeyword = ref('')
|
||||
const searchHistory = ref([])
|
||||
const searchSuggestions = ref([])
|
||||
|
||||
const customerList = ref([])
|
||||
const totalCount = ref(0)
|
||||
const activeCount = ref(0)
|
||||
const riskCount = ref(0)
|
||||
|
||||
const filterOptions = ref([
|
||||
{ label: '全部', value: 'all' },
|
||||
{ label: '活跃', value: 'active' },
|
||||
{ label: '风险', value: 'risk' },
|
||||
{ label: '养殖业', value: 'breeding' },
|
||||
{ label: '种植业', value: 'planting' },
|
||||
{ label: '加工业', value: 'processing' }
|
||||
])
|
||||
|
||||
// 计算属性
|
||||
const filteredCustomers = computed(() => {
|
||||
if (activeFilter.value === 'all') {
|
||||
return customerList.value
|
||||
}
|
||||
return customerList.value.filter(customer => {
|
||||
switch (activeFilter.value) {
|
||||
case 'active':
|
||||
return customer.status === 'active'
|
||||
case 'risk':
|
||||
return customer.riskLevel === '高风险'
|
||||
case 'breeding':
|
||||
return customer.businessType.includes('养殖')
|
||||
case 'planting':
|
||||
return customer.businessType.includes('种植')
|
||||
case 'processing':
|
||||
return customer.businessType.includes('加工')
|
||||
default:
|
||||
return true
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 方法
|
||||
const initPageData = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
await Promise.all([
|
||||
getCustomerList(),
|
||||
getCustomerStats()
|
||||
])
|
||||
} catch (error) {
|
||||
console.error('初始化页面数据失败:', error)
|
||||
appStore.showToast('数据加载失败', 'error')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const getCustomerList = async (isLoadMore = false) => {
|
||||
try {
|
||||
const params = {
|
||||
page: isLoadMore ? currentPage.value : 1,
|
||||
pageSize: pageSize.value,
|
||||
filter: activeFilter.value,
|
||||
keyword: searchKeyword.value
|
||||
}
|
||||
|
||||
const response = await get('/api/customers', params)
|
||||
|
||||
if (response.success) {
|
||||
const newData = response.data.list || []
|
||||
|
||||
if (isLoadMore) {
|
||||
customerList.value = [...customerList.value, ...newData]
|
||||
} else {
|
||||
customerList.value = newData
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
hasMore.value = newData.length === pageSize.value
|
||||
totalCount.value = response.data.total || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取客户列表失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const getCustomerStats = async () => {
|
||||
try {
|
||||
const response = await get('/api/customers/stats')
|
||||
if (response.success) {
|
||||
totalCount.value = response.data.total || 0
|
||||
activeCount.value = response.data.active || 0
|
||||
riskCount.value = response.data.risk || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取客户统计失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const onRefresh = async () => {
|
||||
refreshing.value = true
|
||||
currentPage.value = 1
|
||||
await initPageData()
|
||||
refreshing.value = false
|
||||
}
|
||||
|
||||
const loadMore = async () => {
|
||||
if (loading.value || !hasMore.value) return
|
||||
|
||||
loading.value = true
|
||||
currentPage.value++
|
||||
|
||||
try {
|
||||
await getCustomerList(true)
|
||||
} catch (error) {
|
||||
currentPage.value--
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const changeFilter = async (filterValue) => {
|
||||
if (activeFilter.value === filterValue) return
|
||||
|
||||
activeFilter.value = filterValue
|
||||
currentPage.value = 1
|
||||
await getCustomerList()
|
||||
}
|
||||
|
||||
const viewCustomerDetail = (customer) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/customers/detail?id=${customer.id}`
|
||||
})
|
||||
}
|
||||
|
||||
const callCustomer = (customer) => {
|
||||
uni.makePhoneCall({
|
||||
phoneNumber: customer.phone,
|
||||
fail: (error) => {
|
||||
appStore.showToast('拨打电话失败', 'error')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const messageCustomer = (customer) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/message/chat?customerId=${customer.id}&customerName=${customer.name}`
|
||||
})
|
||||
}
|
||||
|
||||
const showSearchModal = () => {
|
||||
// 加载搜索历史
|
||||
const history = uni.getStorageSync('customerSearchHistory') || []
|
||||
searchHistory.value = history
|
||||
|
||||
// 显示搜索弹窗
|
||||
this.$refs.searchPopup.open()
|
||||
}
|
||||
|
||||
const hideSearchModal = () => {
|
||||
this.$refs.searchPopup.close()
|
||||
searchKeyword.value = ''
|
||||
searchSuggestions.value = []
|
||||
}
|
||||
|
||||
const onSearchInput = async () => {
|
||||
if (!searchKeyword.value.trim()) {
|
||||
searchSuggestions.value = []
|
||||
return
|
||||
}
|
||||
|
||||
// 获取搜索建议
|
||||
try {
|
||||
const response = await get('/api/customers/search-suggestions', {
|
||||
keyword: searchKeyword.value
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
searchSuggestions.value = response.data || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取搜索建议失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const performSearch = async () => {
|
||||
if (!searchKeyword.value.trim()) return
|
||||
|
||||
// 保存搜索历史
|
||||
saveSearchHistory(searchKeyword.value)
|
||||
|
||||
// 执行搜索
|
||||
hideSearchModal()
|
||||
currentPage.value = 1
|
||||
await getCustomerList()
|
||||
}
|
||||
|
||||
const searchByHistory = (keyword) => {
|
||||
searchKeyword.value = keyword
|
||||
performSearch()
|
||||
}
|
||||
|
||||
const searchBySuggestion = (suggestion) => {
|
||||
searchKeyword.value = suggestion
|
||||
performSearch()
|
||||
}
|
||||
|
||||
const saveSearchHistory = (keyword) => {
|
||||
let history = uni.getStorageSync('customerSearchHistory') || []
|
||||
|
||||
// 移除重复项
|
||||
history = history.filter(item => item !== keyword)
|
||||
|
||||
// 添加到开头
|
||||
history.unshift(keyword)
|
||||
|
||||
// 限制历史记录数量
|
||||
if (history.length > 10) {
|
||||
history = history.slice(0, 10)
|
||||
}
|
||||
|
||||
uni.setStorageSync('customerSearchHistory', history)
|
||||
searchHistory.value = history
|
||||
}
|
||||
|
||||
const clearSearchHistory = () => {
|
||||
uni.removeStorageSync('customerSearchHistory')
|
||||
searchHistory.value = []
|
||||
}
|
||||
|
||||
const formatAmount = (amount) => {
|
||||
if (!amount) return '0'
|
||||
|
||||
const num = parseFloat(amount)
|
||||
if (num >= 10000) {
|
||||
return (num / 10000).toFixed(1) + '万'
|
||||
}
|
||||
return num.toLocaleString()
|
||||
}
|
||||
|
||||
const goBack = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
initPageData()
|
||||
})
|
||||
|
||||
return {
|
||||
refreshing,
|
||||
loading,
|
||||
hasMore,
|
||||
activeFilter,
|
||||
searchKeyword,
|
||||
searchHistory,
|
||||
searchSuggestions,
|
||||
customerList,
|
||||
totalCount,
|
||||
activeCount,
|
||||
riskCount,
|
||||
filterOptions,
|
||||
filteredCustomers,
|
||||
onRefresh,
|
||||
loadMore,
|
||||
changeFilter,
|
||||
viewCustomerDetail,
|
||||
callCustomer,
|
||||
messageCustomer,
|
||||
showSearchModal,
|
||||
hideSearchModal,
|
||||
onSearchInput,
|
||||
performSearch,
|
||||
searchByHistory,
|
||||
searchBySuggestion,
|
||||
clearSearchHistory,
|
||||
formatAmount,
|
||||
goBack
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/styles/variables.scss';
|
||||
|
||||
.customers-container {
|
||||
min-height: 100vh;
|
||||
background-color: $background-color;
|
||||
}
|
||||
|
||||
.custom-navbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
background-color: white;
|
||||
border-bottom: 1px solid $border-color;
|
||||
padding-top: var(--status-bar-height);
|
||||
|
||||
.navbar-content {
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 $spacing-md;
|
||||
|
||||
.navbar-left, .navbar-right {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.iconfont {
|
||||
font-size: $font-size-lg;
|
||||
color: $text-color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-title {
|
||||
font-size: $font-size-lg;
|
||||
font-weight: 600;
|
||||
color: $text-color-primary;
|
||||
}
|
||||
|
||||
.action-item {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: $border-radius-sm;
|
||||
background-color: $background-color;
|
||||
|
||||
.iconfont {
|
||||
font-size: $font-size-md;
|
||||
color: $text-color-secondary;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page-content {
|
||||
padding-top: calc(var(--status-bar-height) + 44px);
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
background-color: white;
|
||||
border-bottom: 1px solid $border-color;
|
||||
|
||||
.filter-scroll {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.filter-list {
|
||||
display: flex;
|
||||
padding: $spacing-sm $spacing-md;
|
||||
|
||||
.filter-item {
|
||||
flex-shrink: 0;
|
||||
padding: $spacing-sm $spacing-md;
|
||||
margin-right: $spacing-sm;
|
||||
border-radius: $border-radius-lg;
|
||||
background-color: $background-color;
|
||||
border: 1px solid transparent;
|
||||
|
||||
&.active {
|
||||
background-color: $primary-color;
|
||||
border-color: $primary-color;
|
||||
|
||||
.filter-text {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-text {
|
||||
font-size: $font-size-sm;
|
||||
color: $text-color-secondary;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stats-bar {
|
||||
display: flex;
|
||||
background-color: white;
|
||||
padding: $spacing-md;
|
||||
border-bottom: 1px solid $border-color;
|
||||
|
||||
.stats-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
|
||||
.stats-value {
|
||||
display: block;
|
||||
font-size: $font-size-xl;
|
||||
font-weight: 600;
|
||||
color: $primary-color;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.stats-label {
|
||||
display: block;
|
||||
font-size: $font-size-sm;
|
||||
color: $text-color-secondary;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.customer-list {
|
||||
flex: 1;
|
||||
padding: $spacing-sm;
|
||||
}
|
||||
|
||||
.customer-item {
|
||||
background-color: white;
|
||||
border-radius: $border-radius-md;
|
||||
padding: $spacing-md;
|
||||
margin-bottom: $spacing-sm;
|
||||
box-shadow: $box-shadow-light;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.customer-avatar {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
margin-right: $spacing-md;
|
||||
|
||||
.avatar-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.avatar-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
background-color: $primary-color;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.avatar-text {
|
||||
color: white;
|
||||
font-size: $font-size-lg;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.customer-info {
|
||||
flex: 1;
|
||||
|
||||
.customer-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: $spacing-sm;
|
||||
|
||||
.customer-name {
|
||||
font-size: $font-size-lg;
|
||||
font-weight: 600;
|
||||
color: $text-color-primary;
|
||||
}
|
||||
|
||||
.customer-status {
|
||||
padding: 2px 8px;
|
||||
border-radius: $border-radius-sm;
|
||||
font-size: $font-size-xs;
|
||||
|
||||
&.active {
|
||||
background-color: rgba($success-color, 0.1);
|
||||
color: $success-color;
|
||||
}
|
||||
|
||||
&.inactive {
|
||||
background-color: rgba($text-color-placeholder, 0.1);
|
||||
color: $text-color-placeholder;
|
||||
}
|
||||
|
||||
&.risk {
|
||||
background-color: rgba($danger-color, 0.1);
|
||||
color: $danger-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.customer-details {
|
||||
display: flex;
|
||||
margin-bottom: $spacing-sm;
|
||||
|
||||
.detail-item {
|
||||
font-size: $font-size-sm;
|
||||
color: $text-color-secondary;
|
||||
margin-right: $spacing-md;
|
||||
}
|
||||
}
|
||||
|
||||
.customer-metrics {
|
||||
display: flex;
|
||||
|
||||
.metric-item {
|
||||
margin-right: $spacing-lg;
|
||||
|
||||
.metric-label {
|
||||
display: block;
|
||||
font-size: $font-size-xs;
|
||||
color: $text-color-placeholder;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
display: block;
|
||||
font-size: $font-size-sm;
|
||||
font-weight: 500;
|
||||
color: $text-color-primary;
|
||||
margin-top: 2px;
|
||||
|
||||
&.low-risk { color: $success-color; }
|
||||
&.medium-risk { color: $warning-color; }
|
||||
&.high-risk { color: $danger-color; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.customer-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.action-btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: $border-radius-sm;
|
||||
background-color: $background-color;
|
||||
margin-bottom: $spacing-sm;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
font-size: $font-size-md;
|
||||
color: $text-color-secondary;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.load-more, .no-more, .empty-state {
|
||||
text-align: center;
|
||||
padding: $spacing-lg;
|
||||
|
||||
.load-text, .no-more-text, .empty-text {
|
||||
font-size: $font-size-sm;
|
||||
color: $text-color-placeholder;
|
||||
}
|
||||
}
|
||||
|
||||
.search-modal {
|
||||
background-color: white;
|
||||
min-height: 50vh;
|
||||
|
||||
.search-header {
|
||||
padding: $spacing-md;
|
||||
border-bottom: 1px solid $border-color;
|
||||
|
||||
.search-input-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: $background-color;
|
||||
border-radius: $border-radius-md;
|
||||
padding: $spacing-sm $spacing-md;
|
||||
|
||||
.search-icon {
|
||||
font-size: $font-size-md;
|
||||
color: $text-color-placeholder;
|
||||
margin-right: $spacing-sm;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
font-size: $font-size-md;
|
||||
color: $text-color-primary;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.search-cancel {
|
||||
font-size: $font-size-md;
|
||||
color: $primary-color;
|
||||
margin-left: $spacing-sm;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-history {
|
||||
padding: $spacing-md;
|
||||
|
||||
.history-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: $spacing-md;
|
||||
|
||||
.history-title {
|
||||
font-size: $font-size-md;
|
||||
color: $text-color-primary;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.history-clear {
|
||||
font-size: $font-size-sm;
|
||||
color: $text-color-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
.history-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.history-item {
|
||||
padding: $spacing-sm $spacing-md;
|
||||
margin-right: $spacing-sm;
|
||||
margin-bottom: $spacing-sm;
|
||||
background-color: $background-color;
|
||||
border-radius: $border-radius-md;
|
||||
|
||||
.history-text {
|
||||
font-size: $font-size-sm;
|
||||
color: $text-color-secondary;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-suggestions {
|
||||
.suggestion-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: $spacing-md;
|
||||
border-bottom: 1px solid $border-color;
|
||||
|
||||
.suggestion-icon {
|
||||
font-size: $font-size-md;
|
||||
color: $text-color-placeholder;
|
||||
margin-right: $spacing-md;
|
||||
}
|
||||
|
||||
.suggestion-text {
|
||||
font-size: $font-size-md;
|
||||
color: $text-color-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,833 +0,0 @@
|
||||
<template>
|
||||
<view class="dashboard-container">
|
||||
<!-- 自定义导航栏 -->
|
||||
<view class="custom-navbar">
|
||||
<view class="navbar-content">
|
||||
<text class="navbar-title">监管仪表盘</text>
|
||||
<view class="navbar-actions">
|
||||
<view class="action-item" @click="refreshData">
|
||||
<text class="iconfont icon-refresh"></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 页面内容 -->
|
||||
<scroll-view
|
||||
class="page-content"
|
||||
scroll-y
|
||||
refresher-enabled
|
||||
:refresher-triggered="refreshing"
|
||||
@refresherrefresh="onRefresh"
|
||||
>
|
||||
<!-- 统计概览 -->
|
||||
<view class="stats-overview">
|
||||
<view class="stats-grid">
|
||||
<view
|
||||
class="stats-item"
|
||||
v-for="(item, index) in statsData"
|
||||
:key="index"
|
||||
@click="navigateToDetail(item.type)"
|
||||
>
|
||||
<view class="stats-icon" :class="item.iconClass">
|
||||
<text class="iconfont" :class="item.icon"></text>
|
||||
</view>
|
||||
<view class="stats-info">
|
||||
<text class="stats-value">{{ item.value }}</text>
|
||||
<text class="stats-label">{{ item.label }}</text>
|
||||
</view>
|
||||
<view class="stats-trend" :class="item.trendClass">
|
||||
<text class="trend-text">{{ item.trend }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 监管指标 -->
|
||||
<view class="section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">监管指标</text>
|
||||
<text class="section-more" @click="navigateTo('/pages/risk/risk')">查看更多</text>
|
||||
</view>
|
||||
<view class="indicators-list">
|
||||
<view
|
||||
class="indicator-item"
|
||||
v-for="(indicator, index) in indicators"
|
||||
:key="index"
|
||||
>
|
||||
<view class="indicator-info">
|
||||
<text class="indicator-name">{{ indicator.name }}</text>
|
||||
<text class="indicator-desc">{{ indicator.description }}</text>
|
||||
</view>
|
||||
<view class="indicator-value">
|
||||
<text class="value-text" :class="indicator.statusClass">{{ indicator.value }}</text>
|
||||
<text class="value-unit">{{ indicator.unit }}</text>
|
||||
</view>
|
||||
<view class="indicator-status" :class="indicator.statusClass">
|
||||
<text class="status-text">{{ indicator.status }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 资产分布 -->
|
||||
<view class="section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">资产分布</text>
|
||||
<text class="section-more" @click="navigateTo('/pages/assets/assets')">查看详情</text>
|
||||
</view>
|
||||
<view class="asset-distribution">
|
||||
<view class="chart-container">
|
||||
<!-- 这里可以集成图表组件 -->
|
||||
<view class="chart-placeholder">
|
||||
<text class="placeholder-text">资产分布图表</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="asset-legend">
|
||||
<view
|
||||
class="legend-item"
|
||||
v-for="(item, index) in assetDistribution"
|
||||
:key="index"
|
||||
>
|
||||
<view class="legend-color" :style="{ backgroundColor: item.color }"></view>
|
||||
<text class="legend-label">{{ item.label }}</text>
|
||||
<text class="legend-value">{{ item.percentage }}%</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 最新动态 -->
|
||||
<view class="section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">最新动态</text>
|
||||
<text class="section-more" @click="navigateTo('/pages/news/news')">查看全部</text>
|
||||
</view>
|
||||
<view class="news-list">
|
||||
<view
|
||||
class="news-item"
|
||||
v-for="(news, index) in newsList"
|
||||
:key="index"
|
||||
@click="viewNewsDetail(news)"
|
||||
>
|
||||
<view class="news-content">
|
||||
<text class="news-title">{{ news.title }}</text>
|
||||
<text class="news-summary">{{ news.summary }}</text>
|
||||
<view class="news-meta">
|
||||
<text class="news-time">{{ formatTime(news.createTime) }}</text>
|
||||
<text class="news-category" :class="news.categoryClass">{{ news.category }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="news-arrow">
|
||||
<text class="iconfont icon-arrow-right"></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 快捷操作 -->
|
||||
<view class="section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">快捷操作</text>
|
||||
</view>
|
||||
<view class="quick-actions">
|
||||
<view
|
||||
class="action-item"
|
||||
v-for="(action, index) in quickActions"
|
||||
:key="index"
|
||||
@click="handleQuickAction(action)"
|
||||
>
|
||||
<view class="action-icon" :class="action.iconClass">
|
||||
<text class="iconfont" :class="action.icon"></text>
|
||||
</view>
|
||||
<text class="action-label">{{ action.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useUserStore, useAppStore } from '@/store'
|
||||
import { get } from '@/utils/request'
|
||||
|
||||
export default {
|
||||
name: 'Dashboard',
|
||||
setup() {
|
||||
const userStore = useUserStore()
|
||||
const appStore = useAppStore()
|
||||
|
||||
// 响应式数据
|
||||
const refreshing = ref(false)
|
||||
const statsData = ref([
|
||||
{
|
||||
type: 'customers',
|
||||
label: '监管客户',
|
||||
value: '1,234',
|
||||
trend: '+12%',
|
||||
icon: 'icon-user',
|
||||
iconClass: 'primary',
|
||||
trendClass: 'up'
|
||||
},
|
||||
{
|
||||
type: 'assets',
|
||||
label: '资产总额',
|
||||
value: '¥8.9亿',
|
||||
trend: '+5.6%',
|
||||
icon: 'icon-money',
|
||||
iconClass: 'success',
|
||||
trendClass: 'up'
|
||||
},
|
||||
{
|
||||
type: 'transactions',
|
||||
label: '今日交易',
|
||||
value: '567',
|
||||
trend: '-2.1%',
|
||||
icon: 'icon-transaction',
|
||||
iconClass: 'warning',
|
||||
trendClass: 'down'
|
||||
},
|
||||
{
|
||||
type: 'risk',
|
||||
label: '风险预警',
|
||||
value: '23',
|
||||
trend: '+8',
|
||||
icon: 'icon-warning',
|
||||
iconClass: 'danger',
|
||||
trendClass: 'up'
|
||||
}
|
||||
])
|
||||
|
||||
const indicators = ref([
|
||||
{
|
||||
name: '资本充足率',
|
||||
description: '核心一级资本充足率',
|
||||
value: '12.5',
|
||||
unit: '%',
|
||||
status: '正常',
|
||||
statusClass: 'normal'
|
||||
},
|
||||
{
|
||||
name: '不良贷款率',
|
||||
description: '不良贷款占比',
|
||||
value: '1.8',
|
||||
unit: '%',
|
||||
status: '关注',
|
||||
statusClass: 'warning'
|
||||
},
|
||||
{
|
||||
name: '流动性比率',
|
||||
description: '流动性覆盖率',
|
||||
value: '125.6',
|
||||
unit: '%',
|
||||
status: '正常',
|
||||
statusClass: 'normal'
|
||||
}
|
||||
])
|
||||
|
||||
const assetDistribution = ref([
|
||||
{ label: '养殖业', percentage: 45, color: '#1890ff' },
|
||||
{ label: '种植业', percentage: 30, color: '#52c41a' },
|
||||
{ label: '农产品加工', percentage: 15, color: '#faad14' },
|
||||
{ label: '其他', percentage: 10, color: '#f5222d' }
|
||||
])
|
||||
|
||||
const newsList = ref([
|
||||
{
|
||||
id: 1,
|
||||
title: '农业银行发布新版监管政策',
|
||||
summary: '针对农业贷款风险管控的新政策正式实施',
|
||||
category: '政策',
|
||||
categoryClass: 'policy',
|
||||
createTime: new Date().getTime() - 3600000
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '养殖业贷款违约率上升预警',
|
||||
summary: '近期养殖业贷款违约率有所上升,需加强监管',
|
||||
category: '预警',
|
||||
categoryClass: 'warning',
|
||||
createTime: new Date().getTime() - 7200000
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '数字化监管系统升级完成',
|
||||
summary: '新版监管系统已上线,功能更加完善',
|
||||
category: '系统',
|
||||
categoryClass: 'system',
|
||||
createTime: new Date().getTime() - 10800000
|
||||
}
|
||||
])
|
||||
|
||||
const quickActions = ref([
|
||||
{
|
||||
label: '客户管理',
|
||||
icon: 'icon-user-manage',
|
||||
iconClass: 'primary',
|
||||
action: 'customers'
|
||||
},
|
||||
{
|
||||
label: '资产监控',
|
||||
icon: 'icon-monitor',
|
||||
iconClass: 'success',
|
||||
action: 'assets'
|
||||
},
|
||||
{
|
||||
label: '风险评估',
|
||||
icon: 'icon-risk',
|
||||
iconClass: 'warning',
|
||||
action: 'risk'
|
||||
},
|
||||
{
|
||||
label: '报表生成',
|
||||
icon: 'icon-report',
|
||||
iconClass: 'info',
|
||||
action: 'report'
|
||||
}
|
||||
])
|
||||
|
||||
// 方法
|
||||
const initPageData = async () => {
|
||||
try {
|
||||
appStore.setLoading(true)
|
||||
|
||||
// 并发获取数据
|
||||
await Promise.all([
|
||||
getDashboardStats(),
|
||||
getIndicators(),
|
||||
getAssetDistribution(),
|
||||
getNewsList()
|
||||
])
|
||||
} catch (error) {
|
||||
console.error('初始化页面数据失败:', error)
|
||||
appStore.showToast('数据加载失败', 'error')
|
||||
} finally {
|
||||
appStore.setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const getDashboardStats = async () => {
|
||||
try {
|
||||
const response = await get('/api/dashboard/stats')
|
||||
if (response.success) {
|
||||
statsData.value = response.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取统计数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const getIndicators = async () => {
|
||||
try {
|
||||
const response = await get('/api/dashboard/indicators')
|
||||
if (response.success) {
|
||||
indicators.value = response.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取监管指标失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const getAssetDistribution = async () => {
|
||||
try {
|
||||
const response = await get('/api/dashboard/asset-distribution')
|
||||
if (response.success) {
|
||||
assetDistribution.value = response.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取资产分布失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const getNewsList = async () => {
|
||||
try {
|
||||
const response = await get('/api/news/latest', { limit: 3 })
|
||||
if (response.success) {
|
||||
newsList.value = response.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取新闻列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const onRefresh = async () => {
|
||||
refreshing.value = true
|
||||
await initPageData()
|
||||
refreshing.value = false
|
||||
}
|
||||
|
||||
const refreshData = () => {
|
||||
initPageData()
|
||||
}
|
||||
|
||||
const navigateToDetail = (type) => {
|
||||
const routeMap = {
|
||||
customers: '/pages/customers/customers',
|
||||
assets: '/pages/assets/assets',
|
||||
transactions: '/pages/transactions/transactions',
|
||||
risk: '/pages/risk/risk'
|
||||
}
|
||||
|
||||
const route = routeMap[type]
|
||||
if (route) {
|
||||
navigateTo(route)
|
||||
}
|
||||
}
|
||||
|
||||
const navigateTo = (url) => {
|
||||
uni.navigateTo({ url })
|
||||
}
|
||||
|
||||
const viewNewsDetail = (news) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/news/detail?id=${news.id}`
|
||||
})
|
||||
}
|
||||
|
||||
const handleQuickAction = (action) => {
|
||||
const actionMap = {
|
||||
customers: '/pages/customers/customers',
|
||||
assets: '/pages/assets/assets',
|
||||
risk: '/pages/risk/risk',
|
||||
report: '/pages/report/report'
|
||||
}
|
||||
|
||||
const route = actionMap[action.action]
|
||||
if (route) {
|
||||
navigateTo(route)
|
||||
}
|
||||
}
|
||||
|
||||
const formatTime = (timestamp) => {
|
||||
const now = new Date().getTime()
|
||||
const diff = now - timestamp
|
||||
|
||||
if (diff < 3600000) { // 1小时内
|
||||
return `${Math.floor(diff / 60000)}分钟前`
|
||||
} else if (diff < 86400000) { // 24小时内
|
||||
return `${Math.floor(diff / 3600000)}小时前`
|
||||
} else {
|
||||
const date = new Date(timestamp)
|
||||
return `${date.getMonth() + 1}-${date.getDate()}`
|
||||
}
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
initPageData()
|
||||
})
|
||||
|
||||
return {
|
||||
refreshing,
|
||||
statsData,
|
||||
indicators,
|
||||
assetDistribution,
|
||||
newsList,
|
||||
quickActions,
|
||||
onRefresh,
|
||||
refreshData,
|
||||
navigateToDetail,
|
||||
navigateTo,
|
||||
viewNewsDetail,
|
||||
handleQuickAction,
|
||||
formatTime
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '@/styles/variables.scss';
|
||||
|
||||
.dashboard-container {
|
||||
min-height: 100vh;
|
||||
background-color: $background-color;
|
||||
}
|
||||
|
||||
.custom-navbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
background-color: $primary-color;
|
||||
padding-top: var(--status-bar-height);
|
||||
|
||||
.navbar-content {
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 $spacing-md;
|
||||
|
||||
.navbar-title {
|
||||
color: white;
|
||||
font-size: $font-size-lg;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.navbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.action-item {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: $border-radius-sm;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
|
||||
.iconfont {
|
||||
color: white;
|
||||
font-size: $font-size-md;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.page-content {
|
||||
padding-top: calc(var(--status-bar-height) + 44px);
|
||||
padding-bottom: $spacing-lg;
|
||||
}
|
||||
|
||||
.stats-overview {
|
||||
padding: $spacing-md;
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: $spacing-md;
|
||||
|
||||
.stats-item {
|
||||
background-color: white;
|
||||
border-radius: $border-radius-md;
|
||||
padding: $spacing-md;
|
||||
box-shadow: $box-shadow-light;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.stats-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: $border-radius-md;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: $spacing-sm;
|
||||
|
||||
&.primary { background-color: rgba($primary-color, 0.1); }
|
||||
&.success { background-color: rgba($success-color, 0.1); }
|
||||
&.warning { background-color: rgba($warning-color, 0.1); }
|
||||
&.danger { background-color: rgba($danger-color, 0.1); }
|
||||
|
||||
.iconfont {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
&.primary .iconfont { color: $primary-color; }
|
||||
&.success .iconfont { color: $success-color; }
|
||||
&.warning .iconfont { color: $warning-color; }
|
||||
&.danger .iconfont { color: $danger-color; }
|
||||
}
|
||||
|
||||
.stats-info {
|
||||
flex: 1;
|
||||
|
||||
.stats-value {
|
||||
display: block;
|
||||
font-size: $font-size-xl;
|
||||
font-weight: 600;
|
||||
color: $text-color-primary;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.stats-label {
|
||||
display: block;
|
||||
font-size: $font-size-sm;
|
||||
color: $text-color-secondary;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.stats-trend {
|
||||
font-size: $font-size-sm;
|
||||
font-weight: 500;
|
||||
|
||||
&.up { color: $success-color; }
|
||||
&.down { color: $danger-color; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.section {
|
||||
margin: $spacing-md;
|
||||
background-color: white;
|
||||
border-radius: $border-radius-md;
|
||||
box-shadow: $box-shadow-light;
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: $spacing-md;
|
||||
border-bottom: 1px solid $border-color;
|
||||
|
||||
.section-title {
|
||||
font-size: $font-size-lg;
|
||||
font-weight: 600;
|
||||
color: $text-color-primary;
|
||||
}
|
||||
|
||||
.section-more {
|
||||
font-size: $font-size-sm;
|
||||
color: $primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.indicators-list {
|
||||
.indicator-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: $spacing-md;
|
||||
border-bottom: 1px solid $border-color;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.indicator-info {
|
||||
flex: 1;
|
||||
|
||||
.indicator-name {
|
||||
display: block;
|
||||
font-size: $font-size-md;
|
||||
color: $text-color-primary;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.indicator-desc {
|
||||
display: block;
|
||||
font-size: $font-size-sm;
|
||||
color: $text-color-secondary;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.indicator-value {
|
||||
margin-right: $spacing-md;
|
||||
text-align: right;
|
||||
|
||||
.value-text {
|
||||
display: block;
|
||||
font-size: $font-size-lg;
|
||||
font-weight: 600;
|
||||
|
||||
&.normal { color: $success-color; }
|
||||
&.warning { color: $warning-color; }
|
||||
&.danger { color: $danger-color; }
|
||||
}
|
||||
|
||||
.value-unit {
|
||||
font-size: $font-size-sm;
|
||||
color: $text-color-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
.indicator-status {
|
||||
padding: 4px 8px;
|
||||
border-radius: $border-radius-sm;
|
||||
font-size: $font-size-xs;
|
||||
|
||||
&.normal {
|
||||
background-color: rgba($success-color, 0.1);
|
||||
color: $success-color;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
background-color: rgba($warning-color, 0.1);
|
||||
color: $warning-color;
|
||||
}
|
||||
|
||||
&.danger {
|
||||
background-color: rgba($danger-color, 0.1);
|
||||
color: $danger-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.asset-distribution {
|
||||
padding: $spacing-md;
|
||||
|
||||
.chart-container {
|
||||
height: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: $background-color;
|
||||
border-radius: $border-radius-md;
|
||||
margin-bottom: $spacing-md;
|
||||
|
||||
.placeholder-text {
|
||||
color: $text-color-secondary;
|
||||
font-size: $font-size-md;
|
||||
}
|
||||
}
|
||||
|
||||
.asset-legend {
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: $spacing-sm;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.legend-color {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 2px;
|
||||
margin-right: $spacing-sm;
|
||||
}
|
||||
|
||||
.legend-label {
|
||||
flex: 1;
|
||||
font-size: $font-size-sm;
|
||||
color: $text-color-primary;
|
||||
}
|
||||
|
||||
.legend-value {
|
||||
font-size: $font-size-sm;
|
||||
color: $text-color-secondary;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.news-list {
|
||||
.news-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: $spacing-md;
|
||||
border-bottom: 1px solid $border-color;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.news-content {
|
||||
flex: 1;
|
||||
|
||||
.news-title {
|
||||
display: block;
|
||||
font-size: $font-size-md;
|
||||
color: $text-color-primary;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.news-summary {
|
||||
display: block;
|
||||
font-size: $font-size-sm;
|
||||
color: $text-color-secondary;
|
||||
line-height: 1.4;
|
||||
margin-bottom: $spacing-sm;
|
||||
}
|
||||
|
||||
.news-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.news-time {
|
||||
font-size: $font-size-xs;
|
||||
color: $text-color-placeholder;
|
||||
margin-right: $spacing-sm;
|
||||
}
|
||||
|
||||
.news-category {
|
||||
padding: 2px 6px;
|
||||
border-radius: $border-radius-sm;
|
||||
font-size: $font-size-xs;
|
||||
|
||||
&.policy {
|
||||
background-color: rgba($primary-color, 0.1);
|
||||
color: $primary-color;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
background-color: rgba($warning-color, 0.1);
|
||||
color: $warning-color;
|
||||
}
|
||||
|
||||
&.system {
|
||||
background-color: rgba($info-color, 0.1);
|
||||
color: $info-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.news-arrow {
|
||||
margin-left: $spacing-sm;
|
||||
|
||||
.iconfont {
|
||||
color: $text-color-placeholder;
|
||||
font-size: $font-size-sm;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
padding: $spacing-md;
|
||||
|
||||
.action-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: $spacing-md $spacing-sm;
|
||||
|
||||
.action-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: $border-radius-md;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: $spacing-sm;
|
||||
|
||||
&.primary { background-color: rgba($primary-color, 0.1); }
|
||||
&.success { background-color: rgba($success-color, 0.1); }
|
||||
&.warning { background-color: rgba($warning-color, 0.1); }
|
||||
&.info { background-color: rgba($info-color, 0.1); }
|
||||
|
||||
.iconfont {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
&.primary .iconfont { color: $primary-color; }
|
||||
&.success .iconfont { color: $success-color; }
|
||||
&.warning .iconfont { color: $warning-color; }
|
||||
&.info .iconfont { color: $info-color; }
|
||||
}
|
||||
|
||||
.action-label {
|
||||
font-size: $font-size-sm;
|
||||
color: $text-color-primary;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,392 +0,0 @@
|
||||
<template>
|
||||
<view class="index-page">
|
||||
<!-- 头部欢迎区域 -->
|
||||
<view class="header-section">
|
||||
<view class="welcome-card">
|
||||
<view class="user-info">
|
||||
<image class="avatar" :src="userInfo.avatar || '/static/images/default-avatar.svg'" mode="aspectFill"></image>
|
||||
<view class="user-details">
|
||||
<text class="username">{{ userInfo.realName || '银行用户' }}</text>
|
||||
<text class="role">{{ userInfo.roleName || '信贷经理' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="weather-info">
|
||||
<text class="date">{{ currentDate }}</text>
|
||||
<text class="weather">{{ weather }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 数据概览 -->
|
||||
<view class="stats-section">
|
||||
<view class="stats-grid">
|
||||
<view class="stat-item" @click="navigateTo('/pages/customers/customers')">
|
||||
<view class="stat-number text-primary">{{ stats.totalCustomers }}</view>
|
||||
<view class="stat-label">客户总数</view>
|
||||
</view>
|
||||
<view class="stat-item" @click="navigateTo('/pages/assets/assets')">
|
||||
<view class="stat-number text-success">{{ stats.totalAssets }}</view>
|
||||
<view class="stat-label">监管资产</view>
|
||||
</view>
|
||||
<view class="stat-item" @click="navigateTo('/pages/risk/risk')">
|
||||
<view class="stat-number text-warning">{{ stats.riskAlerts }}</view>
|
||||
<view class="stat-label">风险预警</view>
|
||||
</view>
|
||||
<view class="stat-item" @click="navigateTo('/pages/transactions/transactions')">
|
||||
<view class="stat-number text-info">{{ stats.todayTransactions }}</view>
|
||||
<view class="stat-label">今日交易</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 快捷功能 -->
|
||||
<view class="quick-actions">
|
||||
<view class="section-title">快捷功能</view>
|
||||
<view class="action-grid">
|
||||
<view class="action-item" @click="navigateTo('/pages/customers/customers')">
|
||||
<image class="action-icon" src="/static/images/icon-customers.svg" mode="aspectFit"></image>
|
||||
<text class="action-text">客户管理</text>
|
||||
</view>
|
||||
<view class="action-item" @click="navigateTo('/pages/assets/assets')">
|
||||
<image class="action-icon" src="/static/images/icon-assets.svg" mode="aspectFit"></image>
|
||||
<text class="action-text">资产监管</text>
|
||||
</view>
|
||||
<view class="action-item" @click="navigateTo('/pages/transactions/transactions')">
|
||||
<image class="action-icon" src="/static/images/icon-transactions.svg" mode="aspectFit"></image>
|
||||
<text class="action-text">交易管理</text>
|
||||
</view>
|
||||
<view class="action-item" @click="navigateTo('/pages/risk/risk')">
|
||||
<image class="action-icon" src="/static/images/icon-risk.svg" mode="aspectFit"></image>
|
||||
<text class="action-text">风险监控</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 最新动态 -->
|
||||
<view class="news-section">
|
||||
<view class="section-title">最新动态</view>
|
||||
<view class="news-list">
|
||||
<view class="news-item" v-for="item in newsList" :key="item.id" @click="viewNewsDetail(item)">
|
||||
<view class="news-content">
|
||||
<view class="news-title">{{ item.title }}</view>
|
||||
<view class="news-summary">{{ item.summary }}</view>
|
||||
<view class="news-time">{{ item.createTime }}</view>
|
||||
</view>
|
||||
<view class="news-arrow">></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
|
||||
export default {
|
||||
name: 'IndexPage',
|
||||
setup() {
|
||||
const userInfo = reactive({
|
||||
avatar: '',
|
||||
realName: '',
|
||||
roleName: ''
|
||||
})
|
||||
|
||||
const stats = reactive({
|
||||
totalCustomers: 0,
|
||||
totalAssets: 0,
|
||||
riskAlerts: 0,
|
||||
todayTransactions: 0
|
||||
})
|
||||
|
||||
const newsList = ref([])
|
||||
const currentDate = ref('')
|
||||
const weather = ref('晴朗')
|
||||
|
||||
// 初始化页面数据
|
||||
const initPageData = async () => {
|
||||
// 获取当前日期
|
||||
const now = new Date()
|
||||
currentDate.value = `${now.getMonth() + 1}月${now.getDate()}日`
|
||||
|
||||
// 获取用户信息
|
||||
await getUserInfo()
|
||||
|
||||
// 获取统计数据
|
||||
await getStatsData()
|
||||
|
||||
// 获取最新动态
|
||||
await getNewsList()
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
const getUserInfo = async () => {
|
||||
try {
|
||||
// 这里应该调用实际的API
|
||||
userInfo.realName = '张经理'
|
||||
userInfo.roleName = '信贷经理'
|
||||
userInfo.avatar = '/static/images/default-avatar.png'
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取统计数据
|
||||
const getStatsData = async () => {
|
||||
try {
|
||||
// 这里应该调用实际的API
|
||||
stats.totalCustomers = 156
|
||||
stats.totalAssets = 89
|
||||
stats.riskAlerts = 3
|
||||
stats.todayTransactions = 24
|
||||
} catch (error) {
|
||||
console.error('获取统计数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取最新动态
|
||||
const getNewsList = async () => {
|
||||
try {
|
||||
// 这里应该调用实际的API
|
||||
newsList.value = [
|
||||
{
|
||||
id: 1,
|
||||
title: '新增客户风险预警',
|
||||
summary: '客户张三的抵押物价值出现异常波动',
|
||||
createTime: '2小时前'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '系统维护通知',
|
||||
summary: '系统将于今晚22:00-24:00进行维护升级',
|
||||
createTime: '4小时前'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '新功能上线',
|
||||
summary: '资产监控模块新增实时定位功能',
|
||||
createTime: '1天前'
|
||||
}
|
||||
]
|
||||
} catch (error) {
|
||||
console.error('获取最新动态失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 页面导航
|
||||
const navigateTo = (url) => {
|
||||
uni.navigateTo({ url })
|
||||
}
|
||||
|
||||
// 查看动态详情
|
||||
const viewNewsDetail = (item) => {
|
||||
uni.showToast({
|
||||
title: '功能开发中',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
|
||||
// 下拉刷新
|
||||
const onPullDownRefresh = async () => {
|
||||
await initPageData()
|
||||
uni.stopPullDownRefresh()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initPageData()
|
||||
})
|
||||
|
||||
return {
|
||||
userInfo,
|
||||
stats,
|
||||
newsList,
|
||||
currentDate,
|
||||
weather,
|
||||
navigateTo,
|
||||
viewNewsDetail,
|
||||
onPullDownRefresh
|
||||
}
|
||||
},
|
||||
onPullDownRefresh() {
|
||||
this.onPullDownRefresh()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.index-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(180deg, #2c5aa0 0%, #f5f7fa 30%);
|
||||
}
|
||||
|
||||
.header-section {
|
||||
padding: 40rpx 20rpx 20rpx;
|
||||
}
|
||||
|
||||
.welcome-card {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 40rpx;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.user-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.role {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.weather-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.date {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.weather {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.stats-section {
|
||||
padding: 0 20rpx 20rpx;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
background: #fff;
|
||||
border-radius: 12rpx;
|
||||
padding: 32rpx 24rpx;
|
||||
text-align: center;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 48rpx;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.quick-actions, .news-section {
|
||||
background: #fff;
|
||||
margin: 20rpx;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.action-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.action-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.action-text {
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.news-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.news-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20rpx 0;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.news-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.news-title {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.news-summary {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.news-time {
|
||||
font-size: 20rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.news-arrow {
|
||||
font-size: 24rpx;
|
||||
color: #ccc;
|
||||
margin-left: 20rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -1,400 +0,0 @@
|
||||
<template>
|
||||
<view class="login-page">
|
||||
<!-- 自定义导航栏 -->
|
||||
<view class="custom-navbar">
|
||||
<view class="navbar-content">
|
||||
<text class="navbar-title">银行监管系统</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 登录表单 -->
|
||||
<view class="login-container">
|
||||
<!-- Logo区域 -->
|
||||
<view class="logo-section">
|
||||
<image class="logo" src="/static/images/bank-logo.png" mode="aspectFit"></image>
|
||||
<text class="app-name">银行监管小程序</text>
|
||||
<text class="app-desc">专业的银行信贷风险管理平台</text>
|
||||
</view>
|
||||
|
||||
<!-- 登录表单 -->
|
||||
<view class="form-section">
|
||||
<view class="form-item">
|
||||
<view class="form-label">
|
||||
<image class="form-icon" src="/static/images/icon-user.png" mode="aspectFit"></image>
|
||||
</view>
|
||||
<input
|
||||
class="form-input"
|
||||
type="text"
|
||||
placeholder="请输入用户名"
|
||||
v-model="loginForm.username"
|
||||
:disabled="loading"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<view class="form-label">
|
||||
<image class="form-icon" src="/static/images/icon-password.png" mode="aspectFit"></image>
|
||||
</view>
|
||||
<input
|
||||
class="form-input"
|
||||
type="password"
|
||||
placeholder="请输入密码"
|
||||
v-model="loginForm.password"
|
||||
:disabled="loading"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 登录按钮 -->
|
||||
<button
|
||||
class="login-btn"
|
||||
:class="{ 'login-btn-disabled': loading }"
|
||||
:disabled="loading"
|
||||
@click="handleLogin"
|
||||
>
|
||||
<text v-if="loading">登录中...</text>
|
||||
<text v-else>登录</text>
|
||||
</button>
|
||||
|
||||
<!-- 微信登录 -->
|
||||
<view class="wechat-login">
|
||||
<view class="divider">
|
||||
<text class="divider-text">或</text>
|
||||
</view>
|
||||
<button
|
||||
class="wechat-btn"
|
||||
open-type="getUserInfo"
|
||||
@getuserinfo="handleWechatLogin"
|
||||
:disabled="loading"
|
||||
>
|
||||
<image class="wechat-icon" src="/static/images/icon-wechat.png" mode="aspectFit"></image>
|
||||
<text>微信快速登录</text>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部信息 -->
|
||||
<view class="footer-section">
|
||||
<text class="footer-text">仅限银行内部人员使用</text>
|
||||
<text class="version-text">版本 v1.0.0</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { reactive, ref } from 'vue'
|
||||
import { useUserStore } from '@/store/user'
|
||||
|
||||
export default {
|
||||
name: 'LoginPage',
|
||||
setup() {
|
||||
const userStore = useUserStore()
|
||||
|
||||
const loginForm = reactive({
|
||||
username: '',
|
||||
password: ''
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
// 用户名密码登录
|
||||
const handleLogin = async () => {
|
||||
if (!loginForm.username.trim()) {
|
||||
uni.showToast({
|
||||
title: '请输入用户名',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!loginForm.password.trim()) {
|
||||
uni.showToast({
|
||||
title: '请输入密码',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
// 调用登录API
|
||||
const result = await userStore.login({
|
||||
username: loginForm.username,
|
||||
password: loginForm.password
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
uni.showToast({
|
||||
title: '登录成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 跳转到首页
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({
|
||||
url: '/pages/index/index'
|
||||
})
|
||||
}, 1500)
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: result.message || '登录失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error)
|
||||
uni.showToast({
|
||||
title: '网络错误,请重试',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 微信登录
|
||||
const handleWechatLogin = async (e) => {
|
||||
const { userInfo } = e.detail
|
||||
|
||||
if (!userInfo) {
|
||||
uni.showToast({
|
||||
title: '授权失败',
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
// 获取微信登录code
|
||||
const loginRes = await uni.login({
|
||||
provider: 'weixin'
|
||||
})
|
||||
|
||||
if (loginRes.code) {
|
||||
// 调用微信登录API
|
||||
const result = await userStore.wechatLogin({
|
||||
code: loginRes.code,
|
||||
userInfo: userInfo
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
uni.showToast({
|
||||
title: '登录成功',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({
|
||||
url: '/pages/index/index'
|
||||
})
|
||||
}, 1500)
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: result.message || '微信登录失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('微信登录失败:', error)
|
||||
uni.showToast({
|
||||
title: '微信登录失败',
|
||||
icon: 'none'
|
||||
})
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
loginForm,
|
||||
loading,
|
||||
handleLogin,
|
||||
handleWechatLogin
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.login-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #2c5aa0 0%, #1e3a8a 100%);
|
||||
}
|
||||
|
||||
.custom-navbar {
|
||||
height: 88rpx;
|
||||
padding-top: var(--status-bar-height);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.navbar-content {
|
||||
height: 88rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.navbar-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
padding: 60rpx 40rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: calc(100vh - 88rpx);
|
||||
}
|
||||
|
||||
.logo-section {
|
||||
text-align: center;
|
||||
margin-bottom: 80rpx;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.app-name {
|
||||
display: block;
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.app-desc {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.form-section {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12rpx;
|
||||
margin-bottom: 32rpx;
|
||||
padding: 0 24rpx;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.form-label {
|
||||
width: 60rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.form-icon {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
flex: 1;
|
||||
height: 88rpx;
|
||||
font-size: 28rpx;
|
||||
color: #fff;
|
||||
padding-left: 20rpx;
|
||||
}
|
||||
|
||||
.form-input::placeholder {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
background: #fff;
|
||||
color: #2c5aa0;
|
||||
border: none;
|
||||
border-radius: 12rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
margin-top: 40rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.login-btn-disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.wechat-login {
|
||||
margin-top: 60rpx;
|
||||
}
|
||||
|
||||
.divider {
|
||||
text-align: center;
|
||||
margin-bottom: 40rpx;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.divider::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1rpx;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.divider-text {
|
||||
background: #2c5aa0;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
padding: 0 20rpx;
|
||||
font-size: 24rpx;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.wechat-btn {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #fff;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 12rpx;
|
||||
font-size: 28rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.wechat-icon {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
|
||||
.footer-section {
|
||||
text-align: center;
|
||||
margin-top: 60rpx;
|
||||
}
|
||||
|
||||
.footer-text {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.version-text {
|
||||
display: block;
|
||||
font-size: 20rpx;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,6 +0,0 @@
|
||||
<!-- SVG placeholder for default avatar -->
|
||||
<svg width="80" height="80" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="40" cy="40" r="40" fill="#f0f0f0"/>
|
||||
<circle cx="40" cy="30" r="12" fill="#d0d0d0"/>
|
||||
<path d="M20 65c0-11 9-20 20-20s20 9 20 20" fill="#d0d0d0"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 309 B |
@@ -1,5 +0,0 @@
|
||||
<svg width="80" height="80" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="40" cy="40" r="40" fill="#f0f0f0"/>
|
||||
<circle cx="40" cy="30" r="12" fill="#d0d0d0"/>
|
||||
<path d="M20 65c0-11 9-20 20-20s20 9 20 20" fill="#d0d0d0"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 265 B |
@@ -1,4 +0,0 @@
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="48" height="48" rx="8" fill="#50C878"/>
|
||||
<path d="M14 16h20c1.1 0 2 .9 2 2v16c0 1.1-.9 2-2 2H14c-1.1 0-2-.9-2-2V18c0-1.1.9-2 2-2zm0 2v4h20v-4H14zm0 6v10h20V24H14zm4 2h8v2h-8v-2zm0 4h6v2h-6v-2z" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 327 B |
@@ -1,4 +0,0 @@
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="48" height="48" rx="8" fill="#4A90E2"/>
|
||||
<path d="M24 14c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6 2.69-6 6-6zm0 20c-6.63 0-12-3.37-12-7.5V24c0-1.1.9-2 2-2h20c1.1 0 2 .9 2 2v2.5c0 4.13-5.37 7.5-12 7.5z" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 335 B |
@@ -1,4 +0,0 @@
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="48" height="48" rx="8" fill="#FF4757"/>
|
||||
<path d="M24 14l8 14H16l8-14zm-2 6v4h4v-4h-4zm0 6v2h4v-2h-4z" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 236 B |
@@ -1,4 +0,0 @@
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="48" height="48" rx="8" fill="#FF6B35"/>
|
||||
<path d="M24 12l8 8h-4v8h-8v-8h-4l8-8zm-8 20h16v4H16v-4z" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 232 B |
@@ -1,305 +0,0 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useAppStore = defineStore('app', {
|
||||
state: () => ({
|
||||
// 应用配置
|
||||
appConfig: {
|
||||
version: '1.0.0',
|
||||
name: '银行监管小程序',
|
||||
theme: 'light'
|
||||
},
|
||||
|
||||
// 系统信息
|
||||
systemInfo: null,
|
||||
|
||||
// 网络状态
|
||||
networkStatus: 'unknown',
|
||||
|
||||
// 加载状态
|
||||
loading: false,
|
||||
|
||||
// 全局提示信息
|
||||
toast: {
|
||||
show: false,
|
||||
message: '',
|
||||
type: 'info' // info, success, warning, error
|
||||
},
|
||||
|
||||
// 页面栈
|
||||
pageStack: [],
|
||||
|
||||
// 当前页面
|
||||
currentPage: '',
|
||||
|
||||
// 全局数据缓存
|
||||
globalCache: new Map()
|
||||
}),
|
||||
|
||||
getters: {
|
||||
// 是否为开发环境
|
||||
isDevelopment: () => process.env.NODE_ENV === 'development',
|
||||
|
||||
// 获取应用版本
|
||||
appVersion: (state) => state.appConfig.version,
|
||||
|
||||
// 获取应用名称
|
||||
appName: (state) => state.appConfig.name,
|
||||
|
||||
// 是否在线
|
||||
isOnline: (state) => state.networkStatus !== 'none',
|
||||
|
||||
// 获取缓存数据
|
||||
getCacheData: (state) => (key) => {
|
||||
return state.globalCache.get(key)
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
/**
|
||||
* 初始化应用
|
||||
*/
|
||||
async initApp() {
|
||||
try {
|
||||
// 获取系统信息
|
||||
await this.getSystemInfo()
|
||||
|
||||
// 监听网络状态
|
||||
this.watchNetworkStatus()
|
||||
|
||||
// 初始化应用配置
|
||||
await this.loadAppConfig()
|
||||
|
||||
console.log('应用初始化完成')
|
||||
} catch (error) {
|
||||
console.error('应用初始化失败:', error)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取系统信息
|
||||
*/
|
||||
async getSystemInfo() {
|
||||
return new Promise((resolve) => {
|
||||
uni.getSystemInfo({
|
||||
success: (res) => {
|
||||
this.systemInfo = res
|
||||
resolve(res)
|
||||
},
|
||||
fail: (error) => {
|
||||
console.error('获取系统信息失败:', error)
|
||||
resolve(null)
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 监听网络状态
|
||||
*/
|
||||
watchNetworkStatus() {
|
||||
// 获取当前网络状态
|
||||
uni.getNetworkType({
|
||||
success: (res) => {
|
||||
this.networkStatus = res.networkType
|
||||
}
|
||||
})
|
||||
|
||||
// 监听网络状态变化
|
||||
uni.onNetworkStatusChange((res) => {
|
||||
this.networkStatus = res.networkType
|
||||
|
||||
if (!res.isConnected) {
|
||||
this.showToast('网络连接已断开', 'warning')
|
||||
} else {
|
||||
this.showToast('网络连接已恢复', 'success')
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载应用配置
|
||||
*/
|
||||
async loadAppConfig() {
|
||||
try {
|
||||
// 从本地存储加载配置
|
||||
const localConfig = uni.getStorageSync('appConfig')
|
||||
if (localConfig) {
|
||||
this.appConfig = { ...this.appConfig, ...localConfig }
|
||||
}
|
||||
|
||||
// 这里可以从服务器获取最新配置
|
||||
// const serverConfig = await get('/api/app/config')
|
||||
// if (serverConfig.success) {
|
||||
// this.appConfig = { ...this.appConfig, ...serverConfig.data }
|
||||
// uni.setStorageSync('appConfig', this.appConfig)
|
||||
// }
|
||||
} catch (error) {
|
||||
console.error('加载应用配置失败:', error)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新应用配置
|
||||
*/
|
||||
updateAppConfig(config) {
|
||||
this.appConfig = { ...this.appConfig, ...config }
|
||||
uni.setStorageSync('appConfig', this.appConfig)
|
||||
},
|
||||
|
||||
/**
|
||||
* 设置加载状态
|
||||
*/
|
||||
setLoading(loading) {
|
||||
this.loading = loading
|
||||
|
||||
if (loading) {
|
||||
uni.showLoading({
|
||||
title: '加载中...',
|
||||
mask: true
|
||||
})
|
||||
} else {
|
||||
uni.hideLoading()
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 显示提示信息
|
||||
*/
|
||||
showToast(message, type = 'info', duration = 2000) {
|
||||
this.toast = {
|
||||
show: true,
|
||||
message,
|
||||
type
|
||||
}
|
||||
|
||||
// 使用uni-app的提示
|
||||
const iconMap = {
|
||||
success: 'success',
|
||||
error: 'error',
|
||||
warning: 'none',
|
||||
info: 'none'
|
||||
}
|
||||
|
||||
uni.showToast({
|
||||
title: message,
|
||||
icon: iconMap[type] || 'none',
|
||||
duration,
|
||||
mask: false
|
||||
})
|
||||
|
||||
// 自动隐藏
|
||||
setTimeout(() => {
|
||||
this.toast.show = false
|
||||
}, duration)
|
||||
},
|
||||
|
||||
/**
|
||||
* 隐藏提示信息
|
||||
*/
|
||||
hideToast() {
|
||||
this.toast.show = false
|
||||
uni.hideToast()
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新页面栈
|
||||
*/
|
||||
updatePageStack() {
|
||||
const pages = getCurrentPages()
|
||||
this.pageStack = pages.map(page => page.route)
|
||||
this.currentPage = pages[pages.length - 1]?.route || ''
|
||||
},
|
||||
|
||||
/**
|
||||
* 设置缓存数据
|
||||
*/
|
||||
setCacheData(key, data, expireTime = null) {
|
||||
const cacheItem = {
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
expireTime
|
||||
}
|
||||
this.globalCache.set(key, cacheItem)
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取缓存数据
|
||||
*/
|
||||
getCacheData(key) {
|
||||
const cacheItem = this.globalCache.get(key)
|
||||
|
||||
if (!cacheItem) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 检查是否过期
|
||||
if (cacheItem.expireTime && Date.now() > cacheItem.expireTime) {
|
||||
this.globalCache.delete(key)
|
||||
return null
|
||||
}
|
||||
|
||||
return cacheItem.data
|
||||
},
|
||||
|
||||
/**
|
||||
* 清除缓存数据
|
||||
*/
|
||||
clearCacheData(key) {
|
||||
if (key) {
|
||||
this.globalCache.delete(key)
|
||||
} else {
|
||||
this.globalCache.clear()
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 检查应用更新
|
||||
*/
|
||||
async checkAppUpdate() {
|
||||
// #ifdef MP-WEIXIN
|
||||
if (uni.canIUse('getUpdateManager')) {
|
||||
const updateManager = uni.getUpdateManager()
|
||||
|
||||
updateManager.onCheckForUpdate((res) => {
|
||||
if (res.hasUpdate) {
|
||||
this.showToast('发现新版本,正在下载...', 'info')
|
||||
}
|
||||
})
|
||||
|
||||
updateManager.onUpdateReady(() => {
|
||||
uni.showModal({
|
||||
title: '更新提示',
|
||||
content: '新版本已准备好,是否重启应用?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
updateManager.applyUpdate()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
updateManager.onUpdateFailed(() => {
|
||||
this.showToast('新版本下载失败', 'error')
|
||||
})
|
||||
}
|
||||
// #endif
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取设备信息
|
||||
*/
|
||||
getDeviceInfo() {
|
||||
return {
|
||||
platform: this.systemInfo?.platform || 'unknown',
|
||||
system: this.systemInfo?.system || 'unknown',
|
||||
version: this.systemInfo?.version || 'unknown',
|
||||
model: this.systemInfo?.model || 'unknown',
|
||||
brand: this.systemInfo?.brand || 'unknown',
|
||||
screenWidth: this.systemInfo?.screenWidth || 0,
|
||||
screenHeight: this.systemInfo?.screenHeight || 0,
|
||||
windowWidth: this.systemInfo?.windowWidth || 0,
|
||||
windowHeight: this.systemInfo?.windowHeight || 0
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,9 +0,0 @@
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
const pinia = createPinia()
|
||||
|
||||
export default pinia
|
||||
|
||||
// 导出所有store
|
||||
export { useUserStore } from './user'
|
||||
export { useAppStore } from './app'
|
||||
@@ -1,217 +0,0 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import request from '@/utils/request'
|
||||
import api from '@/config/api'
|
||||
|
||||
export const useUserStore = defineStore('user', {
|
||||
state: () => ({
|
||||
// 用户信息
|
||||
userInfo: null,
|
||||
// 登录状态
|
||||
isLoggedIn: false,
|
||||
// 用户权限
|
||||
permissions: [],
|
||||
// 用户角色
|
||||
roles: []
|
||||
}),
|
||||
|
||||
getters: {
|
||||
// 获取用户ID
|
||||
userId: (state) => state.userInfo?.id,
|
||||
|
||||
// 获取用户名
|
||||
username: (state) => state.userInfo?.username,
|
||||
|
||||
// 获取真实姓名
|
||||
realName: (state) => state.userInfo?.realName,
|
||||
|
||||
// 获取用户角色名称
|
||||
roleName: (state) => state.userInfo?.roleName,
|
||||
|
||||
// 检查是否有特定权限
|
||||
hasPermission: (state) => (permission) => {
|
||||
return state.permissions.includes(permission)
|
||||
},
|
||||
|
||||
// 检查是否有特定角色
|
||||
hasRole: (state) => (role) => {
|
||||
return state.roles.includes(role)
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
/**
|
||||
* 用户名密码登录
|
||||
*/
|
||||
async login(loginData) {
|
||||
try {
|
||||
const response = await request.post(api.USER.LOGIN, loginData)
|
||||
|
||||
if (response.success) {
|
||||
// 保存token
|
||||
uni.setStorageSync('token', response.data.token)
|
||||
|
||||
// 保存用户信息
|
||||
this.userInfo = response.data.userInfo
|
||||
this.isLoggedIn = true
|
||||
this.permissions = response.data.permissions || []
|
||||
this.roles = response.data.roles || []
|
||||
|
||||
// 持久化用户信息
|
||||
uni.setStorageSync('userInfo', this.userInfo)
|
||||
|
||||
return { success: true }
|
||||
} else {
|
||||
return { success: false, message: response.message }
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error)
|
||||
return { success: false, message: error.message }
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 微信登录
|
||||
*/
|
||||
async wechatLogin(wechatData) {
|
||||
try {
|
||||
const response = await request.post(api.USER.WECHAT_LOGIN, wechatData)
|
||||
|
||||
if (response.success) {
|
||||
// 保存token
|
||||
uni.setStorageSync('token', response.data.token)
|
||||
|
||||
// 保存用户信息
|
||||
this.userInfo = response.data.userInfo
|
||||
this.isLoggedIn = true
|
||||
this.permissions = response.data.permissions || []
|
||||
this.roles = response.data.roles || []
|
||||
|
||||
// 持久化用户信息
|
||||
uni.setStorageSync('userInfo', this.userInfo)
|
||||
|
||||
return { success: true }
|
||||
} else {
|
||||
return { success: false, message: response.message }
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('微信登录失败:', error)
|
||||
return { success: false, message: error.message }
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
*/
|
||||
async logout() {
|
||||
try {
|
||||
// 调用退出登录接口
|
||||
await request.post(api.USER.LOGOUT)
|
||||
} catch (error) {
|
||||
console.error('退出登录接口调用失败:', error)
|
||||
} finally {
|
||||
// 清除本地数据
|
||||
this.clearUserData()
|
||||
|
||||
// 跳转到登录页
|
||||
uni.reLaunch({
|
||||
url: '/pages/login/login'
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
*/
|
||||
async getUserInfo() {
|
||||
try {
|
||||
const response = await request.get(api.USER.INFO)
|
||||
|
||||
if (response.success) {
|
||||
this.userInfo = response.data
|
||||
this.permissions = response.data.permissions || []
|
||||
this.roles = response.data.roles || []
|
||||
|
||||
// 更新本地存储
|
||||
uni.setStorageSync('userInfo', this.userInfo)
|
||||
|
||||
return { success: true, data: response.data }
|
||||
} else {
|
||||
return { success: false, message: response.message }
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败:', error)
|
||||
return { success: false, message: error.message }
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新用户信息
|
||||
*/
|
||||
async updateUserInfo(updateData) {
|
||||
try {
|
||||
const response = await request.post(api.USER.UPDATE, updateData)
|
||||
|
||||
if (response.success) {
|
||||
// 更新本地用户信息
|
||||
this.userInfo = { ...this.userInfo, ...response.data }
|
||||
uni.setStorageSync('userInfo', this.userInfo)
|
||||
|
||||
return { success: true }
|
||||
} else {
|
||||
return { success: false, message: response.message }
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新用户信息失败:', error)
|
||||
return { success: false, message: error.message }
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 检查登录状态
|
||||
*/
|
||||
checkLoginStatus() {
|
||||
const token = uni.getStorageSync('token')
|
||||
const userInfo = uni.getStorageSync('userInfo')
|
||||
|
||||
if (token && userInfo) {
|
||||
this.userInfo = userInfo
|
||||
this.isLoggedIn = true
|
||||
return true
|
||||
} else {
|
||||
this.clearUserData()
|
||||
return false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 清除用户数据
|
||||
*/
|
||||
clearUserData() {
|
||||
this.userInfo = null
|
||||
this.isLoggedIn = false
|
||||
this.permissions = []
|
||||
this.roles = []
|
||||
|
||||
// 清除本地存储
|
||||
uni.removeStorageSync('token')
|
||||
uni.removeStorageSync('userInfo')
|
||||
},
|
||||
|
||||
/**
|
||||
* 初始化用户状态
|
||||
*/
|
||||
initUserState() {
|
||||
// 从本地存储恢复用户状态
|
||||
const token = uni.getStorageSync('token')
|
||||
const userInfo = uni.getStorageSync('userInfo')
|
||||
|
||||
if (token && userInfo) {
|
||||
this.userInfo = userInfo
|
||||
this.isLoggedIn = true
|
||||
|
||||
// 异步获取最新用户信息
|
||||
this.getUserInfo()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,184 +0,0 @@
|
||||
@import './variables.scss';
|
||||
|
||||
/* 重置样式 */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
page {
|
||||
background-color: $bg-color;
|
||||
font-size: $font-size-base;
|
||||
line-height: 1.6;
|
||||
color: $text-color;
|
||||
}
|
||||
|
||||
/* 布局类 */
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex-column {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.flex-row {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.justify-around {
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.align-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.align-start {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.align-end {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.flex-1 {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 文本类 */
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.text-xs {
|
||||
font-size: $font-size-xs;
|
||||
}
|
||||
|
||||
.text-sm {
|
||||
font-size: $font-size-sm;
|
||||
}
|
||||
|
||||
.text-base {
|
||||
font-size: $font-size-base;
|
||||
}
|
||||
|
||||
.text-lg {
|
||||
font-size: $font-size-lg;
|
||||
}
|
||||
|
||||
.text-xl {
|
||||
font-size: $font-size-xl;
|
||||
}
|
||||
|
||||
.text-xxl {
|
||||
font-size: $font-size-xxl;
|
||||
}
|
||||
|
||||
.font-bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.font-medium {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.font-normal {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
/* 间距类 */
|
||||
.m-0 { margin: 0; }
|
||||
.m-xs { margin: $spacing-xs; }
|
||||
.m-sm { margin: $spacing-sm; }
|
||||
.m-base { margin: $spacing-base; }
|
||||
.m-lg { margin: $spacing-lg; }
|
||||
.m-xl { margin: $spacing-xl; }
|
||||
.m-xxl { margin: $spacing-xxl; }
|
||||
|
||||
.mt-0 { margin-top: 0; }
|
||||
.mt-xs { margin-top: $spacing-xs; }
|
||||
.mt-sm { margin-top: $spacing-sm; }
|
||||
.mt-base { margin-top: $spacing-base; }
|
||||
.mt-lg { margin-top: $spacing-lg; }
|
||||
.mt-xl { margin-top: $spacing-xl; }
|
||||
.mt-xxl { margin-top: $spacing-xxl; }
|
||||
|
||||
.mb-0 { margin-bottom: 0; }
|
||||
.mb-xs { margin-bottom: $spacing-xs; }
|
||||
.mb-sm { margin-bottom: $spacing-sm; }
|
||||
.mb-base { margin-bottom: $spacing-base; }
|
||||
.mb-lg { margin-bottom: $spacing-lg; }
|
||||
.mb-xl { margin-bottom: $spacing-xl; }
|
||||
.mb-xxl { margin-bottom: $spacing-xxl; }
|
||||
|
||||
.ml-0 { margin-left: 0; }
|
||||
.ml-xs { margin-left: $spacing-xs; }
|
||||
.ml-sm { margin-left: $spacing-sm; }
|
||||
.ml-base { margin-left: $spacing-base; }
|
||||
.ml-lg { margin-left: $spacing-lg; }
|
||||
.ml-xl { margin-left: $spacing-xl; }
|
||||
.ml-xxl { margin-left: $spacing-xxl; }
|
||||
|
||||
.mr-0 { margin-right: 0; }
|
||||
.mr-xs { margin-right: $spacing-xs; }
|
||||
.mr-sm { margin-right: $spacing-sm; }
|
||||
.mr-base { margin-right: $spacing-base; }
|
||||
.mr-lg { margin-right: $spacing-lg; }
|
||||
.mr-xl { margin-right: $spacing-xl; }
|
||||
.mr-xxl { margin-right: $spacing-xxl; }
|
||||
|
||||
.p-0 { padding: 0; }
|
||||
.p-xs { padding: $spacing-xs; }
|
||||
.p-sm { padding: $spacing-sm; }
|
||||
.p-base { padding: $spacing-base; }
|
||||
.p-lg { padding: $spacing-lg; }
|
||||
.p-xl { padding: $spacing-xl; }
|
||||
.p-xxl { padding: $spacing-xxl; }
|
||||
|
||||
.pt-0 { padding-top: 0; }
|
||||
.pt-xs { padding-top: $spacing-xs; }
|
||||
.pt-sm { padding-top: $spacing-sm; }
|
||||
.pt-base { padding-top: $spacing-base; }
|
||||
.pt-lg { padding-top: $spacing-lg; }
|
||||
.pt-xl { padding-top: $spacing-xl; }
|
||||
.pt-xxl { padding-top: $spacing-xxl; }
|
||||
|
||||
.pb-0 { padding-bottom: 0; }
|
||||
.pb-xs { padding-bottom: $spacing-xs; }
|
||||
.pb-sm { padding-bottom: $spacing-sm; }
|
||||
.pb-base { padding-bottom: $spacing-base; }
|
||||
.pb-lg { padding-bottom: $spacing-lg; }
|
||||
.pb-xl { padding-bottom: $spacing-xl; }
|
||||
.pb-xxl { padding-bottom: $spacing-xxl; }
|
||||
|
||||
.pl-0 { padding-left: 0; }
|
||||
.pl-xs { padding-left: $spacing-xs; }
|
||||
.pl-sm { padding-left: $spacing-sm; }
|
||||
.pl-base { padding-left: $spacing-base; }
|
||||
.pl-lg { padding-left: $spacing-lg; }
|
||||
.pl-xl { padding-left: $spacing-xl; }
|
||||
.pl-xxl { padding-left: $spacing-xxl; }
|
||||
|
||||
.pr-0 { padding-right: 0; }
|
||||
.pr-xs { padding-right: $spacing-xs; }
|
||||
.pr-sm { padding-right: $spacing-sm; }
|
||||
.pr-base { padding-right: $spacing-base; }
|
||||
.pr-lg { padding-right: $spacing-lg; }
|
||||
.pr-xl { padding-right: $spacing-xl; }
|
||||
.pr-xxl { padding-right: $spacing-xxl; }
|
||||
@@ -1,544 +0,0 @@
|
||||
// 混合器文件 - 提供常用的样式混合器和工具类
|
||||
@import './variables.scss';
|
||||
|
||||
// 文本省略
|
||||
@mixin text-ellipsis($lines: 1) {
|
||||
@if $lines == 1 {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
} @else {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: $lines;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
// 清除浮动
|
||||
@mixin clearfix {
|
||||
&::after {
|
||||
content: '';
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
}
|
||||
|
||||
// 居中对齐
|
||||
@mixin center($type: 'both') {
|
||||
position: absolute;
|
||||
|
||||
@if $type == 'both' {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
} @else if $type == 'horizontal' {
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
} @else if $type == 'vertical' {
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
// Flex 布局
|
||||
@mixin flex($direction: row, $justify: flex-start, $align: stretch, $wrap: nowrap) {
|
||||
display: flex;
|
||||
flex-direction: $direction;
|
||||
justify-content: $justify;
|
||||
align-items: $align;
|
||||
flex-wrap: $wrap;
|
||||
}
|
||||
|
||||
// Flex 居中
|
||||
@mixin flex-center {
|
||||
@include flex(row, center, center);
|
||||
}
|
||||
|
||||
// 响应式断点
|
||||
@mixin respond-to($breakpoint) {
|
||||
@if $breakpoint == 'mobile' {
|
||||
@media (max-width: 767px) {
|
||||
@content;
|
||||
}
|
||||
} @else if $breakpoint == 'tablet' {
|
||||
@media (min-width: 768px) and (max-width: 1023px) {
|
||||
@content;
|
||||
}
|
||||
} @else if $breakpoint == 'desktop' {
|
||||
@media (min-width: 1024px) {
|
||||
@content;
|
||||
}
|
||||
} @else if $breakpoint == 'large-desktop' {
|
||||
@media (min-width: 1200px) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 按钮样式
|
||||
@mixin button-style($type: 'primary', $size: 'medium') {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
border-radius: $border-radius-sm;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
user-select: none;
|
||||
|
||||
// 尺寸
|
||||
@if $size == 'small' {
|
||||
padding: $spacing-xs $spacing-sm;
|
||||
font-size: $font-size-sm;
|
||||
min-height: 32px;
|
||||
} @else if $size == 'medium' {
|
||||
padding: $spacing-sm $spacing-md;
|
||||
font-size: $font-size-md;
|
||||
min-height: 40px;
|
||||
} @else if $size == 'large' {
|
||||
padding: $spacing-md $spacing-lg;
|
||||
font-size: $font-size-lg;
|
||||
min-height: 48px;
|
||||
}
|
||||
|
||||
// 类型
|
||||
@if $type == 'primary' {
|
||||
background: $primary-color;
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
background: $primary-color-light;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: $primary-color-dark;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: $text-color-disabled;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
} @else if $type == 'secondary' {
|
||||
background: white;
|
||||
color: $primary-color;
|
||||
border: 1px solid $primary-color;
|
||||
|
||||
&:hover {
|
||||
background: rgba($primary-color, 0.05);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: rgba($primary-color, 0.1);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: $text-color-disabled;
|
||||
border-color: $text-color-disabled;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
} @else if $type == 'text' {
|
||||
background: transparent;
|
||||
color: $primary-color;
|
||||
|
||||
&:hover {
|
||||
background: rgba($primary-color, 0.05);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: rgba($primary-color, 0.1);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: $text-color-disabled;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 卡片样式
|
||||
@mixin card-style($shadow: true, $border: true, $radius: true) {
|
||||
background: white;
|
||||
|
||||
@if $border {
|
||||
border: 1px solid $border-color-light;
|
||||
}
|
||||
|
||||
@if $radius {
|
||||
border-radius: $border-radius-md;
|
||||
}
|
||||
|
||||
@if $shadow {
|
||||
box-shadow: $shadow-light;
|
||||
}
|
||||
}
|
||||
|
||||
// 输入框样式
|
||||
@mixin input-style($size: 'medium') {
|
||||
width: 100%;
|
||||
border: 1px solid $border-color;
|
||||
border-radius: $border-radius-sm;
|
||||
background: white;
|
||||
color: $text-color-primary;
|
||||
font-size: $font-size-md;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
@if $size == 'small' {
|
||||
padding: $spacing-xs $spacing-sm;
|
||||
font-size: $font-size-sm;
|
||||
min-height: 32px;
|
||||
} @else if $size == 'medium' {
|
||||
padding: $spacing-sm $spacing-md;
|
||||
font-size: $font-size-md;
|
||||
min-height: 40px;
|
||||
} @else if $size == 'large' {
|
||||
padding: $spacing-md $spacing-lg;
|
||||
font-size: $font-size-lg;
|
||||
min-height: 48px;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: $text-color-placeholder;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: $primary-color;
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px rgba($primary-color, 0.2);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: $bg-color-light;
|
||||
color: $text-color-disabled;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&.error {
|
||||
border-color: $error-color;
|
||||
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 2px rgba($error-color, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 滚动条样式
|
||||
@mixin scrollbar($width: 6px, $track-color: transparent, $thumb-color: rgba(0, 0, 0, 0.2)) {
|
||||
&::-webkit-scrollbar {
|
||||
width: $width;
|
||||
height: $width;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: $track-color;
|
||||
border-radius: $width / 2;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: $thumb-color;
|
||||
border-radius: $width / 2;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 动画
|
||||
@mixin fade-in($duration: 0.3s) {
|
||||
animation: fadeIn $duration ease-in-out;
|
||||
}
|
||||
|
||||
@mixin fade-out($duration: 0.3s) {
|
||||
animation: fadeOut $duration ease-in-out;
|
||||
}
|
||||
|
||||
@mixin slide-up($duration: 0.3s) {
|
||||
animation: slideUp $duration ease-out;
|
||||
}
|
||||
|
||||
@mixin slide-down($duration: 0.3s) {
|
||||
animation: slideDown $duration ease-out;
|
||||
}
|
||||
|
||||
@mixin bounce-in($duration: 0.5s) {
|
||||
animation: bounceIn $duration ease-out;
|
||||
}
|
||||
|
||||
// 关键帧动画
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
transform: translateY(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
transform: translateY(-100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bounceIn {
|
||||
0% {
|
||||
transform: scale(0.3);
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
70% {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 阴影
|
||||
@mixin shadow($level: 1) {
|
||||
@if $level == 1 {
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
} @else if $level == 2 {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
} @else if $level == 3 {
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
|
||||
} @else if $level == 4 {
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
}
|
||||
|
||||
// 边框
|
||||
@mixin border($position: 'all', $color: $border-color, $width: 1px, $style: solid) {
|
||||
@if $position == 'all' {
|
||||
border: $width $style $color;
|
||||
} @else if $position == 'top' {
|
||||
border-top: $width $style $color;
|
||||
} @else if $position == 'right' {
|
||||
border-right: $width $style $color;
|
||||
} @else if $position == 'bottom' {
|
||||
border-bottom: $width $style $color;
|
||||
} @else if $position == 'left' {
|
||||
border-left: $width $style $color;
|
||||
} @else if $position == 'horizontal' {
|
||||
border-left: $width $style $color;
|
||||
border-right: $width $style $color;
|
||||
} @else if $position == 'vertical' {
|
||||
border-top: $width $style $color;
|
||||
border-bottom: $width $style $color;
|
||||
}
|
||||
}
|
||||
|
||||
// 渐变背景
|
||||
@mixin gradient($direction: 'to right', $colors...) {
|
||||
background: linear-gradient(#{$direction}, $colors);
|
||||
}
|
||||
|
||||
// 毛玻璃效果
|
||||
@mixin glass($blur: 10px, $opacity: 0.8) {
|
||||
backdrop-filter: blur($blur);
|
||||
background: rgba(255, 255, 255, $opacity);
|
||||
}
|
||||
|
||||
// 文本样式
|
||||
@mixin text-style($size: 'medium', $weight: normal, $color: $text-color-primary) {
|
||||
@if $size == 'xs' {
|
||||
font-size: $font-size-xs;
|
||||
} @else if $size == 'sm' {
|
||||
font-size: $font-size-sm;
|
||||
} @else if $size == 'md' {
|
||||
font-size: $font-size-md;
|
||||
} @else if $size == 'lg' {
|
||||
font-size: $font-size-lg;
|
||||
} @else if $size == 'xl' {
|
||||
font-size: $font-size-xl;
|
||||
}
|
||||
|
||||
font-weight: $weight;
|
||||
color: $color;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
// 安全区域适配
|
||||
@mixin safe-area($position: 'bottom', $property: 'padding') {
|
||||
@if $position == 'top' {
|
||||
#{$property}-top: env(safe-area-inset-top);
|
||||
} @else if $position == 'bottom' {
|
||||
#{$property}-bottom: env(safe-area-inset-bottom);
|
||||
} @else if $position == 'left' {
|
||||
#{$property}-left: env(safe-area-inset-left);
|
||||
} @else if $position == 'right' {
|
||||
#{$property}-right: env(safe-area-inset-right);
|
||||
}
|
||||
}
|
||||
|
||||
// 1px 边框解决方案
|
||||
@mixin hairline($position: 'all', $color: $border-color) {
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
|
||||
@if $position == 'all' {
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
border: 1px solid $color;
|
||||
transform-origin: 0 0;
|
||||
transform: scale(0.5);
|
||||
} @else if $position == 'top' {
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background: $color;
|
||||
transform-origin: 0 0;
|
||||
transform: scaleY(0.5);
|
||||
} @else if $position == 'bottom' {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background: $color;
|
||||
transform-origin: 0 100%;
|
||||
transform: scaleY(0.5);
|
||||
} @else if $position == 'left' {
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
background: $color;
|
||||
transform-origin: 0 0;
|
||||
transform: scaleX(0.5);
|
||||
} @else if $position == 'right' {
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
background: $color;
|
||||
transform-origin: 100% 0;
|
||||
transform: scaleX(0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 网格布局
|
||||
@mixin grid($columns: 2, $gap: $spacing-md) {
|
||||
display: grid;
|
||||
grid-template-columns: repeat($columns, 1fr);
|
||||
gap: $gap;
|
||||
}
|
||||
|
||||
// 固定宽高比
|
||||
@mixin aspect-ratio($ratio: 1) {
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: block;
|
||||
padding-top: percentage(1 / $ratio);
|
||||
}
|
||||
|
||||
> * {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// 隐藏元素但保持可访问性
|
||||
@mixin visually-hidden {
|
||||
position: absolute !important;
|
||||
width: 1px !important;
|
||||
height: 1px !important;
|
||||
padding: 0 !important;
|
||||
margin: -1px !important;
|
||||
overflow: hidden !important;
|
||||
clip: rect(0, 0, 0, 0) !important;
|
||||
white-space: nowrap !important;
|
||||
border: 0 !important;
|
||||
}
|
||||
|
||||
// 重置按钮样式
|
||||
@mixin reset-button {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
// 重置列表样式
|
||||
@mixin reset-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
// 图片适配
|
||||
@mixin image-fit($fit: cover) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: $fit;
|
||||
object-position: center;
|
||||
}
|
||||
@@ -1,476 +0,0 @@
|
||||
// 工具类样式文件 - 提供常用的原子化CSS类
|
||||
@import './variables.scss';
|
||||
@import './mixins.scss';
|
||||
|
||||
// 间距工具类
|
||||
@each $name, $value in (
|
||||
'xs': $spacing-xs,
|
||||
'sm': $spacing-sm,
|
||||
'md': $spacing-md,
|
||||
'lg': $spacing-lg,
|
||||
'xl': $spacing-xl,
|
||||
'xxl': $spacing-xxl
|
||||
) {
|
||||
// 内边距
|
||||
.p-#{$name} { padding: $value !important; }
|
||||
.pt-#{$name} { padding-top: $value !important; }
|
||||
.pr-#{$name} { padding-right: $value !important; }
|
||||
.pb-#{$name} { padding-bottom: $value !important; }
|
||||
.pl-#{$name} { padding-left: $value !important; }
|
||||
.px-#{$name} {
|
||||
padding-left: $value !important;
|
||||
padding-right: $value !important;
|
||||
}
|
||||
.py-#{$name} {
|
||||
padding-top: $value !important;
|
||||
padding-bottom: $value !important;
|
||||
}
|
||||
|
||||
// 外边距
|
||||
.m-#{$name} { margin: $value !important; }
|
||||
.mt-#{$name} { margin-top: $value !important; }
|
||||
.mr-#{$name} { margin-right: $value !important; }
|
||||
.mb-#{$name} { margin-bottom: $value !important; }
|
||||
.ml-#{$name} { margin-left: $value !important; }
|
||||
.mx-#{$name} {
|
||||
margin-left: $value !important;
|
||||
margin-right: $value !important;
|
||||
}
|
||||
.my-#{$name} {
|
||||
margin-top: $value !important;
|
||||
margin-bottom: $value !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 特殊间距
|
||||
.p-0 { padding: 0 !important; }
|
||||
.m-0 { margin: 0 !important; }
|
||||
.m-auto { margin: auto !important; }
|
||||
.mx-auto {
|
||||
margin-left: auto !important;
|
||||
margin-right: auto !important;
|
||||
}
|
||||
.my-auto {
|
||||
margin-top: auto !important;
|
||||
margin-bottom: auto !important;
|
||||
}
|
||||
|
||||
// 文本工具类
|
||||
.text-left { text-align: left !important; }
|
||||
.text-center { text-align: center !important; }
|
||||
.text-right { text-align: right !important; }
|
||||
.text-justify { text-align: justify !important; }
|
||||
|
||||
// 文本颜色
|
||||
.text-primary { color: $primary-color !important; }
|
||||
.text-secondary { color: $text-color-secondary !important; }
|
||||
.text-success { color: $success-color !important; }
|
||||
.text-warning { color: $warning-color !important; }
|
||||
.text-error { color: $error-color !important; }
|
||||
.text-info { color: $info-color !important; }
|
||||
.text-disabled { color: $text-color-disabled !important; }
|
||||
.text-white { color: white !important; }
|
||||
.text-black { color: black !important; }
|
||||
|
||||
// 文本大小
|
||||
.text-xs { font-size: $font-size-xs !important; }
|
||||
.text-sm { font-size: $font-size-sm !important; }
|
||||
.text-md { font-size: $font-size-md !important; }
|
||||
.text-lg { font-size: $font-size-lg !important; }
|
||||
.text-xl { font-size: $font-size-xl !important; }
|
||||
|
||||
// 文本粗细
|
||||
.font-thin { font-weight: 100 !important; }
|
||||
.font-light { font-weight: 300 !important; }
|
||||
.font-normal { font-weight: 400 !important; }
|
||||
.font-medium { font-weight: 500 !important; }
|
||||
.font-semibold { font-weight: 600 !important; }
|
||||
.font-bold { font-weight: 700 !important; }
|
||||
.font-extrabold { font-weight: 800 !important; }
|
||||
.font-black { font-weight: 900 !important; }
|
||||
|
||||
// 文本装饰
|
||||
.underline { text-decoration: underline !important; }
|
||||
.line-through { text-decoration: line-through !important; }
|
||||
.no-underline { text-decoration: none !important; }
|
||||
|
||||
// 文本省略
|
||||
.text-ellipsis { @include text-ellipsis(1); }
|
||||
.text-ellipsis-2 { @include text-ellipsis(2); }
|
||||
.text-ellipsis-3 { @include text-ellipsis(3); }
|
||||
|
||||
// 文本换行
|
||||
.text-nowrap { white-space: nowrap !important; }
|
||||
.text-wrap { white-space: normal !important; }
|
||||
.text-break { word-break: break-all !important; }
|
||||
|
||||
// 背景颜色
|
||||
.bg-primary { background-color: $primary-color !important; }
|
||||
.bg-success { background-color: $success-color !important; }
|
||||
.bg-warning { background-color: $warning-color !important; }
|
||||
.bg-error { background-color: $error-color !important; }
|
||||
.bg-info { background-color: $info-color !important; }
|
||||
.bg-white { background-color: white !important; }
|
||||
.bg-light { background-color: $bg-color-light !important; }
|
||||
.bg-gray { background-color: $bg-color-gray !important; }
|
||||
.bg-transparent { background-color: transparent !important; }
|
||||
|
||||
// 边框
|
||||
.border { border: 1px solid $border-color !important; }
|
||||
.border-t { border-top: 1px solid $border-color !important; }
|
||||
.border-r { border-right: 1px solid $border-color !important; }
|
||||
.border-b { border-bottom: 1px solid $border-color !important; }
|
||||
.border-l { border-left: 1px solid $border-color !important; }
|
||||
.border-0 { border: none !important; }
|
||||
|
||||
// 边框颜色
|
||||
.border-primary { border-color: $primary-color !important; }
|
||||
.border-success { border-color: $success-color !important; }
|
||||
.border-warning { border-color: $warning-color !important; }
|
||||
.border-error { border-color: $error-color !important; }
|
||||
.border-info { border-color: $info-color !important; }
|
||||
.border-light { border-color: $border-color-light !important; }
|
||||
|
||||
// 圆角
|
||||
.rounded-none { border-radius: 0 !important; }
|
||||
.rounded-sm { border-radius: $border-radius-sm !important; }
|
||||
.rounded { border-radius: $border-radius-md !important; }
|
||||
.rounded-lg { border-radius: $border-radius-lg !important; }
|
||||
.rounded-full { border-radius: 50% !important; }
|
||||
|
||||
// 阴影
|
||||
.shadow-none { box-shadow: none !important; }
|
||||
.shadow-sm { @include shadow(1); }
|
||||
.shadow { @include shadow(2); }
|
||||
.shadow-lg { @include shadow(3); }
|
||||
.shadow-xl { @include shadow(4); }
|
||||
|
||||
// 显示/隐藏
|
||||
.block { display: block !important; }
|
||||
.inline { display: inline !important; }
|
||||
.inline-block { display: inline-block !important; }
|
||||
.flex { display: flex !important; }
|
||||
.inline-flex { display: inline-flex !important; }
|
||||
.grid { display: grid !important; }
|
||||
.hidden { display: none !important; }
|
||||
|
||||
// Flex 布局
|
||||
.flex-row { flex-direction: row !important; }
|
||||
.flex-col { flex-direction: column !important; }
|
||||
.flex-wrap { flex-wrap: wrap !important; }
|
||||
.flex-nowrap { flex-wrap: nowrap !important; }
|
||||
|
||||
// Flex 对齐
|
||||
.justify-start { justify-content: flex-start !important; }
|
||||
.justify-end { justify-content: flex-end !important; }
|
||||
.justify-center { justify-content: center !important; }
|
||||
.justify-between { justify-content: space-between !important; }
|
||||
.justify-around { justify-content: space-around !important; }
|
||||
.justify-evenly { justify-content: space-evenly !important; }
|
||||
|
||||
.items-start { align-items: flex-start !important; }
|
||||
.items-end { align-items: flex-end !important; }
|
||||
.items-center { align-items: center !important; }
|
||||
.items-baseline { align-items: baseline !important; }
|
||||
.items-stretch { align-items: stretch !important; }
|
||||
|
||||
// Flex 项目
|
||||
.flex-1 { flex: 1 1 0% !important; }
|
||||
.flex-auto { flex: 1 1 auto !important; }
|
||||
.flex-initial { flex: 0 1 auto !important; }
|
||||
.flex-none { flex: none !important; }
|
||||
.flex-shrink-0 { flex-shrink: 0 !important; }
|
||||
.flex-grow { flex-grow: 1 !important; }
|
||||
|
||||
// 定位
|
||||
.relative { position: relative !important; }
|
||||
.absolute { position: absolute !important; }
|
||||
.fixed { position: fixed !important; }
|
||||
.sticky { position: sticky !important; }
|
||||
.static { position: static !important; }
|
||||
|
||||
// 定位偏移
|
||||
.top-0 { top: 0 !important; }
|
||||
.right-0 { right: 0 !important; }
|
||||
.bottom-0 { bottom: 0 !important; }
|
||||
.left-0 { left: 0 !important; }
|
||||
|
||||
// 宽度
|
||||
.w-full { width: 100% !important; }
|
||||
.w-auto { width: auto !important; }
|
||||
.w-0 { width: 0 !important; }
|
||||
|
||||
@for $i from 1 through 12 {
|
||||
.w-#{$i} { width: percentage($i / 12) !important; }
|
||||
}
|
||||
|
||||
// 高度
|
||||
.h-full { height: 100% !important; }
|
||||
.h-auto { height: auto !important; }
|
||||
.h-0 { height: 0 !important; }
|
||||
.h-screen { height: 100vh !important; }
|
||||
|
||||
// 最大/最小宽高
|
||||
.max-w-full { max-width: 100% !important; }
|
||||
.max-h-full { max-height: 100% !important; }
|
||||
.min-w-0 { min-width: 0 !important; }
|
||||
.min-h-0 { min-height: 0 !important; }
|
||||
|
||||
// 溢出
|
||||
.overflow-auto { overflow: auto !important; }
|
||||
.overflow-hidden { overflow: hidden !important; }
|
||||
.overflow-visible { overflow: visible !important; }
|
||||
.overflow-scroll { overflow: scroll !important; }
|
||||
.overflow-x-auto { overflow-x: auto !important; }
|
||||
.overflow-y-auto { overflow-y: auto !important; }
|
||||
.overflow-x-hidden { overflow-x: hidden !important; }
|
||||
.overflow-y-hidden { overflow-y: hidden !important; }
|
||||
|
||||
// 透明度
|
||||
.opacity-0 { opacity: 0 !important; }
|
||||
.opacity-25 { opacity: 0.25 !important; }
|
||||
.opacity-50 { opacity: 0.5 !important; }
|
||||
.opacity-75 { opacity: 0.75 !important; }
|
||||
.opacity-100 { opacity: 1 !important; }
|
||||
|
||||
// 层级
|
||||
.z-0 { z-index: 0 !important; }
|
||||
.z-10 { z-index: 10 !important; }
|
||||
.z-20 { z-index: 20 !important; }
|
||||
.z-30 { z-index: 30 !important; }
|
||||
.z-40 { z-index: 40 !important; }
|
||||
.z-50 { z-index: 50 !important; }
|
||||
.z-auto { z-index: auto !important; }
|
||||
|
||||
// 光标
|
||||
.cursor-auto { cursor: auto !important; }
|
||||
.cursor-default { cursor: default !important; }
|
||||
.cursor-pointer { cursor: pointer !important; }
|
||||
.cursor-wait { cursor: wait !important; }
|
||||
.cursor-text { cursor: text !important; }
|
||||
.cursor-move { cursor: move !important; }
|
||||
.cursor-not-allowed { cursor: not-allowed !important; }
|
||||
|
||||
// 用户选择
|
||||
.select-none { user-select: none !important; }
|
||||
.select-text { user-select: text !important; }
|
||||
.select-all { user-select: all !important; }
|
||||
.select-auto { user-select: auto !important; }
|
||||
|
||||
// 指针事件
|
||||
.pointer-events-none { pointer-events: none !important; }
|
||||
.pointer-events-auto { pointer-events: auto !important; }
|
||||
|
||||
// 变换
|
||||
.transform { transform: translateZ(0) !important; }
|
||||
.scale-0 { transform: scale(0) !important; }
|
||||
.scale-50 { transform: scale(0.5) !important; }
|
||||
.scale-75 { transform: scale(0.75) !important; }
|
||||
.scale-90 { transform: scale(0.9) !important; }
|
||||
.scale-95 { transform: scale(0.95) !important; }
|
||||
.scale-100 { transform: scale(1) !important; }
|
||||
.scale-105 { transform: scale(1.05) !important; }
|
||||
.scale-110 { transform: scale(1.1) !important; }
|
||||
.scale-125 { transform: scale(1.25) !important; }
|
||||
.scale-150 { transform: scale(1.5) !important; }
|
||||
|
||||
// 旋转
|
||||
.rotate-0 { transform: rotate(0deg) !important; }
|
||||
.rotate-45 { transform: rotate(45deg) !important; }
|
||||
.rotate-90 { transform: rotate(90deg) !important; }
|
||||
.rotate-180 { transform: rotate(180deg) !important; }
|
||||
.rotate-270 { transform: rotate(270deg) !important; }
|
||||
|
||||
// 过渡
|
||||
.transition-none { transition: none !important; }
|
||||
.transition-all { transition: all 0.3s ease !important; }
|
||||
.transition-colors { transition: color 0.3s ease, background-color 0.3s ease, border-color 0.3s ease !important; }
|
||||
.transition-opacity { transition: opacity 0.3s ease !important; }
|
||||
.transition-shadow { transition: box-shadow 0.3s ease !important; }
|
||||
.transition-transform { transition: transform 0.3s ease !important; }
|
||||
|
||||
// 动画
|
||||
.animate-spin { animation: spin 1s linear infinite !important; }
|
||||
.animate-ping { animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite !important; }
|
||||
.animate-pulse { animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite !important; }
|
||||
.animate-bounce { animation: bounce 1s infinite !important; }
|
||||
|
||||
@keyframes ping {
|
||||
75%, 100% {
|
||||
transform: scale(2);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 100% {
|
||||
transform: translateY(-25%);
|
||||
animation-timing-function: cubic-bezier(0.8, 0, 1, 1);
|
||||
}
|
||||
50% {
|
||||
transform: none;
|
||||
animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 滤镜
|
||||
.blur-none { filter: blur(0) !important; }
|
||||
.blur-sm { filter: blur(4px) !important; }
|
||||
.blur { filter: blur(8px) !important; }
|
||||
.blur-lg { filter: blur(16px) !important; }
|
||||
.blur-xl { filter: blur(24px) !important; }
|
||||
|
||||
// 亮度
|
||||
.brightness-0 { filter: brightness(0) !important; }
|
||||
.brightness-50 { filter: brightness(0.5) !important; }
|
||||
.brightness-75 { filter: brightness(0.75) !important; }
|
||||
.brightness-90 { filter: brightness(0.9) !important; }
|
||||
.brightness-95 { filter: brightness(0.95) !important; }
|
||||
.brightness-100 { filter: brightness(1) !important; }
|
||||
.brightness-105 { filter: brightness(1.05) !important; }
|
||||
.brightness-110 { filter: brightness(1.1) !important; }
|
||||
.brightness-125 { filter: brightness(1.25) !important; }
|
||||
.brightness-150 { filter: brightness(1.5) !important; }
|
||||
.brightness-200 { filter: brightness(2) !important; }
|
||||
|
||||
// 对比度
|
||||
.contrast-0 { filter: contrast(0) !important; }
|
||||
.contrast-50 { filter: contrast(0.5) !important; }
|
||||
.contrast-75 { filter: contrast(0.75) !important; }
|
||||
.contrast-100 { filter: contrast(1) !important; }
|
||||
.contrast-125 { filter: contrast(1.25) !important; }
|
||||
.contrast-150 { filter: contrast(1.5) !important; }
|
||||
.contrast-200 { filter: contrast(2) !important; }
|
||||
|
||||
// 灰度
|
||||
.grayscale-0 { filter: grayscale(0) !important; }
|
||||
.grayscale { filter: grayscale(100%) !important; }
|
||||
|
||||
// 色相旋转
|
||||
.hue-rotate-0 { filter: hue-rotate(0deg) !important; }
|
||||
.hue-rotate-15 { filter: hue-rotate(15deg) !important; }
|
||||
.hue-rotate-30 { filter: hue-rotate(30deg) !important; }
|
||||
.hue-rotate-60 { filter: hue-rotate(60deg) !important; }
|
||||
.hue-rotate-90 { filter: hue-rotate(90deg) !important; }
|
||||
.hue-rotate-180 { filter: hue-rotate(180deg) !important; }
|
||||
|
||||
// 饱和度
|
||||
.saturate-0 { filter: saturate(0) !important; }
|
||||
.saturate-50 { filter: saturate(0.5) !important; }
|
||||
.saturate-100 { filter: saturate(1) !important; }
|
||||
.saturate-150 { filter: saturate(1.5) !important; }
|
||||
.saturate-200 { filter: saturate(2) !important; }
|
||||
|
||||
// 深褐色
|
||||
.sepia-0 { filter: sepia(0) !important; }
|
||||
.sepia { filter: sepia(100%) !important; }
|
||||
|
||||
// 反转
|
||||
.invert-0 { filter: invert(0) !important; }
|
||||
.invert { filter: invert(100%) !important; }
|
||||
|
||||
// 投影
|
||||
.drop-shadow-sm { filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.05)) !important; }
|
||||
.drop-shadow { filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.1)) drop-shadow(0 1px 2px rgba(0, 0, 0, 0.06)) !important; }
|
||||
.drop-shadow-md { filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.07)) drop-shadow(0 2px 4px rgba(0, 0, 0, 0.06)) !important; }
|
||||
.drop-shadow-lg { filter: drop-shadow(0 10px 15px rgba(0, 0, 0, 0.1)) drop-shadow(0 4px 6px rgba(0, 0, 0, 0.05)) !important; }
|
||||
.drop-shadow-xl { filter: drop-shadow(0 20px 25px rgba(0, 0, 0, 0.15)) drop-shadow(0 8px 10px rgba(0, 0, 0, 0.04)) !important; }
|
||||
.drop-shadow-2xl { filter: drop-shadow(0 25px 50px rgba(0, 0, 0, 0.25)) !important; }
|
||||
.drop-shadow-none { filter: drop-shadow(0 0 #0000) !important; }
|
||||
|
||||
// 响应式工具类
|
||||
@include respond-to('mobile') {
|
||||
.mobile\:hidden { display: none !important; }
|
||||
.mobile\:block { display: block !important; }
|
||||
.mobile\:flex { display: flex !important; }
|
||||
.mobile\:text-center { text-align: center !important; }
|
||||
.mobile\:text-left { text-align: left !important; }
|
||||
.mobile\:text-sm { font-size: $font-size-sm !important; }
|
||||
.mobile\:p-sm { padding: $spacing-sm !important; }
|
||||
.mobile\:m-sm { margin: $spacing-sm !important; }
|
||||
}
|
||||
|
||||
@include respond-to('tablet') {
|
||||
.tablet\:hidden { display: none !important; }
|
||||
.tablet\:block { display: block !important; }
|
||||
.tablet\:flex { display: flex !important; }
|
||||
.tablet\:text-center { text-align: center !important; }
|
||||
.tablet\:text-left { text-align: left !important; }
|
||||
}
|
||||
|
||||
@include respond-to('desktop') {
|
||||
.desktop\:hidden { display: none !important; }
|
||||
.desktop\:block { display: block !important; }
|
||||
.desktop\:flex { display: flex !important; }
|
||||
.desktop\:text-center { text-align: center !important; }
|
||||
.desktop\:text-left { text-align: left !important; }
|
||||
}
|
||||
|
||||
// 打印样式
|
||||
@media print {
|
||||
.print\:hidden { display: none !important; }
|
||||
.print\:block { display: block !important; }
|
||||
.print\:text-black { color: black !important; }
|
||||
.print\:bg-white { background-color: white !important; }
|
||||
}
|
||||
|
||||
// 暗色主题
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.dark\:text-white { color: white !important; }
|
||||
.dark\:text-gray-300 { color: #d1d5db !important; }
|
||||
.dark\:bg-gray-800 { background-color: #1f2937 !important; }
|
||||
.dark\:bg-gray-900 { background-color: #111827 !important; }
|
||||
.dark\:border-gray-600 { border-color: #4b5563 !important; }
|
||||
}
|
||||
|
||||
// 高对比度
|
||||
@media (prefers-contrast: high) {
|
||||
.high-contrast\:border-black { border-color: black !important; }
|
||||
.high-contrast\:text-black { color: black !important; }
|
||||
.high-contrast\:bg-white { background-color: white !important; }
|
||||
}
|
||||
|
||||
// 减少动画
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.motion-reduce\:animate-none { animation: none !important; }
|
||||
.motion-reduce\:transition-none { transition: none !important; }
|
||||
}
|
||||
|
||||
// 自定义工具类
|
||||
.clearfix { @include clearfix; }
|
||||
.center { @include center; }
|
||||
.center-x { @include center('horizontal'); }
|
||||
.center-y { @include center('vertical'); }
|
||||
.flex-center { @include flex-center; }
|
||||
.visually-hidden { @include visually-hidden; }
|
||||
.reset-button { @include reset-button; }
|
||||
.reset-list { @include reset-list; }
|
||||
|
||||
// 滚动条样式
|
||||
.scrollbar-thin { @include scrollbar(4px); }
|
||||
.scrollbar-none {
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// 安全区域
|
||||
.safe-top { @include safe-area('top'); }
|
||||
.safe-bottom { @include safe-area('bottom'); }
|
||||
.safe-left { @include safe-area('left'); }
|
||||
.safe-right { @include safe-area('right'); }
|
||||
|
||||
// 1px 边框
|
||||
.hairline { @include hairline; }
|
||||
.hairline-top { @include hairline('top'); }
|
||||
.hairline-bottom { @include hairline('bottom'); }
|
||||
.hairline-left { @include hairline('left'); }
|
||||
.hairline-right { @include hairline('right'); }
|
||||
|
||||
// 宽高比
|
||||
.aspect-square { @include aspect-ratio(1); }
|
||||
.aspect-video { @include aspect-ratio(16/9); }
|
||||
.aspect-photo { @include aspect-ratio(4/3); }
|
||||
@@ -1,54 +0,0 @@
|
||||
// 主题色彩
|
||||
$primary-color: #2c5aa0;
|
||||
$primary-light: #4a7bc8;
|
||||
$primary-dark: #1e3a8a;
|
||||
|
||||
$success-color: #52c41a;
|
||||
$warning-color: #faad14;
|
||||
$danger-color: #ff4d4f;
|
||||
$info-color: #1890ff;
|
||||
|
||||
// 中性色
|
||||
$text-color: #333333;
|
||||
$text-secondary: #666666;
|
||||
$text-muted: #999999;
|
||||
$text-light: #cccccc;
|
||||
|
||||
$bg-color: #f5f7fa;
|
||||
$bg-white: #ffffff;
|
||||
$bg-gray: #f8f9fa;
|
||||
|
||||
$border-color: #e8e8e8;
|
||||
$border-light: #f0f0f0;
|
||||
|
||||
// 字体大小
|
||||
$font-size-xs: 20rpx;
|
||||
$font-size-sm: 24rpx;
|
||||
$font-size-base: 28rpx;
|
||||
$font-size-lg: 32rpx;
|
||||
$font-size-xl: 36rpx;
|
||||
$font-size-xxl: 40rpx;
|
||||
|
||||
// 间距
|
||||
$spacing-xs: 8rpx;
|
||||
$spacing-sm: 12rpx;
|
||||
$spacing-base: 16rpx;
|
||||
$spacing-lg: 24rpx;
|
||||
$spacing-xl: 32rpx;
|
||||
$spacing-xxl: 48rpx;
|
||||
|
||||
// 圆角
|
||||
$border-radius-sm: 4rpx;
|
||||
$border-radius-base: 8rpx;
|
||||
$border-radius-lg: 12rpx;
|
||||
$border-radius-xl: 16rpx;
|
||||
|
||||
// 阴影
|
||||
$box-shadow-sm: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
|
||||
$box-shadow-base: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
$box-shadow-lg: 0 4rpx 20rpx rgba(0, 0, 0, 0.15);
|
||||
|
||||
// 动画时间
|
||||
$transition-base: 0.3s;
|
||||
$transition-fast: 0.2s;
|
||||
$transition-slow: 0.5s;
|
||||
@@ -1,379 +0,0 @@
|
||||
// 银行端小程序认证工具类
|
||||
|
||||
import { useUserStore } from '@/store'
|
||||
|
||||
/**
|
||||
* 检查用户是否已登录
|
||||
* @returns {boolean} 登录状态
|
||||
*/
|
||||
export const isLoggedIn = () => {
|
||||
const token = uni.getStorageSync('token')
|
||||
const userInfo = uni.getStorageSync('userInfo')
|
||||
return !!(token && userInfo)
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否有特定权限
|
||||
* @param {string} permission 权限标识
|
||||
* @returns {boolean} 是否有权限
|
||||
*/
|
||||
export const hasPermission = (permission) => {
|
||||
const userStore = useUserStore()
|
||||
return userStore.hasPermission(permission)
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否有特定角色
|
||||
* @param {string} role 角色标识
|
||||
* @returns {boolean} 是否有角色
|
||||
*/
|
||||
export const hasRole = (role) => {
|
||||
const userStore = useUserStore()
|
||||
return userStore.hasRole(role)
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否有任一权限
|
||||
* @param {Array} permissions 权限数组
|
||||
* @returns {boolean} 是否有任一权限
|
||||
*/
|
||||
export const hasAnyPermission = (permissions) => {
|
||||
if (!Array.isArray(permissions) || permissions.length === 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
const userStore = useUserStore()
|
||||
return permissions.some(permission => userStore.hasPermission(permission))
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否有所有权限
|
||||
* @param {Array} permissions 权限数组
|
||||
* @returns {boolean} 是否有所有权限
|
||||
*/
|
||||
export const hasAllPermissions = (permissions) => {
|
||||
if (!Array.isArray(permissions) || permissions.length === 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
const userStore = useUserStore()
|
||||
return permissions.every(permission => userStore.hasPermission(permission))
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否有任一角色
|
||||
* @param {Array} roles 角色数组
|
||||
* @returns {boolean} 是否有任一角色
|
||||
*/
|
||||
export const hasAnyRole = (roles) => {
|
||||
if (!Array.isArray(roles) || roles.length === 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
const userStore = useUserStore()
|
||||
return roles.some(role => userStore.hasRole(role))
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户信息
|
||||
* @returns {Object|null} 用户信息
|
||||
*/
|
||||
export const getCurrentUser = () => {
|
||||
const userStore = useUserStore()
|
||||
return userStore.userInfo
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户ID
|
||||
* @returns {string|null} 用户ID
|
||||
*/
|
||||
export const getCurrentUserId = () => {
|
||||
const userStore = useUserStore()
|
||||
return userStore.userId
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户角色
|
||||
* @returns {Array} 用户角色数组
|
||||
*/
|
||||
export const getCurrentUserRoles = () => {
|
||||
const userStore = useUserStore()
|
||||
return userStore.roles || []
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户权限
|
||||
* @returns {Array} 用户权限数组
|
||||
*/
|
||||
export const getCurrentUserPermissions = () => {
|
||||
const userStore = useUserStore()
|
||||
return userStore.permissions || []
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转到登录页
|
||||
*/
|
||||
export const redirectToLogin = () => {
|
||||
uni.reLaunch({
|
||||
url: '/pages/login/login'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限验证装饰器
|
||||
* @param {string|Array} requiredPermissions 必需的权限
|
||||
* @param {Function} callback 回调函数
|
||||
* @param {Function} onDenied 权限不足时的回调
|
||||
*/
|
||||
export const requirePermission = (requiredPermissions, callback, onDenied) => {
|
||||
// 检查登录状态
|
||||
if (!isLoggedIn()) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '请先登录',
|
||||
showCancel: false,
|
||||
success: () => {
|
||||
redirectToLogin()
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查权限
|
||||
let hasRequiredPermission = false
|
||||
|
||||
if (typeof requiredPermissions === 'string') {
|
||||
hasRequiredPermission = hasPermission(requiredPermissions)
|
||||
} else if (Array.isArray(requiredPermissions)) {
|
||||
hasRequiredPermission = hasAnyPermission(requiredPermissions)
|
||||
} else {
|
||||
hasRequiredPermission = true
|
||||
}
|
||||
|
||||
if (hasRequiredPermission) {
|
||||
if (typeof callback === 'function') {
|
||||
callback()
|
||||
}
|
||||
} else {
|
||||
if (typeof onDenied === 'function') {
|
||||
onDenied()
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '权限不足',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 角色验证装饰器
|
||||
* @param {string|Array} requiredRoles 必需的角色
|
||||
* @param {Function} callback 回调函数
|
||||
* @param {Function} onDenied 角色不足时的回调
|
||||
*/
|
||||
export const requireRole = (requiredRoles, callback, onDenied) => {
|
||||
// 检查登录状态
|
||||
if (!isLoggedIn()) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '请先登录',
|
||||
showCancel: false,
|
||||
success: () => {
|
||||
redirectToLogin()
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查角色
|
||||
let hasRequiredRole = false
|
||||
|
||||
if (typeof requiredRoles === 'string') {
|
||||
hasRequiredRole = hasRole(requiredRoles)
|
||||
} else if (Array.isArray(requiredRoles)) {
|
||||
hasRequiredRole = hasAnyRole(requiredRoles)
|
||||
} else {
|
||||
hasRequiredRole = true
|
||||
}
|
||||
|
||||
if (hasRequiredRole) {
|
||||
if (typeof callback === 'function') {
|
||||
callback()
|
||||
}
|
||||
} else {
|
||||
if (typeof onDenied === 'function') {
|
||||
onDenied()
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '角色权限不足',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录检查装饰器
|
||||
* @param {Function} callback 回调函数
|
||||
*/
|
||||
export const requireLogin = (callback) => {
|
||||
if (isLoggedIn()) {
|
||||
if (typeof callback === 'function') {
|
||||
callback()
|
||||
}
|
||||
} else {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '请先登录',
|
||||
showCancel: false,
|
||||
success: () => {
|
||||
redirectToLogin()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面权限检查中间件
|
||||
* @param {Object} pageConfig 页面配置
|
||||
* @returns {boolean} 是否有权限访问
|
||||
*/
|
||||
export const checkPagePermission = (pageConfig = {}) => {
|
||||
const {
|
||||
requireAuth = true,
|
||||
permissions = [],
|
||||
roles = [],
|
||||
onDenied
|
||||
} = pageConfig
|
||||
|
||||
// 如果不需要认证,直接通过
|
||||
if (!requireAuth) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查登录状态
|
||||
if (!isLoggedIn()) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '请先登录',
|
||||
showCancel: false,
|
||||
success: () => {
|
||||
redirectToLogin()
|
||||
}
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查权限
|
||||
if (permissions.length > 0 && !hasAnyPermission(permissions)) {
|
||||
if (typeof onDenied === 'function') {
|
||||
onDenied('permission')
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '权限不足',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
})
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 2000)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查角色
|
||||
if (roles.length > 0 && !hasAnyRole(roles)) {
|
||||
if (typeof onDenied === 'function') {
|
||||
onDenied('role')
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '角色权限不足',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
})
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 2000)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限常量定义
|
||||
*/
|
||||
export const PERMISSIONS = {
|
||||
// 客户管理权限
|
||||
CUSTOMER_VIEW: 'customer:view',
|
||||
CUSTOMER_CREATE: 'customer:create',
|
||||
CUSTOMER_EDIT: 'customer:edit',
|
||||
CUSTOMER_DELETE: 'customer:delete',
|
||||
|
||||
// 资产管理权限
|
||||
ASSET_VIEW: 'asset:view',
|
||||
ASSET_CREATE: 'asset:create',
|
||||
ASSET_EDIT: 'asset:edit',
|
||||
ASSET_DELETE: 'asset:delete',
|
||||
ASSET_APPROVE: 'asset:approve',
|
||||
|
||||
// 交易管理权限
|
||||
TRANSACTION_VIEW: 'transaction:view',
|
||||
TRANSACTION_CREATE: 'transaction:create',
|
||||
TRANSACTION_APPROVE: 'transaction:approve',
|
||||
TRANSACTION_REJECT: 'transaction:reject',
|
||||
|
||||
// 风险管理权限
|
||||
RISK_VIEW: 'risk:view',
|
||||
RISK_ASSESS: 'risk:assess',
|
||||
RISK_MANAGE: 'risk:manage',
|
||||
|
||||
// 报表权限
|
||||
REPORT_VIEW: 'report:view',
|
||||
REPORT_EXPORT: 'report:export',
|
||||
|
||||
// 系统管理权限
|
||||
SYSTEM_CONFIG: 'system:config',
|
||||
USER_MANAGE: 'user:manage',
|
||||
ROLE_MANAGE: 'role:manage',
|
||||
PERMISSION_MANAGE: 'permission:manage'
|
||||
}
|
||||
|
||||
/**
|
||||
* 角色常量定义
|
||||
*/
|
||||
export const ROLES = {
|
||||
SUPER_ADMIN: 'super_admin', // 超级管理员
|
||||
ADMIN: 'admin', // 管理员
|
||||
SUPERVISOR: 'supervisor', // 主管
|
||||
OFFICER: 'officer', // 业务员
|
||||
AUDITOR: 'auditor', // 审计员
|
||||
VIEWER: 'viewer' // 查看者
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认导出
|
||||
*/
|
||||
export default {
|
||||
isLoggedIn,
|
||||
hasPermission,
|
||||
hasRole,
|
||||
hasAnyPermission,
|
||||
hasAllPermissions,
|
||||
hasAnyRole,
|
||||
getCurrentUser,
|
||||
getCurrentUserId,
|
||||
getCurrentUserRoles,
|
||||
getCurrentUserPermissions,
|
||||
redirectToLogin,
|
||||
requirePermission,
|
||||
requireRole,
|
||||
requireLogin,
|
||||
checkPagePermission,
|
||||
PERMISSIONS,
|
||||
ROLES
|
||||
}
|
||||
@@ -1,485 +0,0 @@
|
||||
// 银行端小程序权限管理工具
|
||||
|
||||
import { PERMISSIONS, ROLES } from './auth'
|
||||
|
||||
/**
|
||||
* 权限配置映射
|
||||
*/
|
||||
export const PERMISSION_CONFIG = {
|
||||
// 页面权限配置
|
||||
pages: {
|
||||
'/pages/index/index': {
|
||||
requireAuth: false,
|
||||
permissions: [],
|
||||
roles: []
|
||||
},
|
||||
'/pages/login/login': {
|
||||
requireAuth: false,
|
||||
permissions: [],
|
||||
roles: []
|
||||
},
|
||||
'/pages/dashboard/dashboard': {
|
||||
requireAuth: true,
|
||||
permissions: [],
|
||||
roles: [ROLES.OFFICER, ROLES.SUPERVISOR, ROLES.ADMIN, ROLES.SUPER_ADMIN]
|
||||
},
|
||||
'/pages/customers/customers': {
|
||||
requireAuth: true,
|
||||
permissions: [PERMISSIONS.CUSTOMER_VIEW],
|
||||
roles: [ROLES.OFFICER, ROLES.SUPERVISOR, ROLES.ADMIN, ROLES.SUPER_ADMIN]
|
||||
},
|
||||
'/pages/customers/detail': {
|
||||
requireAuth: true,
|
||||
permissions: [PERMISSIONS.CUSTOMER_VIEW],
|
||||
roles: [ROLES.OFFICER, ROLES.SUPERVISOR, ROLES.ADMIN, ROLES.SUPER_ADMIN]
|
||||
},
|
||||
'/pages/assets/assets': {
|
||||
requireAuth: true,
|
||||
permissions: [PERMISSIONS.ASSET_VIEW],
|
||||
roles: [ROLES.OFFICER, ROLES.SUPERVISOR, ROLES.ADMIN, ROLES.SUPER_ADMIN]
|
||||
},
|
||||
'/pages/assets/detail': {
|
||||
requireAuth: true,
|
||||
permissions: [PERMISSIONS.ASSET_VIEW],
|
||||
roles: [ROLES.OFFICER, ROLES.SUPERVISOR, ROLES.ADMIN, ROLES.SUPER_ADMIN]
|
||||
},
|
||||
'/pages/transactions/transactions': {
|
||||
requireAuth: true,
|
||||
permissions: [PERMISSIONS.TRANSACTION_VIEW],
|
||||
roles: [ROLES.OFFICER, ROLES.SUPERVISOR, ROLES.ADMIN, ROLES.SUPER_ADMIN]
|
||||
},
|
||||
'/pages/transactions/detail': {
|
||||
requireAuth: true,
|
||||
permissions: [PERMISSIONS.TRANSACTION_VIEW],
|
||||
roles: [ROLES.OFFICER, ROLES.SUPERVISOR, ROLES.ADMIN, ROLES.SUPER_ADMIN]
|
||||
},
|
||||
'/pages/risk/risk': {
|
||||
requireAuth: true,
|
||||
permissions: [PERMISSIONS.RISK_VIEW],
|
||||
roles: [ROLES.SUPERVISOR, ROLES.ADMIN, ROLES.SUPER_ADMIN, ROLES.AUDITOR]
|
||||
},
|
||||
'/pages/profile/profile': {
|
||||
requireAuth: true,
|
||||
permissions: [],
|
||||
roles: []
|
||||
}
|
||||
},
|
||||
|
||||
// 功能权限配置
|
||||
features: {
|
||||
// 客户管理功能
|
||||
'customer-create': {
|
||||
permissions: [PERMISSIONS.CUSTOMER_CREATE],
|
||||
roles: [ROLES.OFFICER, ROLES.SUPERVISOR, ROLES.ADMIN, ROLES.SUPER_ADMIN]
|
||||
},
|
||||
'customer-edit': {
|
||||
permissions: [PERMISSIONS.CUSTOMER_EDIT],
|
||||
roles: [ROLES.OFFICER, ROLES.SUPERVISOR, ROLES.ADMIN, ROLES.SUPER_ADMIN]
|
||||
},
|
||||
'customer-delete': {
|
||||
permissions: [PERMISSIONS.CUSTOMER_DELETE],
|
||||
roles: [ROLES.SUPERVISOR, ROLES.ADMIN, ROLES.SUPER_ADMIN]
|
||||
},
|
||||
|
||||
// 资产管理功能
|
||||
'asset-create': {
|
||||
permissions: [PERMISSIONS.ASSET_CREATE],
|
||||
roles: [ROLES.OFFICER, ROLES.SUPERVISOR, ROLES.ADMIN, ROLES.SUPER_ADMIN]
|
||||
},
|
||||
'asset-edit': {
|
||||
permissions: [PERMISSIONS.ASSET_EDIT],
|
||||
roles: [ROLES.OFFICER, ROLES.SUPERVISOR, ROLES.ADMIN, ROLES.SUPER_ADMIN]
|
||||
},
|
||||
'asset-delete': {
|
||||
permissions: [PERMISSIONS.ASSET_DELETE],
|
||||
roles: [ROLES.SUPERVISOR, ROLES.ADMIN, ROLES.SUPER_ADMIN]
|
||||
},
|
||||
'asset-approve': {
|
||||
permissions: [PERMISSIONS.ASSET_APPROVE],
|
||||
roles: [ROLES.SUPERVISOR, ROLES.ADMIN, ROLES.SUPER_ADMIN]
|
||||
},
|
||||
|
||||
// 交易管理功能
|
||||
'transaction-create': {
|
||||
permissions: [PERMISSIONS.TRANSACTION_CREATE],
|
||||
roles: [ROLES.OFFICER, ROLES.SUPERVISOR, ROLES.ADMIN, ROLES.SUPER_ADMIN]
|
||||
},
|
||||
'transaction-approve': {
|
||||
permissions: [PERMISSIONS.TRANSACTION_APPROVE],
|
||||
roles: [ROLES.SUPERVISOR, ROLES.ADMIN, ROLES.SUPER_ADMIN]
|
||||
},
|
||||
'transaction-reject': {
|
||||
permissions: [PERMISSIONS.TRANSACTION_REJECT],
|
||||
roles: [ROLES.SUPERVISOR, ROLES.ADMIN, ROLES.SUPER_ADMIN]
|
||||
},
|
||||
|
||||
// 风险管理功能
|
||||
'risk-assess': {
|
||||
permissions: [PERMISSIONS.RISK_ASSESS],
|
||||
roles: [ROLES.SUPERVISOR, ROLES.ADMIN, ROLES.SUPER_ADMIN, ROLES.AUDITOR]
|
||||
},
|
||||
'risk-manage': {
|
||||
permissions: [PERMISSIONS.RISK_MANAGE],
|
||||
roles: [ROLES.ADMIN, ROLES.SUPER_ADMIN]
|
||||
},
|
||||
|
||||
// 报表功能
|
||||
'report-view': {
|
||||
permissions: [PERMISSIONS.REPORT_VIEW],
|
||||
roles: [ROLES.SUPERVISOR, ROLES.ADMIN, ROLES.SUPER_ADMIN, ROLES.AUDITOR]
|
||||
},
|
||||
'report-export': {
|
||||
permissions: [PERMISSIONS.REPORT_EXPORT],
|
||||
roles: [ROLES.ADMIN, ROLES.SUPER_ADMIN]
|
||||
},
|
||||
|
||||
// 系统管理功能
|
||||
'system-config': {
|
||||
permissions: [PERMISSIONS.SYSTEM_CONFIG],
|
||||
roles: [ROLES.ADMIN, ROLES.SUPER_ADMIN]
|
||||
},
|
||||
'user-manage': {
|
||||
permissions: [PERMISSIONS.USER_MANAGE],
|
||||
roles: [ROLES.ADMIN, ROLES.SUPER_ADMIN]
|
||||
},
|
||||
'role-manage': {
|
||||
permissions: [PERMISSIONS.ROLE_MANAGE],
|
||||
roles: [ROLES.SUPER_ADMIN]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 角色权限映射
|
||||
*/
|
||||
export const ROLE_PERMISSIONS = {
|
||||
[ROLES.SUPER_ADMIN]: [
|
||||
// 拥有所有权限
|
||||
...Object.values(PERMISSIONS)
|
||||
],
|
||||
|
||||
[ROLES.ADMIN]: [
|
||||
// 客户管理
|
||||
PERMISSIONS.CUSTOMER_VIEW,
|
||||
PERMISSIONS.CUSTOMER_CREATE,
|
||||
PERMISSIONS.CUSTOMER_EDIT,
|
||||
PERMISSIONS.CUSTOMER_DELETE,
|
||||
|
||||
// 资产管理
|
||||
PERMISSIONS.ASSET_VIEW,
|
||||
PERMISSIONS.ASSET_CREATE,
|
||||
PERMISSIONS.ASSET_EDIT,
|
||||
PERMISSIONS.ASSET_DELETE,
|
||||
PERMISSIONS.ASSET_APPROVE,
|
||||
|
||||
// 交易管理
|
||||
PERMISSIONS.TRANSACTION_VIEW,
|
||||
PERMISSIONS.TRANSACTION_CREATE,
|
||||
PERMISSIONS.TRANSACTION_APPROVE,
|
||||
PERMISSIONS.TRANSACTION_REJECT,
|
||||
|
||||
// 风险管理
|
||||
PERMISSIONS.RISK_VIEW,
|
||||
PERMISSIONS.RISK_ASSESS,
|
||||
PERMISSIONS.RISK_MANAGE,
|
||||
|
||||
// 报表
|
||||
PERMISSIONS.REPORT_VIEW,
|
||||
PERMISSIONS.REPORT_EXPORT,
|
||||
|
||||
// 系统管理
|
||||
PERMISSIONS.SYSTEM_CONFIG,
|
||||
PERMISSIONS.USER_MANAGE
|
||||
],
|
||||
|
||||
[ROLES.SUPERVISOR]: [
|
||||
// 客户管理
|
||||
PERMISSIONS.CUSTOMER_VIEW,
|
||||
PERMISSIONS.CUSTOMER_CREATE,
|
||||
PERMISSIONS.CUSTOMER_EDIT,
|
||||
PERMISSIONS.CUSTOMER_DELETE,
|
||||
|
||||
// 资产管理
|
||||
PERMISSIONS.ASSET_VIEW,
|
||||
PERMISSIONS.ASSET_CREATE,
|
||||
PERMISSIONS.ASSET_EDIT,
|
||||
PERMISSIONS.ASSET_DELETE,
|
||||
PERMISSIONS.ASSET_APPROVE,
|
||||
|
||||
// 交易管理
|
||||
PERMISSIONS.TRANSACTION_VIEW,
|
||||
PERMISSIONS.TRANSACTION_CREATE,
|
||||
PERMISSIONS.TRANSACTION_APPROVE,
|
||||
PERMISSIONS.TRANSACTION_REJECT,
|
||||
|
||||
// 风险管理
|
||||
PERMISSIONS.RISK_VIEW,
|
||||
PERMISSIONS.RISK_ASSESS,
|
||||
|
||||
// 报表
|
||||
PERMISSIONS.REPORT_VIEW
|
||||
],
|
||||
|
||||
[ROLES.OFFICER]: [
|
||||
// 客户管理
|
||||
PERMISSIONS.CUSTOMER_VIEW,
|
||||
PERMISSIONS.CUSTOMER_CREATE,
|
||||
PERMISSIONS.CUSTOMER_EDIT,
|
||||
|
||||
// 资产管理
|
||||
PERMISSIONS.ASSET_VIEW,
|
||||
PERMISSIONS.ASSET_CREATE,
|
||||
PERMISSIONS.ASSET_EDIT,
|
||||
|
||||
// 交易管理
|
||||
PERMISSIONS.TRANSACTION_VIEW,
|
||||
PERMISSIONS.TRANSACTION_CREATE,
|
||||
|
||||
// 风险管理
|
||||
PERMISSIONS.RISK_VIEW
|
||||
],
|
||||
|
||||
[ROLES.AUDITOR]: [
|
||||
// 客户管理
|
||||
PERMISSIONS.CUSTOMER_VIEW,
|
||||
|
||||
// 资产管理
|
||||
PERMISSIONS.ASSET_VIEW,
|
||||
|
||||
// 交易管理
|
||||
PERMISSIONS.TRANSACTION_VIEW,
|
||||
|
||||
// 风险管理
|
||||
PERMISSIONS.RISK_VIEW,
|
||||
PERMISSIONS.RISK_ASSESS,
|
||||
|
||||
// 报表
|
||||
PERMISSIONS.REPORT_VIEW
|
||||
],
|
||||
|
||||
[ROLES.VIEWER]: [
|
||||
// 客户管理
|
||||
PERMISSIONS.CUSTOMER_VIEW,
|
||||
|
||||
// 资产管理
|
||||
PERMISSIONS.ASSET_VIEW,
|
||||
|
||||
// 交易管理
|
||||
PERMISSIONS.TRANSACTION_VIEW,
|
||||
|
||||
// 风险管理
|
||||
PERMISSIONS.RISK_VIEW
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取页面权限配置
|
||||
* @param {string} pagePath 页面路径
|
||||
* @returns {Object} 权限配置
|
||||
*/
|
||||
export const getPagePermissionConfig = (pagePath) => {
|
||||
return PERMISSION_CONFIG.pages[pagePath] || {
|
||||
requireAuth: true,
|
||||
permissions: [],
|
||||
roles: []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取功能权限配置
|
||||
* @param {string} featureKey 功能键
|
||||
* @returns {Object} 权限配置
|
||||
*/
|
||||
export const getFeaturePermissionConfig = (featureKey) => {
|
||||
return PERMISSION_CONFIG.features[featureKey] || {
|
||||
permissions: [],
|
||||
roles: []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据角色获取权限列表
|
||||
* @param {string} role 角色
|
||||
* @returns {Array} 权限列表
|
||||
*/
|
||||
export const getPermissionsByRole = (role) => {
|
||||
return ROLE_PERMISSIONS[role] || []
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据角色列表获取所有权限
|
||||
* @param {Array} roles 角色列表
|
||||
* @returns {Array} 权限列表
|
||||
*/
|
||||
export const getPermissionsByRoles = (roles) => {
|
||||
if (!Array.isArray(roles)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const permissions = new Set()
|
||||
|
||||
roles.forEach(role => {
|
||||
const rolePermissions = getPermissionsByRole(role)
|
||||
rolePermissions.forEach(permission => {
|
||||
permissions.add(permission)
|
||||
})
|
||||
})
|
||||
|
||||
return Array.from(permissions)
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查角色是否有特定权限
|
||||
* @param {string} role 角色
|
||||
* @param {string} permission 权限
|
||||
* @returns {boolean} 是否有权限
|
||||
*/
|
||||
export const roleHasPermission = (role, permission) => {
|
||||
const permissions = getPermissionsByRole(role)
|
||||
return permissions.includes(permission)
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限级别定义
|
||||
*/
|
||||
export const PERMISSION_LEVELS = {
|
||||
READ: 1, // 只读
|
||||
WRITE: 2, // 读写
|
||||
DELETE: 3, // 删除
|
||||
APPROVE: 4, // 审批
|
||||
ADMIN: 5 // 管理
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限分组
|
||||
*/
|
||||
export const PERMISSION_GROUPS = {
|
||||
CUSTOMER: {
|
||||
name: '客户管理',
|
||||
permissions: [
|
||||
PERMISSIONS.CUSTOMER_VIEW,
|
||||
PERMISSIONS.CUSTOMER_CREATE,
|
||||
PERMISSIONS.CUSTOMER_EDIT,
|
||||
PERMISSIONS.CUSTOMER_DELETE
|
||||
]
|
||||
},
|
||||
|
||||
ASSET: {
|
||||
name: '资产管理',
|
||||
permissions: [
|
||||
PERMISSIONS.ASSET_VIEW,
|
||||
PERMISSIONS.ASSET_CREATE,
|
||||
PERMISSIONS.ASSET_EDIT,
|
||||
PERMISSIONS.ASSET_DELETE,
|
||||
PERMISSIONS.ASSET_APPROVE
|
||||
]
|
||||
},
|
||||
|
||||
TRANSACTION: {
|
||||
name: '交易管理',
|
||||
permissions: [
|
||||
PERMISSIONS.TRANSACTION_VIEW,
|
||||
PERMISSIONS.TRANSACTION_CREATE,
|
||||
PERMISSIONS.TRANSACTION_APPROVE,
|
||||
PERMISSIONS.TRANSACTION_REJECT
|
||||
]
|
||||
},
|
||||
|
||||
RISK: {
|
||||
name: '风险管理',
|
||||
permissions: [
|
||||
PERMISSIONS.RISK_VIEW,
|
||||
PERMISSIONS.RISK_ASSESS,
|
||||
PERMISSIONS.RISK_MANAGE
|
||||
]
|
||||
},
|
||||
|
||||
REPORT: {
|
||||
name: '报表管理',
|
||||
permissions: [
|
||||
PERMISSIONS.REPORT_VIEW,
|
||||
PERMISSIONS.REPORT_EXPORT
|
||||
]
|
||||
},
|
||||
|
||||
SYSTEM: {
|
||||
name: '系统管理',
|
||||
permissions: [
|
||||
PERMISSIONS.SYSTEM_CONFIG,
|
||||
PERMISSIONS.USER_MANAGE,
|
||||
PERMISSIONS.ROLE_MANAGE,
|
||||
PERMISSIONS.PERMISSION_MANAGE
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取权限显示名称
|
||||
* @param {string} permission 权限标识
|
||||
* @returns {string} 显示名称
|
||||
*/
|
||||
export const getPermissionDisplayName = (permission) => {
|
||||
const nameMap = {
|
||||
[PERMISSIONS.CUSTOMER_VIEW]: '查看客户',
|
||||
[PERMISSIONS.CUSTOMER_CREATE]: '创建客户',
|
||||
[PERMISSIONS.CUSTOMER_EDIT]: '编辑客户',
|
||||
[PERMISSIONS.CUSTOMER_DELETE]: '删除客户',
|
||||
|
||||
[PERMISSIONS.ASSET_VIEW]: '查看资产',
|
||||
[PERMISSIONS.ASSET_CREATE]: '创建资产',
|
||||
[PERMISSIONS.ASSET_EDIT]: '编辑资产',
|
||||
[PERMISSIONS.ASSET_DELETE]: '删除资产',
|
||||
[PERMISSIONS.ASSET_APPROVE]: '审批资产',
|
||||
|
||||
[PERMISSIONS.TRANSACTION_VIEW]: '查看交易',
|
||||
[PERMISSIONS.TRANSACTION_CREATE]: '创建交易',
|
||||
[PERMISSIONS.TRANSACTION_APPROVE]: '审批交易',
|
||||
[PERMISSIONS.TRANSACTION_REJECT]: '拒绝交易',
|
||||
|
||||
[PERMISSIONS.RISK_VIEW]: '查看风险',
|
||||
[PERMISSIONS.RISK_ASSESS]: '风险评估',
|
||||
[PERMISSIONS.RISK_MANAGE]: '风险管理',
|
||||
|
||||
[PERMISSIONS.REPORT_VIEW]: '查看报表',
|
||||
[PERMISSIONS.REPORT_EXPORT]: '导出报表',
|
||||
|
||||
[PERMISSIONS.SYSTEM_CONFIG]: '系统配置',
|
||||
[PERMISSIONS.USER_MANAGE]: '用户管理',
|
||||
[PERMISSIONS.ROLE_MANAGE]: '角色管理',
|
||||
[PERMISSIONS.PERMISSION_MANAGE]: '权限管理'
|
||||
}
|
||||
|
||||
return nameMap[permission] || permission
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取角色显示名称
|
||||
* @param {string} role 角色标识
|
||||
* @returns {string} 显示名称
|
||||
*/
|
||||
export const getRoleDisplayName = (role) => {
|
||||
const nameMap = {
|
||||
[ROLES.SUPER_ADMIN]: '超级管理员',
|
||||
[ROLES.ADMIN]: '管理员',
|
||||
[ROLES.SUPERVISOR]: '主管',
|
||||
[ROLES.OFFICER]: '业务员',
|
||||
[ROLES.AUDITOR]: '审计员',
|
||||
[ROLES.VIEWER]: '查看者'
|
||||
}
|
||||
|
||||
return nameMap[role] || role
|
||||
}
|
||||
|
||||
export default {
|
||||
PERMISSION_CONFIG,
|
||||
ROLE_PERMISSIONS,
|
||||
PERMISSION_LEVELS,
|
||||
PERMISSION_GROUPS,
|
||||
getPagePermissionConfig,
|
||||
getFeaturePermissionConfig,
|
||||
getPermissionsByRole,
|
||||
getPermissionsByRoles,
|
||||
roleHasPermission,
|
||||
getPermissionDisplayName,
|
||||
getRoleDisplayName
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import axios from 'axios'
|
||||
import apiConfig from '../config/api'
|
||||
import { getToken } from './auth'
|
||||
|
||||
// 创建axios实例
|
||||
const service = axios.create({
|
||||
baseURL: apiConfig.BASE_URL,
|
||||
timeout: apiConfig.TIMEOUT
|
||||
})
|
||||
|
||||
// 请求拦截器
|
||||
service.interceptors.request.use(
|
||||
config => {
|
||||
// 添加token
|
||||
const token = getToken()
|
||||
if (token) {
|
||||
config.headers['Authorization'] = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
},
|
||||
error => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
service.interceptors.response.use(
|
||||
response => {
|
||||
return response.data
|
||||
},
|
||||
error => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export default service
|
||||
@@ -1,148 +0,0 @@
|
||||
import { config } from '@vue/test-utils'
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
// 全局测试配置
|
||||
config.global.plugins = [createPinia()]
|
||||
|
||||
// Mock uni-app API
|
||||
const mockUni = {
|
||||
// 导航相关
|
||||
navigateTo: jest.fn(),
|
||||
navigateBack: jest.fn(),
|
||||
redirectTo: jest.fn(),
|
||||
switchTab: jest.fn(),
|
||||
reLaunch: jest.fn(),
|
||||
|
||||
// 界面相关
|
||||
showToast: jest.fn(),
|
||||
showModal: jest.fn(),
|
||||
showLoading: jest.fn(),
|
||||
hideLoading: jest.fn(),
|
||||
showActionSheet: jest.fn(),
|
||||
|
||||
// 网络请求
|
||||
request: jest.fn(),
|
||||
uploadFile: jest.fn(),
|
||||
downloadFile: jest.fn(),
|
||||
|
||||
// 数据存储
|
||||
setStorage: jest.fn(),
|
||||
getStorage: jest.fn(),
|
||||
removeStorage: jest.fn(),
|
||||
clearStorage: jest.fn(),
|
||||
setStorageSync: jest.fn(),
|
||||
getStorageSync: jest.fn(),
|
||||
removeStorageSync: jest.fn(),
|
||||
clearStorageSync: jest.fn(),
|
||||
|
||||
// 设备信息
|
||||
getSystemInfo: jest.fn(),
|
||||
getSystemInfoSync: jest.fn(),
|
||||
|
||||
// 位置信息
|
||||
getLocation: jest.fn(),
|
||||
chooseLocation: jest.fn(),
|
||||
openLocation: jest.fn(),
|
||||
|
||||
// 媒体
|
||||
chooseImage: jest.fn(),
|
||||
previewImage: jest.fn(),
|
||||
chooseVideo: jest.fn(),
|
||||
|
||||
// 文件
|
||||
saveFile: jest.fn(),
|
||||
getSavedFileList: jest.fn(),
|
||||
getSavedFileInfo: jest.fn(),
|
||||
removeSavedFile: jest.fn(),
|
||||
openDocument: jest.fn(),
|
||||
|
||||
// 其他
|
||||
scanCode: jest.fn(),
|
||||
setClipboardData: jest.fn(),
|
||||
getClipboardData: jest.fn(),
|
||||
makePhoneCall: jest.fn(),
|
||||
|
||||
// 页面相关
|
||||
onLoad: jest.fn(),
|
||||
onShow: jest.fn(),
|
||||
onHide: jest.fn(),
|
||||
onUnload: jest.fn(),
|
||||
onPullDownRefresh: jest.fn(),
|
||||
onReachBottom: jest.fn(),
|
||||
|
||||
// 分享
|
||||
onShareAppMessage: jest.fn(),
|
||||
onShareTimeline: jest.fn(),
|
||||
|
||||
// 支付
|
||||
requestPayment: jest.fn(),
|
||||
|
||||
// 登录
|
||||
login: jest.fn(),
|
||||
checkSession: jest.fn(),
|
||||
getUserInfo: jest.fn(),
|
||||
getUserProfile: jest.fn(),
|
||||
|
||||
// 授权
|
||||
authorize: jest.fn(),
|
||||
getSetting: jest.fn(),
|
||||
openSetting: jest.fn()
|
||||
}
|
||||
|
||||
// 设置全局 uni 对象
|
||||
;(global as any).uni = mockUni
|
||||
;(global as any).wx = mockUni
|
||||
;(global as any).getCurrentPages = jest.fn(() => [])
|
||||
;(global as any).getApp = jest.fn(() => ({}))
|
||||
|
||||
// Mock console 方法(可选)
|
||||
global.console = {
|
||||
...console,
|
||||
// 在测试中静默某些日志
|
||||
log: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn()
|
||||
}
|
||||
|
||||
// 设置默认的 mock 返回值
|
||||
mockUni.getSystemInfoSync.mockReturnValue({
|
||||
platform: 'devtools',
|
||||
system: 'iOS 14.0',
|
||||
version: '8.0.5',
|
||||
SDKVersion: '2.19.4',
|
||||
brand: 'iPhone',
|
||||
model: 'iPhone 12',
|
||||
pixelRatio: 3,
|
||||
screenWidth: 375,
|
||||
screenHeight: 812,
|
||||
windowWidth: 375,
|
||||
windowHeight: 812,
|
||||
statusBarHeight: 44,
|
||||
safeArea: {
|
||||
left: 0,
|
||||
right: 375,
|
||||
top: 44,
|
||||
bottom: 778,
|
||||
width: 375,
|
||||
height: 734
|
||||
}
|
||||
})
|
||||
|
||||
mockUni.request.mockImplementation(({ success }) => {
|
||||
if (success) {
|
||||
success({
|
||||
statusCode: 200,
|
||||
data: { code: 0, message: 'success', data: {} }
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
mockUni.showToast.mockImplementation(() => Promise.resolve())
|
||||
mockUni.showModal.mockImplementation(() => Promise.resolve({ confirm: true }))
|
||||
mockUni.showLoading.mockImplementation(() => Promise.resolve())
|
||||
mockUni.hideLoading.mockImplementation(() => Promise.resolve())
|
||||
|
||||
// 导出 mock 对象供测试使用
|
||||
export { mockUni }
|
||||
@@ -1,241 +0,0 @@
|
||||
import { mount, VueWrapper } from '@vue/test-utils'
|
||||
import { createPinia } from 'pinia'
|
||||
import type { ComponentMountingOptions } from '@vue/test-utils'
|
||||
|
||||
/**
|
||||
* 创建测试用的 Pinia 实例
|
||||
*/
|
||||
export function createTestPinia() {
|
||||
return createPinia()
|
||||
}
|
||||
|
||||
/**
|
||||
* 挂载组件的辅助函数
|
||||
*/
|
||||
export function mountComponent<T>(
|
||||
component: T,
|
||||
options: ComponentMountingOptions<T> = {}
|
||||
): VueWrapper<any> {
|
||||
const pinia = createTestPinia()
|
||||
|
||||
return mount(component, {
|
||||
global: {
|
||||
plugins: [pinia],
|
||||
...options.global
|
||||
},
|
||||
...options
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待 Vue 的下一个 tick
|
||||
*/
|
||||
export async function nextTick(): Promise<void> {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(resolve, 0)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待指定时间
|
||||
*/
|
||||
export function sleep(ms: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟 uni-app 的页面跳转
|
||||
*/
|
||||
export function mockNavigation() {
|
||||
const navigateTo = jest.fn()
|
||||
const navigateBack = jest.fn()
|
||||
const redirectTo = jest.fn()
|
||||
const switchTab = jest.fn()
|
||||
const reLaunch = jest.fn()
|
||||
|
||||
;(global as any).uni = {
|
||||
...(global as any).uni,
|
||||
navigateTo,
|
||||
navigateBack,
|
||||
redirectTo,
|
||||
switchTab,
|
||||
reLaunch
|
||||
}
|
||||
|
||||
return {
|
||||
navigateTo,
|
||||
navigateBack,
|
||||
redirectTo,
|
||||
switchTab,
|
||||
reLaunch
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟 uni-app 的网络请求
|
||||
*/
|
||||
export function mockRequest() {
|
||||
const request = jest.fn()
|
||||
|
||||
;(global as any).uni = {
|
||||
...(global as any).uni,
|
||||
request
|
||||
}
|
||||
|
||||
return { request }
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟 uni-app 的存储
|
||||
*/
|
||||
export function mockStorage() {
|
||||
const storage: Record<string, any> = {}
|
||||
|
||||
const setStorageSync = jest.fn((key: string, value: any) => {
|
||||
storage[key] = value
|
||||
})
|
||||
|
||||
const getStorageSync = jest.fn((key: string) => {
|
||||
return storage[key]
|
||||
})
|
||||
|
||||
const removeStorageSync = jest.fn((key: string) => {
|
||||
delete storage[key]
|
||||
})
|
||||
|
||||
const clearStorageSync = jest.fn(() => {
|
||||
Object.keys(storage).forEach(key => {
|
||||
delete storage[key]
|
||||
})
|
||||
})
|
||||
|
||||
;(global as any).uni = {
|
||||
...(global as any).uni,
|
||||
setStorageSync,
|
||||
getStorageSync,
|
||||
removeStorageSync,
|
||||
clearStorageSync
|
||||
}
|
||||
|
||||
return {
|
||||
setStorageSync,
|
||||
getStorageSync,
|
||||
removeStorageSync,
|
||||
clearStorageSync,
|
||||
storage
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟 uni-app 的界面反馈
|
||||
*/
|
||||
export function mockUI() {
|
||||
const showToast = jest.fn()
|
||||
const showModal = jest.fn()
|
||||
const showLoading = jest.fn()
|
||||
const hideLoading = jest.fn()
|
||||
const showActionSheet = jest.fn()
|
||||
|
||||
;(global as any).uni = {
|
||||
...(global as any).uni,
|
||||
showToast,
|
||||
showModal,
|
||||
showLoading,
|
||||
hideLoading,
|
||||
showActionSheet
|
||||
}
|
||||
|
||||
return {
|
||||
showToast,
|
||||
showModal,
|
||||
showLoading,
|
||||
hideLoading,
|
||||
showActionSheet
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建模拟的响应数据
|
||||
*/
|
||||
export function createMockResponse<T>(data: T, code = 0, message = 'success') {
|
||||
return {
|
||||
statusCode: 200,
|
||||
data: {
|
||||
code,
|
||||
message,
|
||||
data
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建模拟的错误响应
|
||||
*/
|
||||
export function createMockErrorResponse(code = -1, message = 'error') {
|
||||
return {
|
||||
statusCode: 500,
|
||||
data: {
|
||||
code,
|
||||
message,
|
||||
data: null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发组件事件的辅助函数
|
||||
*/
|
||||
export async function triggerEvent(
|
||||
wrapper: VueWrapper<any>,
|
||||
selector: string,
|
||||
event: string,
|
||||
payload?: any
|
||||
) {
|
||||
const element = wrapper.find(selector)
|
||||
await element.trigger(event, payload)
|
||||
await nextTick()
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待组件更新完成
|
||||
*/
|
||||
export async function waitForUpdate(wrapper: VueWrapper<any>) {
|
||||
await wrapper.vm.$nextTick()
|
||||
await nextTick()
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查元素是否存在
|
||||
*/
|
||||
export function expectElementExists(wrapper: VueWrapper<any>, selector: string) {
|
||||
expect(wrapper.find(selector).exists()).toBe(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查元素是否不存在
|
||||
*/
|
||||
export function expectElementNotExists(wrapper: VueWrapper<any>, selector: string) {
|
||||
expect(wrapper.find(selector).exists()).toBe(false)
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查元素文本内容
|
||||
*/
|
||||
export function expectElementText(
|
||||
wrapper: VueWrapper<any>,
|
||||
selector: string,
|
||||
text: string
|
||||
) {
|
||||
expect(wrapper.find(selector).text()).toBe(text)
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查元素是否包含指定文本
|
||||
*/
|
||||
export function expectElementContainsText(
|
||||
wrapper: VueWrapper<any>,
|
||||
selector: string,
|
||||
text: string
|
||||
) {
|
||||
expect(wrapper.find(selector).text()).toContain(text)
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
@echo off
|
||||
echo 银行端微信小程序启动指南
|
||||
echo ================================
|
||||
echo.
|
||||
|
||||
echo 1. 确保已安装微信开发者工具
|
||||
echo 2. 确保后端服务已启动 (http://localhost:5352)
|
||||
echo 3. 在微信开发者工具中导入项目
|
||||
echo.
|
||||
|
||||
echo 项目配置信息:
|
||||
echo - 项目目录: %cd%
|
||||
echo - AppID: wx1b9c7cd2d0e0bfd3
|
||||
echo - 项目名称: 银行端小程序
|
||||
echo.
|
||||
|
||||
echo 默认登录账号:
|
||||
echo - 用户名: admin
|
||||
echo - 密码: 123456
|
||||
echo.
|
||||
|
||||
echo 功能模块:
|
||||
echo - 首页: 快速访问各功能,查看银行卡和交易
|
||||
echo - 数据看板: 银行统计数据和图表
|
||||
echo - 客户管理: 客户信息管理
|
||||
echo - 交易记录: 交易流水查询
|
||||
echo - 资产管理: 银行卡和资产管理
|
||||
echo - 风险控制: 风险等级评估
|
||||
echo - 报表分析: 财务报表生成
|
||||
echo - 个人中心: 用户信息和设置
|
||||
echo.
|
||||
|
||||
echo 银行特色功能:
|
||||
echo - 银行卡渐变设计
|
||||
echo - 交易状态标识
|
||||
echo - 风险等级评估
|
||||
echo - 数据可视化
|
||||
echo.
|
||||
|
||||
echo 详细使用说明请查看: 银行端微信小程序使用指南.md
|
||||
echo.
|
||||
|
||||
pause
|
||||
74
bank_mini_program/图标说明.md
Normal file
74
bank_mini_program/图标说明.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# 银行端小程序图标说明
|
||||
|
||||
## 需要添加的图标文件
|
||||
|
||||
根据新的底部导航栏配置,需要在 `images/` 目录下添加以下图标文件:
|
||||
|
||||
### 1. 日检预警图标
|
||||
- `warning.png` - 未选中状态的日检预警图标
|
||||
- `warning-active.png` - 选中状态的日检预警图标
|
||||
|
||||
**建议图标样式:**
|
||||
- 使用闪电图标 ⚡ 或警告图标 ⚠️
|
||||
- 未选中:灰色 (#7A7E83)
|
||||
- 选中:蓝色 (#1890ff)
|
||||
|
||||
### 2. 项目清单图标
|
||||
- `projects.png` - 未选中状态的项目清单图标
|
||||
- `projects-active.png` - 选中状态的项目清单图标
|
||||
|
||||
**建议图标样式:**
|
||||
- 使用列表图标 📋 或文件夹图标 📁
|
||||
- 未选中:灰色 (#7A7E83)
|
||||
- 选中:蓝色 (#1890ff)
|
||||
|
||||
### 3. 业务管理图标
|
||||
- `business.png` - 未选中状态的业务管理图标
|
||||
- `business-active.png` - 选中状态的业务管理图标
|
||||
|
||||
**建议图标样式:**
|
||||
- 使用钱袋图标 💰 或齿轮图标 ⚙️
|
||||
- 未选中:灰色 (#7A7E83)
|
||||
- 选中:蓝色 (#1890ff)
|
||||
|
||||
### 4. 我的图标(已存在)
|
||||
- `profile.png` - 未选中状态的我的图标
|
||||
- `profile-active.png` - 选中状态的我的图标
|
||||
|
||||
## 图标规格要求
|
||||
|
||||
- **尺寸:** 建议 40x40 像素
|
||||
- **格式:** PNG 格式,支持透明背景
|
||||
- **风格:** 简洁的线性图标,与现有图标风格保持一致
|
||||
- **颜色:**
|
||||
- 未选中状态:#7A7E83(灰色)
|
||||
- 选中状态:#1890ff(蓝色)
|
||||
|
||||
## 临时解决方案
|
||||
|
||||
如果暂时没有图标文件,可以:
|
||||
|
||||
1. 使用现有的图标文件作为临时替代
|
||||
2. 或者将图标路径设置为空字符串,使用纯文字显示
|
||||
|
||||
```json
|
||||
{
|
||||
"pagePath": "pages/warning/warning",
|
||||
"iconPath": "",
|
||||
"selectedIconPath": "",
|
||||
"text": "日检预警"
|
||||
}
|
||||
```
|
||||
|
||||
## 图标制作建议
|
||||
|
||||
可以使用以下工具制作图标:
|
||||
- Figma
|
||||
- Sketch
|
||||
- Adobe Illustrator
|
||||
- 在线图标生成器
|
||||
|
||||
或者从以下网站下载免费图标:
|
||||
- Iconfont
|
||||
- Feather Icons
|
||||
- Material Design Icons
|
||||
Reference in New Issue
Block a user