AssetManager.UniApp/pages/strategies/edit/edit.vue
claw_bot bc313d92aa fix: 策略编辑页面资产配置填充问题 - 增强调试日志
- 添加 JSON 解析容错处理
- 增加详细调试日志输出
- 检查 config 是否为字符串类型再解析
2026-03-14 10:28:11 +00:00

1008 lines
28 KiB
Vue
Executable File
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
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;
console.log('📊 从 parameters 获取参数:', params);
} else if (data.config) {
try {
params = typeof data.config === 'string' ? JSON.parse(data.config) : data.config;
console.log('📊 从 config 解析参数:', params);
} catch (e) {
console.error('📊 config 解析失败:', e, '原始值:', data.config);
params = {};
}
}
console.log('📊 策略类型:', data.type, '参数:', JSON.stringify(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:', JSON.stringify(params.assets));
if (params.assets && Array.isArray(params.assets) && params.assets.length > 0) {
// 使用 splice 保持响应式引用
formData.value.assets.splice(0, formData.value.assets.length);
params.assets.forEach(asset => {
formData.value.assets.push({
symbol: asset.symbol || '',
targetWeight: asset.targetWeight != null ? String(asset.targetWeight) : ''
});
});
console.log('📊 填充后的 formData.assets:', JSON.stringify(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>