修改政府端前端,银行端小程序和后端接口

This commit is contained in:
2025-09-26 17:52:50 +08:00
parent 852adbcfff
commit 00dfa83fd1
237 changed files with 9172 additions and 33500 deletions

View File

@@ -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": "我的"
}
]

View File

@@ -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() {
}
})
})

View File

@@ -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>

View File

@@ -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;
}

View 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();
});
}
});

View File

@@ -0,0 +1,3 @@
{
"usingComponents": {}
}

View 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>

View 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;
}

View File

@@ -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();
});
}
});

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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();
});
}
});

View File

@@ -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>

View File

@@ -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;
}

View 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();
});
}
});

View File

@@ -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>

View File

@@ -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;
}

View 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();
});
}
});

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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}`
})
}
})
})

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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() {
}
})
})

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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() {
}
})
})

View File

@@ -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>

View File

@@ -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;
}

View 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('加载更多项目');
}
});

View File

@@ -0,0 +1,3 @@
{
"usingComponents": {}
}

View 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>

View 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;
}

View File

@@ -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() {
}
})
})

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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() {
}
})
})

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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() {
}
})
})

View File

@@ -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>

View File

@@ -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;
}

View 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'
});
}
});

View File

@@ -0,0 +1,3 @@
{
"usingComponents": {}
}

View 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>

View 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;
}

View 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
};

View File

@@ -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>

View File

@@ -1,14 +0,0 @@
{
"pages": [
"pages/index/index",
"pages/login/login"
],
"window": {
"navigationBarTitleText": "银行小程序",
"navigationBarBackgroundColor": "#1976D2",
"navigationBarTextStyle": "white"
},
"usingComponents": {
"van-button": "@vant/weapp/button/index"
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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')
})
})

View File

@@ -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'
}
}

View File

@@ -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
}
}

View File

@@ -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": {}
}

View File

@@ -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": []
}
}

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
}
}
}
})

View File

@@ -1,9 +0,0 @@
import { createPinia } from 'pinia'
const pinia = createPinia()
export default pinia
// 导出所有store
export { useUserStore } from './user'
export { useAppStore } from './app'

View File

@@ -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()
}
}
}
})

View File

@@ -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; }

View File

@@ -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;
}

View File

@@ -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); }

View File

@@ -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;

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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 }

View File

@@ -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)
}

View File

@@ -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

View 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