- strategies/strategies.vue: 清理未使用的btn-secondary样式,优化按钮样式 - strategies/edit/edit.vue: 已替换u-input为原生input - 添加OPTIMIZATION.md优化计划文档 - 保留index.vue的骨架屏自定义样式(小程序兼容性更好)
998 lines
28 KiB
Vue
Executable File
998 lines
28 KiB
Vue
Executable File
<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
|
||
v-model="formData.name"
|
||
class="form-input"
|
||
placeholder="例如: 双均线趋势策略"
|
||
/>
|
||
</view>
|
||
|
||
<view class="form-item">
|
||
<text class="label">策略描述</text>
|
||
<input
|
||
v-model="formData.description"
|
||
class="form-input"
|
||
placeholder="描述策略的用途和特点"
|
||
/>
|
||
</view>
|
||
|
||
<view class="form-item">
|
||
<text class="label">风险等级</text>
|
||
<picker :range="['low', 'medium', 'high']" @change="onRiskLevelChange">
|
||
<view class="picker-display">
|
||
<text>{{ formData.riskLevel || 'medium' }}</text>
|
||
<uni-icons type="bottom" size="14" color="#9CA3AF"></uni-icons>
|
||
</view>
|
||
</picker>
|
||
</view>
|
||
|
||
<view class="form-item">
|
||
<text class="label">标签</text>
|
||
<input
|
||
v-model="formData.tags"
|
||
class="form-input"
|
||
placeholder="用逗号分隔,如:趋势,均线"
|
||
/>
|
||
</view>
|
||
|
||
<template v-if="currentType === 'ma_trend'">
|
||
<view class="flex-row gap-3">
|
||
<view class="form-item flex-1">
|
||
<text class="label">短期周期</text>
|
||
<input
|
||
v-model="formData.shortPeriod"
|
||
type="number"
|
||
class="form-input"
|
||
placeholder="20"
|
||
/>
|
||
</view>
|
||
<view class="form-item flex-1">
|
||
<text class="label">长期周期</text>
|
||
<input
|
||
v-model="formData.longPeriod"
|
||
type="number"
|
||
class="form-input"
|
||
placeholder="60"
|
||
/>
|
||
</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 === 'risk_parity'">
|
||
<view class="form-item">
|
||
<text class="label">回看周期</text>
|
||
<input
|
||
v-model="formData.lookbackPeriod"
|
||
type="number"
|
||
class="form-input"
|
||
placeholder="60"
|
||
/>
|
||
</view>
|
||
<view class="form-item">
|
||
<text class="label">再平衡阈值</text>
|
||
<input
|
||
v-model="formData.rebalanceThreshold"
|
||
type="digit"
|
||
class="form-input"
|
||
placeholder="0.05"
|
||
/>
|
||
</view>
|
||
|
||
<view class="form-item">
|
||
<text class="label">资产配置</text>
|
||
<view class="assets-list">
|
||
<view class="asset-item" v-for="(asset, index) in formData.assets" :key="index">
|
||
<view class="asset-header">
|
||
<text class="asset-title">资产 #{{ index + 1 }}</text>
|
||
<uni-icons type="trash" size="18" color="#EF4444" @click="removeAsset(index)" v-if="formData.assets.length > 1"></uni-icons>
|
||
</view>
|
||
<view class="asset-inputs">
|
||
<view class="asset-input relative">
|
||
<text class="asset-label">代码</text>
|
||
<input
|
||
v-model="asset.symbol"
|
||
class="stock-input"
|
||
placeholder="如 AAPL"
|
||
@input="(e) => onStockInput(e, index)"
|
||
/>
|
||
<!-- 搜索下拉列表 -->
|
||
<view class="search-dropdown" v-if="searchResults.length > 0 && activeAssetIndex === index">
|
||
<view
|
||
class="dropdown-item"
|
||
v-for="(result, idx) in searchResults.filter(r => r.assetIndex === index)"
|
||
:key="idx"
|
||
@click="selectStock(result)"
|
||
>
|
||
<view class="item-left">
|
||
<text class="item-ticker">{{ result.ticker }}</text>
|
||
<text class="item-type" v-if="result.assetType">{{ result.assetType }}</text>
|
||
</view>
|
||
<text class="item-exchange">{{ result.exchange }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="asset-input">
|
||
<text class="asset-label">目标权重</text>
|
||
<input
|
||
v-model="asset.targetWeight"
|
||
type="digit"
|
||
class="form-input-sm"
|
||
placeholder="0.6"
|
||
/>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="add-asset-btn" @click="addAsset">
|
||
<uni-icons type="plus" size="16" color="#064E3B"></uni-icons>
|
||
<text class="add-asset-text">添加资产</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="info-tag bg-green-50">
|
||
<text class="text-green-700 text-xs">当资产权重偏离目标超过此阈值时触发再平衡。</text>
|
||
</view>
|
||
</template>
|
||
|
||
<template v-if="currentType === 'chandelier_exit'">
|
||
<view class="form-item">
|
||
<text class="label">ATR 周期</text>
|
||
<input
|
||
v-model="formData.period"
|
||
type="number"
|
||
class="form-input"
|
||
placeholder="22"
|
||
/>
|
||
</view>
|
||
<view class="form-item">
|
||
<text class="label">ATR 倍数</text>
|
||
<input
|
||
v-model="formData.multiplier"
|
||
type="digit"
|
||
class="form-input"
|
||
placeholder="3.0"
|
||
/>
|
||
</view>
|
||
<view class="form-item">
|
||
<text class="label">使用收盘价</text>
|
||
<picker :range="['是', '否']" @change="onUseCloseChange">
|
||
<view class="picker-display">
|
||
<text>{{ formData.useClose ? '是' : '否' }}</text>
|
||
<uni-icons type="bottom" size="14" color="#9CA3AF"></uni-icons>
|
||
</view>
|
||
</picker>
|
||
</view>
|
||
<view class="info-tag bg-orange-50">
|
||
<text class="text-orange-700 text-xs">动态止损策略,随着价格上涨止损点上移。</text>
|
||
</view>
|
||
</template>
|
||
|
||
</view>
|
||
</view>
|
||
|
||
<view class="footer-bar">
|
||
<view class="btn-row" v-if="isEditMode">
|
||
<button class="btn-delete" @click="deleteStrategy">删除策略</button>
|
||
<button class="btn-save" @click="submit">更新策略配置</button>
|
||
</view>
|
||
<button v-else class="btn-save btn-full" @click="submit">保存策略配置</button>
|
||
</view>
|
||
|
||
</view>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, onMounted } from 'vue';
|
||
import { api } from '../../../utils/api';
|
||
|
||
const isEditMode = ref(false);
|
||
const strategyId = ref('');
|
||
const currentType = ref('ma_trend'); // 当前选中的策略类型
|
||
|
||
const strategyTypes = [
|
||
{
|
||
key: 'ma_trend',
|
||
name: '双均线策略',
|
||
tag: '趋势跟踪',
|
||
icon: 'navigate-filled',
|
||
bgClass: 'bg-blue-100',
|
||
iconColor: '#2563EB',
|
||
description: '经典技术分析策略,通过短期和长期移动平均线的金叉死叉信号捕捉价格趋势。金叉买入,死叉卖出,过滤短期噪音。'
|
||
},
|
||
{
|
||
key: 'risk_parity',
|
||
name: '风险平价策略',
|
||
tag: '资产配置',
|
||
icon: 'pie-chart-filled',
|
||
bgClass: 'bg-green-100',
|
||
iconColor: '#059669',
|
||
description: '为投资组合中的每个资产设定固定的目标权重比例,通过定期再平衡将资产配置恢复到目标权重。适合长期投资和多元化资产配置(如60/40组合)。'
|
||
},
|
||
{
|
||
key: 'chandelier_exit',
|
||
name: '吊灯止损策略',
|
||
tag: '风险控制',
|
||
icon: 'fire-filled',
|
||
bgClass: 'bg-orange-100',
|
||
iconColor: '#EA580C',
|
||
description: '结合移动平均线和ATR(平均真实波幅)生成的动态止损点。随着价格上涨止损点上移,价格回撤触及止损点时离场,有效锁住利润。'
|
||
}
|
||
];
|
||
|
||
const formData = ref({
|
||
name: '',
|
||
description: '',
|
||
riskLevel: 'medium',
|
||
tags: [],
|
||
maType: 'SMA',
|
||
shortPeriod: '',
|
||
longPeriod: '',
|
||
lookbackPeriod: '',
|
||
rebalanceThreshold: '',
|
||
period: '',
|
||
multiplier: '',
|
||
useClose: false,
|
||
assets: [
|
||
{ symbol: '', targetWeight: '' }
|
||
]
|
||
});
|
||
|
||
// 股票搜索相关
|
||
const searchResults = ref([]);
|
||
const activeAssetIndex = ref(-1);
|
||
const searchTimer = ref(null);
|
||
|
||
const onStockInput = (e, assetIndex) => {
|
||
const keyword = e.detail.value;
|
||
console.log('🔍 策略页面股票输入:', keyword, 'assetIndex:', assetIndex);
|
||
searchStock(keyword, assetIndex);
|
||
};
|
||
|
||
const searchStock = async (keyword, assetIndex) => {
|
||
console.log('🔍 searchStock 调用:', keyword, 'assetIndex:', assetIndex);
|
||
if (searchTimer.value) clearTimeout(searchTimer.value);
|
||
activeAssetIndex.value = assetIndex;
|
||
if (!keyword || keyword.length < 1) {
|
||
searchResults.value = [];
|
||
activeAssetIndex.value = -1;
|
||
return;
|
||
}
|
||
|
||
searchTimer.value = setTimeout(async () => {
|
||
try {
|
||
console.log('📤 调用 api.ticker.search:', keyword);
|
||
const res = await api.ticker.search(keyword);
|
||
console.log('📥 搜索结果:', res);
|
||
if (res.code === 200) {
|
||
searchResults.value = res.data.map(item => ({
|
||
...item,
|
||
assetIndex: assetIndex
|
||
}));
|
||
activeAssetIndex.value = assetIndex;
|
||
}
|
||
} catch (err) {
|
||
console.error('搜索股票失败:', err);
|
||
searchResults.value = [];
|
||
activeAssetIndex.value = -1;
|
||
}
|
||
}, 300);
|
||
};
|
||
|
||
const selectStock = (result) => {
|
||
const asset = formData.value.assets[result.assetIndex];
|
||
if (asset) {
|
||
asset.symbol = result.ticker;
|
||
}
|
||
searchResults.value = [];
|
||
activeAssetIndex.value = -1;
|
||
};
|
||
|
||
// 计算当前选中的策略信息
|
||
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 onUseCloseChange = (e) => {
|
||
formData.value.useClose = e.detail.value === 0;
|
||
};
|
||
|
||
const onRiskLevelChange = (e) => {
|
||
const levels = ['low', 'medium', 'high'];
|
||
formData.value.riskLevel = levels[e.detail.value];
|
||
};
|
||
|
||
const addAsset = () => {
|
||
formData.value.assets.push({ symbol: '', targetWeight: '' });
|
||
};
|
||
|
||
const removeAsset = (index) => {
|
||
formData.value.assets.splice(index, 1);
|
||
};
|
||
|
||
const validateRiskParityAssets = () => {
|
||
const assets = formData.value.assets;
|
||
|
||
if (assets.length < 2) {
|
||
uni.showToast({ title: '风险平价策略至少需要2个资产', icon: 'none' });
|
||
return false;
|
||
}
|
||
|
||
let totalWeight = 0;
|
||
for (const asset of assets) {
|
||
if (!asset.symbol) {
|
||
uni.showToast({ title: '请填写所有资产代码', icon: 'none' });
|
||
return false;
|
||
}
|
||
if (!asset.targetWeight) {
|
||
uni.showToast({ title: '请填写所有资产目标权重', icon: 'none' });
|
||
return false;
|
||
}
|
||
totalWeight += parseFloat(asset.targetWeight);
|
||
}
|
||
|
||
// 校验权重总和必须等于1(允许1%误差)
|
||
if (Math.abs(totalWeight - 1) > 0.01) {
|
||
uni.showToast({ title: `资产权重总和必须为100%,当前为${(totalWeight * 100).toFixed(0)}%`, icon: 'none' });
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
};
|
||
|
||
const submit = async () => {
|
||
if (!formData.value.name) {
|
||
uni.showToast({ title: '请输入策略名称', icon: 'none' });
|
||
return;
|
||
}
|
||
|
||
// 处理标签,将逗号分隔的字符串转换为数组
|
||
let tags = [];
|
||
if (formData.value.tags) {
|
||
tags = formData.value.tags.split(',').map(tag => tag.trim()).filter(tag => tag);
|
||
}
|
||
if (tags.length === 0) {
|
||
tags = [currentStrategyInfo.value.tag];
|
||
}
|
||
|
||
// 构建参数对象
|
||
const parameters = {};
|
||
|
||
switch (currentType.value) {
|
||
case 'ma_trend':
|
||
if (!formData.value.shortPeriod || !formData.value.longPeriod) {
|
||
uni.showToast({ title: '请输入短期和长期周期', icon: 'none' });
|
||
return;
|
||
}
|
||
parameters.maType = formData.value.maType;
|
||
parameters.shortPeriod = parseInt(formData.value.shortPeriod);
|
||
parameters.longPeriod = parseInt(formData.value.longPeriod);
|
||
break;
|
||
case 'risk_parity':
|
||
if (!formData.value.lookbackPeriod || !formData.value.rebalanceThreshold) {
|
||
uni.showToast({ title: '请输入回看周期和再平衡阈值', icon: 'none' });
|
||
return;
|
||
}
|
||
if (!validateRiskParityAssets()) {
|
||
return;
|
||
}
|
||
parameters.lookbackPeriod = parseInt(formData.value.lookbackPeriod);
|
||
parameters.rebalanceThreshold = parseFloat(formData.value.rebalanceThreshold);
|
||
parameters.assets = formData.value.assets.map(asset => ({
|
||
symbol: asset.symbol,
|
||
targetWeight: parseFloat(asset.targetWeight)
|
||
}));
|
||
break;
|
||
case 'chandelier_exit':
|
||
if (!formData.value.period || !formData.value.multiplier) {
|
||
uni.showToast({ title: '请输入ATR周期和倍数', icon: 'none' });
|
||
return;
|
||
}
|
||
parameters.period = parseInt(formData.value.period);
|
||
parameters.multiplier = parseFloat(formData.value.multiplier);
|
||
parameters.useClose = formData.value.useClose;
|
||
break;
|
||
}
|
||
|
||
const strategyData = {
|
||
name: formData.value.name,
|
||
type: currentType.value,
|
||
description: formData.value.description || currentStrategyInfo.value.description,
|
||
riskLevel: formData.value.riskLevel,
|
||
tags: tags,
|
||
parameters: parameters
|
||
};
|
||
|
||
console.log('保存策略:', strategyData);
|
||
uni.showLoading({ title: isEditMode.value ? '更新中' : '保存中' });
|
||
|
||
try {
|
||
let res;
|
||
if (isEditMode.value) {
|
||
res = await api.strategies.updateStrategy(strategyId.value, strategyData);
|
||
console.log('策略更新成功:', res);
|
||
} else {
|
||
res = await api.strategies.createStrategy(strategyData);
|
||
console.log('策略创建成功:', res);
|
||
}
|
||
|
||
uni.hideLoading();
|
||
uni.showToast({ title: isEditMode.value ? '策略已更新' : '策略已保存', icon: 'success' });
|
||
setTimeout(() => uni.navigateBack(), 1500);
|
||
} catch (error) {
|
||
console.error(isEditMode.value ? '策略更新失败:' : '策略创建失败:', error);
|
||
uni.hideLoading();
|
||
uni.showToast({ title: isEditMode.value ? '更新失败,请重试' : '保存失败,请重试', icon: 'none' });
|
||
}
|
||
};
|
||
|
||
// 加载策略详情
|
||
const loadStrategyDetail = async (id) => {
|
||
try {
|
||
uni.showLoading({ title: '加载中' });
|
||
const response = await api.strategies.getStrategy(id);
|
||
uni.hideLoading();
|
||
|
||
if (response.code === 200) {
|
||
const data = response.data;
|
||
isEditMode.value = true;
|
||
strategyId.value = data.id;
|
||
currentType.value = data.type;
|
||
|
||
// 填充表单数据
|
||
formData.value.name = data.name || '';
|
||
formData.value.description = data.description || '';
|
||
formData.value.riskLevel = data.riskLevel || 'medium';
|
||
formData.value.tags = data.tags ? data.tags.join(', ') : '';
|
||
|
||
// 根据策略类型填充参数
|
||
let params = {};
|
||
if (data.parameters) {
|
||
params = data.parameters;
|
||
} else if (data.config) {
|
||
// 兼容旧格式
|
||
params = JSON.parse(data.config);
|
||
}
|
||
console.log('📊 策略类型:', data.type, '参数:', params);
|
||
|
||
switch (data.type) {
|
||
case 'ma_trend':
|
||
formData.value.maType = params.maType || 'SMA';
|
||
formData.value.shortPeriod = params.shortPeriod?.toString() || '';
|
||
formData.value.longPeriod = params.longPeriod?.toString() || '';
|
||
break;
|
||
case 'risk_parity':
|
||
formData.value.lookbackPeriod = params.lookbackPeriod?.toString() || '';
|
||
formData.value.rebalanceThreshold = params.rebalanceThreshold?.toString() || '';
|
||
console.log('📊 再平衡策略 assets:', params.assets);
|
||
if (params.assets && params.assets.length > 0) {
|
||
formData.value.assets = params.assets.map(asset => ({
|
||
symbol: asset.symbol || '',
|
||
targetWeight: asset.targetWeight?.toString() || ''
|
||
}));
|
||
console.log('📊 填充后的 formData.assets:', formData.value.assets);
|
||
}
|
||
break;
|
||
case 'chandelier_exit':
|
||
formData.value.period = params.period?.toString() || '';
|
||
formData.value.multiplier = params.multiplier?.toString() || '';
|
||
formData.value.useClose = params.useClose || false;
|
||
break;
|
||
}
|
||
|
||
console.log('策略详情加载成功:', data);
|
||
}
|
||
} catch (error) {
|
||
uni.hideLoading();
|
||
console.error('加载策略详情失败:', error);
|
||
uni.showToast({ title: '加载失败', icon: 'none' });
|
||
}
|
||
};
|
||
|
||
// 删除策略
|
||
const deleteStrategy = async () => {
|
||
uni.showModal({
|
||
title: '确认删除',
|
||
content: '删除后无法恢复,确定要删除这个策略吗?',
|
||
success: async (res) => {
|
||
if (res.confirm) {
|
||
uni.showLoading({ title: '删除中' });
|
||
try {
|
||
await api.strategies.deleteStrategy(strategyId.value);
|
||
uni.hideLoading();
|
||
uni.showToast({ title: '删除成功', icon: 'success' });
|
||
setTimeout(() => uni.navigateBack(), 1500);
|
||
} catch (error) {
|
||
uni.hideLoading();
|
||
uni.showToast({ title: '删除失败,请重试', icon: 'none' });
|
||
}
|
||
}
|
||
}
|
||
});
|
||
};
|
||
|
||
// 页面加载时检查是否是编辑模式
|
||
onMounted(() => {
|
||
const pages = getCurrentPages();
|
||
const currentPage = pages[pages.length - 1];
|
||
const id = currentPage.options?.id;
|
||
|
||
if (id) {
|
||
loadStrategyDetail(id);
|
||
}
|
||
});
|
||
</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;
|
||
border-bottom: 1rpx solid #E5E7EB;
|
||
}
|
||
.back-btn {
|
||
width: 60rpx;
|
||
height: 60rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
}
|
||
.nav-title {
|
||
flex: 1;
|
||
text-align: center;
|
||
font-size: 32rpx;
|
||
font-weight: 600;
|
||
color: #111827;
|
||
margin-right: 60rpx;
|
||
}
|
||
|
||
/* 导航栏 (简化版) */
|
||
.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; }
|
||
.bg-green-50 { background-color: #ECFDF5; }
|
||
.bg-orange-50 { background-color: #FFF7ED; }
|
||
.text-blue-700 { color: #1D4ED8; }
|
||
.text-green-700 { color: #047857; }
|
||
.text-orange-700 { color: #C2410C; }
|
||
.text-xs { font-size: 22rpx; }
|
||
|
||
/* 资产配置列表 */
|
||
.assets-list {
|
||
margin-top: 16rpx;
|
||
}
|
||
.asset-item {
|
||
background-color: #F9FAFB;
|
||
border-radius: 16rpx;
|
||
padding: 20rpx;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
.asset-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
.asset-title {
|
||
font-size: 24rpx;
|
||
font-weight: 600;
|
||
color: #374151;
|
||
}
|
||
.asset-inputs {
|
||
display: flex;
|
||
gap: 16rpx;
|
||
}
|
||
.asset-input {
|
||
flex: 1;
|
||
}
|
||
.asset-label {
|
||
font-size: 22rpx;
|
||
color: #6B7280;
|
||
margin-bottom: 8rpx;
|
||
display: block;
|
||
}
|
||
.add-asset-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8rpx;
|
||
background-color: #ECFDF5;
|
||
border: 2rpx dashed #10B981;
|
||
border-radius: 16rpx;
|
||
padding: 20rpx;
|
||
margin-top: 8rpx;
|
||
}
|
||
.add-asset-btn:active {
|
||
background-color: #D1FAFA;
|
||
}
|
||
.add-asset-text {
|
||
font-size: 24rpx;
|
||
color: #064E3B;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.stock-input {
|
||
width: 100%;
|
||
height: 72rpx;
|
||
background-color: #F9FAFB;
|
||
border: 2rpx solid #E5E7EB;
|
||
border-radius: 16rpx;
|
||
padding: 0 20rpx;
|
||
font-size: 26rpx;
|
||
color: #1F2937;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.form-input {
|
||
width: 100%;
|
||
height: 88rpx;
|
||
background-color: #F9FAFB;
|
||
border: 2rpx solid #E5E7EB;
|
||
border-radius: 20rpx;
|
||
padding: 0 24rpx;
|
||
font-size: 28rpx;
|
||
color: #1F2937;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.form-input-sm {
|
||
width: 100%;
|
||
height: 72rpx;
|
||
background-color: #F9FAFB;
|
||
border: 2rpx solid #E5E7EB;
|
||
border-radius: 16rpx;
|
||
padding: 0 20rpx;
|
||
font-size: 26rpx;
|
||
color: #1F2937;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
/* 搜索下拉列表 */
|
||
.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-left {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
gap: 12rpx;
|
||
}
|
||
|
||
.item-ticker {
|
||
font-size: 26rpx;
|
||
font-weight: 600;
|
||
color: #1F2937;
|
||
}
|
||
|
||
.item-type {
|
||
font-size: 18rpx;
|
||
color: #064E3B;
|
||
background-color: #D1FAE5;
|
||
padding: 2rpx 8rpx;
|
||
border-radius: 4rpx;
|
||
}
|
||
|
||
.item-exchange {
|
||
font-size: 22rpx;
|
||
color: #9CA3AF;
|
||
}
|
||
|
||
/* 底部悬浮按钮栏 */
|
||
.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;
|
||
}
|
||
.btn-row {
|
||
display: flex;
|
||
gap: 16rpx;
|
||
}
|
||
.btn-save {
|
||
flex: 1;
|
||
background-color: #064E3B;
|
||
color: #fff;
|
||
font-weight: 700;
|
||
border-radius: 24rpx;
|
||
height: 96rpx;
|
||
font-size: 30rpx;
|
||
border: none;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.btn-save::after { border: none; }
|
||
.btn-save:active { opacity: 0.9; }
|
||
.btn-full { width: 100%; }
|
||
.btn-delete {
|
||
flex: 0 0 180rpx;
|
||
background-color: #FEE2E2;
|
||
color: #DC2626;
|
||
font-weight: 700;
|
||
border-radius: 24rpx;
|
||
height: 96rpx;
|
||
font-size: 24rpx;
|
||
border: none;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.btn-delete::after { border: none; }
|
||
.btn-delete:active { background-color: #FECACA; }
|
||
</style> |