feat: 添加我的行业和风口功能,优化行业标签管理

This commit is contained in:
傅光孟 2026-02-08 22:11:06 +08:00
parent d3cfb99c62
commit 6b6dc25dea
8 changed files with 593 additions and 338 deletions

View File

@ -106,3 +106,22 @@ export const getMacroList = (data: any) => {
return Request.get("/news/globalMacroList", { params: data });
};
// 我关注的行业
export const getMyIndustries = (data: any) => {
return Request.get("/user/industries", { params: data });
};
// 我关注的风口
export const getMyTags = () => {
return Request.get("/user/tags");
};
// 配置我关注的行业
export const updateMyIndustries = (data: any) => {
return Request.post("/user/industries", data);
};
// 配置我关注的风口
export const updateMyTags = (data: any) => {
return Request.post("/user/tags", data);
};

View File

@ -1,5 +1,5 @@
<template>
<view class="page-container">
<view class="custom-container" @click.stop>
<!-- 导航栏 start -->
<view class="custom-bav-bar">
<view class="left">
@ -22,97 +22,198 @@
borderRadius: '36rpx',
background: '#F3F5F8',
}"
clearable
@confirm="handleSearchTag"
/>
</view>
<!-- 搜搜 end -->
<view class="page-main">
<view class="custom-main">
<view class="my-industry">
<view class="my-industry-top">
<view class="label">我的风口</view>
<view class="label-count">
<text>已选</text>
<text class="color">4</text>
<text class="color">{{ myTagList.length }}</text>
<text>/10</text>
</view>
</view>
<view class="my-industry-list">
<view class="industry-item">
<view class="industry-name">非银金融-证券</view>
<view class="industry-btn"></view>
</view>
<view class="industry-item">
<view class="industry-name">电子-半导体</view>
<view class="industry-btn"></view>
</view>
<view class="industry-item">
<view class="industry-name">计算机-软件开发</view>
<view class="industry-btn"></view>
</view>
<view class="industry-item">
<view class="industry-name">汽车-乘用车</view>
<view class="industry-btn"></view>
</view>
<view class="industry-item">
<view class="industry-name">非银金融-证券</view>
<view class="industry-btn"></view>
</view>
<view class="industry-item">
<view class="industry-name">非银金融-证券</view>
<view class="industry-btn"></view>
<view class="my-industry-list" v-if="myTagList.length > 0">
<view class="industry-item" v-for="item in myTagList">
<view class="industry-name">{{ item.name }}</view>
<view class="industry-btn" @click="handleDeleteTag(item)"></view>
</view>
</view>
<view class="my-industry-empty"> 暂无自选请从下方添加 </view>
<view class="my-industry-empty" v-else> 暂无自选请从下方添加 </view>
</view>
<view class="recommend-industry">
<view class="recommend-industry-top">
<view class="label">预期风口</view>
<view class="right">
<view class="label">推荐行业</view>
<view class="right" @click="selectRandomTags">
<view class="icon"></view>
<view class="text">换一换</view>
</view>
</view>
<view class="recommend-industry-list">
<view class="industry-item active">
<view class="industry-name">非银金融-证券</view>
</view>
<view class="industry-item">
<view class="industry-name">非银金融-证券</view>
</view>
<view class="industry-item">
<view class="industry-name">非银金融-证券</view>
</view>
<view class="industry-item">
<view class="industry-name">非银金融-证券</view>
<view class="recommend-industry-list" v-if="industryList.length > 0">
<view
v-for="(item, index) in industryList"
:key="index"
:class="['industry-item', { active: isSelected(item.content) }]"
@click="handleAddTag(item.content)"
>
<view class="industry-name">{{ item.content }}</view>
</view>
</view>
<view v-else class="no-data"> <u-empty /></view>
</view>
</view>
<view class="page-footer">
<view class="btn-clear">清空</view>
<view class="btn-done">完成配置</view>
<view class="custom-footer">
<view class="btn-clear" @click="handleClear">清空</view>
<view class="btn-done" @click="handleComplete">完成配置</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { getTopConceptPeriod } from "@/api/newsInfo";
import dayjs from "dayjs";
import { onMounted, ref, watch } from "vue";
const props = defineProps({
tagList: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(["onChange", "onBack"]);
const input = ref("");
//
const handleBack = () => {
if (props.tagList.length === 0) {
uni.navigateBack({
delta: 1,
});
} else {
emit("onBack");
}
};
const myTagList = ref([]);
const industryList = ref([]);
const industryListAll = ref([]);
const start_time = dayjs().subtract(1, "month").format("YYYY-MM-DD");
const end_time = dayjs().format("YYYY-MM-DD");
const limit_num = 20;
// top10
async function getTopIndustry_dFn() {
industryListAll.value = await getTopConceptPeriod({
start_time: start_time,
end_time: end_time,
limit_num: limit_num,
});
selectRandomTags();
}
// 10
function selectRandomTags() {
industryList.value = [];
const availableTags = [...industryListAll.value];
for (let i = 0; i < 10; i++) {
if (availableTags.length === 0) break;
const randomIndex = Math.floor(Math.random() * availableTags.length);
industryList.value.push(availableTags[randomIndex]);
availableTags.splice(randomIndex, 1);
}
}
//
function isSelected(tag) {
const index = myTagList.value.findIndex((item) => item.name === tag);
return index === -1 ? false : true;
}
//
function handleAddTag(tag) {
if (isSelected(tag)) return;
if (myTagList.value.length >= 10) {
uni.showToast({
title: "最多添加10个标签",
icon: "none",
});
return;
}
myTagList.value.push({
name: tag,
});
}
//
function handleDeleteTag(tag) {
const index = myTagList.value.findIndex((item) => item.name === tag);
myTagList.value.splice(index, 1);
}
//
function handleSearchTag() {
if (!input.value) {
return;
}
industryList.value = industryListAll.value.filter((item) => {
if (!item || !item.content) return false;
return item.content.includes(input.value);
});
}
//
function handleComplete() {
if (myTagList.value.length < 1) {
uni.showToast({
title: "请先选择标签",
icon: "none",
});
return;
}
emit("onChange", myTagList.value);
}
//
function handleClear() {
myTagList.value = [];
}
watch(
() => props.tagList,
(newVal) => {
if (newVal) {
myTagList.value = [...newVal];
}
},
{
immediate: true,
},
);
onMounted(() => {
getTopIndustry_dFn();
});
</script>
<style scoped lang="scss">
.page-container {
.custom-container {
position: fixed;
inset: 0;
width: 100%;
background-color: #fff;
height: 100%;
overflow: hidden;
background: #fff;
z-index: 9;
padding-bottom: 80rpx;
box-sizing: border-box;
}
.custom-bav-bar {
@ -160,10 +261,12 @@ const handleBack = () => {
z-index: 2;
}
.page-main {
.custom-main {
width: 100%;
padding: 33rpx 30rpx 0;
height: calc(100% - 250rpx);
padding: 33rpx 30rpx;
box-sizing: border-box;
overflow: auto;
.my-industry {
margin-bottom: 80rpx;
@ -200,7 +303,7 @@ const handleBack = () => {
display: flex;
align-items: center;
padding: 18rpx 30rpx;
background: #FFF9EC;
background: #eff7ff;
border-radius: 8rpx;
.industry-name {
@ -296,10 +399,15 @@ const handleBack = () => {
}
}
}
.no-data {
display: flex;
justify-content: center;
}
}
}
.page-footer {
.custom-footer {
position: fixed;
bottom: 0;
left: 0;

View File

@ -0,0 +1,320 @@
<template>
<view class="page-container">
<!-- 导航栏 start -->
<view class="custom-bav-bar">
<view class="left">
<u-icon name="arrow-left" color="#666" size="36rpx" @click="handleBack" />
</view>
<view class="center"> 风口概念 </view>
<view class="right" v-if="userStore.isLogin" @click="handleShowCustom">管理</view>
</view>
<!-- 导航栏 end -->
<!-- tabs start -->
<view class="page-nav-tabs">
<u-tabs
:list="tagList"
lineWidth="28rpx"
lineColor="#222222"
:current="current"
:activeStyle="{
color: '#222',
fontWeight: '500',
fontFamily: 'PingFangSC, PingFang SC',
}"
:inactiveStyle="{
color: '#666',
fontWeight: '400',
fontFamily: 'PingFangSC, PingFang SC',
}"
itemStyle="padding-left: 15px; padding-right: 15px; height: 70rpx;"
@change="handleChangeTag"
>
</u-tabs>
</view>
<!-- tabs end -->
<view class="page-main">
<view class="banner">
<view class="text-1">{{tagList[current]?.name}}</view>
<view class="text-2">风口概念近一个月排名Top5</view>
</view>
<view class="news-list">
<u-loading-icon v-if="loading"></u-loading-icon>
<template v-else-if="data?.length > 0">
<view
:class="['news-item', { mask: !userStore.isLogin }]"
v-for="item in data"
:key="item.id"
@click="goDetail(item)"
>
<view class="title">
<view class="name">
{{ item.title }}
</view>
</view>
<view :class="['content', { mask: isMask }]">
{{ item.summary }}
</view>
<view :class="['source', { mask: isMask }]">
<view>{{ item.tag }}</view>
<view>{{ item.time }}</view>
</view>
</view>
</template>
<u-empty v-else />
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { useUserStore } from "@/stores/user";
import { computed, onMounted, ref, watch } from "vue";
import { getListByTag } from "@/api/detail";
const props = defineProps({
tagList: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(["onShow", "onShowCustom"]);
const loading = ref(false);
const activeTag = ref("");
const current = computed(() => {
return props.tagList.findIndex((item) => item.name === activeTag.value);
});
const handleChangeTag = (item: any) => {
activeTag.value = item.name;
getList(item.name);
};
//
const handleBack = () => {
uni.navigateBack({
delta: 1,
});
};
const userStore = useUserStore();
// |
const isMask = computed(() => {
return !userStore.isUserType;
});
//
function goDetail(item: any) {
if (!userStore.isLogin) {
emit("onShow");
return;
}
//
if (!userStore.isUserType) {
return;
}
uni.navigateTo({
url: `/pages/detail/indexNewsInfo?id=${item.id}`,
});
}
//
const data = ref([]);
//
const getList = async (name: string) => {
loading.value = true;
const result = await getListByTag({ name });
loading.value = false;
if (result.code === 200) {
data.value = result.data;
} else {
data.value = [];
}
};
watch(
() => props.tagList,
(newVal) => {
if (newVal?.length > 0) {
activeTag.value = newVal[0]?.name || "";
getList(activeTag.value);
}
},
{
immediate: true
}
);
const handleShowCustom = () => {
emit("onShowCustom");
};
</script>
<style scoped lang="scss">
.page-container {
position: relative;
width: 100%;
background-color: #fff;
}
.custom-bav-bar {
width: 100%;
height: 88rpx;
display: flex;
align-items: center;
justify-content: center;
position: relative;
.back_icon {
width: 36rpx;
height: 36rpx;
}
.logo_icon {
width: 168rpx;
height: 36rpx;
margin-right: 6rpx;
}
.left {
position: absolute;
top: 24rpx;
left: 32rpx;
}
.center {
display: flex;
align-items: center;
justify-content: center;
font-family: "PingFangSC, PingFang SC";
font-weight: 500;
font-size: 34rpx;
color: #222222;
line-height: 36rpx;
}
.right {
position: absolute;
top: 22rpx;
right: 30rpx;
font-family: "PingFangSC, PingFang SC";
font-weight: 400;
font-size: 30rpx;
color: #333333;
line-height: 42rpx;
}
}
.page-nav-tabs {
padding-top: 20rpx;
}
.page-main {
padding-top: 30rpx;
background-color: #f3f5f8;
.banner {
width: 690rpx;
height: 113rpx;
margin: 0 30rpx 30rpx;
padding: 15rpx 30rpx;
border-radius: 10rpx;
background-image: url("@/assets/images/page/page_5_1@2x.png");
background-repeat: no-repeat;
background-size: 690rpx 113rpx;
box-shadow: 0px 2px 8px 0px rgba(0, 0, 0, 0.12);
box-sizing: border-box;
.text-1 {
font-family: "PingFangSC, PingFang SC";
font-weight: 500;
font-size: 30rpx;
color: #ffa800;
line-height: 42rpx;
}
.text-2 {
font-family: "PingFangSC, PingFang SC";
font-weight: 400;
font-size: 24rpx;
color: #999999;
line-height: 33rpx;
}
}
.news-list {
width: 100%;
padding: 30rpx 30rpx 0;
background: #fff;
border-radius: 24rpx 24rpx 0px 0px;
overflow: hidden;
box-sizing: border-box;
.mask {
filter: blur(5px);
}
.news-item {
width: 100%;
margin-bottom: 30rpx;
padding-bottom: 30rpx;
border-bottom: 2rpx solid #f3f3f5;
box-sizing: border-box;
&:last-child {
border-bottom: none;
}
.title {
display: flex;
align-items: center;
margin-bottom: 16rpx;
.name {
flex: 1;
width: 564rpx;
font-family: "PingFangSC, PingFang SC";
font-weight: 500;
font-size: 28rpx;
color: #222222;
line-height: 40rpx;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
word-break: break-all;
}
}
.content {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
margin-bottom: 16rpx;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
font-family: "PingFangSC, PingFang SC";
font-weight: 400;
font-size: 24rpx;
color: #333333;
line-height: 33rpx;
}
.source {
display: flex;
align-items: center;
justify-content: space-between;
font-family: "PingFangSC, PingFang SC";
font-weight: 400;
font-size: 22rpx;
color: #999999;
line-height: 30rpx;
}
}
}
}
</style>

View File

@ -1,64 +1,20 @@
<template>
<view class="page-container">
<view class="container">
<!-- 自定义配置 start -->
<!-- <CustomView /> -->
<CustomView
v-if="isShowCustom && userStore.isLogin"
:tagList="tagList"
@onChange="handleChangeTag"
@onBack="handleHideCustom"
/>
<!-- 自定义配置 end -->
<!-- 导航栏 start -->
<view class="custom-bav-bar">
<view class="left">
<u-icon name="arrow-left" color="#666" size="36rpx" @click="handleBack" />
</view>
<view class="center"> 风口概念 </view>
<view class="right">管理</view>
</view>
<!-- 导航栏 end -->
<!-- tabs start -->
<view class="page-nav-tabs">
<u-tabs
:list="list4"
lineWidth="28rpx"
lineColor="#222222"
:activeStyle="{
color: '#222',
fontWeight: '500',
fontFamily: 'PingFangSC, PingFang SC',
}"
:inactiveStyle="{
color: '#666',
fontWeight: '400',
fontFamily: 'PingFangSC, PingFang SC',
}"
itemStyle="padding-left: 15px; padding-right: 15px; height: 70rpx;"
>
</u-tabs>
</view>
<!-- tabs end -->
<view class="page-main">
<view class="banner">
<view class="text-1">非银金融-证券</view>
<view class="text-2">热门行业近一个月排名第5</view>
</view>
<view class="news-list">
<view :class="['news-item',{mask: !userStore.isLogin}]" v-for="item in data" :key="item.id" @click="goDetail(item)">
<view class="title">
<view class="name">
{{ item.title }}
</view>
</view>
<view :class="['content', {mask: isMask}]">
{{ item.summary }}
</view>
<view :class="['source', {mask: isMask}]">
<view>{{ item.tag }}</view>
<view>{{ item.time }}</view>
</view>
</view>
</view>
</view>
<List
v-show="!isShowCustom"
:tagList="tagList"
@onShow="handleShowLogin"
@onShowCustom="handleShowCustom"
/>
<!-- 登录弹窗 start -->
<LoginDialog
@ -75,29 +31,54 @@
import { useUserStore } from "@/stores/user";
import { computed, onMounted, ref } from "vue";
import LoginDialog from "@/components/loginPopup/index.vue";
import { getListByTagIndustry } from "@/api/detail";
import CustomView from "./components/CustomView.vue";
import List from "./components/List.vue";
import { getMyTags, updateMyTags } from "@/api";
const list4 = ref([
{ name: "非银金融-证券", id: 1 },
{ name: "电子-半导体", id: 2 },
{ name: "汽车-乘用车", id: 3 },
{ name: "汽车-乘用车", id: 4 },
{ name: "汽车-乘用车", id: 5 },
]);
//
const handleBack = () => {
uni.navigateBack({
delta: 1,
const isShowCustom = ref(false);
const handleChangeTag = async (item: any) => {
const params = item.map((tag: any) => {
const _tag = tag.name.split("-");
return {
primaryName: _tag[0],
secondaryName: _tag[1],
};
});
const result = await updateMyTags(params);
if (result.code === 200) {
getTagList();
handleHideCustom();
}
};
const handleShowCustom = () => {
isShowCustom.value = true;
};
const handleHideCustom = () => {
isShowCustom.value = false;
};
const tagList = ref([]);
const getTagList = async () => {
const result = await getMyTags();
if (result.code === 200) {
tagList.value = result.data.map((item: any) => {
return {
name: item.primaryName + "-" + item.secondaryName,
};
});
} else {
tagList.value = [];
}
if (tagList.value.length === 0) {
handleShowCustom();
}
};
const userStore = useUserStore();
// |
const isMask = computed(() => {
return !userStore.isUserType;
});
const LoginShow = ref(false);
//
const handleShowLogin = () => {
@ -111,206 +92,26 @@ const handleLoginCancel = () => {
//
const handleLoginSuccess = () => {
LoginShow.value = false;
getTagList();
};
//
const handleLoginError = () => {
console.log("登录失败");
};
//
function goDetail(item: any) {
if (!userStore.isLogin) {
handleShowLogin();
return;
}
//
if (!userStore.isUserType) {
return;
}
uni.navigateTo({
url: `/pages/detail/indexNewsInfo?id=${item.id}`,
});
}
const data = ref([]);
const getList = async () => {
const name = "电子-半导体";
const result = await getListByTagIndustry({ name });
if (result.code === 200) {
data.value = result.data;
} else {
data.value = [];
}
};
onMounted(async () => {
if (!userStore.isLogin) {
handleShowLogin();
return;
}
getList();
getTagList();
});
</script>
<style scoped lang="scss">
.page-container {
.container {
position: relative;
width: 100%;
background-color: #fff;
}
.custom-bav-bar {
width: 100%;
height: 88rpx;
display: flex;
align-items: center;
justify-content: center;
position: relative;
.back_icon {
width: 36rpx;
height: 36rpx;
}
.logo_icon {
width: 168rpx;
height: 36rpx;
margin-right: 6rpx;
}
.left {
position: absolute;
top: 24rpx;
left: 32rpx;
}
.center {
display: flex;
align-items: center;
justify-content: center;
font-family: "PingFangSC, PingFang SC";
font-weight: 500;
font-size: 34rpx;
color: #222222;
line-height: 36rpx;
}
.right {
position: absolute;
top: 22rpx;
right: 30rpx;
font-family: "PingFangSC, PingFang SC";
font-weight: 400;
font-size: 30rpx;
color: #333333;
line-height: 42rpx;
}
}
.page-nav-tabs {
padding-top: 20rpx;
}
.page-main {
padding-top: 30rpx;
background-color: #f3f5f8;
.banner {
width: 690rpx;
height: 113rpx;
margin: 0 30rpx 30rpx;
padding: 15rpx 30rpx;
border-radius: 10rpx;
background-image: url("@/assets/images/page/page_5_1@2x.png");
background-repeat: no-repeat;
background-size: 690rpx 113rpx;
box-shadow: 0px 2px 8px 0px rgba(0, 0, 0, 0.12);
box-sizing: border-box;
.text-1 {
font-family: "PingFangSC, PingFang SC";
font-weight: 500;
font-size: 30rpx;
color: #ffa800;
line-height: 42rpx;
}
.text-2 {
font-family: "PingFangSC, PingFang SC";
font-weight: 400;
font-size: 24rpx;
color: #999999;
line-height: 33rpx;
}
}
.news-list {
width: 100%;
padding: 30rpx 30rpx 0;
background: #fff;
border-radius: 24rpx 24rpx 0px 0px;
overflow: hidden;
box-sizing: border-box;
.mask {
filter: blur(5px);
}
.news-item {
width: 100%;
margin-bottom: 30rpx;
padding-bottom: 30rpx;
border-bottom: 2rpx solid #f3f3f5;
box-sizing: border-box;
&:last-child {
border-bottom: none;
}
.title {
display: flex;
align-items: center;
margin-bottom: 16rpx;
.name {
flex: 1;
width: 564rpx;
font-family: "PingFangSC, PingFang SC";
font-weight: 500;
font-size: 28rpx;
color: #222222;
line-height: 40rpx;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
word-break: break-all;
}
}
.content {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
margin-bottom: 16rpx;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
font-family: "PingFangSC, PingFang SC";
font-weight: 400;
font-size: 24rpx;
color: #333333;
line-height: 33rpx;
}
.source {
display: flex;
align-items: center;
justify-content: space-between;
font-family: "PingFangSC, PingFang SC";
font-weight: 400;
font-size: 22rpx;
color: #999999;
line-height: 30rpx;
}
}
}
}
</style>

View File

@ -36,8 +36,8 @@
<view class="page-main">
<view class="banner">
<view class="text-1">非银金融-证券</view>
<view class="text-2">热门行业近一个月排名5</view>
<view class="text-1">{{tagList[current]?.name}}</view>
<view class="text-2">热门行业近一个月排名Top5</view>
</view>
<view class="news-list">

View File

@ -31,15 +31,25 @@
import { useUserStore } from "@/stores/user";
import { computed, onMounted, ref } from "vue";
import LoginDialog from "@/components/loginPopup/index.vue";
import { getListByTagIndustry } from "@/api/detail";
import CustomView from "./components/CustomView.vue";
import List from "./components/List.vue";
import { getMyIndustries, updateMyIndustries } from "@/api";
const isShowCustom = ref(false);
const handleChangeTag = (item: any) => {
console.log("output >>>>> item", item);
const handleChangeTag = async (item: any) => {
const params = item.map((tag: any) => {
const _tag = tag.name.split("-");
return {
primaryName: _tag[0],
secondaryName: _tag[1],
};
});
const result = await updateMyIndustries(params);
if (result.code === 200) {
getTagList();
handleHideCustom();
}
};
const handleShowCustom = () => {
@ -52,23 +62,20 @@ const handleHideCustom = () => {
const tagList = ref([]);
const getTagList = async () => {
new Promise((resolve) => {
setTimeout(() => {
resolve(true);
}, 1000);
}).then(() => {
tagList.value = [
// { name: "-" },
// { name: "-" },
// { name: "-" },
// { name: "-" },
];
if(tagList.value.length === 0) {
handleShowCustom()
}
const result = await getMyIndustries({});
if (result.code === 200) {
tagList.value = result.data.map((item: any) => {
return {
name: item.primaryName + "-" + item.secondaryName,
};
});
} else {
tagList.value = [];
}
if (tagList.value.length === 0) {
handleShowCustom();
}
};
const userStore = useUserStore();

View File

@ -36,7 +36,7 @@
</view>
<view :class="['source', { mask: isMask }]">
<view>{{ news.source }}</view>
<view>{{ news.timeStr }}</view>
<view>{{ `${item.day} ${news.timeStr}` }}</view>
</view>
</view>
</template>

View File

@ -23,7 +23,7 @@ service.interceptors.request.use(
// 在发送请求之前做些什么 token
if (Session.get("token")) {
config.headers!["auth-token"] = `${Session.get("token")}`;
config.headers!["phone"] = `${Session.get("userPhone")}`;
config.headers!["phone"] = `${Session.get("userInfos").phone}`;
}
return config;
},