feat: 升级UI组件为uview-plus

- config.vue: 替换原生picker为u-picker,统一toast为u-toast,按钮为u-button
- detail.vue: 替换交易表单input为u-input,所有按钮为u-button,toast为u-toast
- 清理已替换组件的CSS样式
- 添加todo.md记录后续升级计划
This commit is contained in:
claw_bot 2026-03-13 07:43:16 +00:00
parent 924ac09535
commit a9f3692aa8
3 changed files with 362 additions and 124 deletions

View File

@ -1,5 +1,7 @@
<template>
<view class="page-container">
<!-- uView Toast 组件 -->
<u-toast ref="uToastRef" />
<view class="section-card">
<view class="card-header">
@ -21,27 +23,39 @@
<view class="form-item">
<text class="label">选择逻辑模板</text>
<picker @change="onStrategyChange" :value="strategyIndex" :range="strategies" range-key="name">
<view class="picker-box">
<u-picker
:show="showStrategyPicker"
:columns="[strategies.map(s => s.name)]"
keyName="name"
@confirm="onStrategyPickerConfirm"
@cancel="showStrategyPicker = false"
>
<view class="picker-box" @click="showStrategyPicker = true">
<view class="flex-row items-center gap-2" v-if="selectedStrategy">
<view class="strategy-dot" :style="{ backgroundColor: selectedStrategy.color }"></view>
<text class="picker-text">{{ selectedStrategy.name }}</text>
</view>
<text class="picker-placeholder" v-else>点击选择逻辑规则</text>
<uni-icons type="bottom" size="14" color="#9CA3AF"></uni-icons>
<u-icon name="arrow-down" size="14" color="#9CA3AF"></u-icon>
</view>
</picker>
</u-picker>
<text class="helper-text" v-if="selectedStrategy">{{ selectedStrategy.desc }}</text>
</view>
<view class="form-item">
<text class="label">组合币种</text>
<picker @change="onCurrencyChange" :value="currencyIndex" :range="currencyList" range-key="name">
<view class="picker-box">
<u-picker
:show="showCurrencyPicker"
:columns="[currencyList.map(c => c.name)]"
keyName="name"
@confirm="onCurrencyPickerConfirm"
@cancel="showCurrencyPicker = false"
>
<view class="picker-box" @click="showCurrencyPicker = true">
<text class="picker-text">{{ currencyList[currencyIndex].name }}</text>
<uni-icons type="bottom" size="14" color="#9CA3AF"></uni-icons>
<u-icon name="arrow-down" size="14" color="#9CA3AF"></u-icon>
</view>
</picker>
</u-picker>
<text class="helper-text">创建后币种不可修改所有交易只能使用该币种</text>
</view>
</view>
@ -132,17 +146,36 @@
<text class="summary-label">预计初始投入</text>
<text class="summary-val">¥ {{ totalInvestment }}</text>
</view>
<button class="btn-submit" @click="submitForm">创建组合</button>
<u-button
class="btn-submit"
@click="submitForm"
:customStyle="{
backgroundColor: '#064E3B',
color: '#fff',
fontWeight: '700',
borderRadius: '24rpx',
height: '96rpx',
fontSize: '30rpx',
width: '100%',
border: 'none'
}"
>
创建组合
</u-button>
</view>
</view>
</template>
<script setup>
import { ref, computed, watch } from 'vue';
import { ref, computed, watch, getCurrentInstance } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import { api } from '../../utils/api';
// u-toast
const { proxy } = getCurrentInstance();
const uToastRef = ref();
const strategies = ref([]);
const strategyIndex = ref(-1);
//
@ -153,6 +186,10 @@ const currencyList = ref([
]);
const currencyIndex = ref(0); // CNY
// Picker
const showStrategyPicker = ref(false);
const showCurrencyPicker = ref(false);
//
let isFetching = false;
@ -288,13 +325,49 @@ const onDateChange = (e, index) => {
form.value.stocks[index].date = e.detail.value;
};
const onCurrencyChange = (e) => {
currencyIndex.value = e.detail.value;
// u-picker
const onStrategyPickerConfirm = (e) => {
const { value, index } = e;
strategyIndex.value = index[0];
showStrategyPicker.value = false;
//
const strategy = strategies.value[strategyIndex.value];
if (strategy && strategy.parameters && strategy.parameters.assets) {
form.value.stocks = strategy.parameters.assets.map(asset => ({
name: asset.symbol,
price: '',
amount: '',
date: ''
}));
} else {
form.value.stocks = [{ name: '', price: '', amount: '', date: '' }];
}
};
const onCurrencyPickerConfirm = (e) => {
const { value, index } = e;
currencyIndex.value = index[0];
showCurrencyPicker.value = false;
};
const submitForm = async () => {
if (!form.value.name) return uni.showToast({ title: '请输入组合名称', icon: 'none' });
if (strategyIndex.value === -1) return uni.showToast({ title: '请选择策略', icon: 'none' });
if (!form.value.name) {
proxy?.$refs.uToastRef?.show({
type: 'warning',
message: '请输入组合名称',
icon: 'warning'
});
return;
}
if (strategyIndex.value === -1) {
proxy?.$refs.uToastRef?.show({
type: 'warning',
message: '请选择策略',
icon: 'warning'
});
return;
}
const selected = strategies.value[strategyIndex.value];
@ -324,11 +397,12 @@ const submitForm = async () => {
const deviation = Math.abs(actualWeight - target.targetWeight);
if (deviation > 0.05) {
return uni.showToast({
title: `${stock.name} 权重偏差超过5%,目标${(target.targetWeight*100).toFixed(0)}%,实际${(actualWeight*100).toFixed(0)}%`,
icon: 'none',
proxy?.$refs.uToastRef?.show({
type: 'error',
message: `${stock.name} 权重偏差超过5%,目标${(target.targetWeight*100).toFixed(0)}%,实际${(actualWeight*100).toFixed(0)}%`,
duration: 3000
});
return;
}
}
}
@ -354,13 +428,21 @@ const submitForm = async () => {
const response = await api.assets.createPortfolio(requestData);
if (response.code === 200) {
uni.hideLoading();
uni.showToast({ title: '创建成功', icon: 'success' });
proxy?.$refs.uToastRef?.show({
type: 'success',
message: '创建成功',
icon: 'success'
});
setTimeout(() => uni.navigateBack(), 1500);
}
} catch (error) {
console.error('创建投资组合失败:', error);
uni.hideLoading();
uni.showToast({ title: '创建失败,请重试', icon: 'none' });
proxy?.$refs.uToastRef?.show({
type: 'error',
message: '创建失败,请重试',
icon: 'error'
});
}
};
@ -692,19 +774,8 @@ onShow(async () => {
font-family: 'DIN Alternate';
}
/* .btn-submit 样式已通过 u-button 的 customStyle 设置,此处保留空类避免冲突 */
.btn-submit {
background-color: #064E3B;
color: #fff;
font-weight: 700;
border-radius: 24rpx;
height: 96rpx;
font-size: 30rpx;
width: 100%;
border: none;
display: flex;
align-items: center;
justify-content: center;
/* 样式已内联设置 */
}
.btn-submit::after { border: none; }
.btn-submit:active { opacity: 0.9; }
</style>

View File

@ -1,5 +1,7 @@
<template>
<view class="page-container">
<!-- uView Toast 组件 -->
<u-toast ref="uToastRef" />
<view class="header-section">
@ -170,17 +172,67 @@
</view>
<view class="action-section fixed-bottom">
<button class="btn-delete" @click="deletePortfolio">
<uni-icons type="trash" size="20" color="#DC2626"></uni-icons>
</button>
<button class="btn-buy" @click="handleBuy">
<uni-icons type="download" size="18" color="#FFFFFF"></uni-icons>
<u-button
class="btn-delete"
@click="deletePortfolio"
:customStyle="{
backgroundColor: '#FEF2F2',
color: '#DC2626',
fontWeight: '600',
borderRadius: '20rpx',
height: '80rpx',
fontSize: '28rpx',
width: '80rpx',
border: 'none',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}"
>
<u-icon name="trash" size="20" color="#DC2626"></u-icon>
</u-button>
<u-button
class="btn-buy"
@click="handleBuy"
:customStyle="{
backgroundColor: '#064E3B',
color: '#FFFFFF',
fontWeight: '600',
borderRadius: '20rpx',
height: '80rpx',
fontSize: '28rpx',
flex: '1',
border: 'none',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '8rpx'
}"
>
<u-icon name="download" size="18" color="#FFFFFF"></u-icon>
<text>增加</text>
</button>
<button class="btn-sell" @click="handleSell">
<uni-icons type="upload" size="18" color="#064E3B"></uni-icons>
</u-button>
<u-button
class="btn-sell"
@click="handleSell"
:customStyle="{
backgroundColor: '#D1FAE5',
color: '#064E3B',
fontWeight: '600',
borderRadius: '20rpx',
height: '80rpx',
fontSize: '28rpx',
flex: '1',
border: 'none',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '8rpx'
}"
>
<u-icon name="upload" size="18" color="#064E3B"></u-icon>
<text>减少</text>
</button>
</u-button>
</view>
<!-- 交易表单弹窗 -->
@ -197,11 +249,20 @@
<view class="form-item">
<text class="form-label">{{ transactionType === 'sell' ? '选择持仓' : '股票代码' }}</text>
<view class="relative">
<input
<u-input
v-model="transactionForm.stockCode"
class="stock-input"
:placeholder="transactionType === 'sell' ? '点击选择要卖出的持仓' : '请输入股票代码搜索'"
:disabled="transactionType === 'sell'"
:border="false"
:customStyle="{
backgroundColor: '#F9FAFB',
borderRadius: '16rpx',
height: '80rpx',
padding: '0 20rpx',
border: '2rpx solid #E5E7EB',
fontSize: '28rpx',
color: '#1F2937'
}"
@input="onStockInput"
@click="handleStockInputClick"
/>
@ -274,8 +335,38 @@
</view>
<view class="modal-footer">
<button class="btn-cancel" @click="showTransactionForm = false">取消</button>
<button class="btn-confirm" @click="submitTransaction">确认</button>
<u-button
class="btn-cancel"
@click="showTransactionForm = false"
:customStyle="{
backgroundColor: '#FFFFFF',
color: '#6B7280',
fontWeight: '600',
borderRadius: '16rpx',
height: '80rpx',
fontSize: '28rpx',
flex: '1',
border: '2rpx solid #E5E7EB'
}"
>
取消
</u-button>
<u-button
class="btn-confirm"
@click="submitTransaction"
:customStyle="{
backgroundColor: '#064E3B',
color: '#FFFFFF',
fontWeight: '600',
borderRadius: '16rpx',
height: '80rpx',
fontSize: '28rpx',
flex: '1',
border: 'none'
}"
>
确认
</u-button>
</view>
</view>
</view>
@ -284,9 +375,13 @@
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { ref, onMounted, getCurrentInstance } from 'vue';
import { api } from '../../utils/api';
// u-toast
const { proxy } = getCurrentInstance();
const uToastRef = ref();
//
const getCurrencySymbol = (currency) => {
const symbols = {
@ -428,7 +523,11 @@ const fetchPortfolioData = async () => {
}
} catch (error) {
console.error('获取投资组合数据失败:', error);
uni.showToast({ title: '加载失败,请重试', icon: 'none' });
proxy?.$refs.uToastRef?.show({
type: 'error',
message: '加载失败,请重试',
icon: 'error'
});
}
};
@ -448,7 +547,11 @@ const fetchTransactions = async () => {
}
} catch (error) {
console.error('获取交易记录失败:', error);
uni.showToast({ title: '加载交易记录失败', icon: 'none' });
proxy?.$refs.uToastRef?.show({
type: 'error',
message: '加载交易记录失败',
icon: 'error'
});
}
};
@ -503,18 +606,38 @@ const onDateChange = (e) => {
const submitTransaction = async () => {
//
if (!transactionForm.value.stockCode) {
return uni.showToast({ title: transactionType.value === 'sell' ? '请选择要卖出的持仓' : '请输入股票代码', icon: 'none' });
proxy?.$refs.uToastRef?.show({
type: 'warning',
message: transactionType.value === 'sell' ? '请选择要卖出的持仓' : '请输入股票代码',
icon: 'warning'
});
return;
}
const amount = parseFloat(transactionForm.value.amount);
if (!amount || amount <= 0) {
return uni.showToast({ title: '请输入有效的数量', icon: 'none' });
proxy?.$refs.uToastRef?.show({
type: 'warning',
message: '请输入有效的数量',
icon: 'warning'
});
return;
}
//
if (transactionType.value === 'sell' && amount > maxSellAmount.value) {
return uni.showToast({ title: `卖出数量不能超过持仓数量 ${maxSellAmount.value}`, icon: 'none' });
proxy?.$refs.uToastRef?.show({
type: 'warning',
message: `卖出数量不能超过持仓数量 ${maxSellAmount.value}`,
icon: 'warning'
});
return;
}
if (!transactionForm.value.price || parseFloat(transactionForm.value.price) <= 0) {
return uni.showToast({ title: '请输入有效的价格', icon: 'none' });
proxy?.$refs.uToastRef?.show({
type: 'warning',
message: '请输入有效的价格',
icon: 'warning'
});
return;
}
const transactionData = {
@ -534,7 +657,11 @@ const submitTransaction = async () => {
const response = await api.assets.createTransaction(transactionData);
if (response.code === 200) {
uni.hideLoading();
uni.showToast({ title: '交易提交成功', icon: 'success' });
proxy?.$refs.uToastRef?.show({
type: 'success',
message: '交易提交成功',
icon: 'success'
});
showTransactionForm.value = false;
//
@ -545,7 +672,11 @@ const submitTransaction = async () => {
} catch (error) {
console.error('创建交易失败:', error);
uni.hideLoading();
uni.showToast({ title: '提交失败,请重试', icon: 'none' });
proxy?.$refs.uToastRef?.show({
type: 'error',
message: '提交失败,请重试',
icon: 'error'
});
}
};
@ -571,14 +702,22 @@ const deletePortfolio = async () => {
if (response.statusCode === 200) {
uni.hideLoading();
uni.showToast({ title: '删除成功', icon: 'success' });
proxy?.$refs.uToastRef?.show({
type: 'success',
message: '删除成功',
icon: 'success'
});
setTimeout(() => uni.switchTab({ url: '/pages/index/index' }), 1500);
} else {
throw new Error('删除失败');
}
} catch (error) {
uni.hideLoading();
uni.showToast({ title: '删除失败,请重试', icon: 'none' });
proxy?.$refs.uToastRef?.show({
type: 'error',
message: '删除失败,请重试',
icon: 'error'
});
}
}
}
@ -723,51 +862,19 @@ const deletePortfolio = async () => {
z-index: 999;
}
/* .btn-delete 样式已通过 u-button 的 customStyle 设置,此处保留空类避免冲突 */
.btn-delete {
flex: 0 0 96rpx;
width: 96rpx;
height: 96rpx;
border-radius: 24rpx;
background-color: #FEE2E2;
border: none;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
/* 样式已内联设置 */
}
.btn-delete::after { border: none; }
/* .btn-buy 样式已通过 u-button 的 customStyle 设置,此处保留空类避免冲突 */
.btn-buy {
flex: 1;
height: 96rpx;
border-radius: 24rpx;
background-color: #064E3B;
border: none;
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
color: #FFFFFF;
font-size: 30rpx;
font-weight: 700;
padding: 0;
/* 样式已内联设置 */
}
.btn-buy::after { border: none; }
/* .btn-sell 样式已通过 u-button 的 customStyle 设置,此处保留空类避免冲突 */
.btn-sell {
flex: 1;
height: 96rpx;
border-radius: 24rpx;
background-color: #FFFFFF;
border: 2rpx solid #064E3B;
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
color: #064E3B;
font-size: 30rpx;
font-weight: 700;
padding: 0;
/* 样式已内联设置 */
}
.btn-sell::after { border: none; }
.transaction-modal {
position: fixed;
@ -828,16 +935,9 @@ const deletePortfolio = async () => {
margin-bottom: 12rpx;
}
/* .stock-input 已替换为 u-input样式已通过 customStyle 设置 */
.stock-input {
width: 100%;
height: 80rpx;
background-color: #F9FAFB;
border: 2rpx solid #E5E7EB;
border-radius: 16rpx;
padding: 0 20rpx;
font-size: 26rpx;
color: #1F2937;
box-sizing: border-box;
/* 样式已内联设置 */
}
.form-select {
@ -861,29 +961,11 @@ const deletePortfolio = async () => {
gap: 16rpx;
}
/* .btn-cancel 和 .btn-confirm 样式已通过 u-button 的 customStyle 设置,此处保留空类避免冲突 */
.btn-cancel,
.btn-confirm {
flex: 1;
height: 80rpx;
border-radius: 12rpx;
font-size: 26rpx;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
border: none;
padding: 0;
/* 样式已内联设置 */
}
.btn-cancel {
background-color: #F3F4F6;
color: #6B7280;
}
.btn-cancel::after { border: none; }
.btn-confirm {
background-color: #064E3B;
color: #fff;
}
.btn-confirm::after { border: none; }
.relative { position: relative; }

85
todo.md Normal file
View File

@ -0,0 +1,85 @@
# AssetManager UniApp UI 升级计划
## 当前状态分析
- 已引入 uview-plus 组件库uni_modules 方式)
- 部分页面已开始替换原生组件input、button、card、datetime-picker
- 仍存在混合使用情况uni-icons + uview 组件)
- 微信小程序兼容性问题已部分修复
## 优先级排序
### P0 - 核心组件统一化(本周内)
1. **表单组件标准化**
- [ ] 替换所有原生 `<input>``<u-input>`
- [ ] 统一表单验证样式error状态、placeholder颜色
- [ ] 修复 config.vue 中的日期选择器回退问题
2. **按钮组件统一**
- [ ] 替换所有原生 `<button>``<u-button>`
- [ ] 统一按钮尺寸、圆角、hover效果
- [ ] 微信小程序兼容性测试
3. **卡片组件优化**
- [ ] 检查所有 `<u-card>` 的 props 使用是否正确
- [ ] 统一卡片阴影、边框、内边距
- [ ] 修复 detail.vue 中 u-card 的样式问题
### P1 - 用户体验提升(下周)
4. **加载状态优化**
- [ ] 实现全局骨架屏组件(使用 u-skeleton
- [ ] 添加页面切换加载动画
- [ ] 优化数据加载时的占位符
5. **反馈组件升级**
- [ ] 统一 toast 调用方式(使用 u-toast ref
- [ ] 替换所有 `uni.showToast` 为 uview 版本
- [ ] 添加操作确认模态框u-modal
6. **导航与布局**
- [ ] 检查 navbar 组件是否需要替换为 u-navbar
- [ ] 统一页面间距和布局网格
- [ ] 优化移动端适配
### P2 - 高级功能(下下周)
7. **搜索与筛选**
- [ ] 实现统一的搜索下拉组件u-search + u-dropdown
- [ ] 添加筛选标签组件
- [ ] 优化股票搜索功能性能
8. **图表可视化**
- [ ] 评估 ucharts 或 echarts 集成方案
- [ ] 实现资产走势迷你图表
- [ ] 添加数据可视化卡片
9. **主题与暗色模式**
- [ ] 配置 uview 主题变量
- [ ] 实现暗色模式切换
- [ ] 测试主题色一致性
## 技术债务清理
- [ ] 移除未使用的 CSS 类detail.vue 已清理部分)
- [ ] 统一图标使用(选择 uni-icons 或 u-icon
- [ ] 优化 API 调用错误处理
- [ ] 添加组件使用文档注释
## 兼容性要求
- ✅ 微信小程序(已部分适配)
- ✅ H5 网页版
- ✅ App 端
- ⚠️ 支付宝小程序(待测试)
## 验收标准
1. 所有页面组件使用率达到 90% 以上 uview-plus
2. 微信小程序无警告和错误
3. 页面加载速度提升 20%
4. 代码重复率降低 30%
## 风险点
1. **微信小程序组件限制**:部分 uview 组件在小程序端可能有限制
2. **性能影响**uview 组件库体积较大,需注意分包加载
3. **学习曲线**:团队成员需熟悉 uview-plus API
## 下一步行动
1. 先完成 P0 的表单和按钮统一化
2. 每个页面完成后进行微信小程序真机测试
3. 建立组件使用规范文档