Initial commit of akmon project

This commit is contained in:
2026-01-20 08:04:15 +08:00
commit 77a2bab985
1309 changed files with 343305 additions and 0 deletions

46
pages/mt/chat.uvue Normal file
View File

@@ -0,0 +1,46 @@
<template>
<view :class="['mt-chat', isLargeScreen ? 'large' : 'small']">
<scroll-view class="mt-chat-history">
<view v-for="msg in messages" :key="msg.id" :class="['mt-chat-msg', msg.role]">
<text>{{ msg.content }}</text>
</view>
</scroll-view>
<view class="mt-chat-input">
<input v-model="input" :placeholder="t('mt.chat.placeholder')" @confirm="onSend" />
<button @tap="onSend">{{ t('mt.chat.send') }}</button>
</view>
</view>
</template>
<script lang="uts">
import { ref, onLoad } from 'uni-app'
import { t } from '@/i18n/mt'
import { isLargeScreen } from '@/utils/responsive'
import { useChat } from '@/composables/use-chat'
const messages = ref([])
const input = ref('')
const { sendMessage, loadHistory } = useChat()
onLoad(() => {
loadHistory().then(res => { messages.value = res })
})
function onSend() {
if (!input.value) return
sendMessage(input.value).then(msg => {
messages.value.push(msg)
input.value = ''
})
}
</script>
<style lang="scss">
.mt-chat {
display: flex; flex-direction: column; height: 100vh;
&.large { max-width: 900rpx; margin: 0 auto; }
.mt-chat-history { flex: 1; overflow-y: auto; padding: 24rpx; }
.mt-chat-msg { margin-bottom: 16rpx; }
.mt-chat-msg.user { text-align: right; color: #1976d2; }
.mt-chat-msg.assistant { text-align: left; color: #333; }
.mt-chat-input { display: flex; gap: 12rpx; padding: 16rpx; border-top: 1px solid #eee; }
}
</style>

69
pages/mt/comments.uvue Normal file
View File

@@ -0,0 +1,69 @@
<template>
<view :class="['mt-comments', isLargeScreen ? 'large' : 'small']">
<text class="mt-title">{{ t('mt.comments.title') }}</text>
<scroll-view>
<view v-for="comment in comments" :key="comment.id" class="mt-comment-item">
<view class="mt-comment-header">
<image :src="comment.author_avatar" class="mt-comment-avatar" />
<text class="mt-comment-author">{{ comment.author_name }}</text>
<text class="mt-comment-date">{{ comment.created_at }}</text>
</view>
<text class="mt-comment-content">{{ comment.content }}</text>
<view class="mt-comment-actions">
<button @tap="onLike(comment)">{{ t('mt.comments.like') }}</button>
<button @tap="onReply(comment)">{{ t('mt.comments.reply') }}</button>
</view>
</view>
</scroll-view>
<view class="mt-comment-input">
<input v-model="input" :placeholder="t('mt.comments.input')" @confirm="onSend" />
<button @tap="onSend">{{ t('mt.comments.send') }}</button>
</view>
</view>
</template>
<script lang="uts">
import { ref, onLoad } from 'uni-app'
import { t } from '@/i18n/mt'
import { isLargeScreen } from '@/utils/responsive'
import { getComments, sendComment } from '@/composables/use-comments'
const comments = ref([])
const input = ref('')
const targetId = ref('')
onLoad((params) => {
if (params.id) {
targetId.value = params.id
getComments(params.id).then(res => { comments.value = res })
}
})
function onSend() {
if (!input.value) return
sendComment(targetId.value, input.value).then(res => {
comments.value.push(res)
input.value = ''
})
}
function onLike(comment) {
// 点赞逻辑
}
function onReply(comment) {
// 回复逻辑
}
</script>
<style lang="scss">
.mt-comments {
padding: 24rpx;
&.large { max-width: 900rpx; margin: 0 auto; }
.mt-title { font-size: 40rpx; font-weight: bold; margin-bottom: 24rpx; }
.mt-comment-item { border-bottom: 1px solid #eee; padding: 16rpx 0; }
.mt-comment-header { display: flex; align-items: center; gap: 12rpx; margin-bottom: 8rpx; }
.mt-comment-avatar { width: 48rpx; height: 48rpx; border-radius: 50%; }
.mt-comment-author { font-weight: 500; margin-right: 8rpx; }
.mt-comment-date { color: #aaa; font-size: 22rpx; }
.mt-comment-content { margin-bottom: 8rpx; }
.mt-comment-actions { display: flex; gap: 16rpx; }
.mt-comment-input { display: flex; gap: 12rpx; padding: 16rpx; border-top: 1px solid #eee; }
}
</style>

59
pages/mt/detail.uvue Normal file
View File

@@ -0,0 +1,59 @@
<template>
<view :class="['mt-detail', isLargeScreen ? 'large' : 'small']">
<!-- 标题 -->
<text class="mt-title">{{ t('mt.detail.title') }}</text>
<!-- 内容区 -->
<scroll-view>
<view class="mt-content">
<!-- 资讯内容渲染区域 -->
<text>{{ newsDetail.title }}</text>
<rich-text :nodes="newsDetail.content" />
</view>
<!-- 评论入口 -->
<button @tap="goToComments">{{ t('mt.detail.comments') }}</button>
</scroll-view>
<!-- 收藏/转发/点赞等操作栏 -->
<view class="mt-actions">
<button @tap="onFavorite">{{ t('mt.detail.favorite') }}</button>
<button @tap="onShare">{{ t('mt.detail.share') }}</button>
<button @tap="onLike">{{ t('mt.detail.like') }}</button>
</view>
</view>
</template>
<script lang="uts">
import { ref, onLoad } from 'uni-app'
import { isLargeScreen } from '@/utils/responsive'
import { getNewsDetail } from '@/composables/use-news'
const newsDetail = ref({})
onLoad((params) => {
if (params.id) {
getNewsDetail(params.id).then(res => {
newsDetail.value = res
})
}
})
function goToComments() {
uni.navigateTo({ url: `/pages/mt/comments.uvue?id=${newsDetail.value.id}` })
}
function onFavorite() {
// 收藏逻辑
}
function onShare() {
// 转发逻辑
}
function onLike() {
// 点赞逻辑
}
</script>
<style lang="scss">
.mt-detail {
padding: 24rpx;
&.large { max-width: 900rpx; margin: 0 auto; }
.mt-title { font-size: 40rpx; font-weight: bold; margin-bottom: 24rpx; }
.mt-content { margin-bottom: 32rpx; }
.mt-actions { display: flex; gap: 24rpx; justify-content: flex-end; }
}
</style>

261
pages/mt/index.uvue Normal file
View File

@@ -0,0 +1,261 @@
<template>
<view class="index-container">
<!-- Banner 区域,可轮播 -->
<swiper class="banner-swiper" :indicator-dots="true" :autoplay="true" interval="4000" circular>
<swiper-item v-for="(item, idx) in banners" :key="idx">
<image class="banner-img" :src="item.cover" @click="onBannerClick(item)"></image>
<view class="banner-title">{{ item.title }}</view>
</swiper-item>
</swiper>
<!-- 分类Tab -->
<scroll-view class="category-tabs" scroll-x>
<view v-for="(cat, idx) in categories" :key="cat.id" :class="['tab-item', currentCategory === cat.id ? 'active' : '']" @click="switchCategory(cat.id)">
{{ cat.name }}
</view>
</scroll-view>
<!-- 资讯列表 -->
<list-view style="flex:1;" refresher-enabled=true @refresherrefresh="onRefresh" :refresher-triggered="refresherTriggered" enable-back-to-top="true">
<list-item v-for="(news, idx) in newsList" :key="news.id" type=2>
<view class="news-card" hover-class="news-card-hover" @click="goDetail(news, idx)">
<image class="news-cover" :src="news.cover"></image>
<view class="news-body">
<text class="news-title">{{ news.title }}</text>
<text class="news-summary">{{ news.summary }}</text>
<view class="news-meta">
<text class="news-author">{{ news.author_name }}</text>
<text class="news-time">{{ news.published_at }}</text>
<view class="news-tags">
<text v-for="tag in news.tags" :key="tag" class="news-tag">{{ tag }}</text>
</view>
</view>
</view>
</view>
</list-item>
<view v-if="loading" class="loading-text">{{ $t('status.loading') }}</view>
<view v-if="!loading && newsList.length === 0" class="empty-text">{{ $t('status.noData') }}</view>
</list-view>
</view>
</template>
<script>
type Banner = {
cover: string,
title: string,
id: string
}
type NewsItem = {
id: string,
title: string,
summary: string,
author_name: string,
cover: string,
published_at: string,
tags: string[]
}
type Category = {
id: string,
name: string
}
export default {
data() {
return {
banners: [] as Banner[],
categories: [
{ id: 'all', name: this.$t('title.news') },
{ id: 'technology', name: this.$t('title.technology') },
{ id: 'sports', name: this.$t('title.sports') },
{ id: 'health', name: this.$t('title.health') },
{ id: 'entertainment', name: this.$t('title.entertainment') }
] as Category[],
currentCategory: 'all',
newsList: [] as NewsItem[],
refresherTriggered: false,
loading: false
};
},
onLoad() {
this.fetchBanners();
this.fetchNewsList();
},
methods: {
fetchBanners() {
// TODO: 替换为实际API
this.banners = [
{ cover: 'https://example.com/banner1.jpg', title: 'AI科技突破', id: 'b1' },
{ cover: 'https://example.com/banner2.jpg', title: '体育赛事速递', id: 'b2' }
];
},
fetchNewsList() {
this.loading = true;
// TODO: 替换为实际API按currentCategory过滤
setTimeout(() => {
this.newsList = [
{
id: 'n1',
title: 'AI助力医疗新进展',
summary: '人工智能正在改变医疗行业,提升诊断效率。',
author_name: '张三',
cover: 'https://example.com/news1.jpg',
published_at: '2025-06-29',
tags: ['AI', '医疗']
},
{
id: 'n2',
title: '全球体育赛事回顾',
summary: '本周全球体育赛事精彩纷呈,亮点不断。',
author_name: '李四',
cover: 'https://example.com/news2.jpg',
published_at: '2025-06-28',
tags: ['体育']
}
];
this.loading = false;
this.refresherTriggered = false;
}, 800);
},
switchCategory(catId: string) {
this.currentCategory = catId;
this.fetchNewsList();
},
goDetail(news: NewsItem, idx: number) {
uni.navigateTo({
url: `/pages/mt/detail.uvue?id=${news.id}`
});
},
onBannerClick(item: Banner) {
// 可跳转到专题或详情
uni.navigateTo({
url: `/pages/mt/detail.uvue?id=${item.id}`
});
},
onRefresh() {
this.refresherTriggered = true;
this.fetchNewsList();
}
}
}
</script>
<style>
.index-container {
display: flex;
flex-direction: column;
width: 100vw;
min-height: 100vh;
background: #f7f8fa;
}
.banner-swiper {
width: 100vw;
height: 180px;
}
.banner-img {
width: 100vw;
height: 180px;
object-fit: cover;
}
.banner-title {
position: absolute;
left: 20px;
bottom: 20px;
color: #fff;
font-size: 18px;
font-weight: bold;
text-shadow: 0 2px 8px rgba(0,0,0,0.2);
}
.category-tabs {
display: flex;
flex-direction: row;
background: #fff;
padding: 0 10px;
height: 44px;
align-items: center;
border-bottom: 1px solid #eee;
}
.tab-item {
margin-right: 18px;
font-size: 15px;
color: #666;
padding: 6px 10px;
border-radius: 16px;
}
.tab-item.active {
background: #007aff;
color: #fff;
}
.news-card {
display: flex;
flex-direction: row;
background: #fff;
margin: 10px 10px 0 10px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.03);
overflow: hidden;
}
.news-cover {
width: 110px;
height: 80px;
object-fit: cover;
border-radius: 8px 0 0 8px;
}
.news-body {
flex: 1;
padding: 10px 12px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.news-title {
font-size: 16px;
font-weight: 500;
color: #222;
margin-bottom: 6px;
line-height: 1.3;
}
.news-summary {
font-size: 13px;
color: #666;
margin-bottom: 8px;
line-height: 1.4;
}
.news-meta {
display: flex;
flex-direction: row;
align-items: center;
font-size: 12px;
color: #999;
justify-content: space-between;
}
.news-author {
margin-right: 10px;
}
.news-tags {
display: flex;
flex-direction: row;
}
.news-tag {
background: #f0f0f0;
color: #007aff;
font-size: 11px;
border-radius: 10px;
padding: 2px 8px;
margin-left: 4px;
}
.loading-text, .empty-text {
text-align: center;
color: #bbb;
font-size: 14px;
margin: 20px 0;
}
@media (min-width: 900px) {
.index-container {
max-width: 900px;
margin: 0 auto;
}
.banner-swiper, .banner-img {
width: 900px;
}
}
</style>

218
pages/mt/list-news.uvue Normal file
View File

@@ -0,0 +1,218 @@
<template>
<list-view style="flex: 1;" refresher-enabled=true @refresherrefresh="onRefresherrefresh"
:refresher-triggered="refresherTriggered" enable-back-to-top="true">
<list-item class="banner" @click="bannerClick(banner)" type=1>
<image class="banner-img" :src="banner.cover"></image>
<text class="banner-title">{{ banner.title }}</text>
</list-item>
<sticky-header>
<view
style="width: 100%;height:44px;background-color: #f5f5f5;flex-direction: row;justify-content:center;align-items:center;">
<text v-for="(name,index) in th_item" :key="index" @click="clickTH(index)" style="margin-right: 20px;">
{{name}}
</text>
</view>
</sticky-header>
<list-item v-for="(value, index) in listData" :key="index" type=2>
<view class="uni-list-cell" hover-class="uni-list-cell-hover" @click="goDetail(value, index)">
<view class="uni-media-list">
<share-element :share-key="'image_'+index">
<image class="uni-media-list-logo" :src="value.cover"></image>
</share-element>
<view class="uni-media-list-body">
<text class="uni-media-list-text-top">{{ value.title }}</text>
<view class="uni-media-list-text-bottom">
<text class="uni-media-list-text">{{ value.author_name }}</text>
<text class="uni-media-list-text">{{ value.published_at }}</text>
</view>
</view>
</view>
</view>
</list-item>
</list-view>
</template>
<script>
type Banner = {
cover : string | null,
title : string | null,
post_id : string | null
}
type Item = {
author_name : string,
cover : string,
id : number,
post_id : string,
published_at : string,
title : string
}
export default {
data() {
return {
th_item: ["排序", "筛选"],
refresherTriggered: false,
banner: {} as Banner,
listData: [] as Item[],
last_id: '',
};
},
onLoad() {
this.getBanner();
this.getList();
},
onUnload() {
},
methods: {
getBanner() {
let data = {
column: 'id,post_id,title,author_name,cover,published_at' //需要的字段名
};
uni.request<Banner>({
url: 'https://unidemo.dcloud.net.cn/api/banner/36kr',
data: data,
success: data => {
this.refresherTriggered = false
if (data.statusCode == 200) {
const result = data.data
if (result != null) {
this.banner = result;
}
}
},
fail: (e) => {
console.log('fail', e);
}
});
},
getList() {
let url = "https://unidemo.dcloud.net.cn/api/news?column=id,post_id,title,author_name,cover,published_at";
/* if (this.last_id != "") {
const minId = parseInt((this.last_id))
const time = new Date().getTime() + '';
const pageSize = 10;
url = url + "&minId=" + minId + "&time=" + time + "&pageSize=" + pageSize;
} */
uni.request<Item[]>({
url: url,
method: "GET",
success: (res) => {
if (res.statusCode == 200) {
console.log(res);
const result = res.data
if (result != null) {
this.listData = result //因本接口没有更多分页数据所以重新赋值。正常有分页的列表应该如下面push方式增加数组项
// this.listData.push(...result)
// this.last_id = this.listData[0].id + "";
}
this.refresherTriggered = false;
}
},
fail: (res) => {
console.log('fail', res);
this.refresherTriggered = false
}
});
},
goDetail(e : Item, key : number) {
const detail = e;
const post_id = detail.post_id;
let cover = detail.cover;
// #ifdef APP-ANDROID
cover = btoa(cover);
// #endif
const title = detail.title;
uni.navigateTo({
url: '/pages/template/list-news/detail/detail?post_id=' + post_id + "&cover=" + cover + "&title=" + title +"&shareKey=image_"+key
});
},
bannerClick(e : Banner) {
const detail = e;
const post_id = detail.post_id;
let cover = detail.cover ?? "";
// #ifdef APP-ANDROID
cover = btoa(cover);
// #endif
const title = detail.title;
uni.navigateTo({
url: '/pages/template/list-news/detail/detail?post_id=' + post_id + "&cover=" + cover + "&title=" + title
});
},
clickTH(index : number) {
uni.showModal({
content: "点击表头项:" + index,
showCancel: false
});
},
onRefresherrefresh() {
this.refresherTriggered = true
this.getBanner();
this.getList();
}
}
};
</script>
<style>
.banner {
height: 180px;
overflow: hidden;
position: relative;
background-color: #ccc;
}
.banner-img {
width: 100%;
}
.banner-title {
max-height: 42px;
overflow: hidden;
position: absolute;
left: 15px;
bottom: 15px;
width: 90%;
font-size: 16px;
font-weight: 400;
line-height: 21px;
color: white;
}
.uni-media-list {
padding: 11px 15px;
box-sizing: border-box;
display: flex;
width: 100%;
flex-direction: row;
}
.uni-media-list-logo {
width: 90px;
height: 70px;
}
.uni-media-list-body {
flex: 1;
padding-left: 7px;
justify-content: space-around;
}
.uni-media-list-text-top {
/* height: 37px; */
font-size: 14px;
lines: 2;
overflow: hidden;
}
.uni-media-list-text-bottom {
display: flex;
flex-direction: row;
justify-content: space-between;
}
.uni-media-list-text {
color: #9D9D9F;
font-size: 13px;
}
</style>

41
pages/mt/search.uvue Normal file
View File

@@ -0,0 +1,41 @@
<template>
<view :class="['mt-search', isLargeScreen ? 'large' : 'small']">
<view class="mt-search-bar">
<input v-model="keyword" :placeholder="t('mt.search.placeholder')" @confirm="onSearch" />
<button @tap="onSearch">{{ t('mt.search.button') }}</button>
</view>
<scroll-view>
<view v-for="item in searchResults" :key="item.id" class="mt-search-item" @tap="goToDetail(item.id)">
<text>{{ item.title }}</text>
<text class="mt-summary">{{ item.summary }}</text>
</view>
</scroll-view>
</view>
</template>
<script lang="uts">
import { ref } from 'uni-app'
import { t } from '@/i18n/mt'
import { isLargeScreen } from '@/utils/responsive'
import { searchNews } from '@/composables/use-news'
const keyword = ref('')
const searchResults = ref([])
function onSearch() {
searchNews(keyword.value).then(res => {
searchResults.value = res
})
}
function goToDetail(id: string) {
uni.navigateTo({ url: `/pages/mt/detail.uvue?id=${id}` })
}
</script>
<style lang="scss">
.mt-search {
padding: 24rpx;
&.large { max-width: 900rpx; margin: 0 auto; }
.mt-search-bar { display: flex; gap: 16rpx; margin-bottom: 24rpx; }
.mt-search-item { padding: 16rpx 0; border-bottom: 1px solid #eee; }
.mt-summary { color: #888; font-size: 24rpx; }
}
</style>

49
pages/mt/settings.uvue Normal file
View File

@@ -0,0 +1,49 @@
<template>
<view :class="['mt-settings', isLargeScreen ? 'large' : 'small']">
<text class="mt-title">{{ t('mt.settings.title') }}</text>
<view class="mt-setting-item">
<text>{{ t('mt.settings.language') }}</text>
<picker :range="languages" v-model="currentLang" @change="onLangChange" />
</view>
<view class="mt-setting-item">
<text>{{ t('mt.settings.theme') }}</text>
<switch v-model="isDark" @change="onThemeChange" />
</view>
<!-- admin入口 -->
<button v-if="isAdmin" @tap="goToAdmin">{{ t('mt.settings.admin') }}</button>
</view>
</template>
<script lang="uts">
import { ref } from 'uni-app'
import { t } from '@/i18n/mt'
import { isLargeScreen } from '@/utils/responsive'
import { getUserRole } from '@/composables/use-auth'
const languages = ref(['zh-CN', 'en', 'ja'])
const currentLang = ref('zh-CN')
const isDark = ref(false)
const isAdmin = ref(false)
onLoad(() => {
// 加载用户设置
isAdmin.value = getUserRole() === 'admin'
})
function onLangChange(e) {
// 切换语言逻辑
}
function onThemeChange(e) {
// 切换主题逻辑
}
function goToAdmin() {
uni.navigateTo({ url: '/pages/mt/admin.uvue' })
}
</script>
<style lang="scss">
.mt-settings {
padding: 32rpx;
&.large { max-width: 900rpx; margin: 0 auto; }
.mt-title { font-size: 40rpx; font-weight: bold; margin-bottom: 32rpx; }
.mt-setting-item { display: flex; align-items: center; justify-content: space-between; margin-bottom: 24rpx; }
}
</style>

View File

@@ -0,0 +1,57 @@
<template>
<view :class="['mt-topic-detail', isLargeScreen ? 'large' : 'small']">
<image :src="topic.cover_image_url" class="mt-topic-cover" />
<text class="mt-topic-title">{{ topic.title }}</text>
<text class="mt-topic-desc">{{ topic.description }}</text>
<view class="mt-topic-actions">
<button @tap="onSubscribe">{{ t('mt.topic.subscribe') }}</button>
<button @tap="onFavorite">{{ t('mt.topic.favorite') }}</button>
<button @tap="onShare">{{ t('mt.topic.share') }}</button>
</view>
<view class="mt-topic-contents">
<text class="mt-topic-section">{{ t('mt.topic.contents') }}</text>
<view v-for="item in topic.contents" :key="item.id" class="mt-topic-content-item" @tap="goToDetail(item.id)">
<text>{{ item.title }}</text>
</view>
</view>
</view>
</template>
<script lang="uts">
import { ref, onLoad } from 'uni-app'
import { t } from '@/i18n/mt'
import { isLargeScreen } from '@/utils/responsive'
import { getTopicDetail } from '@/composables/use-topics'
const topic = ref({ contents: [] })
onLoad((params) => {
if (params.id) {
getTopicDetail(params.id).then(res => { topic.value = res })
}
})
function onSubscribe() {
// 订阅逻辑
}
function onFavorite() {
// 收藏逻辑
}
function onShare() {
// 分享逻辑
}
function goToDetail(id: string) {
uni.navigateTo({ url: `/pages/mt/detail.uvue?id=${id}` })
}
</script>
<style lang="scss">
.mt-topic-detail {
padding: 24rpx;
&.large { max-width: 900rpx; margin: 0 auto; }
.mt-topic-cover { width: 100%; height: 240rpx; border-radius: 16rpx; margin-bottom: 24rpx; }
.mt-topic-title { font-size: 40rpx; font-weight: bold; margin-bottom: 12rpx; }
.mt-topic-desc { color: #888; font-size: 28rpx; margin-bottom: 24rpx; }
.mt-topic-actions { display: flex; gap: 16rpx; margin-bottom: 24rpx; }
.mt-topic-section { font-size: 32rpx; font-weight: 500; margin-bottom: 12rpx; }
.mt-topic-content-item { padding: 12rpx 0; border-bottom: 1px solid #eee; }
}
</style>

42
pages/mt/topics.uvue Normal file
View File

@@ -0,0 +1,42 @@
<template>
<view :class="['mt-topics', isLargeScreen ? 'large' : 'small']">
<text class="mt-title">{{ t('mt.topics.title') }}</text>
<scroll-view>
<view v-for="topic in topics" :key="topic.id" class="mt-topic-item" @tap="goToTopic(topic.id)">
<image :src="topic.cover_image_url" class="mt-topic-cover" />
<view class="mt-topic-info">
<text class="mt-topic-title">{{ topic.title }}</text>
<text class="mt-topic-desc">{{ topic.description }}</text>
</view>
</view>
</scroll-view>
</view>
</template>
<script lang="uts">
import { ref, onLoad } from 'uni-app'
import { t } from '@/i18n/mt'
import { isLargeScreen } from '@/utils/responsive'
import { getTopics } from '@/composables/use-topics'
const topics = ref([])
onLoad(() => {
getTopics().then(res => { topics.value = res })
})
function goToTopic(id: string) {
uni.navigateTo({ url: `/pages/mt/topic-detail.uvue?id=${id}` })
}
</script>
<style lang="scss">
.mt-topics {
padding: 24rpx;
&.large { max-width: 900rpx; margin: 0 auto; }
.mt-title { font-size: 40rpx; font-weight: bold; margin-bottom: 24rpx; }
.mt-topic-item { display: flex; gap: 16rpx; margin-bottom: 24rpx; align-items: center; }
.mt-topic-cover { width: 120rpx; height: 120rpx; border-radius: 12rpx; }
.mt-topic-info { flex: 1; }
.mt-topic-title { font-size: 32rpx; font-weight: 500; }
.mt-topic-desc { color: #888; font-size: 24rpx; }
}
</style>

51
pages/mt/video.uvue Normal file
View File

@@ -0,0 +1,51 @@
<template>
<view :class="['mt-video', isLargeScreen ? 'large' : 'small']">
<video :src="videoUrl" controls class="mt-video-player" />
<text class="mt-video-title">{{ videoTitle }}</text>
<view class="mt-video-actions">
<button @tap="onFavorite">{{ $t('mt.video.favorite') }}</button>
<button @tap="onShare">{{ $t('mt.video.share') }}</button>
<button @tap="onLike">{{ $t('mt.video.like') }}</button>
</view>
<view class="mt-video-desc">{{ videoDesc }}</view>
</view>
</template>
<script lang="uts">
import { responsiveState } from '@/utils/utils'
import { ChatDataService } from '@/utils/chatDataService.uts'
const videoUrl = ref('')
const videoTitle = ref('')
const videoDesc = ref('')
function onFavorite() {
// 收藏逻辑
}
function onShare() {
// 分享逻辑
}
function onLike() {
// 点赞逻辑
}
onLoad((params) => {
if (params.url) {
const u = params.url as string
// resolve to cached local path; fallback to url if fails
;(async () => { videoUrl.value = await ChatDataService.resolveVideoSource(u) })()
}
if (params.title) videoTitle.value = params.title
if (params.desc) videoDesc.value = params.desc
})
</script>
<style lang="scss">
.mt-video {
padding: 24rpx;
&.large { max-width: 900rpx; margin: 0 auto; }
.mt-video-player { width: 100%; height: 400rpx; border-radius: 12rpx; margin-bottom: 24rpx; }
.mt-video-title { font-size: 36rpx; font-weight: bold; margin-bottom: 12rpx; }
.mt-video-actions { display: flex; gap: 16rpx; margin-bottom: 16rpx; }
.mt-video-desc { color: #888; font-size: 26rpx; }
}
</style>