feat: 添加海外先机功能,优化用户权限控制和组件展示

This commit is contained in:
傅光孟 2026-02-07 14:08:53 +08:00
parent f9c0f69cd6
commit d45e50f413
10 changed files with 296 additions and 192 deletions

View File

@ -90,3 +90,10 @@ export const getEtfIndexList = (data: any) => {
export const fetchEtfDetail = (data: any) => {
return Request.post("/news/etfList", data);
};
// 海外先机
export const getForeignList = (data: any) => {
return Request.get("/news/exclusiveList", data);
};

View File

@ -1,11 +1,9 @@
<template>
<view class="page-container">
<view class="main">
<view class="title" :class="{ mohu: !userStore.isLogin }">{{
props.data.title
}}</view>
<view class="title" :class="{ mohu: isMask }">{{ props.data.title }}</view>
<view class="author">
<view class="name" :class="{ mohu: !userStore.isLogin }">
<view class="name" :class="{ mohu: isMask }">
<text class="text">来源:</text>
<text class="text" v-if="intoType === 'etf'">中国证券报</text>
<text class="text" v-else>{{
@ -17,13 +15,11 @@
<text class="text">编辑:</text>
<text class="text">{{ props.data.editor }}</text>
</view> -->
<view class="time" :class="{ mohu: !userStore.isLogin }">{{
props.data.publishTime
}}</view>
<view class="time" :class="{ mohu: isMask }">{{ props.data.publishTime }}</view>
</view>
<!-- 两个标签 start -->
<view class="r_r_tags" :class="{ mohu: !userStore.isLogin }">
<view class="r_r_tags" :class="{ mohu: isMask }">
<view style="display: flex; margin-top: 20rpx; overflow-x: auto; width: 95vw">
<view class="r_tags">
<view
@ -51,7 +47,7 @@
<!-- 两个标签 end -->
<!-- 摘要 -->
<view class="desc" v-if="props.data.summary" :class="{ mohu: !userStore.isLogin }">
<view class="desc" v-if="props.data.summary" :class="{ mohu: isMask }">
<!-- <view class="bill_icon"></view> -->
<image :src="zhaiyaoImg" mode="scaleToFill" class="zhaiyao_icon" />
<view>
@ -63,7 +59,7 @@
<image :src="props.data.picture" mode="widthFix" />
</view>
<view style="padding: 35rpx" :class="{ mohu: !userStore.isLogin }">
<view style="padding: 35rpx" :class="{ mohu: isMask }">
<text
class="articleDes"
:class="props?.data?.needpay && 'needpay'"
@ -92,6 +88,10 @@
</view>
</view>
<view class="company-tag" v-if="props.data.companyName">
{{ props.data.companyName }}
</view>
<!-- <view class="r_etf" style="margin-top: 80rpx" v-if="intoType == 'etf'">
<text class="etf_title">AI关联标的</text>
<view class="etfs">
@ -121,12 +121,12 @@
<view class="code">{{ item.code }}</view>
<!-- 暂无涨跌数据保留样式 -->
<!-- <view>
<view class="name">{{ item.name }}</view>
<view class="code">{{ item.code }}</view>
</view>
<view>
<view class="percent up">-0.28%</view>
</view> -->
<view class="name">{{ item.name }}</view>
<view class="code">{{ item.code }}</view>
</view>
<view>
<view class="percent up">-0.28%</view>
</view> -->
</view>
</template>
<template v-if="intoType == 'etf'">
@ -144,7 +144,11 @@
</view>
<!-- 拓展阅读 -->
<view class="more-news" style="margin-top: 20rpx" v-if="furtherReadData.length > 0">
<view
:class="['more-news', { mohu: isMask }]"
style="margin-top: 20rpx"
v-if="furtherReadData.length > 0"
>
<text class="more-news-title">拓展阅读</text>
<view class="more-news-list">
<view
@ -173,7 +177,7 @@
</template>
<script setup lang="ts">
import { ref } from "vue";
import { computed, ref } from "vue";
import {
onLaunch,
onShow,
@ -211,6 +215,10 @@ const tagList1 = ref([
]);
const userStore = useUserStore();
// |
const isMask = computed(() => {
return !userStore.isLogin || !userStore.isUserType;
});
//
const LoginShow = ref(false);
@ -257,7 +265,7 @@ onLoad((option) => {
intoType.value = option?.intoType || null;
if (!userStore.isLogin) {
// LoginShow.value = true;
handleShowLogin();
}
});
@ -587,6 +595,19 @@ const getMoreNews = () => {
}
}
.company-tag {
padding: 30rpx 40rpx 0;
display: flex;
.txt {
margin-right: 16rpx;
font-family: "PingFangSC, PingFang SC";
font-weight: 400;
font-size: 28rpx;
color: #d6a68c;
line-height: 33rpx;
}
}
.more-news {
display: flex;
flex-direction: column;

View File

@ -16,125 +16,41 @@
</view>
<!-- 标题 end -->
<view class="page-timeline">
<view class="timeline">
<view :class="['page-main']">
<view :class="['timeline', { mask: isMask }]" v-for="item in data" :key="item.day">
<view class="line"></view>
<view class="content">
<view class="date">2025/11/26</view>
<view class="date">{{ item.day }}</view>
<view class="news-list">
<view class="news">
<view
class="news"
v-for="news in item.list"
:key="news.id"
@click="goDetail(news)"
>
<view class="news-top">
<view class="time">09:30:00</view>
<view class="time">{{ news.timeStr }}</view>
<view class="source">
<view class="t-1">来自</view>
<view class="t-2">中国证券报</view>
<view class="t-2">{{ news.source }}</view>
</view>
</view>
<view class="news-title">
<view class="icon-hot"></view>
<view class="name">
<text class="text"
>行业龙头最新财报行业龙头最新财报行业龙头最新财报行业龙头最新财报</text
>
<text class="text">{{ news.title }}</text>
</view>
</view>
<view class="news-content">
英伟达发布2026财年三季度业绩及四季度指引均超市场预期收入方场预期收入方面三亿美元
英伟达发布2026财年三季度业绩及四季度指引均超市场预期收入方场预期收入方面三亿美元
英伟达发布2026财年三季度业绩及四季度指引均超市场预期收入方场预期收入方面三亿美元
{{ news.summary }}
</view>
<view class="tags">
<text class="tag">#苹果产业链</text>
<text class="tag">#苹果产业链</text>
<view class="tags" v-if="news.companyName">
<text class="tag">{{ news.companyName }}</text>
</view>
<view class="stocks">
<view class="stock">
<text class="name">工业富联</text>
<text class="code">6011138</text>
</view>
<view class="stock">
<text class="name">工业富联</text>
<text class="code">6011138</text>
</view>
</view>
</view>
</view>
</view>
</view>
<view class="timeline">
<view class="line"></view>
<view class="content">
<view class="date">2025/11/26</view>
<view class="news-list">
<view class="news">
<view class="news-top">
<view class="time">09:30:00</view>
<view class="source">
<view class="t-1">来自</view>
<view class="t-2">中国证券报</view>
</view>
</view>
<view class="news-title">
<view class="icon-hot"></view>
<view class="name">
<text class="text"
>行业龙头最新财报行业龙头最新财报行业龙头最新财报行业龙头最新财报</text
>
</view>
</view>
<view class="news-content">
英伟达发布2026财年三季度业绩及四季度指引均超市场预期收入方场预期收入方面三亿美元
英伟达发布2026财年三季度业绩及四季度指引均超市场预期收入方场预期收入方面三亿美元
英伟达发布2026财年三季度业绩及四季度指引均超市场预期收入方场预期收入方面三亿美元
</view>
<view class="tags">
<text class="tag">#苹果产业链</text>
<text class="tag">#苹果产业链</text>
</view>
<view class="stocks">
<view class="stock">
<text class="name">工业富联</text>
<text class="code">6011138</text>
</view>
<view class="stock">
<text class="name">工业富联</text>
<text class="code">6011138</text>
</view>
</view>
</view>
<view class="news">
<view class="news-top">
<view class="time">09:30:00</view>
<view class="source">
<view class="t-1">来自</view>
<view class="t-2">中国证券报</view>
</view>
</view>
<view class="news-title">
<view class="icon-hot"></view>
<view class="name">
<text class="text"
>行业龙头最新财报行业龙头最新财报行业龙头最新财报行业龙头最新财报</text
>
</view>
</view>
<view class="news-content">
英伟达发布2026财年三季度业绩及四季度指引均超市场预期收入方场预期收入方面三亿美元
英伟达发布2026财年三季度业绩及四季度指引均超市场预期收入方场预期收入方面三亿美元
英伟达发布2026财年三季度业绩及四季度指引均超市场预期收入方场预期收入方面三亿美元
</view>
<view class="tags">
<text class="tag">#苹果产业链</text>
<text class="tag">#苹果产业链</text>
</view>
<view class="stocks">
<view class="stock">
<text class="name">工业富联</text>
<text class="code">6011138</text>
</view>
<view class="stock">
<text class="name">工业富联</text>
<text class="code">6011138</text>
<view class="stocks" v-if="news.stocks?.length > 0">
<view class="stock" v-for="(stock, index) in news.stocks" :key="index">
<text class="name">{{ stock.name }}</text>
<text class="code">{{ stock.code }}</text>
</view>
</view>
</view>
@ -142,11 +58,23 @@
</view>
</view>
</view>
<!-- 登录弹窗 start -->
<LoginDialog
:show="LoginShow"
@onSuccess="handleLoginSuccess"
@onCancel="handleLoginCancel"
@onError="handleLoginError"
/>
<!-- 登录弹窗 end -->
</view>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { getForeignList } from "@/api";
import { useUserStore } from "@/stores/user";
import { computed, onMounted, ref } from "vue";
import LoginDialog from "@/components/loginPopup/index.vue";
//
const handleBack = () => {
@ -154,6 +82,65 @@ const handleBack = () => {
delta: 1,
});
};
const userStore = useUserStore();
// |
const isMask = computed(() => {
return !userStore.isLogin || !userStore.isUserType;
});
const LoginShow = ref(false);
//
const handleShowLogin = () => {
LoginShow.value = true;
};
//
const handleLoginCancel = () => {
LoginShow.value = false;
};
//
const handleLoginSuccess = () => {
LoginShow.value = false;
};
//
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 result = await getForeignList({});
if (result.code === 200) {
const { list, total } = result.data;
data.value = list;
} else {
data.value = [];
}
};
onMounted(async () => {
if (!userStore.isLogin) {
handleShowLogin();
}
getList();
});
</script>
<style scoped lang="scss">
@ -227,7 +214,7 @@ const handleBack = () => {
}
}
.page-timeline {
.page-main {
width: 100%;
padding: 60rpx 30rpx 0;
background: #f3f5f8;
@ -237,6 +224,9 @@ const handleBack = () => {
.timeline {
display: flex;
&.mask {
filter: blur(5px);
}
.line {
position: relative;
top: 12rpx;

View File

@ -10,28 +10,27 @@
indicator-color="rgba(199, 199, 199, 1)"
indicator-active-color="#3F80FA"
>
<swiper-item>
<view class="swiper-item" @click.stop="handlClick({ id: 1 })">
<swiper-item v-for="(item, index) in data" :key="index">
<view class="swiper-item" @click.stop="goDetail(item)">
<view class="source">
<view>
<text class="label">来自</text>
<text class="name">证报</text>
<text class="name"></text>
</view>
<view>
<text>00:12:34</text>
<text>{{ item.timeStr }}</text>
</view>
</view>
<view class="title">三星生物风险投资基金将投资拓济医药</view>
<view class="content"
>三星集团宣布其生物风险基金将投资中国生物技术公司phronline三星集团宣布其生物风险基金将投资中国生物技术公司phronline
Biopharma以扩大其抗体药物偶联物ADC合作伙伴关系</view
>
<view class="title">{{ item.title }}</view>
<view class="content">{{ item.summary }}</view>
<view class="tag">
<text class="txt">#三星生物制药</text>
<text class="txt">#三星生物制药</text>
<text class="txt">{{ item.companyName }}</text>
<!-- <text class="txt">#三星生物制药</text> -->
</view>
<view class="industry">
<text class="txt">**能源</text>
<view class="industry" v-if="item?.stocks && item?.stocks.length > 0">
<text class="txt" v-for="stock in item?.stocks">{{
maskStockNameProportional(stock?.name)
}}</text>
</view>
</view>
</swiper-item>
@ -40,13 +39,47 @@
</template>
<script setup lang="ts">
import { ref } from "vue";
import { getForeignList } from "@/api";
import { useUserStore } from "@/stores/user";
import { maskStockNameProportional } from "@/utils/util";
import { onMounted, ref } from "vue";
const emit = defineEmits(["onClick"]);
const emit = defineEmits(["onShow"]);
const handlClick = (item: any) => {
emit("onClick", item);
const userStore = useUserStore();
//
function goDetail(item: any) {
if (userStore.isLogin) {
uni.navigateTo({
url: `/pages/detail/indexNewsInfo?id=${item.id}`,
});
} else {
emit("onShow");
}
}
const data = ref([]);
const getList = async () => {
const result = await getForeignList({});
if (result.code === 200) {
const { list, total } = result.data;
for (const item of list) {
for (const o of item.list) {
data.value.push(o);
if (data.value.length >= 3) {
break;
}
}
if (data.value.length >= 3) {
break;
}
}
}
};
onMounted(async () => {
getList();
});
</script>
<style scoped lang="scss">

View File

@ -5,7 +5,7 @@
<view class="time">近一个月热门</view>
</view>
<view class="tag-box">
<view v-for="(item, index) in topConceptList_d" :key="index" class="tag">
<view v-for="(item, index) in topConceptList" :key="index" class="tag">
{{ item.content }}
</view>
</view>
@ -17,8 +17,9 @@
<script setup lang="ts">
import { getListByTag } from "@/api/detail";
import { getTopConcept_d } from "@/api/newsInfo";
import { getTopConceptPeriod } from "@/api/newsInfo";
import { useUserStore } from "@/stores/user";
import dayjs from "dayjs";
import { computed, onMounted, ref } from "vue";
const emit = defineEmits(["onShow"]);
@ -26,19 +27,17 @@ const active = ref(0);
const topConceptList = ref([]);
const oneData = ref({});
const userStore = useUserStore();
const topConceptList_d = computed(() => {
return topConceptList.value.slice(0, 3);
});
const handleClickTag = (index: number, item: any) => {
active.value = index;
getList(item);
};
const start_time = dayjs().subtract(1, "month").format("YYYY-MM-DD");
const end_time = dayjs().format("YYYY-MM-DD");
const limit_num = 3
// top10
async function getTopConcept_dFn() {
topConceptList.value = await getTopConcept_d({});
topConceptList.value = await getTopConceptPeriod({
start_time: start_time,
end_time: end_time,
limit_num: limit_num,
});
}
async function getList() {
@ -52,24 +51,34 @@ async function getList() {
//
function goDetail(item: any) {
if (userStore.isLogin) {
uni.navigateTo({
url: `/pages/detail/indexNewsInfo?id=${item.id}`,
});
} else {
if (!userStore.isLogin) {
emit("onShow");
return;
}
uni.navigateTo({
url: `/pages/detail/indexNewsInfo?id=${item.id}`,
});
}
//
function navigateTo() {
if (userStore.isLogin) {
uni.navigateTo({
url: `/pages/concept/index`,
});
} else {
if (!userStore.isLogin) {
emit("onShow");
return;
}
//
if (!userStore.isUserType) {
uni.showToast({
title: "暂未开通本栏目",
icon: "none",
});
return;
}
uni.navigateTo({
url: `/pages/industry/index`,
});
}
onMounted(async () => {

View File

@ -67,18 +67,18 @@ const goto = (path: string, auth: string) => {
return;
}
if (userInfos.value.accountType === 1) {
//
uni.navigateTo({
url: path,
});
} else {
//
//
if (!userStore.isUserType) {
uni.showToast({
title: "无权限访问",
title: "暂未开通本栏目",
icon: "none",
});
return;
}
uni.navigateTo({
url: path,
});
};
</script>

View File

@ -6,7 +6,7 @@
</view>
<view class="tag-box">
<view
v-for="(item, index) in industryList_d"
v-for="(item, index) in industryList"
:class="['tag', { 'tag-active': active === index }]"
@click="handleClickTag(index, item)"
>{{ item.content }}</view
@ -28,19 +28,19 @@
<script setup lang="ts">
import { getListByTagIndustry } from "@/api/detail";
import { getTopIndustry_d } from "@/api/newsInfo";
import { getTopIndustryPeriod } from "@/api/newsInfo";
import { useUserStore } from "@/stores/user";
import { computed, onMounted, ref } from "vue";
import dayjs from "dayjs";
import { onMounted, ref } from "vue";
const emit = defineEmits(["onShow"]);
const active = ref(0);
const industryList = ref([]);
const oneData = ref({});
const userStore = useUserStore();
const industryList_d = computed(() => {
return industryList.value.slice(0, 4);
});
const start_time = dayjs().subtract(1, "month").format("YYYY-MM-DD");
const end_time = dayjs().format("YYYY-MM-DD");
const limit_num = 4;
const handleClickTag = (index: number, item: any) => {
active.value = index;
@ -49,7 +49,11 @@ const handleClickTag = (index: number, item: any) => {
// top10
async function getTopIndustry_dFn() {
industryList.value = await getTopIndustry_d({});
industryList.value = await getTopIndustryPeriod({
start_time: start_time,
end_time: end_time,
limit_num: limit_num,
});
}
async function getList() {
@ -63,24 +67,34 @@ async function getList() {
//
function goDetail(item: any) {
if (userStore.isLogin) {
uni.navigateTo({
url: `/pages/detail/indexNewsInfo?id=${item.id}`,
});
} else {
if (!userStore.isLogin) {
emit("onShow");
return;
}
uni.navigateTo({
url: `/pages/detail/indexNewsInfo?id=${item.id}`,
});
}
//
function navigateTo() {
if (userStore.isLogin) {
uni.navigateTo({
url: `/pages/industry/index`,
});
} else {
if (!userStore.isLogin) {
emit("onShow");
return;
}
//
if (!userStore.isUserType) {
uni.showToast({
title: "暂未开通本栏目",
icon: "none",
});
return;
}
uni.navigateTo({
url: `/pages/industry/index`,
});
}
onMounted(async () => {

View File

@ -30,7 +30,7 @@
</view>
</view>
<!-- 24小时榜 end -->
<view v-if="!loading" :class="['page-main ', { blur: !userStore.isLogin }]">
<view v-if="!loading" :class="['page-main ', { mask: !userStore.isLogin }]">
<view class="news-list-top">
<view class="news-item" v-for="(item, index) in newsListTop" :key="item.news_id" @click="goDetail(item)">
<view class="news-no"> {{ index + 1 }} </view>
@ -196,6 +196,7 @@ const chooseDate = reactive({
//
const calendarShow = ref(false);
function showCalendar() {
if(!userStore.isLogin || !userStore.isUserType) return;
calendarShow.value = true;
}
//
@ -334,7 +335,7 @@ onMounted(() => {
}
.page-main {
&.blur {
&.mask {
filter: blur(5px);
}
.news-list-top {

View File

@ -50,6 +50,11 @@ const storeSetup = () => {
return !!token.value;
});
// 试用 | 正式
const isUserType = computed(() => {
return getUserInfos().accountType === 1 ? true : false;
});
// 登录
const onLogin = async (data: any): Promise<any> => {
return new Promise(async (resolve, reject) => {
@ -96,7 +101,7 @@ const storeSetup = () => {
});
};
return { isLogin, userInfos, getToken, onLogin, getUserInfos };
return { isLogin, userInfos, getToken, onLogin, getUserInfos, isUserType };
};
export const useUserStore = defineStore("user", storeSetup);

View File

@ -82,3 +82,27 @@ export const jumpUrl = (url) => {
};
// 半遮蔽个股
export function maskStockNameProportional(name: string) {
if (!name || name.length === 0) return '';
// 根据名称长度动态决定屏蔽比例
let maskRatio;
if (name.length <= 2) {
maskRatio = 0.5; // 短名称屏蔽一半
} else if (name.length <= 4) {
maskRatio = 0.6; // 中等长度屏蔽60%
} else if (name.length <= 6) {
maskRatio = 0.7; // 较长名称屏蔽70%
} else {
maskRatio = 0.8; // 长名称屏蔽80%
}
// 计算需要屏蔽的字符数
const maskLength = Math.ceil(name.length * maskRatio);
// 确保至少显示一个字符
const finalMaskLength = Math.min(maskLength, name.length - 1);
return '*'.repeat(finalMaskLength) + name.substring(finalMaskLength);
}