feat: 新增股票代码实时搜索功能(新建组合/增加交易/策略编辑)

This commit is contained in:
虾球 2026-03-06 14:25:19 +00:00
parent 4a14739be8
commit 8d9619b51d
4 changed files with 323 additions and 17 deletions

View File

@ -49,9 +49,27 @@
</view>
<view class="item-grid">
<view class="grid-col">
<view class="grid-col relative">
<text class="sub-label">单元名称/代码</text>
<input class="mini-input" v-model="item.name" placeholder="如 TMF" disabled />
<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>
@ -89,11 +107,9 @@
</template>
<script setup>
import { ref, computed, getCurrentInstance } from 'vue';
import { ref, computed, watch } from 'vue';
import { onShow } from '@dcloudio/uni-app';
const { proxy } = getCurrentInstance();
const api = proxy.$api;
import { api } from '../../utils/api';
const strategies = ref([]);
const strategyIndex = ref(-1);
@ -108,6 +124,44 @@ const form = ref({
]
});
//
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];
@ -420,6 +474,53 @@ onShow(async () => {
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;

View File

@ -155,13 +155,26 @@
</view>
<view class="form-content">
<view class="form-item">
<view class="form-item relative">
<text class="form-label">股票代码</text>
<input
v-model="transactionForm.stockCode"
class="form-input"
placeholder="请输入股票代码"
@input="(e) => searchStock(e.detail.value)"
/>
<!-- 搜索下拉列表 -->
<view class="search-dropdown" v-if="searchResults.length > 0">
<view
class="dropdown-item"
v-for="(result, idx) in searchResults"
:key="idx"
@click="selectStock(result)"
>
<text class="item-ticker">{{ result.Ticker }}</text>
<text class="item-exchange">{{ result.Exchange }}</text>
</view>
</view>
</view>
<view class="form-item">
@ -214,10 +227,8 @@
</template>
<script setup>
import { ref, onMounted, getCurrentInstance } from 'vue';
const { proxy } = getCurrentInstance();
const api = proxy.$api;
import { ref, onMounted, watch } from 'vue';
import { api } from '../../utils/api';
const portfolioId = ref('');
const portfolioData = ref({
@ -253,6 +264,35 @@ const transactionForm = ref({
remark: ''
});
//
const searchResults = ref([]);
const searchTimer = ref(null);
const searchStock = async (keyword) => {
//
if (searchTimer.value) clearTimeout(searchTimer.value);
if (!keyword || keyword.length < 2) {
searchResults.value = [];
return;
}
searchTimer.value = setTimeout(async () => {
try {
const res = await api.ticker.search(keyword);
if (res.code === 200) {
searchResults.value = res.data;
}
} catch (err) {
console.error('搜索股票失败:', err);
searchResults.value = [];
}
}, 300);
};
const selectStock = (result) => {
transactionForm.value.stockCode = result.Ticker;
searchResults.value = [];
};
const fetchPortfolioData = async () => {
try {
const pages = getCurrentPages();
@ -629,6 +669,53 @@ const submitTransaction = async () => {
color: #9CA3AF;
}
/* 搜索下拉列表 */
.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;
}
.form-select {
width: 100%;
height: 72rpx;

View File

@ -105,9 +105,26 @@
<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">
<view class="asset-input relative">
<text class="asset-label">代码</text>
<input class="input-field" v-model="asset.symbol" placeholder="如 AAPL" />
<input
class="input-field"
v-model="asset.symbol"
placeholder="如 AAPL"
@input="(e) => searchStock(e.detail.value, 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)"
>
<text class="item-ticker">{{ result.Ticker }}</text>
<text class="item-exchange">{{ result.Exchange }}</text>
</view>
</view>
</view>
<view class="asset-input">
<text class="asset-label">目标权重</text>
@ -161,10 +178,8 @@
</template>
<script setup>
import { ref, computed, getCurrentInstance, onMounted } from 'vue';
const { proxy } = getCurrentInstance();
const api = proxy.$api;
import { ref, computed, onMounted } from 'vue';
import { api } from '../../../utils/api';
const isEditMode = ref(false);
const strategyId = ref('');
@ -218,6 +233,46 @@ const formData = ref({
]
});
//
const searchResults = ref([]);
const activeAssetIndex = ref(-1);
const searchTimer = ref(null);
const searchStock = async (keyword, assetIndex) => {
//
if (searchTimer.value) clearTimeout(searchTimer.value);
if (!keyword || keyword.length < 2) {
searchResults.value = [];
activeAssetIndex.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,
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);
@ -705,6 +760,53 @@ onMounted(() => {
font-weight: 600;
}
/* 搜索下拉列表 */
.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;
}
/* 底部悬浮按钮栏 */
.footer-bar {
position: fixed;

View File

@ -419,6 +419,22 @@ export const api = {
console.log('📤 发起 updateUserInfo 请求:', data);
return put('/api/v1/user/info', data);
}
},
/**
* 股票代码相关接口
*/
ticker: {
/**
* 模糊搜索股票代码
* @param {string} keyword - 搜索关键词
* @param {number} limit - 返回数量上限
* @returns {Promise} 返回搜索结果
*/
search: (keyword, limit = 20) => {
console.log('📤 发起 ticker.search 请求:', { keyword, limit });
return get('/api/v1/ticker/search', { keyword, limit });
}
}
};