no message

This commit is contained in:
hajimi
2026-06-11 12:15:29 +08:00
parent 10ebe39c30
commit 96efa1d905
5859 changed files with 815501 additions and 5 deletions
@@ -0,0 +1,158 @@
<template>
<!-- modal:隐私授权弹窗-->
<view v-if="show" class="modal-box" @tap.stop>
<view class="dialog" @tap.stop>
<view class="title">隐私政策提示</view>
<view class="content">
欢迎使用{{
appStore.getWebsiteConfig.shop_name
}}小程序请您在使用前点击
<text
class="text-[#243245]"
hover-class="hover"
@click="openContract"
>
{{ name }}
</text>
并仔细阅读如您同意全部内容请点击同意开始使用我们的服务
</view>
<view class="btn-box">
<button
class="btn disagree"
hover-class="hover"
@click="disagreePrivacy"
>
不同意
</button>
<button
class="btn bg-primary text-white"
hover-class="hover"
id="agree-btn"
open-type="agreePrivacyAuthorization"
@agreeprivacyauthorization="agreePrivacy"
>
同意
</button>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { useAppStore } from '@/stores/app'
const appStore = useAppStore()
const name = ref<string>('')
const show = ref<boolean>(false)
interface PrivacyRes {
errMsg: string
privacyContractName: string
needAuthorization: boolean
}
if (wx.getPrivacySetting) {
wx.getPrivacySetting({
success(res: PrivacyRes) {
console.log(res)
name.value = res.privacyContractName
show.value = res.needAuthorization
}
})
}
const openContract = () => {
wx.openPrivacyContract({
success: () => {},
fail: () => {}
})
}
const disagreeHandle = () => {
// 用户点击拒绝后
show.value = false
}
const disagreePrivacy = () => {
uni.$u.toast('同意隐私政策后可继续使用')
// wx.exitMiniProgram()
}
const agreePrivacy = () => {
show.value = false
}
</script>
<style scoped>
.modal-box {
height: 100vh;
width: 100vw;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
z-index: 99999;
}
.modal-box .dialog {
box-sizing: border-box;
position: absolute;
bottom: 0;
width: 100%;
padding: 40rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
background: #ffffff;
border-radius: 20rpx 20rpx 0 0;
}
.modal-box .title {
text-align: center;
color: #333;
font-weight: bold;
font-size: 34rpx;
}
.modal-box .content {
display: block;
font-size: 28rpx;
color: #666;
margin-top: 20rpx;
text-align: justify;
line-height: 1.6;
padding: 10rpx 20rpx;
}
.modal-box .btn-box {
margin-top: 50rpx;
padding: 0 30rpx;
padding-bottom: 30rpx;
display: flex;
text-align: center;
}
.modal-box .btn::after {
border: none;
display: none;
}
.modal-box .btn-box .btn {
width: 50%;
height: 76rpx;
line-height: 76rpx;
margin: 0 10rpx;
padding: 0;
align-items: center;
justify-content: center;
border-radius: 60px;
font-size: 28rpx;
font-weight: 500;
}
.modal-box .disagree {
color: #0f0f0f;
background: #f5f5f5;
}
</style>
+563
View File
@@ -0,0 +1,563 @@
<template>
<view class="index">
<!-- 顶部导航栏 -->
<view class="top-bar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="top-bar__content">
<view class="top-bar__logo">
<image src="/static/images/logo.png" mode="aspectFit" class="top-bar__logo-img" />
</view>
<view class="top-bar__search" @tap="goSearch">
<UIcon name="search" size="28" color="#999" />
<text class="top-bar__search-text">{{ searchPlaceholder }}</text>
</view>
<view class="top-bar__actions" />
</view>
</view>
<!-- Feed流内容 -->
<scroll-view scroll-y class="feed-wrapper" :style="{ height: feedHeight + 'px' }" @scrolltolower="loadMore"
:lower-threshold="100">
<view class="assistant-entry" @tap="goAssistant">
<view class="assistant-entry__icon">AI</view>
<view class="assistant-entry__body">
<text class="assistant-entry__title">世博头条 AI 助手</text>
<text class="assistant-entry__desc">问资讯赛事社区彩票和行情</text>
</view>
<u-icon name="arrow-right" size="28" color="#185dff" />
</view>
<!-- 频道Tab栏 -->
<view class="channel-bar">
<scroll-view scroll-x class="channel-bar__scroll" :scroll-left="channelScrollLeft"
scroll-with-animation>
<view class="channel-bar__list">
<view v-for="(item, index) in allChannels" :key="item.channelKey" class="channel-bar__item"
:class="{ 'channel-bar__item--active': currentChannel === index }"
@tap="switchChannel(index)">
<text>{{ item.name }}</text>
</view>
</view>
</scroll-view>
<view class="channel-bar__more" @tap="goChannelManage">
<UIcon name="list" size="38" color="#666" />
</view>
</view>
<template v-for="item in feedList" :key="item.id">
<FeedTranslateCard v-if="hasTranslatedContent(item)" :item="item" />
<FeedVideoCard v-else-if="item.is_video" :item="item" />
<FeedCard v-else :item="item" />
</template>
<view class="feed-loading" v-if="loading">
<ULoading mode="circle" />
<text class="feed-loading__text">加载中...</text>
</view>
<view class="feed-bottom" v-if="noMore && feedList.length">
<text>没有更多了</text>
</view>
<view class="feed-empty" v-if="!loading && !feedList.length">
<text>暂无内容</text>
</view>
</scroll-view>
<!-- #ifdef MP -->
<MpPrivacyPopup></MpPrivacyPopup>
<!-- #endif -->
</view>
</template>
<script setup lang="ts">
import { onLoad, onShow, onUnload } from '@dcloudio/uni-app'
import { ref, nextTick } from 'vue'
import { getArticleList, getArticleCate } from '@/api/news'
import { resolveShortLink } from '@/api/share'
import { useUserStore } from '@/stores/user'
import FeedCard from '@/components/feed-card/feed-card.vue'
import FeedTranslateCard from '@/components/feed-translate-card/feed-translate-card.vue'
import FeedVideoCard from '@/components/feed-video-card/feed-video-card.vue'
import UIcon from '@/uni_modules/vk-uview-ui/components/u-icon/u-icon.vue'
import ULoading from '@/uni_modules/vk-uview-ui/components/u-loading/u-loading.vue'
const userStore = useUserStore()
// #ifdef MP
import MpPrivacyPopup from './component/mp-privacy-popup.vue'
// #endif
const statusBarHeight = ref(0)
const feedHeight = ref(500)
let windowHeight = 667
uni.getSystemInfo({
success: (res) => {
// #ifdef APP-PLUS || MP
statusBarHeight.value = res.statusBarHeight || 44
// #endif
// #ifdef H5
statusBarHeight.value = 0
// #endif
windowHeight = res.windowHeight || 667
const topBarH = statusBarHeight.value + 44
const tabbarH = uni.upx2px(100)
feedHeight.value = windowHeight - topBarH - tabbarH
}
})
const calcFeedHeight = () => {
const query = uni.createSelectorQuery()
query.select('.top-bar').boundingClientRect()
query.exec((rects: any[]) => {
const topBarH = rects[0]?.height || (statusBarHeight.value + 44)
const tabbarH = uni.upx2px(100)
feedHeight.value = windowHeight - topBarH - tabbarH
})
}
const searchPlaceholder = ref('搜索乐博头条')
const RECOMMEND_CHANNEL = {
id: '__recommend__',
name: '推荐',
specialType: 'recommend',
channelKey: '__recommend__'
}
const MY_CHANNEL = {
id: '__mychannel__',
name: '我的频道',
specialType: 'mychannel',
channelKey: '__mychannel__'
}
const articleChannels = ref<any[]>([])
const allChannels = ref<any[]>([])
const currentChannel = ref(0)
const channelScrollLeft = ref(0)
const feedList = ref<any[]>([])
const loading = ref(false)
const noMore = ref(false)
const pageNo = ref(1)
const pageSize = 10
const buildAllChannels = (channels: any[] = []) => {
const merged = [{ ...RECOMMEND_CHANNEL }]
if (userStore.isLogin) {
merged.push({ ...MY_CHANNEL })
}
const dynamicChannels = (channels || []).map((item: any) => ({
...item,
specialType: 'category',
channelKey: `cate_${item.id}`
}))
return [...merged, ...dynamicChannels]
}
const syncAllChannels = (channels: any[] = articleChannels.value) => {
const previousKey = allChannels.value[currentChannel.value]?.channelKey || RECOMMEND_CHANNEL.channelKey
allChannels.value = buildAllChannels(channels)
const nextIndex = allChannels.value.findIndex((item: any) => item.channelKey === previousKey)
currentChannel.value = nextIndex >= 0 ? nextIndex : 0
}
const findChannelIndexById = (id: any) => {
return allChannels.value.findIndex((item: any) => {
return item.specialType === 'category' && String(item.id) === String(id)
})
}
const loadChannels = async () => {
try {
const allCate = await getArticleCate()
articleChannels.value = allCate || []
syncAllChannels()
} catch (e) {
console.log('获取分类失败=>', e)
articleChannels.value = []
syncAllChannels()
}
}
const switchChannel = (index: number) => {
if (currentChannel.value === index) return
currentChannel.value = index
reloadFeed()
}
const reloadFeed = () => {
pageNo.value = 1
noMore.value = false
feedList.value = []
fetchFeedList()
}
const fetchFeedList = async () => {
if (loading.value || noMore.value) return
loading.value = true
try {
const params: Record<string, any> = {
page_no: pageNo.value,
page_size: pageSize
}
const currentChannelItem = allChannels.value[currentChannel.value]
if (!currentChannelItem || currentChannelItem.specialType === 'recommend') {
params.is_recommend = 1
} else if (currentChannelItem.specialType === 'mychannel') {
params.is_mychannel = 1
} else if (currentChannelItem.id) {
params.cid = currentChannelItem.id
}
const { lists, count } = await getArticleList(params)
const newList = lists || []
if (pageNo.value === 1) {
feedList.value = newList
} else {
feedList.value = [...feedList.value, ...newList]
}
if (feedList.value.length >= count || newList.length < pageSize) {
noMore.value = true
}
pageNo.value++
} catch (e) {
console.log('feed加载失败=>', e)
uni.showToast({ title: '加载失败,请稍后重试', icon: 'none' })
} finally {
loading.value = false
}
}
const loadMore = () => {
fetchFeedList()
}
const hasTranslatedContent = (item: any) => {
return !!String(item?.translated_content || item?.worldcup_translated_content || '').trim()
}
const goSearch = () => {
uni.navigateTo({ url: '/pages/search/search' })
}
const goChannelManage = () => {
uni.navigateTo({ url: '/pages/channel/channel-manage' })
}
const goAssistant = () => {
uni.navigateTo({ url: '/pages/ai_assistant/ai_assistant' })
}
onLoad(async (options: any) => {
// 处理短链接跳转参数
handleRedirect(options)
await loadChannels()
fetchFeedList()
nextTick(() => {
setTimeout(calcFeedHeight, 100)
})
})
const handleRedirect = async (options: any) => {
// onLoad options 或 URL query 中获取 slc
let slc = options?.slc || ''
// #ifdef H5
if (!slc) {
const urlParams = new URLSearchParams(window.location.search)
slc = urlParams.get('slc') || ''
}
// #endif
if (!slc) return
try {
const fingerprint = generateFingerprint()
const res = await resolveShortLink({
code: slc,
fingerprint,
referer: document?.referrer || ''
})
// 存储邀请码
if (res.params?.invite_code) {
uni.setStorageSync('invite_code', res.params.invite_code)
}
// 跳转到目标页面
if (res.path && res.page_type !== 'invite') {
setTimeout(() => {
uni.navigateTo({ url: res.path })
}, 300)
}
} catch (e) {
console.error('短链接解析失败', e)
}
}
const generateFingerprint = (): string => {
try {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
if (ctx) {
ctx.textBaseline = 'top'
ctx.font = '14px Arial'
ctx.fillText('fp', 2, 2)
}
const raw =
navigator.userAgent +
navigator.language +
(screen.width + 'x' + screen.height) +
screen.colorDepth +
new Date().getTimezoneOffset() +
canvas.toDataURL()
let hash = 0
for (let i = 0; i < raw.length; i++) {
hash = (hash << 5) - hash + raw.charCodeAt(i)
hash |= 0
}
return Math.abs(hash).toString(16)
} catch {
return ''
}
}
let channelsLoaded = false
onShow(async () => {
syncAllChannels()
// iOS授权弹窗关闭后数据可能未加载,检测并重试
if (!channelsLoaded && articleChannels.value.length === 0) {
await loadChannels()
fetchFeedList()
}
if (channelsLoaded && allChannels.value[currentChannel.value]?.specialType === 'mychannel') {
reloadFeed()
}
channelsLoaded = true
})
const onChannelChanged = (data: any) => {
if (data.channels) {
articleChannels.value = data.channels
syncAllChannels()
}
if (data.selectIndex >= 0) {
const selectedChannel = articleChannels.value[data.selectIndex]
const selectedIndex = findChannelIndexById(selectedChannel?.id)
switchChannel(selectedIndex >= 0 ? selectedIndex : 0)
} else {
reloadFeed()
}
}
uni.$on('channelChanged', onChannelChanged)
onUnload(() => {
uni.$off('channelChanged', onChannelChanged)
})
</script>
<style lang="scss" scoped>
.index {
height: 100vh;
background: #f5f5f5;
overflow: hidden;
display: flex;
flex-direction: column;
}
/* 顶部导航栏 */
.top-bar {
flex-shrink: 0;
z-index: 100;
background: #fff;
&__content {
display: flex;
align-items: center;
padding: 12rpx 24rpx;
gap: 16rpx;
}
&__logo {
width: 64rpx;
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
&-img {
width: 64rpx;
height: 64rpx;
border-radius: 12rpx;
}
}
&__search {
flex: 1;
height: 64rpx;
background: #f5f5f5;
border-radius: 16rpx;
display: flex;
align-items: center;
padding: 0 24rpx;
gap: 12rpx;
&-text {
font-size: 26rpx;
color: #999;
}
}
&__actions {
display: flex;
align-items: center;
gap: 16rpx;
flex-shrink: 0;
}
&__publish {
width: 64rpx;
height: 64rpx;
display: flex;
align-items: center;
justify-content: center;
background: #e5edff;
border-radius: 16rpx;
}
}
/* 频道Tab栏 */
.assistant-entry {
margin: 20rpx 24rpx 16rpx;
padding: 22rpx 24rpx;
border-radius: 8rpx;
background: #fff;
border: 1rpx solid #dbe6f5;
display: flex;
align-items: center;
gap: 18rpx;
&__icon {
width: 60rpx;
height: 60rpx;
border-radius: 8rpx;
background: #185dff;
color: #fff;
font-size: 24rpx;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
&__body {
flex: 1;
min-width: 0;
}
&__title {
display: block;
font-size: 30rpx;
font-weight: 700;
color: #111827;
}
&__desc {
display: block;
margin-top: 4rpx;
font-size: 23rpx;
color: #6b7280;
}
}
/* 频道Tab栏 */
.channel-bar {
display: flex;
flex-direction: row;
align-items: center;
background: #fff;
border-bottom: 1rpx solid #f0f0f0;
height: 80rpx;
overflow: hidden;
flex-shrink: 0;
&__scroll {
flex: 1;
width: 0;
height: 80rpx;
}
&__list {
white-space: nowrap;
height: 80rpx;
padding: 0 8rpx;
}
&__item {
display: inline-block;
padding: 0 16rpx;
height: 80rpx;
line-height: 80rpx;
font-size: 28rpx;
color: #666;
position: relative;
text-align: center;
&--active {
color: #0053d8;
font-weight: bold;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 48rpx;
height: 6rpx;
background: #0053d8;
border-radius: 3rpx;
}
}
}
&__more {
width: 80rpx;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
border-left: 1rpx solid #f0f0f0;
flex-shrink: 0;
background: #fff;
}
}
/* Feed流 */
.feed-wrapper {
flex: 1;
min-height: 0;
background: #f5f5f5;
}
.feed-loading {
display: flex;
align-items: center;
justify-content: center;
padding: 30rpx 0;
gap: 12rpx;
&__text {
font-size: 24rpx;
color: #999;
}
}
.feed-bottom {
text-align: center;
padding: 30rpx 0;
font-size: 24rpx;
color: #ccc;
}
.feed-empty {
text-align: center;
padding: 100rpx 0;
font-size: 28rpx;
color: #999;
}
</style>