Compare commits

...

3 Commits

Author SHA1 Message Date
eb58634edf Merge branch 'master' of https://git.mmlizi.com/lizi/shop-toy
# Conflicts:
#	src/mock/goods.ts
2025-11-29 20:26:16 +08:00
152e11cc7c 修改信用额度 2025-11-29 20:22:24 +08:00
4c0221cb22 修改模拟数据 2025-11-29 20:22:12 +08:00
9 changed files with 322 additions and 48 deletions

View File

@@ -33,8 +33,13 @@ const emit = defineEmits<{
function handleClick(item: Category) { function handleClick(item: Category) {
emit('click', item) emit('click', item)
// 跳转到分类页面 // 跳转到分类页面
uni.navigateTo({ // 注意:因为分类页面是 tabBar 页面,需要使用 switchTab 而不是 navigateTo
url: `/pages/sort/index?categoryId=${item.id}`, uni.switchTab({
url: `/pages/sort/index`,
success: () => {
// 跳转成功后,通过全局事件通知分类页面选中指定分类
uni.$emit('selectCategory', item.id)
}
}) })
} }
</script> </script>

View File

@@ -1,26 +1,29 @@
<template> <template>
<view class="credit-card"> <view class="credit-card" @click="handleClick">
<view class="header"> <view class="header">
<text class="title">{{ title }}</text> <view class="title-wrapper">
<text class="date">更新于 {{ updateTime }}</text> <text class="title">{{ title }}</text>
<view v-if="isExhausted" class="status-tag exhausted">额度已用尽</view>
<view v-else-if="isHighUsage" class="status-tag warning">额度紧张</view>
</view>
</view> </view>
<view class="content"> <view class="content">
<view class="row"> <view class="row">
<view class="item"> <view class="item">
<text class="label">额度</text> <text class="label">可用额度</text>
<text class="value">¥{{ formatPrice(totalLimit) }}</text> <text class="value" :class="{ 'exhausted': isExhausted }">¥{{ formatPrice(availableLimit) }}</text>
</view> </view>
<view class="item"> <view class="item">
<text class="label">可用额度</text> <text class="label">额度</text>
<text class="value highlight">¥{{ formatPrice(availableLimit) }}</text> <text class="value">¥{{ formatPrice(totalLimit) }}</text>
</view> </view>
</view> </view>
<!-- 进度条 --> <!-- 进度条 -->
<view class="progress-wrapper"> <view class="progress-wrapper">
<view class="progress-bg"> <view class="progress-bg">
<view class="progress-bar" :style="{ width: percent + '%' }"></view> <view class="progress-bar" :style="{ width: percent + '%', background: progressColor }"></view>
</view> </view>
<view class="progress-text"> <view class="progress-text">
<text>已用 {{ percent }}%</text> <text>已用 {{ percent }}%</text>
@@ -38,18 +41,45 @@ interface Props {
usedLimit: number usedLimit: number
availableLimit: number availableLimit: number
updateTime: string updateTime: string
hideUpdateTime?: boolean
} }
const props = defineProps<Props>() const props = withDefaults(defineProps<Props>(), {
hideUpdateTime: false
})
const emit = defineEmits(['click'])
// 计算使用百分比
const percent = computed(() => { const percent = computed(() => {
if (props.totalLimit === 0) return 0 if (props.totalLimit === 0) return 0
return Math.min(100, Math.round((props.usedLimit / props.totalLimit) * 100)) return Math.min(100, Math.round((props.usedLimit / props.totalLimit) * 100))
}) })
// 判断是否额度已用尽
const isExhausted = computed(() => {
return props.availableLimit <= 0 || percent.value >= 100
})
// 判断是否高使用率
const isHighUsage = computed(() => {
return percent.value >= 80 && percent.value < 100
})
// 根据使用率返回进度条颜色
const progressColor = computed(() => {
if (isExhausted.value) return '#ff4d4f' // 红色
if (isHighUsage.value) return '#faad14' // 橙色
return '#52c41a' // 绿色
})
function formatPrice(price: number) { function formatPrice(price: number) {
return price.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) return price.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
} }
function handleClick() {
emit('click')
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -59,6 +89,40 @@ function formatPrice(price: number) {
padding: 30rpx; padding: 30rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.05); box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.05);
margin-bottom: 24rpx; margin-bottom: 24rpx;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
&:active {
transform: scale(0.98);
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
}
// 添加左侧彩色边框
&::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 6rpx;
background: #1890ff;
border-radius: 3rpx 0 0 3rpx;
}
// 额度已用尽的卡片
&.exhausted {
&::before {
background: #ff4d4f;
}
}
// 额度紧张的卡片
&.warning {
&::before {
background: #faad14;
}
}
} }
.header { .header {
@@ -67,29 +131,33 @@ function formatPrice(price: number) {
align-items: center; align-items: center;
margin-bottom: 30rpx; margin-bottom: 30rpx;
.title-wrapper {
display: flex;
align-items: center;
gap: 16rpx;
}
.title { .title {
font-size: 32rpx; font-size: 32rpx;
font-weight: 600; font-weight: 600;
color: #333; color: #333;
position: relative;
padding-left: 20rpx;
&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 8rpx;
height: 32rpx;
background: #ff4d4f;
border-radius: 4rpx;
}
} }
.date { .status-tag {
font-size: 24rpx; padding: 6rpx 12rpx;
color: #999; border-radius: 20rpx;
font-size: 20rpx;
font-weight: 500;
&.exhausted {
background: rgba(255, 77, 79, 0.1);
color: #ff4d4f;
}
&.warning {
background: rgba(250, 173, 20, 0.1);
color: #faad14;
}
} }
} }
@@ -114,8 +182,8 @@ function formatPrice(price: number) {
font-weight: 600; font-weight: 600;
color: #333; color: #333;
&.highlight { &.exhausted {
color: #52c41a; color: #ff4d4f;
} }
} }
} }
@@ -123,18 +191,17 @@ function formatPrice(price: number) {
.progress-wrapper { .progress-wrapper {
.progress-bg { .progress-bg {
height: 12rpx; height: 16rpx;
background: #f5f5f5; background: #f5f5f5;
border-radius: 6rpx; border-radius: 8rpx;
overflow: hidden; overflow: hidden;
margin-bottom: 12rpx; margin-bottom: 12rpx;
} }
.progress-bar { .progress-bar {
height: 100%; height: 100%;
background: linear-gradient(90deg, #ff9c6e 0%, #ff4d4f 100%); border-radius: 8rpx;
border-radius: 6rpx; transition: width 0.5s ease, background 0.3s ease;
transition: width 0.3s ease;
} }
.progress-text { .progress-text {

View File

@@ -347,4 +347,24 @@ export const mockGoodsList: Goods[] = [
categoryId: 'cat_005', categoryId: 'cat_005',
categoryName: '文创工艺品', categoryName: '文创工艺品',
}, },
{
id: 'goods_018',
shopId: 'merchant_007',
shopName: '四脚公园',
name: '洗面奶专用清爽控油祛痘洁面乳去油去黑头',
cover: '/static/product/18/1.jpg',
images: ['/static/product/18/1.jpg'],
detailImage: '/static/product/18/2.jpg',
price: 45,
originalPrice: 85,
stock: 200,
sales: 456,
description: '曼秀雷敦男士洗面奶专用清爽控油祛痘洁面乳去油去黑头旗舰店正品。采用天然植物成分,温和清洁肌肤,有效去除油脂,改善痘痘问题,让肌肤清爽透亮。',
specs: [
{ name: '瓶', values: ['小瓶', '大瓶'] },
],
tags: ['热销'],
categoryId: 'cat_008',
categoryName: '美妆',
},
] ]

View File

@@ -1,6 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useFinanceStore } from '@/store/finance' import { useFinanceStore } from '@/store/finance'
import CreditCard from '@/components/finance/CreditCard.vue' import CreditCard from '@/components/finance/CreditCard.vue'
import type { CreditLimit } from '@/typings/mall'
definePage({ definePage({
style: { style: {
@@ -12,6 +13,25 @@ definePage({
const financeStore = useFinanceStore() const financeStore = useFinanceStore()
const loading = ref(true) const loading = ref(true)
// 计算总额度
const totalCreditLimit = computed(() => {
return financeStore.creditLimits.reduce((sum, item) => sum + item.totalLimit, 0)
})
// 计算使用百分比
const usagePercent = computed(() => {
if (totalCreditLimit.value === 0) return 0
return Math.min(100, Math.round((financeStore.totalUsedLimit / totalCreditLimit.value) * 100))
})
// 根据使用率返回进度条颜色
const progressColor = computed(() => {
const percent = usagePercent.value
if (percent < 50) return '#52c41a' // 绿色
if (percent < 80) return '#faad14' // 橙色
return '#ff4d4f' // 红色
})
onShow(() => { onShow(() => {
loadData() loadData()
}) })
@@ -26,6 +46,43 @@ async function loadData() {
onPullDownRefresh(() => { onPullDownRefresh(() => {
loadData() loadData()
}) })
// 快捷功能处理函数
function handleIncreaseLimit() {
uni.showToast({
title: '跳转到提升额度页面',
icon: 'none'
})
// 实际项目中跳转到额度申请页面
// uni.navigateTo({ url: '/pages/finance/increase-limit' })
}
function handleViewBills() {
uni.showToast({
title: '跳转到账单明细页面',
icon: 'none'
})
// 实际项目中跳转到账单明细页面
// uni.navigateTo({ url: '/pages/finance/bill-detail' })
}
function handleManageLimit() {
uni.showToast({
title: '跳转到额度管理页面',
icon: 'none'
})
// 实际项目中跳转到额度管理页面
// uni.navigateTo({ url: '/pages/finance/limit-manage' })
}
// 商户卡片点击处理
function handleMerchantClick(merchant: CreditLimit) {
uni.showModal({
title: merchant.merchantName,
content: `可用额度:¥${merchant.availableLimit.toLocaleString('zh-CN', { minimumFractionDigits: 2 })}\n总额度¥${merchant.totalLimit.toLocaleString('zh-CN', { minimumFractionDigits: 2 })}\n已使用¥${merchant.usedLimit.toLocaleString('zh-CN', { minimumFractionDigits: 2 })}`,
showCancel: false
})
}
</script> </script>
<template> <template>
@@ -35,9 +92,35 @@ onPullDownRefresh(() => {
<view class="label">总可用额度 ()</view> <view class="label">总可用额度 ()</view>
<view class="amount">{{ financeStore.totalAvailableLimit.toLocaleString('zh-CN', { minimumFractionDigits: 2 }) }}</view> <view class="amount">{{ financeStore.totalAvailableLimit.toLocaleString('zh-CN', { minimumFractionDigits: 2 }) }}</view>
<view class="sub-info"> <view class="sub-info">
<text>额度 ¥{{ financeStore.creditLimits.reduce((sum, item) => sum + item.totalLimit, 0).toLocaleString() }}</text> <text>信用额度 ¥{{ totalCreditLimit.toLocaleString() }} | 已使用 ¥{{ financeStore.totalUsedLimit.toLocaleString() }} ({{ usagePercent }}%)</text>
<text class="divider">|</text> </view>
<text>已用 ¥{{ financeStore.totalUsedLimit.toLocaleString() }}</text>
<!-- 进度条 -->
<view class="progress-wrapper">
<view class="progress-bg">
<view class="progress-bar" :style="{ width: usagePercent + '%', background: progressColor }"></view>
</view>
</view>
<!-- 统一更新时间 -->
<view class="update-time" v-if="financeStore.creditLimits.length > 0">
数据更新于{{ financeStore.creditLimits[0].updateTime }}
</view>
</view>
<!-- 快捷功能入口 -->
<view class="quick-actions">
<view class="action-item" @click="handleIncreaseLimit">
<view class="icon">📈</view>
<text>提升额度</text>
</view>
<view class="action-item" @click="handleViewBills">
<view class="icon">📋</view>
<text>账单明细</text>
</view>
<view class="action-item" @click="handleManageLimit">
<view class="icon"></view>
<text>额度管理</text>
</view> </view>
</view> </view>
@@ -58,6 +141,12 @@ onPullDownRefresh(() => {
:used-limit="item.usedLimit" :used-limit="item.usedLimit"
:available-limit="item.availableLimit" :available-limit="item.availableLimit"
:update-time="item.updateTime" :update-time="item.updateTime"
:hide-update-time="true"
:class="{
'exhausted': item.availableLimit <= 0 || (item.usedLimit / item.totalLimit) >= 1,
'warning': (item.usedLimit / item.totalLimit) >= 0.8 && (item.usedLimit / item.totalLimit) < 1
}"
@click="handleMerchantClick(item)"
/> />
</view> </view>
</view> </view>
@@ -73,8 +162,10 @@ onPullDownRefresh(() => {
.overview-card { .overview-card {
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%); background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
padding: 60rpx 40rpx; padding: 60rpx 40rpx 40rpx;
color: #fff; color: #fff;
border-radius: 0 0 40rpx 40rpx;
box-shadow: 0 8rpx 24rpx rgba(24, 144, 255, 0.2);
.label { .label {
font-size: 28rpx; font-size: 28rpx;
@@ -83,27 +174,86 @@ onPullDownRefresh(() => {
} }
.amount { .amount {
font-size: 64rpx; font-size: 72rpx;
font-weight: 600; font-weight: 700;
margin-bottom: 30rpx; margin-bottom: 30rpx;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
} }
.sub-info { .sub-info {
display: flex;
align-items: center;
font-size: 26rpx; font-size: 26rpx;
opacity: 0.9; opacity: 0.9;
margin-bottom: 30rpx;
}
.progress-wrapper {
margin-bottom: 30rpx;
.divider { .progress-bg {
margin: 0 20rpx; height: 16rpx;
opacity: 0.5; background: rgba(255, 255, 255, 0.2);
border-radius: 8rpx;
overflow: hidden;
}
.progress-bar {
height: 100%;
border-radius: 8rpx;
transition: width 0.5s ease, background 0.3s ease;
}
}
.update-time {
font-size: 24rpx;
opacity: 0.7;
text-align: center;
}
}
.quick-actions {
display: flex;
justify-content: space-around;
padding: 40rpx 30rpx;
background: #fff;
margin: 20rpx 30rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.05);
.action-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 16rpx;
padding: 20rpx;
border-radius: 12rpx;
transition: all 0.3s ease;
&:active {
background: #f5f5f5;
transform: scale(0.95);
}
.icon {
font-size: 48rpx;
width: 80rpx;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
background: #f0f8ff;
border-radius: 50%;
}
text {
font-size: 24rpx;
color: #333;
} }
} }
} }
.list-container { .list-container {
padding: 30rpx; padding: 30rpx;
margin-top: -40rpx; margin-top: 20rpx;
.section-title { .section-title {
font-size: 30rpx; font-size: 30rpx;
@@ -111,6 +261,19 @@ onPullDownRefresh(() => {
color: #333; color: #333;
margin-bottom: 24rpx; margin-bottom: 24rpx;
padding-left: 10rpx; padding-left: 10rpx;
position: relative;
&::before {
content: '';
position: absolute;
left: -10rpx;
top: 50%;
transform: translateY(-50%);
width: 6rpx;
height: 30rpx;
background: #1890ff;
border-radius: 3rpx;
}
} }
} }

View File

@@ -27,6 +27,25 @@ onLoad((options) => {
loadCategories() loadCategories()
}) })
// 监听全局事件用于接收从首页传递的分类ID
onShow(() => {
// 监听选中分类事件
uni.$on('selectCategory', (categoryId: string) => {
if (categoryId && categoryId !== currentCategoryId.value) {
currentCategoryId.value = categoryId
// 如果分类已加载,直接切换并加载商品
if (categoryList.value.length > 0) {
loadGoods()
}
}
})
})
// 页面卸载时移除事件监听
onUnload(() => {
uni.$off('selectCategory')
})
// 加载分类 // 加载分类
async function loadCategories() { async function loadCategories() {
try { try {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 127 KiB

BIN
src/static/product/18/1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

BIN
src/static/product/18/2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 777 KiB

View File

@@ -8,7 +8,7 @@ export const useUserStore = defineStore('user', {
id: 'user_001', id: 'user_001',
username: 'admin', username: 'admin',
nickname: '测试用户', nickname: '测试用户',
avatar: 'https://picsum.photos/200/200?random=avatar', avatar: '/static/images/avatar.jpg',
phone: '13800138000', phone: '13800138000',
creditLimits: [], // 实际应从 financeStore 获取或关联 creditLimits: [], // 实际应从 financeStore 获取或关联
member: mockMember, member: mockMember,