Compare commits

...

10 Commits

Author SHA1 Message Date
f5d37ddcd8 Merge branch 'main' of http://47.109.137.67:9091/frontend/paip.ai-web
Some checks failed
paipai-h5-dev / build-and-push (push) Failing after 9s
paipai-h5-dev / deploy (push) Has been skipped
paipai-h5-dev / cleanup (push) Failing after 2s
2025-12-05 12:35:35 +08:00
0f9353fcb7 页面为英文 2025-12-05 12:34:26 +08:00
ffe4bacc82 将文本翻译成英语 2025-12-04 18:52:57 +08:00
b4a86baaee 将文本翻译成日语 2025-12-04 15:32:55 +08:00
11ece8872d 更新 .gitea/workflows/build-dev.yaml 2025-11-25 10:05:31 +00:00
4bb67779d6 添加 .gitea/workflows/build-prod.yaml 2025-11-25 09:49:52 +00:00
ef5e8a8b35 更新 .gitea/workflows/build-dev.yaml 2025-11-25 09:44:42 +00:00
078f0504d4 添加 Dockerfile-dev 2025-11-25 09:41:49 +00:00
e641de13e5 更新 .gitea/workflows/build-dev.yaml 2025-11-25 09:40:54 +00:00
82d8d119c7 更新 .gitea/workflows/build-dev.yaml 2025-11-25 09:38:16 +00:00
15 changed files with 646 additions and 243 deletions

View File

@@ -32,17 +32,17 @@ jobs:
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5
with: with:
images: code.paipai.life/backend/rn-outside images: code.paipai.life/frontend/rn-h5
tags: | tags: |
type=raw,value=latest type=raw,value=latest
- name: Build Docker image - name: Build Docker image
run: | run: |
docker build -t code.paipai.life/backend/rn-outside:latest . docker build -f Dockerfile-dev -t code.paipai.life/frontend/rn-h5:latest .
- name: Push Docker image - name: Push Docker image
run: | run: |
docker push code.paipai.life/backend/rn-outside:latest docker push code.paipai.life/frontend/rn-h5:latest
env: env:
DOCKER_CONTENT_TRUST: 0 DOCKER_CONTENT_TRUST: 0
@@ -63,7 +63,7 @@ jobs:
password: ${{ secrets.DEV_SERVER_PASSWORD }} password: ${{ secrets.DEV_SERVER_PASSWORD }}
port: 22 port: 22
source: "docker-compose-dev.yaml" source: "docker-compose-dev.yaml"
target: "/home/paipai/data/rn-outside" target: "/home/paipai/data/rn-h5"
override: true override: true
- name: Deploy with Docker Compose - name: Deploy with Docker Compose
@@ -74,13 +74,10 @@ jobs:
password: ${{ secrets.DEV_SERVER_PASSWORD }} password: ${{ secrets.DEV_SERVER_PASSWORD }}
port: 22 port: 22
script: | script: |
# 切换到项目目录 cd /home/paipai/data/rn-h5
cd /home/paipai/data/rn-outside
# 拉取最新镜像 docker pull code.paipai.life/frontend/rn-h5:latest
docker pull code.paipai.life/backend/rn-outside:latest
# 使用 docker-compose 部署
docker compose -f docker-compose-dev.yaml down docker compose -f docker-compose-dev.yaml down
docker compose -f docker-compose-dev.yaml up -d docker compose -f docker-compose-dev.yaml up -d

View File

@@ -0,0 +1,111 @@
name: rn-h5-prod
on:
push:
tags: ['v*']
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver-opts: |
network=host
buildkitd-flags: |
--allow-insecure-entitlement security.insecure
- name: Log in to container registry
uses: docker/login-action@v3
with:
registry: 626064810415.dkr.ecr.us-west-1.amazonaws.com
username: ${{ secrets.PROD_DOCKER_USERNAME }}
password: ${{ secrets.PROD_DOCKER_PASSWORD }}
- name: Extract tag name
id: tag
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
- name: Build Docker image
run: |
docker build -t 626064810415.dkr.ecr.us-west-1.amazonaws.com/raveai/rn-h5:${{ steps.tag.outputs.TAG }} .
- name: Push Docker image
run: |
docker push 626064810415.dkr.ecr.us-west-1.amazonaws.com/raveai/rn-h5:${{ steps.tag.outputs.TAG }}
env:
DOCKER_CONTENT_TRUST: 0
deploy:
runs-on: ubuntu-latest
needs: build-and-push
environment: test
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Extract tag name
id: tag
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
- name: Copy files and deploy via SSH
uses: appleboy/scp-action@v0.1.3
with:
host: ${{ secrets.PROD_SERVER_HOST }}
username: ${{ secrets.PROD_SERVER_USERNAME }}
key: ${{ secrets.PROD_SERVER_KEY }}
port: 22
source: "docker-compose.yaml"
target: "/home/ubuntu/docker-compose/rn-h5"
override: true
- name: Deploy with Docker Compose
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.PROD_SERVER_HOST }}
username: ${{ secrets.PROD_SERVER_USERNAME }}
key: ${{ secrets.PROD_SERVER_KEY }}
port: 22
script: |
# 切换到项目目录
cd /home/ubuntu/docker-compose/rn-h5
aws ecr get-login-password --region us-west-1 | docker login --username AWS --password-stdin 626064810415.dkr.ecr.us-west-1.amazonaws.com
# 拉取指定tag的镜像
docker pull 626064810415.dkr.ecr.us-west-1.amazonaws.com/raveai/rn-h5:${{ steps.tag.outputs.TAG }}
TAG=${{ steps.tag.outputs.TAG }} docker compose down
TAG=${{ steps.tag.outputs.TAG }} docker compose up -d
cleanup:
runs-on: ubuntu-latest
needs: deploy
if: always()
steps:
- name: Clean up Docker resources on server
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.PROD_SERVER_HOST }}
username: ${{ secrets.PROD_SERVER_USERNAME }}
key: ${{ secrets.PROD_SERVER_KEY }}
port: 22
script: |
docker image prune -f
docker container prune -f
docker builder prune -f
docker network prune -f
echo "Cleanup completed!"
- name: Clean up GitHub Actions workspace
run: |
echo "Cleaning up GitHub Actions workspace..."
docker system df

16
Dockerfile-dev Normal file
View File

@@ -0,0 +1,16 @@
FROM node:20-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install
COPY . .
RUN npm run build:h5
FROM nginx:stable-alpine AS production
COPY --from=build /app/dist/build/h5 /usr/share/nginx/html
COPY nginx.dev.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

43
package-lock.json generated
View File

@@ -25,6 +25,8 @@
"@dcloudio/uni-mp-weixin": "3.0.0-4070620250821001", "@dcloudio/uni-mp-weixin": "3.0.0-4070620250821001",
"@dcloudio/uni-mp-xhs": "3.0.0-4070620250821001", "@dcloudio/uni-mp-xhs": "3.0.0-4070620250821001",
"@dcloudio/uni-quickapp-webview": "3.0.0-4070620250821001", "@dcloudio/uni-quickapp-webview": "3.0.0-4070620250821001",
"js-md5": "^0.8.3",
"md5": "^2.3.0",
"pinia": "^3.0.3", "pinia": "^3.0.3",
"sass": "^1.77.4", "sass": "^1.77.4",
"vue": "^3.5.22", "vue": "^3.5.22",
@@ -5722,6 +5724,15 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/charenc": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
"integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==",
"license": "BSD-3-Clause",
"engines": {
"node": "*"
}
},
"node_modules/chokidar": { "node_modules/chokidar": {
"version": "3.6.0", "version": "3.6.0",
"resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz", "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz",
@@ -5987,6 +5998,15 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/crypt": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
"integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==",
"license": "BSD-3-Clause",
"engines": {
"node": "*"
}
},
"node_modules/css-font-size-keywords": { "node_modules/css-font-size-keywords": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmmirror.com/css-font-size-keywords/-/css-font-size-keywords-1.0.0.tgz", "resolved": "https://registry.npmmirror.com/css-font-size-keywords/-/css-font-size-keywords-1.0.0.tgz",
@@ -7318,6 +7338,12 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/is-buffer": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
"integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==",
"license": "MIT"
},
"node_modules/is-core-module": { "node_modules/is-core-module": {
"version": "2.16.1", "version": "2.16.1",
"resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.1.tgz", "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.1.tgz",
@@ -8256,6 +8282,12 @@
"integrity": "sha512-9IXdWudL61npZjvLuVe/ktHiA41iE8qFyLB+4VDTblEsWBzeg8WQTlktdUK4CdncUqtUgUg0bbOmTE2bKBKaBQ==", "integrity": "sha512-9IXdWudL61npZjvLuVe/ktHiA41iE8qFyLB+4VDTblEsWBzeg8WQTlktdUK4CdncUqtUgUg0bbOmTE2bKBKaBQ==",
"license": "BSD-3-Clause" "license": "BSD-3-Clause"
}, },
"node_modules/js-md5": {
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/js-md5/-/js-md5-0.8.3.tgz",
"integrity": "sha512-qR0HB5uP6wCuRMrWPTrkMaev7MJZwJuuw4fnwAzRgP4J4/F8RwtodOKpGp4XpqsLBFzzgqIO42efFAyz2Et6KQ==",
"license": "MIT"
},
"node_modules/js-tokens": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -8653,6 +8685,17 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/md5": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz",
"integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==",
"license": "BSD-3-Clause",
"dependencies": {
"charenc": "0.0.2",
"crypt": "0.0.2",
"is-buffer": "~1.1.6"
}
},
"node_modules/media-typer": { "node_modules/media-typer": {
"version": "0.3.0", "version": "0.3.0",
"resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-0.3.0.tgz", "resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-0.3.0.tgz",

View File

@@ -27,6 +27,8 @@
"@dcloudio/uni-mp-weixin": "3.0.0-4070620250821001", "@dcloudio/uni-mp-weixin": "3.0.0-4070620250821001",
"@dcloudio/uni-mp-xhs": "3.0.0-4070620250821001", "@dcloudio/uni-mp-xhs": "3.0.0-4070620250821001",
"@dcloudio/uni-quickapp-webview": "3.0.0-4070620250821001", "@dcloudio/uni-quickapp-webview": "3.0.0-4070620250821001",
"js-md5": "^0.8.3",
"md5": "^2.3.0",
"pinia": "^3.0.3", "pinia": "^3.0.3",
"sass": "^1.77.4", "sass": "^1.77.4",
"vue": "^3.5.22", "vue": "^3.5.22",

View File

@@ -4,17 +4,20 @@
<main> <main>
<!-- 评论头部信息 --> <!-- 评论头部信息 -->
<view class="commenthead"> <view class="commenthead">
<text class="commentcount">{{ comments.length }}条评论</text> <text class="commentcount">{{ comments.length }} comments</text>
<view class="headswitch"> <view class="headswitch">
<text class="inact" @tap="handleOpenApp">默认</text> <text class="inact" @tap="handleOpenApp">Default</text>
<view class="act" @tap="handleOpenApp">最新</view> <view class="act" @tap="handleOpenApp">Latest</view>
</view> </view>
</view> </view>
<!-- 评论主体 --> <!-- 评论主体 -->
<!-- 翻译加载中状态 -->
<view v-if="isTranslating" class="translating">
<text>Loading comments...</text>
</view>
<view v-if="comments.list.length > 0"> <view v-else-if="comments.list.length > 0">
<view v-for="commentItem in comments.list" :key="commentItem.id" class="commentdetail" @tap="onDelegateTap"> <view v-for="commentItem in comments.list" :key="commentItem.id" class="commentdetail" @tap="onDelegateTap">
<!-- 头像 --> <!-- 头像 -->
<view class="commentdetailleft"> <view class="commentdetailleft">
@@ -32,8 +35,8 @@
<uni-dateformat v-if="commentItem.createdAt" <uni-dateformat v-if="commentItem.createdAt"
:date="Date.parse(commentItem.createdAt.replace(/-/g, '/'))" :threshold="[0, 0]" format="yyyy-MM-dd" :date="Date.parse(commentItem.createdAt.replace(/-/g, '/'))" :threshold="[0, 0]" format="yyyy-MM-dd"
class="date-text" /> class="date-text" />
<text v-else class="date-text">未知时间</text> <text v-else class="date-text">Unknown time</text>
<text class="replytext" @tap.stop="handleOpenApp">回复</text> <text class="replytext" @tap.stop="handleOpenApp">Reply</text>
</view> </view>
</view> </view>
<view class="spacerview"></view> <view class="spacerview"></view>
@@ -55,8 +58,8 @@
<view class="date-reply"> <view class="date-reply">
<uni-dateformat v-if="child.date" :date="Date.parse(child.date.replace(/-/g, '/'))" <uni-dateformat v-if="child.date" :date="Date.parse(child.date.replace(/-/g, '/'))"
:threshold="[0, 0]" format="yyyy-MM-dd" class="date-text" /> :threshold="[0, 0]" format="yyyy-MM-dd" class="date-text" />
<text v-else class="date-text">未知时间</text> <text v-else class="date-text">Unknown time</text>
<text class="replytext" @tap.stop="handleOpenApp">回复</text> <text class="replytext" @tap.stop="handleOpenApp">Reply</text>
</view> </view>
</view> </view>
<view class="commentlike"> <view class="commentlike">
@@ -70,7 +73,7 @@
<view v-if="commentItem.reply.length" class="expandcomment"> <view v-if="commentItem.reply.length" class="expandcomment">
<view style="width:20px;height:1px;background:rgba(65,60,67,.2)"></view> <view style="width:20px;height:1px;background:rgba(65,60,67,.2)"></view>
<text class="expandcommenttext" :data-cid="commentItem.id"> <text class="expandcommenttext" :data-cid="commentItem.id">
{{ commentItem.showChild ? '收起' : `展开${commentItem.reply.length}条回复` }} {{ commentItem.showChild ? 'Collapse' : `Show ${commentItem.reply.length} replies` }}
</text> </text>
</view> </view>
@@ -80,10 +83,9 @@
</view> </view>
<view v-else class="nocomments"> <view v-else class="nocomments">
<image src="/static/imgs/empty-img/b-empty-img@3x.webp" mode="aspectFit" alt="暂无评论"></image> <image src="/static/imgs/empty-img/b-empty-img@3x.webp" mode="aspectFit" alt="暂无评论"></image>
<text class="nocommentstext">空空如也~</text> <text class="nocommentstext">No comments yet</text>
</view> </view>
<!-- 占位视图 --> <!-- 占位视图 -->
@@ -99,12 +101,14 @@
</template> </template>
<script setup> <script setup>
import { reactive } from 'vue' import { reactive, ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app' import { onLoad } from '@dcloudio/uni-app'
import { getCommentList, getUserImg } from '@/api/api.js' import { getCommentList, getUserImg } from '@/api/api.js'
import { useCommonStore } from '@/stores/common.js' import { useCommonStore } from '@/stores/common.js'
import Findmore from '@/pages/findmore/findmore.vue' import Findmore from '@/pages/findmore/findmore.vue'
import Intereact from '@/pages/intereact/intereact.vue' import Intereact from '@/pages/intereact/intereact.vue'
// 引入翻译函数
import { translateZhToEn } from '@/utils/translate.js';
const common = useCommonStore() const common = useCommonStore()
@@ -120,6 +124,9 @@ const props = defineProps({
} }
}) })
// 翻译加载状态
const isTranslating = ref(false);
// 处理评论点击事件 // 处理评论点击事件
function onDelegateTap(e) { function onDelegateTap(e) {
const cid = e.target?.dataset?.cid const cid = e.target?.dataset?.cid
@@ -139,11 +146,11 @@ function formatCount(count) {
return common.formatCount(count) return common.formatCount(count)
} }
onLoad(() => { onLoad(async () => { // 标记为异步函数
const params = { const params = {
postId: props.postid postId: props.postid
} }
getCommentList(params).then(res => { getCommentList(params).then(async (res) => { // 标记为异步函数
try { try {
// 检查响应状态 // 检查响应状态
if (res.statusCode === 200 && res.data) { if (res.statusCode === 200 && res.data) {
@@ -151,10 +158,32 @@ onLoad(() => {
comments.list = res.data.list || [] comments.list = res.data.list || []
comments.length = res.data.total || 0 comments.length = res.data.total || 0
// 处理每条评论 // 构建父子关系映射对象
comments.list.forEach(comment => { const commentMap = {};
processComment(comment) comments.list.forEach(comment => {
}) commentMap[comment.id] = comment;
comment.children = [];
});
comments.list.forEach(comment => {
if (comment.parentId) {
const parent = commentMap[comment.parentId];
if (parent) {
parent.children.push(comment);
}
}
});
// 开始翻译,显示加载状态
isTranslating.value = true;
// 等待所有评论翻译完成(异步处理)
await Promise.all(
comments.list.map(comment => processComment(comment))
);
// 翻译完成,隐藏加载状态
isTranslating.value = false;
} else { } else {
throw new Error(`请求失败: ${res.statusCode}`) throw new Error(`请求失败: ${res.statusCode}`)
} }
@@ -164,6 +193,8 @@ onLoad(() => {
title: '加载评论失败', title: '加载评论失败',
icon: 'error' icon: 'error'
}) })
// 异常时隐藏加载状态
isTranslating.value = false;
// 设置默认空数据 // 设置默认空数据
comments.list = [] comments.list = []
comments.length = 0 comments.length = 0
@@ -174,6 +205,8 @@ onLoad(() => {
title: '网络连接异常', title: '网络连接异常',
icon: 'error' icon: 'error'
}) })
// 异常时隐藏加载状态
isTranslating.value = false;
// 设置默认空数据 // 设置默认空数据
comments.list = [] comments.list = []
comments.length = 0 comments.length = 0
@@ -181,64 +214,79 @@ onLoad(() => {
}) })
/** /**
* 处理单条评论数据 * 处理单条评论数据(异步,支持翻译)
* @param {Object} comment - 评论对象 * @param {Object} comment - 评论对象
*/ */
function processComment(comment) { async function processComment(comment) {
// 确保comment对象有必要的属性 // 确保comment对象有必要的属性
if (!comment.children || comment.children.length === 0) { if (!comment.children || comment.children.length === 0) {
comment.showChild = false comment.showChild = false;
} }
// 确保user对象存在 // 确保user对象存在
if (!comment.user) { if (!comment.user) {
comment.user = {} comment.user = {};
} }
// 设置评论基本属性 // 设置评论基本属性
comment.content = comment.content || '' comment.content = comment.content || '';
comment.likeCount = comment.likeCount || 0 comment.likeCount = comment.likeCount || 0;
comment.userName = comment.user.nickName || '匿名用户' comment.createdAt = comment.createdAt || comment.date || '';
comment.createdAt = comment.createdAt || comment.date || ''
// 生成renderNodes用于富文本渲染 // 翻译用户名和评论内容(并行处理,提升效率)
comment.renderNodes = common.contentToRenderNodes(comment.content, comment.atUsers) const [translatedUserName, translatedContent] = await Promise.all([
translateZhToEn(comment.user.nickName || '匿名用户'),
translateZhToEn(comment.content)
]);
// 更新为翻译后的日语
comment.userName = translatedUserName;
// 生成日语内容的renderNodes
comment.renderNodes = common.contentToRenderNodes(translatedContent, comment.atUsers);
// 处理用户头像 // 处理用户头像
handleCommentAvatar(comment) handleCommentAvatar(comment);
// 处理子评论 // 处理子评论(异步)
if (comment.reply && comment.reply.length > 0) { if (comment.reply && comment.reply.length > 0) {
// 将reply赋值给children确保模板和代码一致 // 将reply赋值给children确保模板和代码一致
comment.children = comment.reply comment.children = comment.reply;
comment.children.forEach(childComment => { // 并行翻译所有子评论
processChildComment(childComment) await Promise.all(
}) comment.children.map(childComment => processChildComment(childComment))
);
} }
} }
/** /**
* 处理子评论数据 * 处理子评论数据(异步,支持翻译)
* @param {Object} childComment - 子评论对象 * @param {Object} childComment - 子评论对象
*/ */
function processChildComment(childComment) { async function processChildComment(childComment) {
// 确保user对象存在 // 确保user对象存在
if (!childComment.user) { if (!childComment.user) {
childComment.user = {} childComment.user = {};
} }
// 设置子评论基本属性 // 设置子评论基本属性
childComment.content = childComment.content || '' childComment.content = childComment.content || '';
childComment.likeCount = childComment.likeCount || 0 childComment.likeCount = childComment.likeCount || 0;
childComment.userName = childComment.user.nickName || '匿名用户' childComment.date = childComment.createdAt || childComment.date || '';
childComment.date = childComment.createdAt || childComment.date || ''
// 翻译子评论的用户名和内容
const [translatedUserName, translatedContent] = await Promise.all([
translateZhToEn(childComment.user.nickName || 'Anonymous user'),
translateZhToEn(childComment.content)
]);
// 更新为翻译后的日语
childComment.userName = translatedUserName;
// 生成日语内容的renderNodes
childComment.renderNodes = common.contentToRenderNodes(translatedContent, childComment.atUsers);
// 处理子评论头像 // 处理子评论头像
handleCommentAvatar(childComment) handleCommentAvatar(childComment);
// 为子评论生成renderNodes
childComment.renderNodes = common.contentToRenderNodes(childComment.content, childComment.atUsers)
} }
/** /**
@@ -278,8 +326,6 @@ function handleCommentAvatar(comment) {
comment.userAvatar = '' comment.userAvatar = ''
} }
} }
</script> </script>
<style scoped> <style scoped>
@@ -309,8 +355,9 @@ main {
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
width: 100%; width: 100%;
height: 29px; height: 32px;
margin-bottom: 28px; margin-bottom: 28px;
padding: 0 2px;
} }
.commentcount { .commentcount {
@@ -329,36 +376,36 @@ main {
} }
.headswitch { .headswitch {
width: 102px; min-width: 120px;
height: 100%; width: auto;
height: 28px;
flex-grow: 0; flex-grow: 0;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-around; justify-content: space-between;
border-radius: 24px; border-radius: 14px;
padding: 2px; padding: 2px 3px;
box-sizing: border-box; box-sizing: border-box;
background-color: #f0eef1; background-color: #f0eef1;
} }
.headswitch .inact, .headswitch .inact,
.act { .act {
width: 48px; min-width: 54px;
height: 23px; padding: 0 8px;
height: 24px;
font-size: 12px; font-size: 12px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-weight: normal; font-weight: normal;
font-stretch: normal;
font-style: normal;
line-height: normal; line-height: normal;
letter-spacing: normal;
color: #110c13; color: #110c13;
white-space: nowrap;
} }
.headswitch .inact { .headswitch .inact {
border-radius: 24px; border-radius: 12px;
background-color: #fff; background-color: #fff;
} }
@@ -554,4 +601,12 @@ main {
text-align: center; text-align: center;
color: #918e93; color: #918e93;
} }
/* 翻译加载状态样式 */
.translating {
text-align: center;
padding: 20rpx;
font-size: 14px;
color: #918e93;
}
</style> </style>

View File

@@ -1,6 +1,6 @@
<template> <template>
<view class="findmore" @tap="common.openapp"> <view class="findmore" @tap="common.openapp">
<text class="openapptext">APP查看更多</text> <text class="openapptext">View more in APP</text>
</view> </view>
</template> </template>
@@ -15,19 +15,22 @@ const common = useCommonStore()
bottom: 88px; bottom: 88px;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
width: 177px; min-width: 177px;
width: auto;
padding: 0 20px;
height: 52px; height: 52px;
border-radius: 43px; border-radius: 43px;
background-image: linear-gradient(97deg, #7c45ed 1%, #7c68ef 20%, #7bd8f8 92%); background-image: linear-gradient(97deg, #7c45ed 1%, #7c68ef 20%, #7bd8f8 92%);
z-index: 5; z-index: 5;
text-align: center; text-align: center;
line-height: 52px; line-height: 52px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer; cursor: pointer;
} }
.openapptext { .openapptext {
width: 96px;
height: 22px;
font-size: 1em; font-size: 1em;
font-weight: 600; font-weight: 600;
font-stretch: normal; font-stretch: normal;

View File

@@ -2,7 +2,7 @@
<view class="head"> <view class="head">
<image src="/static/imgs/h5logo/h5logo@3x.webp" mode="aspectFit" class="applogo" alt="官网logo" /> <image src="/static/imgs/h5logo/h5logo@3x.webp" mode="aspectFit" class="applogo" alt="官网logo" />
<view class="spacerview"></view> <view class="spacerview"></view>
<view class="download" @tap="common.download">下载应用</view> <view class="download" @tap="common.download">Download App</view>
</view> </view>
</template> </template>
@@ -39,7 +39,8 @@ const common = useCommonStore()
} }
.download { .download {
width: 97px; min-width: 120px;
padding: 0 16px;
height: 35px; height: 35px;
border-radius: 29px; border-radius: 29px;
background-image: linear-gradient(156deg, #7c45ed -1%, #7c68ef 19%, #7bd8f8 97%); background-image: linear-gradient(156deg, #7c45ed -1%, #7c68ef 19%, #7bd8f8 97%);
@@ -49,5 +50,6 @@ const common = useCommonStore()
align-items: center; align-items: center;
justify-content: center; justify-content: center;
cursor: pointer; cursor: pointer;
letter-spacing: 0.3px;
} }
</style> </style>

View File

@@ -1,6 +1,6 @@
<template> <template>
<view class="loading-container"> <view class="loading-container">
<text>页面加载中...</text> <text>Loading...</text>
</view> </view>
</template> </template>
<script setup> <script setup>

View File

@@ -3,14 +3,14 @@
<view class="interaction"> <view class="interaction">
<view class="editarea" @tap="handleOpenApp"> <view class="editarea" @tap="handleOpenApp">
<image src="/static/imgs/editicon/icon@2x.webp" mode="aspectFit" class="editicon" alt="编辑标签"></image> <image src="/static/imgs/editicon/icon@2x.webp" mode="aspectFit" class="editicon" alt="编辑标签"></image>
<text class="edittext">快来互动吧</text> <text class="edittext">Let's interact...</text>
</view> </view>
<view class="spacerview"></view> <view class="spacerview small"></view>
<view class="collection" @tap="handleOpenApp"> <view class="collection" @tap="handleOpenApp">
<image src="@/static/imgs/staricon/icon@3x.webp" mode="aspectFit" class="collectionicon" alt="收藏标签"></image> <image src="@/static/imgs/staricon/icon@3x.webp" mode="aspectFit" class="collectionicon" alt="收藏标签"></image>
<text class="collectioncount">{{ formatCount(collectsum) }}</text> <text class="collectioncount">{{ formatCount(collectsum) }}</text>
</view> </view>
<view class="spacerview"></view> <view class="spacerview small"></view>
<view class="like" @tap="handleOpenApp"> <view class="like" @tap="handleOpenApp">
<image src="@/static/imgs/likeicon/icon@3x.webp" mode="aspectFit" class="likeicon" alt="点赞标签"></image> <image src="@/static/imgs/likeicon/icon@3x.webp" mode="aspectFit" class="likeicon" alt="点赞标签"></image>
<text class="likecount">{{ formatCount(countLike) }}</text> <text class="likecount">{{ formatCount(countLike) }}</text>
@@ -37,11 +37,9 @@ function handleOpenApp() {
common.openapp() common.openapp()
} }
// 格式化数字显示
function formatCount(count) { function formatCount(count) {
return common.formatCount(count) return common.formatCount(count)
} }
</script> </script>
<style scoped> <style scoped>
@@ -50,13 +48,9 @@ function formatCount(count) {
pointer-events: none pointer-events: none
} }
.interaction { .spacerview.small {
width: 100%; flex: 0.3;
max-width: 430px; min-width: 8px;
display: flex;
align-items: center;
box-sizing: border-box;
margin: auto auto 0;
} }
.interaction { .interaction {
@@ -66,19 +60,21 @@ function formatCount(count) {
background-color: #faf9fb; background-color: #faf9fb;
display: flex; display: flex;
align-items: center; align-items: center;
padding: 11.5px 16px; padding: 11.5px 12px;
flex-direction: row; flex-direction: row;
box-sizing: border-box; box-sizing: border-box;
margin: auto auto 0; margin: 15px auto 0;
position: relative;
z-index: 1;
} }
.editarea { .editarea {
flex: 1; flex: 1;
height: 40px; height: 40px;
background-color: #fff; background-color: #fff;
border-radius: 24px; border-radius: 24px;
gap: 12px; gap: 8px;
padding: 0 20px; padding: 0 16px;
box-sizing: border-box; box-sizing: border-box;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -94,16 +90,10 @@ function formatCount(count) {
.edittext { .edittext {
flex: 1; flex: 1;
flex-grow: 0; font-size: 13px;
font-size: 14px;
font-weight: normal; font-weight: normal;
font-stretch: normal;
font-style: normal;
line-height: 40px;
letter-spacing: normal;
text-align: left;
color: #918e93; color: #918e93;
white-space: nowrap; white-space: nowrap;
} }
.collection, .collection,
@@ -112,30 +102,17 @@ function formatCount(count) {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
gap: 5px; gap: 4px;
} }
.collectionicon, .collectionicon,
.likeicon { .likeicon {
width: 24px; width: 22px;
height: 24px; height: 22px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
} }
.collectioncount, .collectioncount,
.likecount { .likecount {
height: 17px; font-size: 13px;
font-family: 'SFPro';
font-size: 14px;
font-weight: 500;
font-stretch: normal;
font-style: normal;
line-height: normal;
letter-spacing: normal;
text-align: left;
color: #000;
} }
</style> </style>

View File

@@ -7,6 +7,15 @@
<section class="content"> <section class="content">
<!-- 新闻区域 --> <!-- 新闻区域 -->
<section class="news"> <section class="news">
<!-- 翻译加载状态 -->
<view v-if="isTranslating" class="translating">
<text>加载新闻内容中...</text>
</view>
<!-- 翻译错误提示 -->
<view v-if="translationError" class="translation-error">
<text>翻译服务暂时不可用将显示原文</text>
</view>
<!-- 轮播图 --> <!-- 轮播图 -->
<swiper v-if="post.imgs && post.imgs.length > 0" :indicator-dots="false" :autoplay="false" :interval="3000" <swiper v-if="post.imgs && post.imgs.length > 0" :indicator-dots="false" :autoplay="false" :interval="3000"
@@ -37,21 +46,21 @@
<!-- 新闻标题 --> <!-- 新闻标题 -->
<view v-if="post.title" class="newstitle"> <view v-if="post.title" class="newstitle">
<text class="title">{{ post.title }}</text> <text class="title">{{ post.translatedTitle || post.title }}</text>
</view> </view>
<!-- 新闻信息 --> <!-- 新闻信息 -->
<view v-if="post.copywriting && post.date" class="newsbottom"> <view v-if="(post.copywriting || post.translatedContent) && post.date" class="newsbottom">
<text class="copywriting">{{ post.copywriting }}</text> <text class="copywriting">{{ post.translatedContent || post.copywriting }}</text>
<view class="meta-info"> <view class="meta-info">
<text v-if="post.source" class="source">{{ post.source }}</text> <text v-if="post.source" class="source">{{ post.translatedSource || post.source }}</text>
<text v-if="post.source" class="date-text">·</text> <text v-if="post.source" class="date-text">·</text>
<view class="datetextview"> <view class="datetextview">
<text class="date-text">{{ common.formatDate(post.time) }}</text> <text class="date-text">{{ common.formatDate(post.time) }}</text>
</view> </view>
<view class="spacerview"></view> <view class="spacerview"></view>
<view class="toseeall" @tap="handleInteraction"> <view class="toseeall" @tap="handleInteraction">
<text class="toseealltext">查看全文</text> <text class="toseealltext">Show Full Text</text>
<image src="@/static/imgs/arrowrightup/arrowrightup@3x.png" class="arrowrightupicon" mode="aspectFit" <image src="@/static/imgs/arrowrightup/arrowrightup@3x.png" class="arrowrightupicon" mode="aspectFit"
alt="查看全文图标" /> alt="查看全文图标" />
</view> </view>
@@ -88,7 +97,8 @@ import Head from '@/pages/head/head.vue'
import Comments from '@/pages/comments/comments.vue' import Comments from '@/pages/comments/comments.vue'
import Findmore from '@/pages/findmore/findmore.vue' import Findmore from '@/pages/findmore/findmore.vue'
import { getPostList, getPostLImage, getPostVideo } from '../api/api.js' import { getPostList, getPostLImage, getPostVideo } from '../api/api.js'
// import Intereact from '@/pages/intereact/intereact.vue' // 引入与评论组件共享的翻译函数和全局缓存
import { translateZhToEn, translationCache } from '@/utils/translate.js';
const common = useCommonStore() const common = useCommonStore()
@@ -100,6 +110,9 @@ const onChange = e => current.value = e.detail.current
const post = ref({}) const post = ref({})
// 资源加载状态 // 资源加载状态
const loading = ref(false) const loading = ref(false)
// 翻译状态管理
const isTranslating = ref(false);
const translationError = ref(false);
// 图片加载成功处理 // 图片加载成功处理
const onImageLoad = (index) => { const onImageLoad = (index) => {
@@ -118,21 +131,73 @@ const onImageError = (index) => {
} }
} }
/**
* 批量翻译新闻内容
* @param {Object} newsData - 原始新闻数据
*/
async function translateNewsContent(newsData) {
// 收集需要翻译的文本
const translateTasks = [];
// 标题翻译
if (newsData.title && !translationCache[newsData.title]) {
translateTasks.push({
key: 'translatedTitle',
text: newsData.title
});
} else if (newsData.title) {
newsData.translatedTitle = translationCache[newsData.title];
}
// 内容翻译
if (newsData.copywriting && !translationCache[newsData.copywriting]) {
translateTasks.push({
key: 'translatedContent',
text: newsData.copywriting
});
} else if (newsData.copywriting) {
newsData.translatedContent = translationCache[newsData.copywriting];
}
// 来源翻译
if (newsData.source && !translationCache[newsData.source]) {
translateTasks.push({
key: 'translatedSource',
text: newsData.source
});
} else if (newsData.source) {
newsData.translatedSource = translationCache[newsData.source];
}
// 执行批量翻译
if (translateTasks.length > 0) {
await Promise.all(
translateTasks.map(task =>
translateZhToEn(task.text).then(result => {
newsData[task.key] = result;
})
)
);
}
return newsData;
}
const handleInteraction = () => { const handleInteraction = () => {
common.openapp(); common.openapp();
} }
// 组件挂载时获取数据 // 组件挂载时获取数据
onLoad(() => { onLoad(() => {
const params = { const params = {
newsFilter: 'news_only' newsFilter: 'news_only'
} }
getPostList(params).then(res => { getPostList(params).then(async (res) => { // 标记为异步函数
try { try {
const data = res.data.data const data = res.data.data
// console.log(data)
// 处理新闻相关字段
// 处理新闻相关字段 - 根据实际API返回字段优化
data.id = data.id || '' data.id = data.id || ''
data.title = data.newsTitle || data.title || '' data.title = data.newsTitle || data.title || ''
data.source = data.newsSource || data.source || '' data.source = data.newsSource || data.source || ''
@@ -142,6 +207,11 @@ onLoad(() => {
data.collectsum = data.collectsum || 0 data.collectsum = data.collectsum || 0
data.comments = data.comments || [] data.comments = data.comments || []
// 翻译新闻内容
isTranslating.value = true;
await translateNewsContent(data);
isTranslating.value = false;
const mediaPromises = [] const mediaPromises = []
// 处理图片资源 // 处理图片资源
@@ -149,15 +219,14 @@ onLoad(() => {
data.images.forEach(image => { data.images.forEach(image => {
mediaPromises.push(getPostLImage(image.original_url).then(imageRes => { mediaPromises.push(getPostLImage(image.original_url).then(imageRes => {
if (imageRes.statusCode === 200 && imageRes.data) { if (imageRes.statusCode === 200 && imageRes.data) {
// 将arraybuffer转换为base64
const base64 = uni.arrayBufferToBase64(imageRes.data) const base64 = uni.arrayBufferToBase64(imageRes.data)
const imageUrl = 'data:image/webp;base64,' + base64 const imageUrl = 'data:image/webp;base64,' + base64
return { return {
type: 'img', type: 'img',
src: imageUrl, src: imageUrl,
original_url: image.original_url, original_url: image.original_url,
loading: true, // 初始加载状态 loading: true,
error: false // 错误状态 error: false
} }
} }
return null return null
@@ -172,15 +241,14 @@ onLoad(() => {
if (data.video && data.video.original_url) { if (data.video && data.video.original_url) {
mediaPromises.push(getPostVideo(data.video.original_url).then(videoRes => { mediaPromises.push(getPostVideo(data.video.original_url).then(videoRes => {
if (videoRes.statusCode === 200 && videoRes.data) { if (videoRes.statusCode === 200 && videoRes.data) {
// 将arraybuffer转换为base64
const base64 = uni.arrayBufferToBase64(videoRes.data) const base64 = uni.arrayBufferToBase64(videoRes.data)
const videoUrl = 'data:video/mp4;base64,' + base64 const videoUrl = 'data:video/mp4;base64,' + base64
return { return {
type: 'video', type: 'video',
src: videoUrl, src: videoUrl,
original_url: data.video.original_url, original_url: data.video.original_url,
loading: true, // 初始加载状态 loading: true,
error: false // 错误状态 error: false
} }
} }
return null return null
@@ -193,15 +261,14 @@ onLoad(() => {
// 统一等待所有媒体资源加载完成 // 统一等待所有媒体资源加载完成
if (mediaPromises.length > 0) { if (mediaPromises.length > 0) {
Promise.all(mediaPromises).then(mediaItems => { Promise.all(mediaPromises).then(mediaItems => {
// 过滤掉null值将有效的媒体资源合并到imgs数组中
data.imgs = mediaItems.filter(item => item !== null) data.imgs = mediaItems.filter(item => item !== null)
post.value = data post.value = data
loading.value = true loading.value = true
}) })
} else { } else {
// 如果没有媒体资源,设置空数组
data.imgs = [] data.imgs = []
post.value = data post.value = data
loading.value = true
} }
} catch (error) { } catch (error) {
@@ -216,14 +283,19 @@ onLoad(() => {
console.error(errorType, error) console.error(errorType, error)
post.value = { post.value = {
title: title, title: title,
translatedTitle: translationCache[title] || title, // 错误标题也翻译
source: '系统', source: '系统',
translatedSource: translationCache['系统'] || '系统',
copywriting: message, copywriting: message,
translatedContent: translationCache[message] || message, // 错误信息也翻译
date: new Date().toISOString(), date: new Date().toISOString(),
countLike: 0, countLike: 0,
collectsum: 0, collectsum: 0,
comments: [], comments: [],
imgs: [] imgs: []
} }
loading.value = true
isTranslating.value = false;
} }
}) })
@@ -231,11 +303,29 @@ onLoad(() => {
</script> </script>
<style scoped> <style scoped>
/* 翻译相关样式 */
.translating {
text-align: center;
padding: 15rpx;
font-size: 14px;
color: #918e93;
margin-bottom: 15rpx;
}
.translation-error {
text-align: center;
padding: 15rpx;
font-size: 14px;
color: #ff6b6b;
margin-bottom: 15rpx;
background-color: #fff5f5;
border-radius: 6rpx;
}
page, page,
.page { .page {
width: 100%; width: 100%;
max-width: 430px; max-width: 430px;
/* height: 100vh; */
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin: 0 auto; margin: 0 auto;
@@ -435,4 +525,4 @@ swiper-item {
max-height: 80%; max-height: 80%;
object-fit: contain; object-fit: contain;
} }
</style> </style>

View File

@@ -11,10 +11,11 @@
<image v-if="post.user && post.user.userImg" :src="post.user.userImg" mode="aspectFill" class="userimg" <image v-if="post.user && post.user.userImg" :src="post.user.userImg" mode="aspectFill" class="userimg"
alt="用户头像" /> alt="用户头像" />
<image v-else src="/static/imgs/default-avatar.png" mode="aspectFill" class="userimg" alt="默认头像" /> <image v-else src="/static/imgs/default-avatar.png" mode="aspectFill" class="userimg" alt="默认头像" />
<text class="username">{{ post.user ? post.user.nickName : '未知用户' }}</text> <!-- 用户名翻译 -->
<text class="username">{{ post.translatedUserName || post.user.nickName }}</text>
<button class="follow" @tap="common.openapp"> <button class="follow" @tap="common.openapp">
<uni-icons v-if="post.isfollow" type="checkmarkempty" size="20" color="#333"></uni-icons> <uni-icons v-if="post.isfollow" type="checkmarkempty" size="20" color="#333"></uni-icons>
<text v-else>关注</text> <text v-else>Follow</text>
</button> </button>
</view> </view>
@@ -27,20 +28,10 @@
@load="onImageLoad(i)" @error="onImageError(i)" /> @load="onImageLoad(i)" @error="onImageError(i)" />
<video v-else-if="item.type === 'video'" id="videoid" :src="item.src" class="swiper-item" <video v-else-if="item.type === 'video'" id="videoid" :src="item.src" class="swiper-item"
:controls="false" @tap="pausevideo" object-fit="contain" alt="动态视频内容" /> :controls="false" @tap="pausevideo" object-fit="contain" alt="动态视频内容" />
<!-- 播放按钮 - 使用浏览器复制的样式 --> <!-- 播放按钮 -->
<view v-if="!isPlaying" class="uni-video-cover" @tap="pausevideo"> <view v-if="!isPlaying" class="uni-video-cover" @tap="pausevideo">
<view class="uni-video-cover-play-button uni-video-icon"></view> <view class="uni-video-cover-play-button uni-video-icon"></view>
</view> </view>
<!-- <cover-view class="swiper-mask" @tap="common.openapp">
</cover-view> -->
<!-- 图片加载状态 -->
<!-- <view v-if="item.loading" class="image-loading">
<text>图片加载中...</text>
</view>
<view v-if="item.error" class="image-error">
<text>图片加载失败</text>
</view> -->
</view> </view>
</swiper-item> </swiper-item>
</swiper> </swiper>
@@ -51,8 +42,9 @@
</view> </view>
<!-- 底部文案 --> <!-- 底部文案 -->
<view v-if="post.copywriting && post.date" class="momentbottom"> <view v-if="(post.copywriting || post.translatedContent) && post.date" class="momentbottom">
<text class="copywriting">{{ post.copywriting }}</text> <!-- 内容翻译 -->
<text class="copywriting">{{ post.translatedContent || post.copywriting }}</text>
<uni-dateformat :date="Date.parse(post.date.replace(/-/g, '/'))" :threshold="[0, 0]" format="yyyy-MM-dd" <uni-dateformat :date="Date.parse(post.date.replace(/-/g, '/'))" :threshold="[0, 0]" format="yyyy-MM-dd"
class="date-text" /> class="date-text" />
</view> </view>
@@ -64,16 +56,13 @@
<!-- 评论区域 + 互动区域 --> <!-- 评论区域 + 互动区域 -->
<Comments :postid="post.id" /> <Comments :postid="post.id" />
<!-- 互动区域 -->
<!-- <Intereact :collectsum="post.collectsum" :likesum="post.likesum" /> -->
<!-- Findmore --> <!-- Findmore -->
<Findmore /> <Findmore />
</view> </view>
<view v-else class="loading-container"> <view v-else class="loading-container">
<text>页面加载中...</text> <text>Loading page...</text>
</view> </view>
</template> </template>
@@ -86,20 +75,27 @@ import Head from '@/pages/head/head.vue'
import Comments from '@/pages/comments/comments.vue' import Comments from '@/pages/comments/comments.vue'
import Findmore from '@/pages/findmore/findmore.vue' import Findmore from '@/pages/findmore/findmore.vue'
import { getPostList, getPostLImage, getPostVideo, getUserImg } from '../api/api.js' import { getPostList, getPostLImage, getPostVideo, getUserImg } from '../api/api.js'
// 引入翻译工具
import { translateZhToEn } from '@/utils/translate.js';
const common = useCommonStore() const common = useCommonStore()
// 当前 dot // 当前轮播图索引
const current = ref(0) const current = ref(0)
const onChange = e => current.value = e.detail.current const onChange = e => current.value = e.detail.current
// 动态数据 // 动态数据 - 初始化翻译相关字段
const post = ref({}) const post = ref({
translatedContent: '', // 内容翻译结果
translatedUserName: '' // 用户名翻译结果
});
// 播放状态 // 播放状态
const isPlaying = ref(true) // 默认播放状态 const isPlaying = ref(true)
// 资源加载状态 // 页面加载状态
const finishloading = ref(false) const finishloading = ref(false)
// 图片加载成功处理 // 图片加载处理
const onImageLoad = (index) => { const onImageLoad = (index) => {
if (post.value.imgs && post.value.imgs[index]) { if (post.value.imgs && post.value.imgs[index]) {
post.value.imgs[index].loading = false post.value.imgs[index].loading = false
@@ -107,7 +103,6 @@ const onImageLoad = (index) => {
} }
} }
// 图片加载失败处理
const onImageError = (index) => { const onImageError = (index) => {
if (post.value.imgs && post.value.imgs[index]) { if (post.value.imgs && post.value.imgs[index]) {
post.value.imgs[index].loading = false post.value.imgs[index].loading = false
@@ -116,13 +111,10 @@ const onImageError = (index) => {
} }
} }
//暂停/播放 // 视频播放/暂停控制
const pausevideo = () => { const pausevideo = () => {
// 获取VideoContext实例
const videoCtx = uni.createVideoContext('videoid', getCurrentInstance()); const videoCtx = uni.createVideoContext('videoid', getCurrentInstance());
// 检查视频状态并切换播放/暂停
if (videoCtx) { if (videoCtx) {
// 使用一个状态变量来跟踪播放状态
if (isPlaying.value) { if (isPlaying.value) {
videoCtx.pause() videoCtx.pause()
isPlaying.value = false isPlaying.value = false
@@ -133,16 +125,14 @@ const pausevideo = () => {
} }
} }
// 组件挂载时获取数据并翻译
// 组件挂载时获取数据
onLoad(() => { onLoad(() => {
const params = '' const params = ''
getPostList(params).then(res => { getPostList(params).then(async (res) => {
try { try {
// console.log(res.data.data)
const data = res.data.data const data = res.data.data
// 处理帖子相关字段 - 根据实际API返回字段优化 // 处理帖子相关字段
data.id = data.id || '' data.id = data.id || ''
data.title = data.newsTitle || data.title || '' data.title = data.newsTitle || data.title || ''
data.source = data.newsSource || data.source || '' data.source = data.newsSource || data.source || ''
@@ -150,9 +140,17 @@ onLoad(() => {
data.date = data.time || data.date || '' data.date = data.time || data.date || ''
data.countLike = data.likeCount || data.countLike || 0 data.countLike = data.likeCount || data.countLike || 0
data.collectsum = data.collectsum || 0 data.collectsum = data.collectsum || 0
// data.user.nickName = data.user.nickName || ''
// data.user.userImg = data.user.avatar || data.userImg || '' // 仅翻译后端返回的特定内容
// data.comments = data.comments || [] // 1. 翻译用户名
if (data.user && data.user.nickName) {
data.translatedUserName = await translateZhToEn(data.user.nickName);
}
// 2. 翻译内容
if (data.copywriting) {
data.translatedContent = await translateZhToEn(data.copywriting);
}
const mediaPromises = [] const mediaPromises = []
@@ -161,15 +159,14 @@ onLoad(() => {
data.imgs.forEach(image => { data.imgs.forEach(image => {
mediaPromises.push(getPostLImage(image.original_url).then(imageRes => { mediaPromises.push(getPostLImage(image.original_url).then(imageRes => {
if (imageRes.statusCode === 200 && imageRes.data) { if (imageRes.statusCode === 200 && imageRes.data) {
// 将arraybuffer转换为base64
const base64 = uni.arrayBufferToBase64(imageRes.data) const base64 = uni.arrayBufferToBase64(imageRes.data)
const imageUrl = 'data:image/webp;base64,' + base64 const imageUrl = 'data:image/webp;base64,' + base64
return { return {
type: 'img', type: 'img',
src: imageUrl, src: imageUrl,
original_url: image.original_url, original_url: image.original_url,
loading: true, // 初始加载状态 loading: true,
error: false // 错误状态 error: false
} }
} }
return null return null
@@ -183,20 +180,19 @@ onLoad(() => {
}) })
} }
//处理视频资源 // 处理视频资源
if (data.videos && Array.isArray(data.videos)) { if (data.videos && Array.isArray(data.videos)) {
data.videos.forEach(video => { data.videos.forEach(video => {
mediaPromises.push(getPostVideo(video.original_url).then(videoRes => { mediaPromises.push(getPostVideo(video.original_url).then(videoRes => {
if (videoRes.statusCode === 200 && videoRes.data) { if (videoRes.statusCode === 200 && videoRes.data) {
// 将arraybuffer转换为base64
const base64 = uni.arrayBufferToBase64(videoRes.data) const base64 = uni.arrayBufferToBase64(videoRes.data)
const videoUrl = 'data:video/mp4;base64,' + base64 const videoUrl = 'data:video/mp4;base64,' + base64
return { return {
type: 'video', type: 'video',
src: videoUrl, src: videoUrl,
original_url: video.original_url, original_url: video.original_url,
loading: true, // 初始加载状态 loading: true,
error: false // 错误状态 error: false
} }
} }
return null return null
@@ -210,14 +206,12 @@ onLoad(() => {
}) })
} }
//处理用户信息 // 处理用户信息
if (data.user && data.user.constructor === Object) { if (data.user && data.user.constructor === Object) {
// 如果有头像路径,则获取头像资源
const avatarPath = data.user.avatar || data.userImg const avatarPath = data.user.avatar || data.userImg
if (avatarPath) { if (avatarPath) {
mediaPromises.push(getUserImg(avatarPath).then(userImgRes => { mediaPromises.push(getUserImg(avatarPath).then(userImgRes => {
if (userImgRes.statusCode === 200 && userImgRes.data) { if (userImgRes.statusCode === 200 && userImgRes.data) {
// 将arraybuffer转换为base64
const base64 = uni.arrayBufferToBase64(userImgRes.data) const base64 = uni.arrayBufferToBase64(userImgRes.data)
const userImgUrl = 'data:image/webp;base64,' + base64 const userImgUrl = 'data:image/webp;base64,' + base64
return { return {
@@ -235,60 +229,51 @@ onLoad(() => {
data.user = { data.user = {
nickName: data.user.nickName || '', nickName: data.user.nickName || '',
userImg: avatarPath || '' // 先使用原始路径后续会替换为base64 userImg: avatarPath || ''
} }
} }
// 统一等待所有媒体资源加载完成 // 等待所有媒体资源加载完成
if (mediaPromises.length > 0) { if (mediaPromises.length > 0) {
Promise.all(mediaPromises).then(mediaItems => { Promise.all(mediaPromises).then(mediaItems => {
// 过滤掉null值将有效的媒体资源合并到imgs数组中
const validMediaItems = mediaItems.filter(item => item !== null) const validMediaItems = mediaItems.filter(item => item !== null)
// 分离用户头像和其他媒体资源
const userImgItem = validMediaItems.find(item => item.type === 'userImg') const userImgItem = validMediaItems.find(item => item.type === 'userImg')
const otherMediaItems = validMediaItems.filter(item => item.type !== 'userImg') const otherMediaItems = validMediaItems.filter(item => item.type !== 'userImg')
// 如果有用户头像资源,更新用户头像
if (userImgItem) { if (userImgItem) {
data.user.userImg = userImgItem.src data.user.userImg = userImgItem.src
} }
// 设置其他媒体资源
data.imgs = otherMediaItems data.imgs = otherMediaItems
// post.value = data
console.log(data)
Object.assign(post.value, data) Object.assign(post.value, data)
finishloading.value = true finishloading.value = true
}) })
} else { } else {
// 如果没有媒体资源,设置空数组
data.imgs = [] data.imgs = []
post.value = data post.value = data
finishloading.value = true
} }
} catch (error) { } catch (error) {
handleError('数据处理错误:', error, '数据加载失败', '抱歉,新闻内容加载失败,请稍后重试') handleError(error)
} }
}).catch(error => { }).catch(error => {
handleError('网络请求错误:', error, '网络错误', '网络连接异常,请检查网络设置') handleError(error)
}) })
// 统一的错误处理函数 // 错误处理函数
const handleError = (errorType, error, title, message) => { const handleError = (error) => {
console.error(errorType, error) console.error('数据处理错误:', error)
post.value = { post.value = {
title: title, user: { nickName: 'Unknown user', userImg: '' },
source: '系统', translatedUserName: 'Unknown user',
copywriting: message, copywriting: 'Could not load content',
translatedContent: 'Could not load content',
date: new Date().toISOString(), date: new Date().toISOString(),
countLike: 0,
collectsum: 0,
comments: [],
imgs: [] imgs: []
} }
finishloading.value = true
} }
}) })
@@ -296,6 +281,7 @@ onLoad(() => {
</script> </script>
<style scoped> <style scoped>
/* 保持原有样式不变 */
page, page,
.page { .page {
width: 100%; width: 100%;
@@ -304,19 +290,16 @@ page,
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin: 0 auto; margin: 0 auto;
/* position: relative; */
} }
.content { .content {
flex-shrink: 0; flex-shrink: 0;
/* overflow: hidden; */
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.moment { .moment {
width: 100%; width: 100%;
/* flex-shrink: 0; */
display: flex; display: flex;
flex-direction: column; flex-direction: column;
box-sizing: border-box; box-sizing: border-box;
@@ -345,7 +328,7 @@ page,
} }
.follow { .follow {
width: 60px; width: 84px;
height: 32px; height: 32px;
flex-shrink: 0; flex-shrink: 0;
font-size: 14px; font-size: 14px;
@@ -433,19 +416,6 @@ swiper-item {
content: '\ea24'; content: '\ea24';
} }
.uni-video-cover {
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 1;
}
.dots-bar { .dots-bar {
display: flex; display: flex;
justify-content: center; justify-content: center;
@@ -498,4 +468,4 @@ swiper-item {
align-items: center; align-items: center;
height: 100vh; height: 100vh;
} }
</style> </style>

View File

@@ -88,7 +88,7 @@
<!-- <Findmore /> --> <!-- <Findmore /> -->
<view v-else class="loading-container"> <view v-else class="loading-container">
<text>页面加载中...</text> <text>Loading...</text>
</view> </view>
</template> </template>
@@ -101,6 +101,8 @@ import Head from '@/pages/head/head.vue'
import Comments from '@/pages/comments/comments.vue' import Comments from '@/pages/comments/comments.vue'
import Intereact from '@/pages/intereact/intereact.vue' import Intereact from '@/pages/intereact/intereact.vue'
import { getPostList, getPostVideo, getUserImg } from '@/api/api.js' import { getPostList, getPostVideo, getUserImg } from '@/api/api.js'
// 导入翻译工具
import { translateZhToEn, translationCache } from '@/utils/translate.js'
const common = useCommonStore() const common = useCommonStore()
const formatCount = common.formatCount const formatCount = common.formatCount
@@ -113,7 +115,6 @@ const finishloading = ref(false)
// 折叠展开状态 // 折叠展开状态
const isExpanded = ref(false) const isExpanded = ref(false)
// const videoRef = ref(null)
// 静音状态 // 静音状态
const isMuted = ref(false) const isMuted = ref(false)
// 播放状态 // 播放状态
@@ -268,20 +269,28 @@ onLoad(() => {
videoData.userImg = avatarResult.url videoData.userImg = avatarResult.url
} }
// 更新视频数据 - 沿用data对象 // 准备需要翻译的字段
data.userName = data.user?.nickName || '匿名用户' const userName = data.user?.nickName || '匿名用户'
data.date = data.time || data.date || '' const copywriting = data.textContent || ''
data.copywriting = data.textContent || ''
data.likesum = data.likeCount || 0 // 并行翻译需要翻译的内容
data.commentsum = data.commentCount || 0 Promise.all([
data.sharesum = data.shareCount || 0 translateZhToEn(userName),
// 标记资源加载完成 translateZhToEn(copywriting)
finishloading.value = true ]).then(([translatedUserName, translatedCopywriting]) => {
// 更新视频数据 - 使用翻译后的内容
data.userName = translatedUserName
data.date = data.time || data.date || ''
data.copywriting = translatedCopywriting
data.likesum = data.likeCount || 0
data.commentsum = data.commentCount || 0
data.sharesum = data.shareCount || 0
// 标记资源加载完成
finishloading.value = true
// 最后用data覆盖整个videoData
Object.assign(videoData, data)
// 最后用data覆盖整个videoData })
Object.assign(videoData, data)
}).catch(error => { }).catch(error => {
console.error('资源加载失败:', error) console.error('资源加载失败:', error)

123
src/utils/translate.js Normal file
View File

@@ -0,0 +1,123 @@
// src/utils/translate.js
import md5 from 'md5';
// 你的APP ID和密钥
const APP_ID = '20251202002510803';
const API_KEY = 'xcSyTk2wV8mt57tg_RJm';
// 并发控制百度免费版支持每秒2-3次设置最大并发数为3
const MAX_CONCURRENT = 3;
let currentConcurrent = 0;
// 全局翻译缓存(跨组件共享,避免重复翻译)
export const translationCache = {};
export const clearTranslationCache = () => {
Object.keys(translationCache).forEach(key => {
delete translationCache[key];
});
};
// 敏感词列表(可根据实际情况扩展)
const SENSITIVE_WORDS = [
'大纪元', // 已确认的敏感词
// 可添加其他敏感词
];
/**
* 过滤文本中的敏感词
* @param {string} text 原始文本
* @returns {Object} { filteredText: 过滤后文本, hasSensitive: 是否包含敏感词, sensitiveWords: 包含的敏感词列表 }
*/
const filterSensitiveWords = (text) => {
if (!text) {
return { filteredText: '', hasSensitive: false, sensitiveWords: [] };
}
let filteredText = text;
const foundSensitiveWords = [];
// 检测并替换敏感词
SENSITIVE_WORDS.forEach(word => {
const regex = new RegExp(word, 'gi');
if (regex.test(filteredText)) {
foundSensitiveWords.push(word);
// 用***替换敏感词
filteredText = filteredText.replace(regex, '***');
}
});
return {
filteredText,
hasSensitive: foundSensitiveWords.length > 0,
sensitiveWords: foundSensitiveWords
};
};
/**
* 带并发控制和敏感词屏蔽的翻译函数
* @param {string} text - 待翻译文本
* @returns {Promise<string>} 翻译结果(敏感词已被***替换)
*/
export const translateZhToEn = async (text) => {
// 空文本直接返回
if (!text || text.trim() === '') return text.trim();
// 优先使用缓存(全局缓存,同一文本只翻译一次)
if (translationCache[text]) {
return translationCache[text];
}
// 过滤敏感词
const { filteredText, hasSensitive, sensitiveWords } = filterSensitiveWords(text);
// 如果有敏感词,在控制台提示
if (hasSensitive) {
console.log(`检测到敏感词: ${sensitiveWords.join(', ')},已自动屏蔽`);
}
// 并发控制:等待当前并发数低于最大值
while (currentConcurrent >= MAX_CONCURRENT) {
await new Promise(resolve => setTimeout(resolve, 100));
}
try {
currentConcurrent++;
const salt = Math.random().toString(36).substring(2, 10);
// 正确签名规则APP_ID + 过滤后的文本 + salt + API_KEY
const sign = md5(APP_ID + filteredText + salt + API_KEY);
const url = `/baidu-translate/api/trans/vip/translate?` +
`q=${encodeURIComponent(filteredText)}&` +
`from=auto&` +
`to=en&` +
`appid=${APP_ID}&` +
`salt=${salt}&` +
`sign=${sign}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`请求失败,状态码: ${response.status}`);
}
const result = await response.json();
if (result.error_code) {
console.warn(`百度翻译API错误: ${result.error_code},信息: ${result.error_msg}`);
// 错误时返回过滤后的原文(包含***
translationCache[text] = filteredText;
return filteredText;
}
const translatedText = result.trans_result?.[0]?.dst?.trim() || filteredText;
// 缓存翻译结果
translationCache[text] = translatedText;
return translatedText;
} catch (error) {
console.warn(`【翻译失败】原文:${text},原因:`, error.message);
// 失败时返回过滤后的原文(包含***
translationCache[text] = filteredText;
return filteredText;
} finally {
currentConcurrent--;
}
};

View File

@@ -8,9 +8,14 @@ export default defineConfig({
server: { server: {
proxy: { proxy: {
'/api': { '/api': {
target: 'http://192.168.1.7:8088', // 请替换为你的实际后端服务器地址 target: 'http://192.168.1.111:58080', // 请替换为你的实际后端服务器地址
changeOrigin: true, changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '/api') rewrite: (path) => path.replace(/^\/api/, '/api')
},
'/baidu-translate': {
target: 'https://fanyi-api.baidu.com', // 百度翻译API的基础地址
changeOrigin: true, // 修改请求头中的Origin模拟同源请求
rewrite: (path) => path.replace(/^\/baidu-translate/, '') // 移除代理前缀
} }
} }
} }