AssetManager.UniApp/pages/detail/detail.vue
claw_bot a76325882b fix: 修复编译错误
- config.vue: 删除重复的 onStrategyChange 声明
- detail.vue: 简化小程序中的input组件,去掉复杂样式和事件
2026-03-13 08:09:36 +00:00

1056 lines
34 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">
<!-- uView Toast 组件 -->
<u-toast ref="uToastRef" />
<view class="header-section">
<view class="asset-card">
<view class="card-watermark">
<uni-icons type="vip-filled" size="120" color="rgba(255,255,255,0.05)"></uni-icons>
</view>
<view class="card-top">
<text class="label-text">组合总额 (NV)</text>
<view class="status-badge">
<text class="status-text">账本追踪中</text>
</view>
</view>
<view class="card-main">
<text class="currency">{{ getCurrencySymbol(portfolioData.currency) }}</text>
<text class="big-number">{{ (portfolioData.portfolioValue || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }}</text>
</view>
<view class="card-bottom">
<view class="stat-item">
<text class="stat-label">总盈亏</text>
<text class="stat-val" :class="(portfolioData.totalReturn || 0) >= 0 ? 'text-red' : 'text-green'">
{{ (portfolioData.totalReturn || 0) >= 0 ? '+' : '' }}{{ getCurrencySymbol(portfolioData.currency) }}{{ (portfolioData.totalReturn || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }}
<text style="font-size: 22rpx; opacity: 0.8; margin-left: 8rpx;">({{ (portfolioData.historicalChange || 0) >= 0 ? '+' : '' }}{{ (portfolioData.historicalChange || 0).toFixed(2) }}%)</text>
</text>
</view>
<view class="stat-item align-right">
<text class="stat-label">当日盈亏</text>
<text class="stat-val" :class="(portfolioData.todayProfit || 0) >= 0 ? 'text-red' : 'text-green'">
{{ (portfolioData.todayProfit || 0) >= 0 ? '+' : '' }}{{ getCurrencySymbol(portfolioData.currency) }}{{ (portfolioData.todayProfit || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }}
</text>
</view>
</view>
</view>
</view>
<view class="section-container pb-0">
<view class="section-header">
<text class="section-title">当前逻辑模型</text>
<view class="flex-row items-center" @click="goStrategyConfig">
<text class="section-sub text-brand">参数配置</text>
<uni-icons type="right" size="12" color="#064E3B"></uni-icons>
</view>
</view>
<u-card
v-if="portfolioData.logicModel"
:border="false"
:shadow="false"
:show-head="false"
:show-foot="false"
class="strategy-info-card"
>
<template #body>
<view class="st-left">
<view class="st-icon-box bg-green-100">
<text class="st-icon-text text-green">{{ portfolioData.logicModel?.charAt(0) || 'S' }}</text>
</view>
<view class="flex-col gap-1">
<text class="st-name">{{ portfolioData.logicModel || '未设置策略' }}</text>
<view class="flex-row gap-2">
<text class="st-tag" v-if="portfolioData.logicModelDescription">{{ portfolioData.logicModelDescription }}</text>
</view>
</view>
</view>
<view class="st-right">
<view class="flex-row items-center gap-1">
<view class="status-dot pulsing"></view>
<text class="st-status-text">{{ portfolioData.logicModelStatus || '监控中' }}</text>
</view>
</view>
</template>
</u-card>
</view>
<view class="section-container">
<view class="section-header">
<text class="section-title">当前记录项 ({{ portfolioData.totalItems || positions.length }})</text>
<text class="section-sub">占比 {{ portfolioData.totalRatio || 100 }}%</text>
</view>
<view class="position-list">
<u-card
v-for="(item, index) in positions"
:key="item.id || index"
:border="false"
:shadow="false"
:show-head="false"
:show-foot="false"
class="position-card"
>
<template #body>
<view class="pos-top">
<view class="flex-row items-center gap-2">
<view class="stock-icon" :class="index % 2 === 0 ? 'bg-blue-100 text-blue' : 'bg-orange-100 text-orange'">
<text class="icon-char">{{ item.stockName?.charAt(0) || item.stockCode?.charAt(0) || 'S' }}</text>
</view>
<view class="flex-col flex-1">
<!-- 第一行:股票名 + 持仓市值 -->
<view class="flex-row justify-between items-center">
<text class="stock-name">{{ item.stockName || item.stockCode }}</text>
<text class="market-val">{{ getCurrencySymbol(portfolioData.currency) }}{{ (item.totalValue || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }}</text>
</view>
<!-- 第二行:代码+数量 + 现价 -->
<view class="flex-row justify-between items-center mt-1">
<text class="stock-code">{{ item.stockCode }} · {{ item.amount }}份</text>
<text class="price-text">{{ getCurrencySymbol(portfolioData.currency) }}{{ (item.currentPrice || 0).toLocaleString('zh-CN', { minimumFractionDigits: 3, maximumFractionDigits: 3 }) }}</text>
</view>
<!-- 第三行:占比 + 成本价 -->
<view class="flex-row justify-between items-center mt-1">
<text class="weight-tag">比例 {{ (item.ratio || 0).toFixed(1) }}%</text>
<text class="cost-text">{{ getCurrencySymbol(portfolioData.currency) }}{{ (item.averagePrice || 0).toLocaleString('zh-CN', { minimumFractionDigits: 3, maximumFractionDigits: 3 }) }}</text>
</view>
<!-- 第四行:(空) + 当日盈亏 -->
<view class="flex-row justify-between items-center mt-1">
<text></text>
<text class="pnl-val" :class="(item.changeAmount || 0) >= 0 ? 'text-red' : 'text-green'">
当日 {{ (item.changeAmount || 0) >= 0 ? '+' : '' }}{{ getCurrencySymbol(portfolioData.currency) }}{{ (item.changeAmount || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }}
</text>
</view>
<!-- 第五行:(空) + 持仓盈亏 -->
<view class="flex-row justify-between items-center mt-1">
<text></text>
<text class="pnl-val" :class="(item.profit || 0) >= 0 ? 'text-red' : 'text-green'">
持仓 {{ (item.profit || 0) >= 0 ? '+' : '' }}{{ getCurrencySymbol(portfolioData.currency) }}{{ (item.profit || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }}
<text class="pnl-rate"> ({{ (item.profitRate || 0) >= 0 ? '+' : '' }}{{ (item.profitRate || 0).toFixed(2) }}%)</text>
</text>
</view>
</view>
</view>
</view>
</template>
</u-card>
</view>
</view>
<view class="section-container">
<view class="section-header">
<text class="section-title">最近交易记录</text>
</view>
<view class="timeline-box">
<view class="timeline-item" v-for="(log, k) in logs" :key="k">
<view class="tl-left">
<text class="tl-date">{{ log.date }}</text>
<text class="tl-time">{{ log.time }}</text>
</view>
<view class="tl-line">
<view class="tl-dot" :class="log.type === 'buy' ? 'bg-red' : 'bg-green'"></view>
<view class="tl-dash" v-if="k !== logs.length - 1"></view>
</view>
<view class="tl-right">
<view class="flex-row justify-between items-center w-full">
<text class="tl-title">{{ log.title }} {{ log.stockCode || '' }}</text>
<text class="tl-amount" :class="log.type === 'buy' ? 'text-red' : 'text-green'">
{{ log.type === 'buy' ? '+' : '-' }}{{ getCurrencySymbol(portfolioData.currency) }}{{ (log.amount || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }}
</text>
</view>
<text class="tl-desc mt-1">{{ log.type === 'buy' ? '买入' : '卖出' }} 操作</text>
</view>
</view>
</view>
</view>
<view class="action-section fixed-bottom">
<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>
</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>
</u-button>
</view>
<!-- 交易表单弹窗 -->
<view v-if="showTransactionForm" class="transaction-modal" @click="showTransactionForm = false">
<view class="modal-content" @click.stop>
<view class="modal-header">
<text class="modal-title">{{ transactionType === 'buy' ? '增加' : '减少' }}</text>
<view class="close-btn" @click="showTransactionForm = false">
<uni-icons type="close" size="20" color="#6B7280"></uni-icons>
</view>
</view>
<view class="form-content">
<view class="form-item">
<text class="form-label">{{ transactionType === 'sell' ? '选择持仓' : '股票代码' }}</text>
<view class="relative">
<!-- #ifdef H5 || APP-PLUS -->
<u-input
v-model="transactionForm.stockCode"
: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"
/>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<input
v-model="transactionForm.stockCode"
placeholder="请输入股票代码"
disabled
/>
<!-- #endif -->
<!-- #ifdef H5 || APP-PLUS -->
<!-- 搜索下拉列表 -->
<view class="search-dropdown" v-if="searchResults.length > 0">
<view
class="dropdown-item"
v-for="(result, idx) in searchResults"
:key="idx"
@click="selectStock(result)"
>
<view class="item-left">
<text class="item-ticker">{{ result.ticker || result.stockCode }}</text>
<text class="item-name">{{ result.name || result.stockName }}</text>
</view>
<view class="item-right">
<text class="item-type" v-if="result.assetType">{{ result.assetType }}</text>
<text class="item-exchange">{{ result.exchange || '' }}</text>
</view>
</view>
</view>
<!-- #endif -->
</view>
</view>
<view class="form-item">
<text class="form-label">数量{{ transactionType === 'sell' && maxSellAmount > 0 ? ` (最多可卖 ${maxSellAmount} 份)` : '' }}</text>
<u-input
v-model="transactionForm.amount"
type="number"
:placeholder="transactionType === 'sell' && maxSellAmount > 0 ? `请输入数量,不超过 ${maxSellAmount}` : '请输入数量'"
:border="false"
:customStyle="{ backgroundColor: '#F9FAFB', borderRadius: '16rpx', height: '80rpx', padding: '0 20rpx', border: '2rpx solid #E5E7EB' }"
/>
</view>
<view class="form-item">
<text class="form-label">价格</text>
<u-input
v-model="transactionForm.price"
type="digit"
placeholder="请输入价格"
:border="false"
:customStyle="{ backgroundColor: '#F9FAFB', borderRadius: '16rpx', height: '80rpx', padding: '0 20rpx', border: '2rpx solid #E5E7EB' }"
/>
</view>
<view class="form-item">
<text class="form-label">交易时间</text>
<u-datetime-picker
v-model="transactionForm.dateTimestamp"
mode="date"
@confirm="onDateChange"
>
<view class="form-select">
<text>{{ transactionForm.transactionDate || '请选择日期' }}</text>
<u-icon name="arrow-down" size="14" color="#9CA3AF"></u-icon>
</view>
</u-datetime-picker>
</view>
<view class="form-item">
<text class="form-label">备注</text>
<u-input
v-model="transactionForm.remark"
placeholder="请输入备注(可选)"
:border="false"
:customStyle="{ backgroundColor: '#F9FAFB', borderRadius: '16rpx', height: '80rpx', padding: '0 20rpx', border: '2rpx solid #E5E7EB' }"
/>
</view>
</view>
<view class="modal-footer">
<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>
</view>
</template>
<script setup>
import { ref, onMounted, getCurrentInstance } from 'vue';
import { api } from '../../utils/api';
// 获取 u-toast 实例
const { proxy } = getCurrentInstance();
const uToastRef = ref();
// 获取货币符号
const getCurrencySymbol = (currency) => {
const symbols = {
'CNY': '¥',
'USD': '$',
'HKD': 'HK$'
};
return symbols[currency] || '¥';
};
const portfolioId = ref('');
const portfolioData = ref({
id: '',
name: '',
currency: 'CNY',
status: '',
portfolioValue: 0,
totalReturn: 0,
todayProfit: 0,
todayProfitCurrency: 'CNY',
historicalChange: 0,
dailyVolatility: 0,
logicModel: '',
logicModelStatus: '',
logicModelDescription: '',
totalItems: 0,
totalRatio: 100,
strategy: null
});
const positions = ref([]);
const logs = ref([]);
// 交易表单
const showTransactionForm = ref(false);
const transactionType = ref('buy'); // buy 或 sell
// 获取当前日期格式化为YYYY-MM-DD
const getCurrentDate = () => {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
const transactionForm = ref({
stockCode: '',
amount: '',
price: '',
currency: '',
transactionDate: getCurrentDate(),
dateTimestamp: Date.now(),
remark: ''
});
const maxSellAmount = ref(0);
// 股票搜索相关
const searchResults = ref([]);
const searchTimer = ref(null);
const handleStockInputClick = () => {
// #ifdef H5 || APP-PLUS
if (transactionType.value === 'sell') {
searchResults.value = positions.value.map(pos => ({
ticker: pos.stockCode,
stockCode: pos.stockCode,
stockName: pos.stockName,
name: pos.stockName,
assetType: pos.assetType || 'Stock',
currency: pos.currency,
amount: pos.amount,
exchange: ''
}));
}
};
const onStockInput = (e) => {
// #ifdef H5 || APP-PLUS
const keyword = e.detail.value;
console.log('🔍 股票输入:', keyword);
searchStock(keyword);
// #endif
// #ifdef MP-WEIXIN
// 小程序中不触发搜索,直接输入代码
// #endif
};
const searchStock = async (keyword) => {
console.log('🔍 searchStock 调用:', keyword);
if (searchTimer.value) clearTimeout(searchTimer.value);
if (!keyword || keyword.length < 1) {
searchResults.value = [];
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;
}
} catch (err) {
console.error('搜索股票失败:', err);
searchResults.value = [];
}
}, 300);
};
const selectStock = (result) => {
transactionForm.value.stockCode = result.ticker || result.Ticker || result.stockCode;
const currency = result.priceCurrency || result.currency;
if (currency) {
transactionForm.value.currency = currency;
}
if (transactionType.value === 'sell') {
const position = positions.value.find(pos => pos.stockCode === transactionForm.value.stockCode);
maxSellAmount.value = position ? position.amount : 0;
} else {
maxSellAmount.value = 0;
}
searchResults.value = [];
};
const fetchPortfolioData = async () => {
try {
const pages = getCurrentPages();
const currentPage = pages[pages.length - 1];
const id = currentPage.options?.id;
if (!id) {
console.error('缺少投资组合ID');
return;
}
portfolioId.value = id;
const response = await api.assets.getPortfolio(id);
if (response.code === 200) {
portfolioData.value = response.data;
positions.value = response.data.positions || [];
console.log('投资组合数据获取成功:', response.data);
}
} catch (error) {
console.error('获取投资组合数据失败:', error);
proxy?.$refs.uToastRef?.show({
type: 'error',
message: '加载失败,请重试',
icon: 'error'
});
}
};
const fetchTransactions = async () => {
try {
if (!portfolioId.value) return;
const response = await api.assets.getTransactions({
portfolioId: portfolioId.value,
limit: 10,
offset: 0
});
if (response.code === 200) {
logs.value = response.data.items || [];
console.log('交易记录获取成功:', response.data);
}
} catch (error) {
console.error('获取交易记录失败:', error);
proxy?.$refs.uToastRef?.show({
type: 'error',
message: '加载交易记录失败',
icon: 'error'
});
}
};
onMounted(async () => {
await fetchPortfolioData();
await fetchTransactions();
});
const goStrategyConfig = () => {
if (portfolioData.value.strategy?.id) {
uni.navigateTo({ url: `/pages/strategies/edit/edit?id=${portfolioData.value.strategy.id}` });
}
};
const handleBuy = () => {
transactionType.value = 'buy';
resetTransactionForm();
transactionForm.value.currency = portfolioData.value.currency;
showTransactionForm.value = true;
};
const handleSell = () => {
transactionType.value = 'sell';
resetTransactionForm();
transactionForm.value.currency = portfolioData.value.currency;
showTransactionForm.value = true;
};
const resetTransactionForm = () => {
transactionForm.value = {
stockCode: '',
amount: '',
price: '',
currency: 'CNY',
transactionDate: getCurrentDate(),
dateTimestamp: Date.now(),
remark: ''
};
searchResults.value = [];
maxSellAmount.value = 0;
};
const onDateChange = (e) => {
const date = new Date(e.value);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
transactionForm.value.transactionDate = `${year}-${month}-${day}`;
transactionForm.value.dateTimestamp = e.value;
};
const submitTransaction = async () => {
// 表单验证
if (!transactionForm.value.stockCode) {
proxy?.$refs.uToastRef?.show({
type: 'warning',
message: transactionType.value === 'sell' ? '请选择要卖出的持仓' : '请输入股票代码',
icon: 'warning'
});
return;
}
const amount = parseFloat(transactionForm.value.amount);
if (!amount || amount <= 0) {
proxy?.$refs.uToastRef?.show({
type: 'warning',
message: '请输入有效的数量',
icon: 'warning'
});
return;
}
// 卖出时校验数量不超过持仓
if (transactionType.value === 'sell' && amount > maxSellAmount.value) {
proxy?.$refs.uToastRef?.show({
type: 'warning',
message: `卖出数量不能超过持仓数量 ${maxSellAmount.value}`,
icon: 'warning'
});
return;
}
if (!transactionForm.value.price || parseFloat(transactionForm.value.price) <= 0) {
proxy?.$refs.uToastRef?.show({
type: 'warning',
message: '请输入有效的价格',
icon: 'warning'
});
return;
}
const transactionData = {
portfolioId: portfolioId.value,
type: transactionType.value,
stockCode: transactionForm.value.stockCode,
amount: parseFloat(transactionForm.value.amount),
price: parseFloat(transactionForm.value.price),
currency: transactionForm.value.currency,
transactionDate: transactionForm.value.transactionDate,
remark: transactionForm.value.remark
};
uni.showLoading({ title: '提交中...', mask: true });
try {
const response = await api.assets.createTransaction(transactionData);
if (response.code === 200) {
uni.hideLoading();
proxy?.$refs.uToastRef?.show({
type: 'success',
message: '交易提交成功',
icon: 'success'
});
showTransactionForm.value = false;
// 重新获取交易记录
await fetchTransactions();
// 重新获取投资组合数据
await fetchPortfolioData();
}
} catch (error) {
console.error('创建交易失败:', error);
uni.hideLoading();
proxy?.$refs.uToastRef?.show({
type: 'error',
message: '提交失败,请重试',
icon: 'error'
});
}
};
// 删除组合
const deletePortfolio = async () => {
uni.showModal({
title: '确认删除',
content: '删除后所有持仓和交易记录都会丢失,确定要删除这个组合吗?',
confirmText: '删除',
confirmColor: '#EF4444',
success: async (res) => {
if (res.confirm) {
uni.showLoading({ title: '删除中', mask: true });
try {
// 调用删除组合接口
const response = await uni.request({
url: `${import.meta.env.VITE_API_BASE_URL || 'https://localhost:7040/'}api/v1/portfolio/${portfolioId.value}`,
method: 'DELETE',
header: {
'Authorization': `Bearer ${uni.getStorageSync('token')}`
}
});
if (response.statusCode === 200) {
uni.hideLoading();
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();
proxy?.$refs.uToastRef?.show({
type: 'error',
message: '删除失败,请重试',
icon: 'error'
});
}
}
}
});
};
</script>
<style scoped>
/* 基础设置 */
.page-container {
min-height: 100vh;
background-color: #F9FAFB;
/* 关键:底部留出空间,防止内容被固定按钮遮挡 */
padding-bottom: 180rpx;
}
.flex-row { display: flex; flex-direction: row; }
.flex-col { display: flex; flex-direction: column; }
.items-center { align-items: center; }
.justify-between { justify-content: space-between; }
.gap-1 { gap: 8rpx; }
.gap-2 { gap: 16rpx; }
.align-right { align-items: flex-end; }
.pb-0 { padding-bottom: 0 !important; }
.mt-1 { margin-top: 8rpx; }
.flex-1 { flex: 1; }
/* 颜色工具 */
.text-red { color: #EF4444; }
.text-green { color: #10B981; }
.text-brand { color: #064E3B; }
.bg-blue-100 { background-color: #EFF6FF; }
.text-blue { color: #2563EB; }
.bg-orange-100 { background-color: #FFF7ED; }
.text-orange { color: #EA580C; }
.bg-green-100 { background-color: #ECFDF5; }
.bg-red { background-color: #EF4444; }
.bg-green { background-color: #10B981; }
.header-section { padding: 20rpx 32rpx; }
.asset-card {
background-color: #064E3B;
border-radius: 40rpx;
padding: 40rpx;
position: relative;
overflow: hidden;
box-shadow: 0 10rpx 30rpx rgba(6, 78, 59, 0.25);
color: #fff;
}
.card-watermark { position: absolute; right: -20rpx; top: -20rpx; opacity: 0.1; transform: rotate(15deg); }
.card-top { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20rpx; }
.label-text { font-size: 26rpx; opacity: 0.8; }
.status-badge { background-color: rgba(255,255,255,0.2); padding: 4rpx 16rpx; border-radius: 20rpx; }
.status-text { font-size: 22rpx; font-weight: 600; }
.card-main { display: flex; align-items: baseline; margin-bottom: 40rpx; }
.currency { font-size: 40rpx; font-weight: 700; margin-right: 8rpx; }
.big-number { font-size: 64rpx; font-weight: 800; font-family: 'DIN Alternate'; }
.card-bottom { display: flex; justify-content: space-between; }
.stat-item { display: flex; flex-direction: column; gap: 8rpx; }
.stat-label { font-size: 24rpx; opacity: 0.7; }
.stat-val { font-size: 32rpx; font-weight: 700; font-family: 'DIN Alternate'; }
/* 策略信息卡片 */
.strategy-info-card {
border-radius: 24rpx;
padding: 24rpx;
display: flex;
justify-content: space-between;
align-items: center;
}
.st-left { display: flex; align-items: center; gap: 20rpx; }
.st-icon-box {
width: 80rpx; height: 80rpx;
border-radius: 20rpx;
display: flex; align-items: center; justify-content: center;
}
.st-icon-text { font-size: 36rpx; font-weight: 800; }
.st-name { font-size: 28rpx; font-weight: 700; color: #1F2937; }
.st-tag {
font-size: 20rpx; color: #6B7280;
background-color: #F3F4F6; padding: 2rpx 10rpx; border-radius: 8rpx;
}
.st-status-text { font-size: 24rpx; font-weight: 600; color: #059669; }
.status-dot {
width: 12rpx; height: 12rpx; border-radius: 50%;
background-color: #10B981;
}
.pulsing { animation: pulse 2s infinite; }
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.4); }
70% { box-shadow: 0 0 0 10rpx rgba(16, 185, 129, 0); }
100% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); }
}
/* 通用容器 */
.section-container { padding: 20rpx 32rpx; }
.section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24rpx; }
.section-title { font-size: 30rpx; font-weight: 800; color: #1F2937; border-left: 8rpx solid #064E3B; padding-left: 16rpx; line-height: 1; }
.section-sub { font-size: 24rpx; color: #9CA3AF; margin-right: 4rpx; }
/* 持仓卡片 */
.position-card {
border-radius: 24rpx;
padding: 32rpx;
margin-bottom: 24rpx;
}
.stock-icon { width: 80rpx; height: 80rpx; border-radius: 20rpx; display: flex; align-items: center; justify-content: center; }
.icon-char { font-size: 32rpx; font-weight: 800; }
.stock-name { font-size: 30rpx; font-weight: 700; color: #1F2937; }
.stock-code { font-size: 24rpx; color: #9CA3AF; margin-top: 4rpx; }
.market-val { font-size: 30rpx; font-weight: 700; color: #1F2937; }
.price-text { font-size: 26rpx; color: #1F2937; }
.cost-text { font-size: 24rpx; color: #6B7280; }
.weight-tag { font-size: 22rpx; color: #6B7280; background-color: #F3F4F6; padding: 2rpx 8rpx; border-radius: 6rpx; }
.pnl-val { font-size: 26rpx; font-weight: 700; }
.pnl-rate { font-size: 22rpx; opacity: 0.9; }
/* 交易明细 */
.timeline-box { padding: 0 16rpx; }
.timeline-item { display: flex; margin-bottom: 0; min-height: 120rpx; }
.tl-left { width: 120rpx; text-align: right; padding-right: 24rpx; display: flex; flex-direction: column; }
.tl-date { font-size: 26rpx; font-weight: 600; color: #374151; }
.tl-time { font-size: 22rpx; color: #9CA3AF; margin-top: 4rpx; }
.tl-line { width: 40rpx; display: flex; flex-direction: column; align-items: center; position: relative; }
.tl-dot { width: 16rpx; height: 16rpx; border-radius: 50%; z-index: 2; margin-top: 10rpx; }
.tl-dash { width: 2rpx; flex: 1; background-color: #E5E7EB; margin-top: 8rpx; }
.tl-right { flex: 1; padding-left: 24rpx; padding-bottom: 40rpx; }
.tl-title { font-size: 28rpx; font-weight: 600; color: #1F2937; }
.tl-amount { font-size: 26rpx; font-weight: 700; }
.tl-desc { font-size: 24rpx; color: #6B7280; margin-top: 8rpx; }
.w-full { width: 100%; }
.fixed-bottom {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #FFFFFF;
display: flex;
gap: 24rpx;
padding: 20rpx 32rpx 50rpx 32rpx;
box-shadow: 0 -4rpx 16rpx rgba(0,0,0,0.05);
z-index: 999;
}
/* .btn-delete 样式已通过 u-button 的 customStyle 设置,此处保留空类避免冲突 */
.btn-delete {
/* 样式已内联设置 */
}
/* .btn-buy 样式已通过 u-button 的 customStyle 设置,此处保留空类避免冲突 */
.btn-buy {
/* 样式已内联设置 */
}
/* .btn-sell 样式已通过 u-button 的 customStyle 设置,此处保留空类避免冲突 */
.btn-sell {
/* 样式已内联设置 */
}
.transaction-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: flex-end;
justify-content: center;
z-index: 1000;
}
.modal-content {
background-color: #fff;
border-radius: 24rpx 24rpx 0 0;
width: 100%;
max-height: 80vh;
overflow-y: auto;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32rpx;
border-bottom: 1rpx solid #E5E7EB;
}
.modal-title {
font-size: 32rpx;
font-weight: 600;
color: #111827;
}
.close-btn {
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
}
.form-content {
padding: 32rpx;
}
.form-item {
margin-bottom: 24rpx;
}
.form-label {
display: block;
font-size: 24rpx;
font-weight: 500;
color: #374151;
margin-bottom: 12rpx;
}
/* .stock-input 已替换为 u-input样式已通过 customStyle 设置 */
.stock-input {
/* 样式已内联设置 */
}
.form-select {
width: 100%;
height: 80rpx;
background-color: #F9FAFB;
border: 2rpx solid #E5E7EB;
border-radius: 16rpx;
padding: 0 20rpx;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 26rpx;
color: #1F2937;
}
.modal-footer {
padding: 32rpx;
border-top: 1rpx solid #E5E7EB;
display: flex;
gap: 16rpx;
}
/* .btn-cancel 和 .btn-confirm 样式已通过 u-button 的 customStyle 设置,此处保留空类避免冲突 */
.btn-cancel,
.btn-confirm {
/* 样式已内联设置 */
}
.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: column;
gap: 4rpx;
}
.item-ticker {
font-size: 26rpx;
font-weight: 600;
color: #1F2937;
}
.item-name {
font-size: 22rpx;
color: #6B7280;
}
.item-right {
display: flex;
flex-direction: row;
align-items: center;
gap: 8rpx;
}
.item-type {
font-size: 18rpx;
color: #064E3B;
background-color: #D1FAE5;
padding: 2rpx 8rpx;
border-radius: 4rpx;
}
.item-exchange {
font-size: 22rpx;
color: #9CA3AF;
}
</style>