feat: 新增股票代码实时搜索功能(新建组合/增加交易/策略编辑)
This commit is contained in:
parent
4a14739be8
commit
8d9619b51d
@ -49,9 +49,27 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="item-grid">
|
<view class="item-grid">
|
||||||
<view class="grid-col">
|
<view class="grid-col relative">
|
||||||
<text class="sub-label">单元名称/代码</text>
|
<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>
|
||||||
<view class="grid-col">
|
<view class="grid-col">
|
||||||
<text class="sub-label">买入均价</text>
|
<text class="sub-label">买入均价</text>
|
||||||
@ -89,11 +107,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, getCurrentInstance } from 'vue';
|
import { ref, computed, watch } from 'vue';
|
||||||
import { onShow } from '@dcloudio/uni-app';
|
import { onShow } from '@dcloudio/uni-app';
|
||||||
|
import { api } from '../../utils/api';
|
||||||
const { proxy } = getCurrentInstance();
|
|
||||||
const api = proxy.$api;
|
|
||||||
|
|
||||||
const strategies = ref([]);
|
const strategies = ref([]);
|
||||||
const strategyIndex = ref(-1);
|
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(() => {
|
const selectedStrategy = computed(() => {
|
||||||
if (strategyIndex.value === -1) return null;
|
if (strategyIndex.value === -1) return null;
|
||||||
return strategies.value[strategyIndex.value];
|
return strategies.value[strategyIndex.value];
|
||||||
@ -420,6 +474,53 @@ onShow(async () => {
|
|||||||
cursor: not-allowed;
|
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 {
|
.date-picker-display {
|
||||||
background-color: #FFFFFF;
|
background-color: #FFFFFF;
|
||||||
|
|||||||
@ -155,13 +155,26 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="form-content">
|
<view class="form-content">
|
||||||
<view class="form-item">
|
<view class="form-item relative">
|
||||||
<text class="form-label">股票代码</text>
|
<text class="form-label">股票代码</text>
|
||||||
<input
|
<input
|
||||||
v-model="transactionForm.stockCode"
|
v-model="transactionForm.stockCode"
|
||||||
class="form-input"
|
class="form-input"
|
||||||
placeholder="请输入股票代码"
|
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>
|
||||||
|
|
||||||
<view class="form-item">
|
<view class="form-item">
|
||||||
@ -214,10 +227,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, getCurrentInstance } from 'vue';
|
import { ref, onMounted, watch } from 'vue';
|
||||||
|
import { api } from '../../utils/api';
|
||||||
const { proxy } = getCurrentInstance();
|
|
||||||
const api = proxy.$api;
|
|
||||||
|
|
||||||
const portfolioId = ref('');
|
const portfolioId = ref('');
|
||||||
const portfolioData = ref({
|
const portfolioData = ref({
|
||||||
@ -253,6 +264,35 @@ const transactionForm = ref({
|
|||||||
remark: ''
|
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 () => {
|
const fetchPortfolioData = async () => {
|
||||||
try {
|
try {
|
||||||
const pages = getCurrentPages();
|
const pages = getCurrentPages();
|
||||||
@ -629,6 +669,53 @@ const submitTransaction = async () => {
|
|||||||
color: #9CA3AF;
|
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 {
|
.form-select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 72rpx;
|
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>
|
<uni-icons type="trash" size="18" color="#EF4444" @click="removeAsset(index)" v-if="formData.assets.length > 1"></uni-icons>
|
||||||
</view>
|
</view>
|
||||||
<view class="asset-inputs">
|
<view class="asset-inputs">
|
||||||
<view class="asset-input">
|
<view class="asset-input relative">
|
||||||
<text class="asset-label">代码</text>
|
<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>
|
||||||
<view class="asset-input">
|
<view class="asset-input">
|
||||||
<text class="asset-label">目标权重</text>
|
<text class="asset-label">目标权重</text>
|
||||||
@ -161,10 +178,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, getCurrentInstance, onMounted } from 'vue';
|
import { ref, computed, onMounted } from 'vue';
|
||||||
|
import { api } from '../../../utils/api';
|
||||||
const { proxy } = getCurrentInstance();
|
|
||||||
const api = proxy.$api;
|
|
||||||
|
|
||||||
const isEditMode = ref(false);
|
const isEditMode = ref(false);
|
||||||
const strategyId = ref('');
|
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(() => {
|
const currentStrategyInfo = computed(() => {
|
||||||
return strategyTypes.find(item => item.key === currentType.value);
|
return strategyTypes.find(item => item.key === currentType.value);
|
||||||
@ -705,6 +760,53 @@ onMounted(() => {
|
|||||||
font-weight: 600;
|
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 {
|
.footer-bar {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|||||||
16
utils/api.js
16
utils/api.js
@ -419,6 +419,22 @@ export const api = {
|
|||||||
console.log('📤 发起 updateUserInfo 请求:', data);
|
console.log('📤 发起 updateUserInfo 请求:', data);
|
||||||
return put('/api/v1/user/info', 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