AssetManager.UniApp/pages/strategies/edit/edit.vue
niannian zheng 58cf092753 feat(策略编辑): 实现策略创建功能并优化API请求
添加策略创建的表单验证和API调用逻辑
移除API请求中对userId的硬编码依赖
使用环境变量配置API基础URL
2026-03-02 15:13:15 +08:00

451 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="page-container">
<view class="section-title">选择逻辑模型</view>
<scroll-view scroll-x class="strategy-scroll" :show-scrollbar="false">
<view class="strategy-row">
<view
v-for="(item, index) in strategyTypes"
:key="index"
class="strategy-card"
:class="{ 'active': currentType === item.key }"
@click="selectType(item.key)"
>
<view class="icon-circle" :class="currentType === item.key ? 'bg-white text-green' : item.bgClass">
<uni-icons :type="item.icon" size="24" :color="currentType === item.key ? '#064E3B' : item.iconColor"></uni-icons>
</view>
<text class="st-name" :class="{ 'text-white': currentType === item.key }">{{ item.name }}</text>
<text class="st-tag" :class="{ 'text-green-light': currentType === item.key }">{{ item.tag }}</text>
<view class="check-mark" v-if="currentType === item.key">
<uni-icons type="checkmarkempty" size="16" color="#064E3B"></uni-icons>
</view>
</view>
</view>
</scroll-view>
<view class="desc-box" v-if="currentStrategyInfo">
<view class="desc-header">
<uni-icons type="info-filled" size="18" color="#064E3B"></uni-icons>
<text class="desc-title">策略原理</text>
</view>
<text class="desc-content">{{ currentStrategyInfo.description }}</text>
</view>
<view class="config-section">
<view class="section-title">参数配置</view>
<view class="form-card">
<view class="form-item">
<text class="label">模型别名</text>
<input class="input-field" v-model="formData.alias" placeholder="例如: 纳指长期定投" />
</view>
<template v-if="currentType === 'weight'">
<view class="form-item">
<text class="label">再平衡周期</text>
<picker :range="['每日', '每周', '每月', '每季度', '每年']" @change="onPeriodChange">
<view class="picker-display">
<text>{{ formData.period || '请选择' }}</text>
<uni-icons type="bottom" size="14" color="#9CA3AF"></uni-icons>
</view>
</picker>
</view>
<view class="form-item">
<text class="label">偏离阈值 (%)</text>
<view class="input-wrapper">
<input class="input-field" type="number" v-model="formData.threshold" placeholder="5" />
<text class="unit">sw</text>
</view>
<text class="helper">当资产权重偏离目标超过此数值时触发调仓。</text>
</view>
</template>
<template v-if="currentType === 'ma'">
<view class="flex-row gap-3">
<view class="form-item flex-1">
<text class="label">快线周期 (Short)</text>
<input class="input-field" type="number" v-model="formData.fastPeriod" placeholder="10" />
</view>
<view class="form-item flex-1">
<text class="label">慢线周期 (Long)</text>
<input class="input-field" type="number" v-model="formData.slowPeriod" placeholder="30" />
</view>
</view>
<view class="form-item">
<text class="label">均线类型</text>
<picker :range="['SMA (简单移动平均)', 'EMA (指数移动平均)']" @change="onMaTypeChange">
<view class="picker-display">
<text>{{ formData.maType || 'SMA' }}</text>
<uni-icons type="bottom" size="14" color="#9CA3AF"></uni-icons>
</view>
</picker>
</view>
<view class="info-tag bg-blue-50">
<text class="text-blue-700 text-xs">规则:快线上穿慢线买入,下穿卖出。</text>
</view>
</template>
<template v-if="currentType === 'chandelier'">
<view class="form-item">
<text class="label">ATR 周期</text>
<input class="input-field" type="number" v-model="formData.atrPeriod" placeholder="22" />
</view>
<view class="form-item">
<text class="label">ATR 倍数 (Multiplier)</text>
<input class="input-field" type="digit" v-model="formData.atrMultiplier" placeholder="3.0" />
</view>
<view class="form-item">
<text class="label">趋势过滤均线 (可选)</text>
<input class="input-field" type="number" v-model="formData.trendMa" placeholder="200 (日线)" />
</view>
</template>
</view>
</view>
<view class="footer-bar">
<button class="submit-btn" @click="submit">保存策略配置</button>
</view>
</view>
</template>
<script setup>
import { ref, computed, getCurrentInstance } from 'vue';
const { proxy } = getCurrentInstance();
const api = proxy.$api;
const currentType = ref('weight'); // 当前选中的策略类型
// 策略定义数据
const strategyTypes = [
{
key: 'weight',
name: '目标权重策略',
tag: '资产配置',
icon: 'pie-chart-filled',
bgClass: 'bg-green-100',
iconColor: '#059669',
description: '为投资组合中的每个资产设定固定的目标权重比例通过定期再平衡将资产配置恢复到目标权重。适合长期投资和多元化资产配置如60/40组合。'
},
{
key: 'ma',
name: '双均线策略',
tag: '趋势跟踪',
icon: 'navigate-filled',
bgClass: 'bg-blue-100',
iconColor: '#2563EB',
description: '经典技术分析策略,通过短期和长期移动平均线的金叉死叉信号捕捉价格趋势。金叉买入,死叉卖出,过滤短期噪音。'
},
{
key: 'chandelier',
name: '吊灯止损策略',
tag: '风险控制',
icon: 'fire-filled',
bgClass: 'bg-orange-100',
iconColor: '#EA580C',
description: '结合移动平均线和ATR平均真实波幅生成的动态止损点。随着价格上涨止损点上移价格回撤触及止损点时离场有效锁住利润。'
}
];
// 表单数据
const formData = ref({
alias: '',
period: '每季度',
threshold: '',
fastPeriod: '',
slowPeriod: '',
maType: 'SMA',
atrPeriod: '',
atrMultiplier: '',
trendMa: ''
});
// 计算当前选中的策略信息
const currentStrategyInfo = computed(() => {
return strategyTypes.find(item => item.key === currentType.value);
});
const selectType = (key) => {
currentType.value = key;
};
const onPeriodChange = (e) => {
const periods = ['每日', '每周', '每月', '每季度', '每年'];
formData.value.period = periods[e.detail.value];
};
const onMaTypeChange = (e) => {
const types = ['SMA', 'EMA'];
formData.value.maType = types[e.detail.value];
};
const submit = async () => {
if (!formData.value.alias) {
uni.showToast({ title: '请输入模型别名', icon: 'none' });
return;
}
const strategyData = {
type: currentType.value,
alias: formData.value.alias,
config: {}
};
switch (currentType.value) {
case 'weight':
if (!formData.value.threshold) {
uni.showToast({ title: '请输入偏离阈值', icon: 'none' });
return;
}
strategyData.config = {
period: formData.value.period,
threshold: parseFloat(formData.value.threshold)
};
break;
case 'ma':
if (!formData.value.fastPeriod || !formData.value.slowPeriod) {
uni.showToast({ title: '请输入快线和慢线周期', icon: 'none' });
return;
}
strategyData.config = {
fastPeriod: parseInt(formData.value.fastPeriod),
slowPeriod: parseInt(formData.value.slowPeriod),
maType: formData.value.maType
};
break;
case 'chandelier':
if (!formData.value.atrPeriod || !formData.value.atrMultiplier) {
uni.showToast({ title: '请输入ATR周期和倍数', icon: 'none' });
return;
}
strategyData.config = {
atrPeriod: parseInt(formData.value.atrPeriod),
atrMultiplier: parseFloat(formData.value.atrMultiplier),
trendMa: formData.value.trendMa ? parseInt(formData.value.trendMa) : null
};
break;
}
console.log('保存策略:', strategyData);
uni.showLoading({ title: '保存中' });
try {
const res = await api.strategies.createStrategy(strategyData);
console.log('策略创建成功:', res);
uni.hideLoading();
uni.showToast({ title: '策略已保存', icon: 'success' });
setTimeout(() => uni.navigateBack(), 1500);
} catch (error) {
console.error('策略创建失败:', error);
uni.hideLoading();
uni.showToast({ title: '保存失败,请重试', icon: 'none' });
}
};
</script>
<style scoped>
.page-container {
min-height: 100vh;
background-color: #F9FAFB;
padding-bottom: 200rpx; /* 增加底部内边距,防止内容被底部按钮遮挡 */
}
/* 导航栏 (简化版) */
.nav-bar {
background-color: #fff;
padding: var(--status-bar-height) 32rpx 20rpx 32rpx;
display: flex;
align-items: center;
height: 88rpx;
box-sizing: content-box;
position: sticky;
top: 0;
z-index: 100;
}
.back-btn {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: flex-start;
}
/* 标题通用 */
.section-title {
padding: 32rpx 32rpx 20rpx 32rpx;
font-size: 28rpx;
font-weight: 700;
color: #374151;
}
/* 策略选择器 */
.strategy-scroll {
white-space: nowrap;
width: 100%;
padding-bottom: 20rpx;
}
.strategy-row {
display: flex;
padding: 0 32rpx;
gap: 24rpx;
}
.strategy-card {
width: 280rpx;
height: 320rpx;
background-color: #FFFFFF;
border-radius: 32rpx;
padding: 32rpx;
display: inline-flex;
flex-direction: column;
justify-content: center;
align-items: center;
border: 2rpx solid transparent;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.03);
position: relative;
transition: all 0.2s;
}
.strategy-card.active {
background-color: #064E3B;
border-color: #064E3B;
transform: translateY(-4rpx);
box-shadow: 0 12rpx 24rpx rgba(6, 78, 59, 0.2);
}
.icon-circle {
width: 96rpx;
height: 96rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 24rpx;
}
.bg-green-100 { background-color: #ECFDF5; }
.bg-blue-100 { background-color: #EFF6FF; }
.bg-orange-100 { background-color: #FFF7ED; }
.st-name { font-size: 30rpx; font-weight: 700; color: #1F2937; margin-bottom: 8rpx; white-space: normal; text-align: center; }
.st-tag { font-size: 22rpx; color: #9CA3AF; }
.text-white { color: #fff !important; }
.text-green { color: #064E3B !important; }
.text-green-light { color: rgba(255,255,255,0.7) !important; }
.check-mark {
position: absolute;
top: 16rpx;
right: 16rpx;
width: 40rpx;
height: 40rpx;
background-color: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
/* 策略描述 */
.desc-box {
margin: 10rpx 32rpx 30rpx 32rpx;
background-color: #ECFDF5;
padding: 24rpx;
border-radius: 20rpx;
border: 1rpx solid #D1FAE5;
}
.desc-header {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 12rpx;
}
.desc-title { font-size: 26rpx; font-weight: 700; color: #064E3B; }
.desc-content {
font-size: 24rpx;
color: #047857;
line-height: 1.6;
text-align: justify;
}
/* 表单区域 */
.config-section { margin-top: 20rpx; }
.form-card {
background-color: #fff;
margin: 0 32rpx;
padding: 32rpx;
border-radius: 32rpx;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.02);
}
.form-item { margin-bottom: 32rpx; }
.label { font-size: 26rpx; font-weight: 600; color: #374151; margin-bottom: 16rpx; display: block; }
.input-field {
background-color: #F9FAFB;
border: 2rpx solid #E5E7EB;
border-radius: 20rpx;
height: 88rpx;
padding: 0 24rpx;
font-size: 28rpx;
color: #1F2937;
}
.input-wrapper {
position: relative;
display: flex;
align-items: center;
}
.unit { position: absolute; right: 24rpx; font-size: 26rpx; color: #9CA3AF; }
.picker-display {
background-color: #F9FAFB;
border: 2rpx solid #E5E7EB;
border-radius: 20rpx;
height: 88rpx;
padding: 0 24rpx;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 28rpx;
color: #1F2937;
}
.helper { font-size: 22rpx; color: #9CA3AF; margin-top: 10rpx; display: block; }
.flex-row { display: flex; flex-direction: row; }
.flex-1 { flex: 1; }
.gap-3 { gap: 24rpx; }
.info-tag { padding: 16rpx; border-radius: 12rpx; margin-top: -10rpx; }
.bg-blue-50 { background-color: #EFF6FF; }
.text-blue-700 { color: #1D4ED8; }
.text-xs { font-size: 22rpx; }
/* 底部悬浮按钮栏 */
.footer-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #fff;
padding: 20rpx 32rpx 50rpx 32rpx; /* 适配底部安全区 */
box-shadow: 0 -4rpx 16rpx rgba(0,0,0,0.05);
z-index: 99;
}
.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>