一、父组件代码:
<template>
<div class="bottom-box">
<!-- 股票收藏板块 -->
<StockCollect v-if="id === 'stockCollect'"></StockCollect>
</div>
</template>
<script setup lang="ts">
import StockCollect from '@/views/personal-center/components/stockCollect.vue'
</script>
<style lang="less" scoped>
.bottom-box {
margin: 0 20px;
}
</style>
二、子组件代码:
<template>
<div class="stock-collect">
<div class="top-box">
<div
class="add-btn"
:style="{
cursor: isAdmin && route.query.userId ? 'no-drop' : 'pointer'
}"
@click="isAdmin && route.query.userId ? '' : showAddDialog()"
>
添加股票收藏
</div>
<div class="right-box">
<el-input
v-model="searchVal"
:maxlength="6"
placeholder="请输入股票关键字或代码"
clearable
class="ipt"
/>
<div class="search-btn" @click="searchUserCollectCompany">搜索</div>
</div>
</div>
<div class="stock-list">
<div
class="check-top"
v-if="collectStockList && collectStockList.length > 0"
>
<el-checkbox
v-model="checkAll"
:disabled="isAdmin && route.query.userId ? true : false"
:indeterminate="isIndeterminate"
@change="checkAllChange"
class="select-name"
>
全选
</el-checkbox>
<div
class="delete-btn"
:style="{
cursor: isAdmin && route.query.userId ? 'no-drop' : 'pointer'
}"
@click="isAdmin && route.query.userId ? '' : deleteStock()"
>
删除
</div>
</div>
<el-checkbox-group
v-model="checkedStockList"
@change="checkedChange"
:disabled="isAdmin && route.query.userId ? true : false"
class="check-list"
v-if="collectStockList && collectStockList.length > 0"
>
<el-checkbox
v-for="item in collectStockList"
:key="item.companyId"
:value="item.companyId"
:label="item.companyId"
class="stock-check"
>
{{ item.companyName }} [{{ item.symbol }}]
</el-checkbox>
</el-checkbox-group>
<div class="nodata" v-else>暂无收藏的股票</div>
</div>
<!-- 添加账号 -->
<stock-check
:dialog-visible="stockCheckVisible"
:title="stockTitle"
:data-list="dataList"
:current-index="currentIndex"
:loading="loading"
@current-index-change="currentIndexChange"
@cancel="stockCheckCancel"
@summit="stockCheckSummit"
/>
</div>
</template>
<script lang="ts" setup>
import { ElMessage, ElMessageBox } from 'element-plus'
import StockCheck from '@/components/StockCheck.vue'
import {
queryUserCollectCompany,
queryUserCompanyTypeDto,
delUserCollectCompany,
addUserCollectCompany,
adminCheckStockList
} from '@/service/stockIndex/index'
const route = useRoute()
const stockCheckVisible = ref(false) // 添加股票弹框
//弹窗title
const stockTitle = ref('添加股票收藏')
const checkAll = ref(false) // 是否全选
const isIndeterminate = ref(false) // 全选中状态
const checkedStockList = ref<any>([]) // 选中的股票列表
const collectStockList = ref<any>([]) // 收藏的股票列表
//添加或编辑账号的所有股票列表数据
const dataList = ref<any>([])
const searchVal = ref<any>('') // 搜索框的值
const isAdmin = ref(false) // 是否管理员
const userInfo = ref<any>({}) // 用户信息
//0 地区分类 1交易所分类
const currentIndex = ref(0)
//弹窗数据请求loading
const loading = ref(false)
// 全选
const checkAllChange = (val: any) => {
let arr: number[] = []
collectStockList.value.forEach((v: any) => {
v.checked = val
arr.push(v.companyId)
})
checkedStockList.value = val ? arr : []
isIndeterminate.value = false
}
// 单个股票勾选
const checkedChange = (value: any) => {
collectStockList.value.forEach((v: any) => {
v.checked = value.includes(v.companyId)
})
const checkedCount = collectStockList.value.filter(
(v: any) => v.checked
).length
checkAll.value = collectStockList.value.every((dto: any) => dto.checked)
isIndeterminate.value =
checkedCount > 0 && checkedCount < collectStockList.value.length
}
// 显示添加弹框
const showAddDialog = () => {
stockCheckVisible.value = true
checkAll.value = false
isIndeterminate.value = false
checkedStockList.value = []
getcheckDataList()
}
// 删除收藏的股票
const deleteStock = () => {
if (checkedStockList.value.length === 0) {
ElMessage({
type: 'error',
message: '请选择要删除的股票'
})
} else {
ElMessageBox.confirm('确认删除选中的股票?', '提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
autofocus: false,
cancelButtonClass: 'cancle-btn',
type: 'warning'
})
.then(() => {
delUserCollectCompany(
checkAll.value ? [] : checkedStockList.value,
checkAll.value ? 1 : 0
).then((res: any) => {
if (res.code === 200) {
getUserCollectCompany()
checkedStockList.value = []
checkAll.value = false
isIndeterminate.value = false
}
ElMessage({
type: res.code === 200 ? 'success' : 'error',
message: res.code === 200 ? '删除成功' : res.message
})
})
})
.catch(() => {})
}
}
//取消添加股票
const stockCheckCancel = () => {
//tab切换置为初始值
currentIndex.value = 0
stockCheckVisible.value = false
}
//确定添加股票
const stockCheckSummit = (companyIds: number[], isAll: number) => {
addUserCollectCompany(companyIds, isAll).then((res: any) => {
if (res.code === 200) {
stockCheckCancel()
getUserCollectCompany()
}
ElMessage({
type: res.code === 200 ? 'success' : 'error',
message: res.code === 200 ? '保存成功' : res.message
})
})
}
//子组件内tab切换
const currentIndexChange = (index: number) => {
currentIndex.value = index
getcheckDataList()
}
// 搜索股票列表
const searchUserCollectCompany = () => {
checkAll.value = false
isIndeterminate.value = false
checkedStockList.value = []
getUserCollectCompany()
}
// 获取用户收藏的股票列表
const getUserCollectCompany = () => {
loading.value = true
dataList.value = []
let userId: string = ''
queryUserCollectCompany(userId, searchVal.value.trim()).then((res: any) => {
if (res.code === 200) {
collectStockList.value = res.data
} else {
ElMessage({
type: 'error',
message: res.message
})
}
loading.value = false
})
}
// 管理员查看用户收藏股票列表
const getAdminCheckStockList = () => {
loading.value = true
dataList.value = []
adminCheckStockList(route.query.userId ? route.query.userId : '').then(
(res: any) => {
if (res.code === 200) {
collectStockList.value = res.data
} else {
ElMessage({
type: 'error',
message: res.message
})
}
loading.value = false
}
)
}
// 获取地区、交易所分类数据
const getcheckDataList = () => {
loading.value = true
dataList.value = []
queryUserCompanyTypeDto(
route.query.userId ? route.query.userId : '',
currentIndex.value
).then((res: any) => {
if (res.code === 200) {
let subData: any[] = []
res.data.forEach((v: any) => {
subData = [...subData, ...v.userCompanyCheckedDtoList]
})
const allData: any[] = [
{
typeName: '全部',
userCompanyCheckedDtoList: subData
}
]
dataList.value = [...allData, ...res.data]
} else {
ElMessage({
type: 'error',
message: res.message
})
}
loading.value = false
})
}
onMounted(() => {
nextTick(() => {
let info: any = localStorage.getItem('userInfo')
userInfo.value = JSON.parse(info)
isAdmin.value = userInfo.value.admin
if (isAdmin.value && route.query.userId) {
getAdminCheckStockList()
} else {
getUserCollectCompany()
}
})
})
</script>
<style lang="less" scoped>
.stock-collect {
margin: 20px 0 0;
.top-box {
display: flex;
align-items: center;
justify-content: space-between;
.add-btn {
width: 120px;
height: 30px;
border-radius: 4px;
border: 1px solid #3a5bb7;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
color: #3a5bb7;
cursor: pointer;
}
.right-box {
display: flex;
align-items: center;
.ipt {
width: 280px;
margin-left: 14px;
font-size: 12px;
}
.search-btn {
width: 60px;
display: flex;
justify-content: center;
align-items: center;
height: 32px;
background-color: #3a5bb7;
border-radius: 4px;
color: #ffffff;
margin-left: 14px;
font-size: 12px;
cursor: pointer;
}
}
}
.stock-list {
margin: 25px 0;
.check-top {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 13px;
.delete-btn {
width: 64px;
height: 30px;
border-radius: 4px;
border: 1px solid #3a5bb7;
cursor: pointer;
font-size: 14px;
color: #3a5bb7;
float: right;
display: flex;
align-items: center;
justify-content: center;
}
}
.nodata {
font-size: 14px;
color: #666666;
display: flex;
justify-content: center;
align-items: center;
}
.stock-check {
margin-bottom: 10px;
margin-right: 45px;
}
.check-list {
max-height: 325px;
overflow-y: auto;
}
.el-checkbox {
--el-checkbox-checked-input-border-color: @text-blue;
--el-checkbox-checked-bg-color: @text-blue;
--el-checkbox-input-border-color-hover: @text-blue;
:deep(.el-checkbox__label) {
padding-left: 10px;
margin-left: 10px;
width: 210px;
height: 30px;
line-height: 30px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #666666;
background: #f5f8fc;
}
&.select-name {
:deep(.el-checkbox__label) {
height: 24px;
line-height: 24px;
font-weight: bold;
color: #333333;
background: transparent;
}
}
&.is-checked {
:deep(.el-checkbox__label) {
color: @text-blue;
}
}
&:nth-of-type(5n) {
margin-right: 0;
}
}
}
}
</style>
三、StockCheck组件代码:
<template>
<el-dialog
:model-value="dialogVisible"
:title="title"
:width="width"
class="stock-check-dialog"
append-to-body
@close="cancle"
>
<div class="stock-check-content">
<div class="acc-input-box mb12" v-if="type === 'addAccount'">
<span>账号</span>
<el-input
v-model.trim="accInput"
placeholder="请输入账号"
class="acc-input"
:disabled="!!editRow"
clearable
maxlength="10"
></el-input>
</div>
<div class="mb12 tabs-box flex">
<ul class="tab-item-box flex">
<li
v-for="(item, index) in tabList"
:key="index"
:class="['item', currentIndex === index ? 'active' : '']"
@click="tabClick(index)"
>
{{ (item as any).label }}
</li>
</ul>
<!-- 搜索框 -->
<el-input
v-model.trim="searchInput"
maxlength="20"
style="width: 200px"
placeholder="请输入股票关键字或代码"
size="small"
clearable
@change="searchKeyChange"
>
<template #suffix>
<el-icon class="cur-p"><Search /></el-icon>
</template>
</el-input>
</div>
<div class="main-content ml12 mr12 flex" v-loading="loading">
<div class="left-box" ref="leftScrollRef">
<ul>
<li
:class="[
'type-item',
typeName === item.typeName ? 'active-name' : ''
]"
v-for="(item, index) in dataList"
:key="index"
@click="typeNameChange(item.typeName)"
>
{{ getTypeName(item.typeName) }}
</li>
</ul>
</div>
<div class="right-box">
<template v-if="subDataList.length">
<el-checkbox
v-show="subDataList.length"
v-model="checkAll"
:indeterminate="isIndeterminate"
@change="handleCheckAllChange"
>全选</el-checkbox
>
<div class="scroll-box" ref="rightScrollRef">
<el-checkbox-group
v-model="checkedStocks"
@change="handleCheckedStocksChange"
>
<el-checkbox
v-for="item in subDataList"
:key="item.companyId"
:value="item.companyId"
:label="item.companyId"
:checked="item.checked"
size="large"
>
{{ `${item.companyName} [${item.symbol}]` }}
</el-checkbox>
</el-checkbox-group>
</div>
</template>
<template v-else>
<div class="center">
<img class="img" src="../../src/assets/img/search_no_data.png" />
</div>
</template>
</div>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button class="cancle-btn" @click="cancle">取消</el-button>
<el-button @click="submit" type="primary"> 提交</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import type {
UserCompanyTypeVO,
UserCompanyCheckedVO,
AccountManagementVO
} from '@/types'
import { Search } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import Common from '@/utils/common'
const props = defineProps({
//弹窗显示隐藏
dialogVisible: {
type: Boolean,
default: false
},
//添加账户页面使用 - 标识类型
type: {
type: String,
default: ''
},
//数据请求loading
loading: {
type: Boolean,
default: false
},
//弹窗title
title: {
type: String,
default: ''
},
//弹窗宽度
width: {
type: String,
default: '1100px'
},
//tab切换数据
tabList: {
type: Array,
default: () => {
return [
{ label: '地区分类', value: 0 },
{ label: '交易所分类', value: 1 }
]
}
},
//当前选择tab
currentIndex: {
type: Number,
default: 0
},
//全部数据
dataList: {
type: Array as () => UserCompanyTypeVO[],
default: () => []
},
//账户信息
editRow: {
type: Object as () => AccountManagementVO | null,
default: () => null
}
})
const emits = defineEmits(['cancel', 'summit', 'currentIndexChange'])
const { dialogVisible, dataList, currentIndex, editRow, type } = toRefs(props)
const rightScrollRef = ref()
const leftScrollRef = ref()
//选择的地区、交易所分类名称
const typeName = ref('')
//全选按钮
const checkAll = ref(false)
//是否开启半选
const isIndeterminate = ref(false)
//勾选的数据
const checkedStocks = ref<number[]>([])
//搜索关键字 - change方法触发赋值
const searchKey = ref('')
//搜索关键字 - 输入的值
const searchInput = ref('')
//账号输入
const accInput = ref('')
//左侧类型下的子数据
const subDataList = computed<UserCompanyCheckedVO[]>(() => {
const filteredList = dataList.value.filter(
(v: UserCompanyTypeVO) => v.typeName === typeName.value
)
let data: UserCompanyCheckedVO[] = []
if (filteredList.length > 0) {
data = filteredList[0].userCompanyCheckedDtoList.filter(
(v: UserCompanyCheckedVO) =>
v.symbol.includes(searchKey.value) ||
v.companyName.includes(searchKey.value)
)
}
return data
})
watch(
() => dataList.value,
(val) => {
if (val.length) {
typeName.value = val[0].typeName
}
}
)
watch(
() => dialogVisible.value,
(val) => {
if (!val) {
typeName.value = ''
searchKey.value = ''
searchInput.value = ''
accInput.value = ''
resetCheckValue()
rightScrollRef.value && rightScrollRef.value.scrollTo({ top: 0 })
leftScrollRef.value && leftScrollRef.value.scrollTo({ top: 0 })
} else {
//编辑时给账号赋值
accInput.value = editRow?.value?.accountName ?? ''
}
}
)
// 使用 watch 来观察 subDataList 的变化
watch(subDataList, (newSubDataList) => {
checkAll.value = newSubDataList.every(
(dto: UserCompanyCheckedVO) => dto.checked
)
const checkedCount = newSubDataList.filter(
(v: UserCompanyCheckedVO) => v.checked
).length
isIndeterminate.value =
checkedCount > 0 && checkedCount < newSubDataList.length
})
//回车或搜索点击
const searchKeyChange = () => {
searchKey.value = searchInput.value
typeName.value = dataList.value[0].typeName
}
//点击全选
const handleCheckAllChange = (val: boolean) => {
let arr: number[] = []
subDataList.value.forEach((v: UserCompanyCheckedVO) => {
v.checked = val
arr.push(v.companyId)
})
checkedStocks.value = val ? arr : []
isIndeterminate.value = false
}
//单选
const handleCheckedStocksChange = (value: number[]) => {
subDataList.value.forEach((v: UserCompanyCheckedVO) => {
v.checked = value.includes(v.companyId)
})
const checkedCount = subDataList.value.filter(
(v: UserCompanyCheckedVO) => v.checked
).length
checkAll.value = subDataList.value.every(
(dto: UserCompanyCheckedVO) => dto.checked
)
isIndeterminate.value =
checkedCount > 0 && checkedCount < subDataList.value.length
}
//类型名称切换
const typeNameChange = (name: string) => {
if (typeName.value === name) return
typeName.value = name
rightScrollRef.value && rightScrollRef.value.scrollTo({ top: 0 })
}
//转换类型名称
const getTypeName = (name: string) => {
let showName = name
if (currentIndex.value === 1) {
switch (name) {
case 'BSE':
showName = '北交所'
break
case 'SSE':
showName = '上交所'
break
case 'SZSE':
showName = '深交所'
break
default:
break
}
}
return showName
}
//确定
const submit = () => {
//处理跨tab选择数据 (因为出现了跨tab勾选情况,所以不能使用checkedStocks,checkedStocks只是起到当前页面勾选数据效果)
//data 全部数据
const data: UserCompanyTypeVO = dataList.value.filter(
(v: UserCompanyTypeVO, index: number) => index === 0
)[0]
let selectCompanyIds: number[] = []
data.userCompanyCheckedDtoList.forEach((v: UserCompanyCheckedVO) => {
v.checked && selectCompanyIds.push(v.companyId)
})
const isAll =
selectCompanyIds.length === data.userCompanyCheckedDtoList.length ? 1 : 0
if (type.value === 'addAccount') {
if (!accInput.value) {
ElMessage({
type: 'error',
message: '请输入账号'
})
return
} else {
if (!Common.isValidAlphanumeric(accInput.value)) {
ElMessage({
type: 'error',
message: '账号请输入字母或数字'
})
return
}
}
}
if (selectCompanyIds.length === 0) {
ElMessage({
type: 'error',
message: '请先勾选数据'
})
return
}
emits('summit', selectCompanyIds, isAll, accInput.value)
}
//取消
const cancle = () => {
emits('cancel')
}
//tab切换
const tabClick = (index: number) => {
if (currentIndex.value === index) return
emits('currentIndexChange', index)
typeName.value = ''
searchKey.value = ''
searchInput.value = ''
resetCheckValue()
}
//重置选择
const resetCheckValue = () => {
checkAll.value = false
isIndeterminate.value = false
checkedStocks.value = []
}
</script>
<style scoped lang="less">
.stock-check-content {
background-color: #f4f7fc;
font-size: 14px;
.acc-input-box {
height: 70px;
background: #ffffff;
padding: 17px 30px;
box-sizing: border-box;
.acc-input {
width: 300px;
height: 36px;
background: #ffffff;
border-radius: 2px;
//border: 1px solid #e5e7ee;
margin-left: 10px;
}
}
.flex {
display: flex;
}
.tabs-box {
height: 44px;
background: #ffffff;
.tab-item-box {
width: 100%;
margin: 0 auto;
.item {
width: 120px;
line-height: 44px;
text-align: center;
position: relative;
cursor: pointer;
&.active {
color: @text-blue;
&:after {
content: '';
position: absolute;
left: 40px;
width: 40px;
height: 3px;
bottom: 0;
background-color: @text-blue;
border-radius: 2px 2px 0 0;
}
}
}
}
}
.main-content {
height: 465px;
background: #ffffff;
border-radius: 4px;
position: relative;
.left-box {
width: 230px;
height: 100%;
overflow: hidden;
overflow-y: auto;
border-right: 1px solid #ebeef8;
.type-item {
height: 40px;
line-height: 40px;
padding-left: 18px;
position: relative;
cursor: pointer;
&:first-child {
margin-top: 12px;
}
&:last-child {
margin-bottom: 12px;
}
&.active-name {
background: #f4f7fc;
color: @text-blue;
&:before {
content: '';
display: block;
position: absolute;
left: 0;
background-color: @text-blue;
width: 3px;
height: 16px;
top: 12px;
}
}
}
}
.right-box {
flex: 1;
padding: 20px;
box-sizing: border-box;
position: relative;
.scroll-box {
height: calc(100% - 20px);
overflow: hidden;
overflow-y: auto;
margin-bottom: 20px;
}
}
}
.center {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
width: 100%;
.img {
height: 163px;
width: 300px;
display: block;
}
}
.cur-p {
cursor: pointer;
}
}
</style>
四、使用到的common方法:
//校验输入是否仅包含数字和字母
isValidAlphanumeric(input: string) {
const alphanumericPattern = /^[a-zA-Z0-9]+$/
return alphanumericPattern.test(input)
}