feat: 新增股票代码实时搜索功能(新建组合/增加交易/策略编辑)
This commit is contained in:
parent
4a14739be8
commit
8d9619b51d
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
16
utils/api.js
16
utils/api.js
@ -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 });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user