AssetManager.UniApp/pages/config/config.vue

586 lines
13 KiB
Vue

<template>
<view class="page-container">
<view class="section-card">
<view class="card-header">
<view class="header-icon bg-emerald-100">
<uni-icons type="settings" size="18" color="#064E3B"></uni-icons>
</view>
<text class="header-title">基础设置</text>
</view>
<view class="form-item">
<text class="label">组合名称</text>
<input class="input-box" type="text" v-model="form.name" placeholder="给你的组合起个名字 (如: 养老定投)"
placeholder-class="placeholder-style" />
</view>
<view class="form-item">
<text class="label">选择逻辑模板</text>
<picker @change="onStrategyChange" :value="strategyIndex" :range="strategies" range-key="name">
<view class="picker-box">
<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>
</view>
</picker>
<text class="helper-text" v-if="selectedStrategy">{{ selectedStrategy.desc }}</text>
</view>
</view>
<view class="section-card">
<view class="card-header">
<view class="flex-row items-center gap-2">
<view class="header-icon bg-blue-100">
<uni-icons type="wallet" size="18" color="#1D4ED8"></uni-icons>
</view>
<text class="header-title">初始化记录</text>
</view>
</view>
<view class="stock-list">
<view class="stock-item" v-for="(item, index) in form.stocks" :key="index">
<view class="item-header">
<text class="item-index">单元 #{{ index + 1 }}</text>
</view>
<view class="item-grid">
<view class="grid-col relative">
<text class="sub-label">单元名称/代码</text>
<input
class="mini-input"
v-model="item.name"
placeholder="如 TMF"
@input="(e) => searchStock(e.detail.value, index)"
@focus="() => { activeSearchIndex.value = -1; searchResults.value = []; }"
/>
<!-- 搜索下拉列表 -->
<view class="search-dropdown" v-if="searchResults.length > 0 && activeSearchIndex.value === index">
<view
class="dropdown-item"
v-for="(result, idx) in searchResults.filter(r => r.stockIndex === index)"
:key="idx"
@click="selectStock(result)"
>
<text class="item-ticker">{{ result.Ticker }}</text>
<text class="item-exchange">{{ result.Exchange }}</text>
</view>
</view>
</view>
<view class="grid-col">
<text class="sub-label">买入均价</text>
<input class="mini-input" type="digit" v-model="item.price" placeholder="0.00" />
</view>
<view class="grid-col">
<text class="sub-label">持有数量</text>
<input class="mini-input" type="number" v-model="item.amount" placeholder="0" />
</view>
</view>
<view class="date-row">
<text class="sub-label">建仓日期</text>
<picker mode="date" :value="item.date" @change="(e) => onDateChange(e, index)">
<view class="date-picker-display">
<text>{{ item.date || '请选择日期' }}</text>
<uni-icons type="calendar" size="16" color="#9CA3AF"></uni-icons>
</view>
</picker>
</view>
</view>
</view>
</view>
<view class="footer-area">
<view class="total-summary">
<text class="summary-label">预计初始投入</text>
<text class="summary-val">¥ {{ totalInvestment }}</text>
</view>
<button class="submit-btn" @click="submitForm">创建组合</button>
</view>
</view>
</template>
<script setup>
import { ref, computed, watch } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import { api } from '../../utils/api';
const strategies = ref([]);
const strategyIndex = ref(-1);
// 防止重复请求的标志
let isFetching = false;
const form = ref({
name: '',
stocks: [
{ name: '', price: '', amount: '', date: '' }
]
});
// 股票搜索相关
const searchResults = ref([]);
const activeSearchIndex = ref(-1);
const searchTimer = ref(null);
const searchStock = async (keyword, stockIndex) => {
// 防抖
if (searchTimer.value) clearTimeout(searchTimer.value);
if (!keyword || keyword.length < 2) {
searchResults.value = [];
activeSearchIndex.value = -1;
return;
}
searchTimer.value = setTimeout(async () => {
try {
const res = await api.ticker.search(keyword);
if (res.code === 200) {
searchResults.value = res.data.map(item => ({
...item,
stockIndex: stockIndex
}));
}
} catch (err) {
console.error('搜索股票失败:', err);
searchResults.value = [];
}
}, 300);
};
const selectStock = (result) => {
const stock = form.value.stocks[result.stockIndex];
if (stock) {
stock.name = result.Ticker;
}
searchResults.value = [];
activeSearchIndex.value = -1;
};
const selectedStrategy = computed(() => {
if (strategyIndex.value === -1) return null;
return strategies.value[strategyIndex.value];
});
const totalInvestment = computed(() => {
let total = 0;
form.value.stocks.forEach(stock => {
const p = parseFloat(stock.price) || 0;
const a = parseFloat(stock.amount) || 0;
total += p * a;
});
return total.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
});
const fetchStrategies = async () => {
try {
const response = await api.strategies.getStrategies();
if (response.code === 200) {
strategies.value = response.data.map(item => ({
id: item.id,
name: item.name,
desc: item.description,
type: item.type,
parameters: item.config ? JSON.parse(item.config) : {},
color: '#10B981'
}));
}
} catch (error) {
console.error('获取策略列表失败:', error);
}
};
const onStrategyChange = (e) => {
strategyIndex.value = e.detail.value;
const strategy = strategies.value[strategyIndex.value];
console.log('选择的策略:', strategy);
console.log('策略参数:', strategy?.parameters);
if (strategy && strategy.parameters && strategy.parameters.assets) {
form.value.stocks = strategy.parameters.assets.map(asset => ({
name: asset.symbol,
price: '',
amount: '',
date: ''
}));
console.log('自动填充标的:', form.value.stocks);
} else {
form.value.stocks = [{ name: '', price: '', amount: '', date: '' }];
console.log('未找到标的配置,使用默认值');
}
};
const onDateChange = (e, index) => {
form.value.stocks[index].date = e.detail.value;
};
const submitForm = async () => {
if (!form.value.name) return uni.showToast({ title: '请输入组合名称', icon: 'none' });
if (strategyIndex.value === -1) return uni.showToast({ title: '请选择策略', icon: 'none' });
const selected = strategies.value[strategyIndex.value];
const requestData = {
name: form.value.name,
strategyId: selected.id,
currency: 'USD',
stocks: form.value.stocks.map(stock => ({
name: stock.name,
code: stock.name,
price: parseFloat(stock.price) || 0,
amount: parseFloat(stock.amount) || 0,
date: stock.date,
currency: 'USD'
}))
};
uni.showLoading({ title: '创建中...' });
try {
const response = await api.assets.createPortfolio(requestData);
if (response.code === 200) {
uni.hideLoading();
uni.showToast({ title: '创建成功', icon: 'success' });
setTimeout(() => uni.navigateBack(), 1500);
}
} catch (error) {
console.error('创建投资组合失败:', error);
uni.hideLoading();
uni.showToast({ title: '创建失败,请重试', icon: 'none' });
}
};
onShow(async () => {
isFetching = true;
await fetchStrategies();
isFetching = false;
});
</script>
<style scoped>
/* 通用布局 */
.page-container {
min-height: 100vh;
background-color: #F3F4F6;
padding-bottom: 200rpx;
/* 给底部按钮留空 */
}
.flex-row {
display: flex;
flex-direction: row;
}
.items-center {
align-items: center;
}
.justify-between {
justify-content: space-between;
}
.gap-2 {
gap: 16rpx;
}
/* 导航栏 */
.nav-bar {
background-color: #fff;
padding: var(--status-bar-height) 32rpx 20rpx 32rpx;
display: flex;
align-items: center;
justify-content: space-between;
height: 88rpx;
box-sizing: content-box;
}
.page-title {
font-size: 34rpx;
font-weight: 700;
color: #111827;
}
/* 卡片容器 */
.section-card {
background-color: #fff;
margin: 32rpx;
padding: 32rpx;
border-radius: 32rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.03);
}
/* 卡片标题头 */
.card-header {
display: flex;
align-items: center;
margin-bottom: 32rpx;
}
.header-icon {
width: 60rpx;
height: 60rpx;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20rpx;
}
.bg-emerald-100 {
background-color: #D1FAE5;
}
.bg-blue-100 {
background-color: #DBEAFE;
}
.header-title {
font-size: 30rpx;
font-weight: 700;
color: #1F2937;
}
/* 表单项 */
.form-item {
margin-bottom: 32rpx;
}
.form-item:last-child {
margin-bottom: 0;
}
.label {
font-size: 26rpx;
font-weight: 600;
color: #374151;
margin-bottom: 16rpx;
display: block;
}
.input-box {
background-color: #F9FAFB;
border: 2rpx solid #E5E7EB;
border-radius: 20rpx;
height: 96rpx;
padding: 0 32rpx;
font-size: 28rpx;
color: #1F2937;
}
.placeholder-style {
color: #9CA3AF;
}
/* 选择器样式 */
.picker-box {
background-color: #F9FAFB;
border: 2rpx solid #E5E7EB;
border-radius: 20rpx;
height: 96rpx;
padding: 0 32rpx;
display: flex;
align-items: center;
justify-content: space-between;
}
.picker-text {
font-size: 28rpx;
color: #1F2937;
font-weight: 500;
}
.picker-placeholder {
font-size: 28rpx;
color: #9CA3AF;
}
.strategy-dot {
width: 16rpx;
height: 16rpx;
border-radius: 50%;
}
.helper-text {
font-size: 22rpx;
color: #6B7280;
margin-top: 12rpx;
display: block;
margin-left: 8rpx;
}
/* --- 动态持仓列表 --- */
.stock-list {
display: flex;
flex-direction: column;
gap: 24rpx;
}
.stock-item {
background-color: #F9FAFB;
border: 1rpx solid #E5E7EB;
border-radius: 24rpx;
padding: 24rpx;
}
.item-header {
display: flex;
justify-content: space-between;
margin-bottom: 20rpx;
}
.item-index {
font-size: 24rpx;
font-weight: 700;
color: #9CA3AF;
}
/* 网格输入布局 */
.item-grid {
display: flex;
gap: 20rpx;
margin-bottom: 20rpx;
}
.grid-col {
flex: 1;
}
.sub-label {
font-size: 20rpx;
color: #6B7280;
margin-bottom: 8rpx;
display: block;
}
.mini-input {
background-color: #FFFFFF;
border: 1rpx solid #E5E7EB;
border-radius: 16rpx;
height: 72rpx;
padding: 0 20rpx;
font-size: 26rpx;
color: #1F2937;
}
.mini-input[disabled] {
background-color: #F3F4F6;
color: #6B7280;
cursor: not-allowed;
}
/* 搜索下拉列表 */
.relative {
position: relative;
}
.search-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
background-color: #FFFFFF;
border: 1rpx solid #E5E7EB;
border-radius: 12rpx;
margin-top: 4rpx;
max-height: 300rpx;
overflow-y: auto;
z-index: 100;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
}
.dropdown-item {
padding: 16rpx 20rpx;
border-bottom: 1rpx solid #F3F4F6;
display: flex;
justify-content: space-between;
align-items: center;
}
.dropdown-item:last-child {
border-bottom: none;
}
.dropdown-item:active {
background-color: #F3F4F6;
}
.item-ticker {
font-size: 26rpx;
font-weight: 600;
color: #1F2937;
}
.item-exchange {
font-size: 22rpx;
color: #9CA3AF;
}
/* 日期选择 */
.date-picker-display {
background-color: #FFFFFF;
border: 1rpx solid #E5E7EB;
border-radius: 16rpx;
height: 72rpx;
padding: 0 20rpx;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 26rpx;
color: #1F2937;
}
/* 底部固定区 */
.footer-area {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #fff;
padding: 20rpx 32rpx 50rpx 32rpx;
/* 适配 iPhone X */
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.05);
display: flex;
flex-direction: column;
gap: 20rpx;
}
.total-summary {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 10rpx;
}
.summary-label {
font-size: 26rpx;
color: #6B7280;
}
.summary-val {
font-size: 36rpx;
font-weight: 700;
color: #064E3B;
font-family: 'DIN Alternate';
}
.submit-btn {
background-color: #064E3B;
color: #fff;
font-weight: 700;
border-radius: 24rpx;
height: 96rpx;
line-height: 96rpx;
font-size: 30rpx;
width: 100%;
}
.submit-btn:active {
opacity: 0.9;
}
</style>