Commit 0521173c by lijiabin

Merge branch 'master' into feature/21402-【YAYA文化岛】优化点整理

parents 286e1e0c c31d6dcb
# . # 企业文化平台前端
基于 `Vue 3 + Vite + TypeScript` 的企业文化平台前端项目,包含前台内容展示、用户中心、内容发布、积分商城、限时竞拍等,以及后台内容与配置管理。
## 项目概览
- 前台业务:
- 首页内容流
- YA 文化 / 问吧等频道页
- 帖子、视频、专栏、专访、问吧、实践、案例详情
- 用户中心、他人主页、我的互动内容
- 视频/长文/案例发布
- 积分商城、竞拍、私信等功能页
- 后台业务:
- 标签、轮播图、案例库管理
- 内容管理:专栏 / 专访 / 视频 / 问吧
- 配置管理:商城、竞拍、每日抽奖
## 技术栈
- 框架:`Vue 3`
- 构建工具:`Vite`
- 语言:`TypeScript`
- 路由:`vue-router`
- 状态管理:`Pinia`
- UI 组件:`Element Plus`
- 原子化样式:`UnoCSS`
- 组合式工具:`VueUse`
- 富文本编辑:`WangEditor`
- HTTP 请求:`Axios`
- 提示通知:`Notivue`
- SVG 图标:`vite-plugin-svg-icons`
- 自动导入:`unplugin-auto-import``unplugin-vue-components`
## 运行环境
- Node.js:`^20.19.0 || >=22.12.0`
- 包管理器:推荐 `pnpm`
## 安装依赖
```bash
pnpm install
```
This template should help get you started developing with Vue 3 in Vite. ## 本地开发
## Recommended IDE Setup ```bash
pnpm dev
```
默认开发端口:
```bash
http://localhost:3000
```
[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur). ## 常用脚本
## Recommended Browser Setup ### 启动开发环境
- Chromium-based browsers (Chrome, Edge, Brave, etc.): ```bash
- [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd) pnpm dev
- [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters) ```
- Firefox:
- [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)
- [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/)
## Type Support for `.vue` Imports in TS ### 类型检查
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types. ```bash
pnpm type-check
```
## Customize configuration ### 代码检查并自动修复
See [Vite Configuration Reference](https://vite.dev/config/). ```bash
pnpm lint
```
## Project Setup ### 代码格式化
```sh ```bash
pnpm install pnpm format
``` ```
### Compile and Hot-Reload for Development ### 本地预览构建结果
```sh ```bash
pnpm dev pnpm preview
``` ```
### Type-Check, Compile and Minify for Production ### 默认构建
```sh ```bash
pnpm build pnpm build
``` ```
### Lint with [ESLint](https://eslint.org/) ### 按环境构建
```sh ```bash
pnpm lint pnpm build:dev
pnpm build:test
pnpm build:prod
```
### 发布脚本
```bash
pnpm deploy:test
pnpm deploy:prod
pnpm deploy:prod:update-info
``` ```
## 环境变量
项目使用 `.env.local` 管理本地环境变量。常见变量如下:
```bash
VITE_AI_API_URL=
MY_OPENAI_API_KEY=
MY_OPENAI_BASE_URL=
BACKEND_API_URL=
DEPLOY_PROD_HOST=
DEPLOY_PROD_PORT=
DEPLOY_PROD_USERNAME=
DEPLOY_PROD_PASSWORD=
DEPLOY_TEST_HOST=
DEPLOY_TEST_PORT=
DEPLOY_TEST_USERNAME=
DEPLOY_TEST_PASSWORD=
```
说明:
- `VITE_` 前缀变量会注入前端运行时。
- `BACKEND_API_URL` 用于业务接口请求地址。
- `MY_OPENAI_*` 用于本地 AI 代理 / OpenAI 兼容接口。
- `DEPLOY_*` 用于部署脚本连接服务器。
建议:
- 不要在 README 中记录真实密钥和密码。
- 团队协作时建议补一个 `.env.example` 作为示例模板。
## 目录结构
```text
.
├─ deploy/ # 测试 / 正式环境部署脚本
├─ public/ # 公共静态资源
├─ server/ # 本地服务或辅助服务代码
├─ src/
│ ├─ api/ # 接口定义与请求封装
│ ├─ assets/ # 图片、SVG、基础样式
│ ├─ components/ # 通用组件
│ ├─ constants/ # 常量、枚举、选项配置
│ ├─ hooks/ # 组合式 hooks
│ ├─ layoutCulture/ # 前台整体布局
│ ├─ plugins/ # Vite / 业务插件
│ ├─ router/ # 路由与守卫
│ ├─ stores/ # Pinia stores
│ ├─ style/ # 全局样式
│ ├─ utils/ # 工具函数、请求、存储、版本检测等
│ ├─ views/ # 页面级视图
│ ├─ App.vue
│ ├─ config.ts
│ └─ main.ts
├─ vite.config.ts # Vite 配置
├─ uno.config.ts # UnoCSS 配置
├─ tsconfig*.json # TypeScript 配置
└─ package.json
```
## 路由结构
### 前台主路由
- `/homePage/homeTab`
- `/homePage/yaTab`
- `/homePage/askTab`
- `/userPage/*`
- `/videoDetail/:id`
- `/articleDetail/:id`
- `/questionDetail/:id`
- `/publishVideo`
- `/publishCase`
- `/publishLongArticle/:type`
- `/pointsStore`
- `/auction`
### 后台路由
- `/backend/tags`
- `/backend/carousel`
- `/backend/caseManage`
- `/backend/goodsDistribution`
- `/backend/contentsMenu/*`
- `/backend/settingsMenu/*`
- `/backend/columnsMenu/*`
## 工程特性
- 基于 `Element Plus` 组件体系构建
- 使用 `UnoCSS` 进行原子化样式开发
- 使用 `AutoImport``Components` 自动导入 Vue / Element Plus 能力
- 支持 SVG 图标自动注册
- 构建阶段按依赖进行 `manualChunks` 拆包
- 生产环境启用版本轮询更新逻辑
## 开发建议
### 推荐编辑器
- `VS Code`
- 安装插件:
- `Vue - Official`
- `ESLint`
- `Prettier`
- `UnoCSS`
### 样式建议
- 优先使用 `UnoCSS`
- 页面结构复杂时再配合少量 `scoped style`
- 公共视觉元素优先沉淀到 `src/components/common`
### 代码组织建议
- 页面接口统一放在 `src/api`
- 复用逻辑统一抽到 `src/hooks`
- 通用方法放在 `src/utils`
- 状态共享走 `Pinia`
## 构建与部署说明
- `pnpm build:*` 用于不同环境构建
- `deploy/` 目录中维护测试与正式环境发布脚本
- 发布前请确认:
- 环境变量配置正确
- 当前分支代码已通过基本功能验证
- 需要的服务器连接信息已更新
...@@ -26,6 +26,7 @@ ...@@ -26,6 +26,7 @@
}, },
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "^2.3.2", "@element-plus/icons-vue": "^2.3.2",
"@types/crypto-js": "^4.2.2",
"@vueuse/components": "^14.0.0", "@vueuse/components": "^14.0.0",
"@vueuse/core": "^14.0.0", "@vueuse/core": "^14.0.0",
"@wangeditor/editor": "^5.1.23", "@wangeditor/editor": "^5.1.23",
...@@ -33,6 +34,7 @@ ...@@ -33,6 +34,7 @@
"@wecom/jssdk": "^2.3.3", "@wecom/jssdk": "^2.3.3",
"archiver": "^7.0.1", "archiver": "^7.0.1",
"axios": "^1.13.0", "axios": "^1.13.0",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.19", "dayjs": "^1.11.19",
"element-plus": "^2.11.5", "element-plus": "^2.11.5",
"inquirer": "^13.0.2", "inquirer": "^13.0.2",
......
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -40,6 +40,8 @@ export interface AddOrUpdatePostDto { ...@@ -40,6 +40,8 @@ export interface AddOrUpdatePostDto {
imgUrl?: string imgUrl?: string
sendTime?: string sendTime?: string
type?: ArticleTypeEnum.POST type?: ArticleTypeEnum.POST
// 1是隐藏
regionHide?: BooleanFlag
} }
interface AddOrUpdateColumnBase { interface AddOrUpdateColumnBase {
...@@ -205,6 +207,7 @@ export interface ArticleItemDto { ...@@ -205,6 +207,7 @@ export interface ArticleItemDto {
videoLocation: VideoPositionEnum videoLocation: VideoPositionEnum
articleVideoUrl: string articleVideoUrl: string
region: string region: string
regionHide: BooleanFlag
recommendSort: number recommendSort: number
isOfficialAccount: BooleanFlag isOfficialAccount: BooleanFlag
deptId: string deptId: string
......
import type { TagItemDto } from '@/api/case/types' import type { TagItemDto } from '@/api/tag/types'
import { UsageStatusEnum, AuditStatusEnum } from '@/constants' import { UsageStatusEnum, AuditStatusEnum } from '@/constants'
import type { PageSearchParams } from '@/utils/request/types' import type { PageSearchParams } from '@/utils/request/types'
......
...@@ -9,7 +9,7 @@ export interface BackendShopListSearchParams extends PageSearchParams { ...@@ -9,7 +9,7 @@ export interface BackendShopListSearchParams extends PageSearchParams {
export interface BackendShopItemDto { export interface BackendShopItemDto {
createTime: number createTime: number
deliveryInfo: string deliveryInfo: string
id: number id?: number
imageUrl: string imageUrl: string
isDelete: null isDelete: null
issueTime: number issueTime: number
......
// 常规的接口 import axios, { type AxiosRequestConfig } from 'axios'
// import service from '@/utils/request/index' import CryptoJS from 'crypto-js'
// import type { FielItemDto } from './types' import { useUserStore } from '@/stores'
/** import { storeToRefs } from 'pinia'
* 获取常规的接口
*/ import type { UploadFileResponseItem, ChunkCheckResponse } from './types'
// export const uploadFile = (file: File, onProgress?: (progress: number) => void) => {
// const formData = new FormData() const OA_UPLOAD_CHUNK_BASE_URL = 'https://oa.yswg.com.cn:8085'
// formData.append('file', file) const OA_UPLOAD_COMMON_BASE_URL = 'http://47.112.96.71:8082'
// return service.request<FielItemDto>({ const CHUNK_UPLOAD_THRESHOLD = 10 * 1024 * 1024
// url: '/mobiles/file-upload/singleUpload', const CHUNK_SIZE = 5 * 1024 * 1024
// method: 'POST', const MAX_CONCURRENT_UPLOADS = 3
// data: formData, const CHUNK_API_AUTH_SECRET = '4821-7395-1642-8053-2971-5604-9182-4307'
// onUploadProgress: (progressEvent) => {
// const percentCompleted = Math.round((progressEvent.loaded * 100) / (progressEvent.total || 1)) function hashBufferMD5(buffer: ArrayBuffer): string {
// onProgress?.(percentCompleted) const wordArray = CryptoJS.lib.WordArray.create(new Uint8Array(buffer))
// }, return CryptoJS.MD5(wordArray).toString(CryptoJS.enc.Hex)
// }) }
// }
function readSlice(file: File, start: number, end: number): Promise<ArrayBuffer> {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = (event) => resolve(event.target?.result as ArrayBuffer)
reader.onerror = () => reject(reader.error ?? new Error('读取文件分片失败'))
reader.readAsArrayBuffer(file.slice(start, end))
})
}
// 返回一个由文件名 + 头部 + 尾部 + 中间抽样共同生成的综合指纹 用于检查文件是否存在 hash不耗事件 主要等待异步io耗时间
// 4GB的视频耗时30s大概
async function fileHashHeadTailMiddle(file: File, chunkSize = 1024 * 1024) {
const fileSize = file.size
const finalHashes: string[] = []
// 对文件名做md5加密
const fileName = file.name.split('_').slice(0, -1).join('_') || file.name
finalHashes.push(CryptoJS.MD5(fileName).toString(CryptoJS.enc.Hex))
// 读取文件头做md5加密
const headBuffer = await readSlice(file, 0, Math.min(chunkSize, fileSize))
finalHashes.push(hashBufferMD5(headBuffer))
// 读取文件尾做md5加密
const tailSize = Math.min(chunkSize, fileSize)
const tailBuffer = await readSlice(file, fileSize - tailSize, fileSize)
finalHashes.push(hashBufferMD5(tailBuffer))
// 循环读取文件中间的部分 然后部分做md5加密
if (fileSize > 2 * chunkSize) {
let offset = chunkSize
while (offset < fileSize - chunkSize) {
const size = Math.min(chunkSize, fileSize - chunkSize - offset)
const buffer = await readSlice(file, offset, offset + size)
const u8 = new Uint8Array(buffer)
const piece = new Uint8Array(10)
// 取前5个字节和后5个字节做md5加密 计算时间比较短
piece.set(u8.slice(0, Math.min(5, u8.length)), 0)
piece.set(u8.slice(Math.max(u8.length - 5, 0)), 5)
finalHashes.push(hashBufferMD5(piece.buffer))
offset += size
}
}
// 将所有md5加密后的结果拼接起来 然后做md5加密
return CryptoJS.MD5(finalHashes.join('')).toString(CryptoJS.enc.Hex)
}
// 将文件进行分片 返回一个Blob数组
function createChunks(file: File, chunkSize: number) {
const chunks: Blob[] = []
let start = 0
while (start < file.size) {
const end = Math.min(start + chunkSize, file.size)
chunks.push(file.slice(start, end))
start = end
}
return chunks
}
// 分片上传文件
/** /**
* 暂时调用oa正式接口 * 第 1 步:给文件生成一个 hash
* 第 2 步:调用 check 询问服务端当前状态
* 第 3 步:根据 check 结果决定是否秒传 或者 断点续传 或者 正常分片上传
* 第 4 步:把大文件切成多个分片
* 第 5 步:创建多个并发 worker,每个 worker 循环领取分片任务 进行分片上传
* 第 6 步:所有分片都上传完成后,通知服务端合并
* 第 7 步:如果文件存在,则合并文件
*/ */
import axios, { type AxiosRequestConfig } from 'axios' async function uploadFileByChunks(
type UploadFileResponseItem = { file: File,
createTime: string options: {
createUser: number onProgress?: (progress: number) => void
fileBucket: string },
fileId: string signal: AbortSignal,
fileName: string ): Promise<UploadFileResponseItem> {
filePath: string const userStore = useUserStore()
fileSizeKb: number const { chunkApiAuthToken } = storeToRefs(userStore)
fileSuffix: string const { onProgress } = options
finalName: string const fileHash = await fileHashHeadTailMiddle(file)
realPath: string const fileSuffix = file.name.includes('.') ? file.name.slice(file.name.lastIndexOf('.')) : ''
updateTime: string
updateUser: string const checkResponse = await axios.post<{ data: ChunkCheckResponse }>(
`${OA_UPLOAD_CHUNK_BASE_URL}/mobiles/file-upload/check`,
{
hash: fileHash,
fileName: file.name,
fileSize: file.size,
fileSuffx: fileSuffix,
},
{ signal, headers: { authorization: chunkApiAuthToken.value } },
)
// 秒传服务检查文件是否存在
const checkData = checkResponse.data.data
if (checkData.status === 200 && checkData.fileUrl) {
onProgress?.(100)
return {
fileId: fileHash,
fileName: file.name,
filePath: checkData.fileUrl,
finalName: file.name,
} as UploadFileResponseItem
}
const chunks = createChunks(file, CHUNK_SIZE)
const totalChunks = chunks.length
// 断点续传 记录已经上传的片数
const uploadedSet = new Set<number>(
Array.isArray(checkData.chunkNumbers)
? checkData.chunkNumbers
.map((item) => Number(item))
.filter((item) => Number.isInteger(item) && item >= 0)
: [],
)
const updateProgress = () => {
const progress = totalChunks > 0 ? Math.floor((uploadedSet.size / totalChunks) * 100) : 0
onProgress?.(progress)
}
// 有可能部分已经上传过了直接更新一次上传进度
updateProgress()
// 上传单个分片
const uploadChunk = async (chunk: Blob, index: number) => {
const formData = new FormData()
formData.append('fileId', fileHash)
formData.append('filePart', chunk, file.name)
formData.append('chunkNumber', String(index))
await axios.post(`${OA_UPLOAD_CHUNK_BASE_URL}/mobiles/file-upload/chunk`, formData, {
signal,
headers: {
'Content-Type': 'multipart/form-data',
authorization: chunkApiAuthToken.value,
},
})
uploadedSet.add(index)
updateProgress()
}
let nextIndex = 0
const worker = async () => {
// 遍历所有分片 如果已经上传过了就跳过 否则上传
// 相当于开了 3个while死循环 同时上传文件 只有等3个线程都上传完了 才结束
// 总有某个 worker 在自己的异步任务完成后恢复执行,然后继续进入下一轮 while,领取新的分片任务。
while (nextIndex < totalChunks) {
const currentIndex = nextIndex
nextIndex += 1
if (uploadedSet.has(currentIndex)) continue
await uploadChunk(chunks[currentIndex]!, currentIndex)
}
}
// 同时开了 3 个异步 worker,每个 worker 里面都有一个 while 循环,不断去“领取下一个还没处理的分片”。
await Promise.all(
Array.from({ length: Math.min(MAX_CONCURRENT_UPLOADS, totalChunks) }, () => worker()),
)
const finishFormData = new FormData()
finishFormData.set('fileName', file.name)
finishFormData.set('fileId', fileHash)
finishFormData.set('chunkTotalCount', String(totalChunks))
// 上传完成 通知后端文件上传完成
const finishResponse = await axios.post<{ data: { fileUrl: string } }>(
`${OA_UPLOAD_CHUNK_BASE_URL}/mobiles/file-upload/finish`,
finishFormData,
{
signal,
headers: {
'Content-Type': 'multipart/form-data',
authorization: chunkApiAuthToken.value,
},
},
)
onProgress?.(100)
return {
fileId: fileHash,
fileName: file.name,
filePath: finishResponse.data.data.fileUrl,
finalName: file.name,
} as UploadFileResponseItem
} }
// 单个文件上传 // 上传文件 10mb以上用nas分片 10mb以下正常上传
export const uploadFile = ( export const uploadFile = (
file: File, file: File,
options: { options: {
...@@ -47,29 +225,60 @@ export const uploadFile = ( ...@@ -47,29 +225,60 @@ export const uploadFile = (
cancel: () => void cancel: () => void
promise: Promise<UploadFileResponseItem> promise: Promise<UploadFileResponseItem>
} => { } => {
const formData = new FormData()
formData.append('fileList', file)
const { onProgress } = options const { onProgress } = options
const controller = new AbortController() const controller = new AbortController()
const axiosOptions: AxiosRequestConfig = { const isChunkUpload = file.size > CHUNK_UPLOAD_THRESHOLD
signal: controller.signal,
}
if (onProgress) { const promise = isChunkUpload
axiosOptions.onUploadProgress = (progressEvent) => { ? uploadFileByChunks(file, options, controller.signal)
const percentCompleted = Math.round((progressEvent.loaded * 100) / (progressEvent.total || 1)) : (() => {
onProgress?.(percentCompleted) // 用IIFE包裹直接返回promise
} const formData = new FormData()
} formData.append('fileList', file)
const axiosOptions: AxiosRequestConfig<FormData> = {
signal: controller.signal,
}
if (onProgress) {
axiosOptions.onUploadProgress = (progressEvent) => {
const percentCompleted = Math.round(
(progressEvent.loaded * 100) / (progressEvent.total || 1),
)
onProgress(percentCompleted)
}
}
return axios
.post(`${OA_UPLOAD_COMMON_BASE_URL}/mobiles/uploadFile`, formData, axiosOptions)
.then((data) => data.data.data[0] as UploadFileResponseItem)
})()
return { return {
cancel: () => { cancel: () => {
controller.abort() controller.abort()
}, },
promise: axios promise,
.post('http://47.112.96.71:8082/mobiles/uploadFile', formData, axiosOptions)
.then((data) => data.data.data[0] as UploadFileResponseItem),
} }
} }
export const getTimestamp = async () => {
const {
data: { data: timestamp },
} = await axios.get(`${OA_UPLOAD_CHUNK_BASE_URL}/api/auth/getTime`)
return timestamp
}
export const getChunkApiAuthToken = async (weChatId: string) => {
const timestamp = await getTimestamp()
const rawStr = CHUNK_API_AUTH_SECRET + timestamp
const {
data: { data },
} = await axios.post(`${OA_UPLOAD_CHUNK_BASE_URL}/api/auth/getToken`, {
timestamp,
weChatId,
secret: CryptoJS.MD5(rawStr).toString(),
})
return data.token as string
}
...@@ -6,3 +6,24 @@ export interface FielItemDto { ...@@ -6,3 +6,24 @@ export interface FielItemDto {
msg: string msg: string
status: number status: number
} }
export interface UploadFileResponseItem {
createTime: string
createUser: number
fileBucket: string
fileId: string
fileName: string
filePath: string
fileSizeKb: number
fileSuffix: string
finalName: string
realPath: string
updateTime: string
updateUser: string
}
export interface ChunkCheckResponse {
status: number
fileUrl?: string
chunkNumbers?: number[]
}
...@@ -120,9 +120,7 @@ ...@@ -120,9 +120,7 @@
<p class="text-sm text-gray-500 mt-1"> <p class="text-sm text-gray-500 mt-1">
{{ dayjs((articleDetail?.createTime || 0) * 1000).format('YYYY-MM-DD HH:mm:ss') }} {{ dayjs((articleDetail?.createTime || 0) * 1000).format('YYYY-MM-DD HH:mm:ss') }}
· {{ articleDetail?.viewCount || 0 }} 阅读 · {{ articleDetail?.viewCount || 0 }} 阅读
<span v-if="!articleDetail?.isOfficialAccount" <span v-if="articleDetail?.region">· {{ articleDetail?.region }}</span>
>· {{ articleDetail?.region || 0 }}</span
>
</p> </p>
</div> </div>
<!-- 再次编辑按钮 --> <!-- 再次编辑按钮 -->
......
...@@ -212,8 +212,7 @@ ...@@ -212,8 +212,7 @@
<span <span
>{{ dayjs(item.createTime * 1000).format('YYYY-MM-DD HH:mm:ss') }} >{{ dayjs(item.createTime * 1000).format('YYYY-MM-DD HH:mm:ss') }}
</span> </span>
<span>·</span> <span v-if="item.region" class="text-gray-500">·{{ item.region }}</span>
<span class="text-gray-500">{{ item.region }}</span>
</span> </span>
<div class="flex gap-2 items-center hover:text-blue-500 transition-colors"> <div class="flex gap-2 items-center hover:text-blue-500 transition-colors">
<div <div
...@@ -299,8 +298,9 @@ ...@@ -299,8 +298,9 @@
dayjs(child.createTime * 1000).format('YYYY-MM-DD HH:mm:ss') dayjs(child.createTime * 1000).format('YYYY-MM-DD HH:mm:ss')
}} }}
</span> </span>
<span>·</span> <span v-if="child.region" class="text-gray-500"
<span class="text-gray-500">{{ child.region }}</span> >·{{ child.region }}</span
>
</span> </span>
<div class="flex gap-2 items-center hover:text-blue-500"> <div class="flex gap-2 items-center hover:text-blue-500">
<div <div
......
...@@ -85,8 +85,10 @@ ...@@ -85,8 +85,10 @@
</div> </div>
<div class="text-sm text-gray-500 flex items-center gap-2"> <div class="text-sm text-gray-500 flex items-center gap-2">
<span>{{ dayjs(parentComment.createTime * 1000).format('MM-DD HH:mm') }}</span> · <span>{{ dayjs(parentComment.createTime * 1000).format('MM-DD HH:mm') }}</span>
<span class="text-xs text-gray-500">{{ parentComment.region }}</span> <span v-if="parentComment.region" class="text-xs text-gray-500"
>·{{ parentComment.region }}</span
>
</div> </div>
</div> </div>
</div> </div>
...@@ -165,8 +167,8 @@ ...@@ -165,8 +167,8 @@
</div> </div>
<div class="text-[14px] text-gray-500 flex items-center gap-2"> <div class="text-[14px] text-gray-500 flex items-center gap-2">
<span>{{ dayjs(item.createTime * 1000).format('MM-DD HH:mm') }}</span> · <span>{{ dayjs(item.createTime * 1000).format('MM-DD HH:mm') }}</span>
<span class="text-xs text-gray-500">{{ item.region }}</span> <span v-if="item.region" class="text-xs text-gray-500">·{{ item.region }}</span>
</div> </div>
<!-- 内嵌回复框 --> <!-- 内嵌回复框 -->
...@@ -278,7 +280,7 @@ provide(IS_REAL_KEY_COMMENT, BooleanFlag.YES) ...@@ -278,7 +280,7 @@ provide(IS_REAL_KEY_COMMENT, BooleanFlag.YES)
// Store // Store
const userStore = useUserStore() const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore) const { userInfo } = storeToRefs(userStore)
const currentUserAvatar = computed(() => userInfo.value.hiddenAvatar) const currentUserAvatar = computed(() => userInfo.value.avatar)
const { confirm } = useMessageBox() const { confirm } = useMessageBox()
// State // State
......
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from 'vue'
import emojis from '@/utils/emoji/face.json' import emojis from '@/utils/emoji/face.json'
import yayas from '@/utils/emoji/yaya.json'
import type { IEmoji } from '@/utils/emoji/type' import type { IEmoji } from '@/utils/emoji/type'
const emit = defineEmits<{ const emit = defineEmits<{
selectEmoji: [emoji: IEmoji] selectEmoji: [emoji: IEmoji]
}>() }>()
type EmojiTab = 'face' | 'yaya'
const activeTab = ref<EmojiTab>('face')
const transitionName = ref('emoji-slide-left')
const emojiTabs: Array<{
key: EmojiTab
label: string
list: Array<Pick<IEmoji, 'name' | 'url' | 'group'>>
}> = [
{
key: 'face',
label: '默认表情',
list: emojis,
},
{
key: 'yaya',
label: 'YAYA表情',
list: yayas,
},
]
const currentEmojiList = computed(() => {
return emojiTabs.find((tab) => tab.key === activeTab.value)?.list ?? []
})
const switchTab = (nextTab: EmojiTab) => {
if (nextTab === activeTab.value) return
const currentIndex = emojiTabs.findIndex((tab) => tab.key === activeTab.value)
const nextIndex = emojiTabs.findIndex((tab) => tab.key === nextTab)
transitionName.value = nextIndex > currentIndex ? 'emoji-slide-left' : 'emoji-slide-right'
activeTab.value = nextTab
}
</script> </script>
<template> <template>
<div <div
class="w-8 h-8 text-gray-500 hover:bg-gray-100 hover:text-gray-700 rounded-lg flex items-center justify-center" class="flex h-8 w-8 items-center justify-center rounded-lg text-gray-500 transition-all hover:bg-orange-50 hover:text-orange-500"
> >
<el-popover placement="bottom" trigger="click" width="384"> <el-popover placement="bottom" trigger="click" :width="384" popper-class="emoji-popover">
<template #reference> <template #reference>
<el-icon size="20" @mousedown.prevent> <el-icon size="20" @mousedown.prevent>
<svg-icon name="icon_face" class="cursor-pointer" <svg-icon name="icon_face" class="cursor-pointer" />
/></el-icon> </el-icon>
</template> </template>
<!-- 表情面板 -->
<el-scrollbar class="h-50"> <div class="emoji-panel">
<div class="flex flex-wrap"> <div class="emoji-tabs">
<span <button
v-for="item in emojis" v-for="tab in emojiTabs"
:key="item.name" :key="tab.key"
class="cursor-pointer hover:bg-gray-100 rounded p-1 flex items-center justify-center" type="button"
@click="emit('selectEmoji', item)" class="emoji-tab"
:class="{ 'emoji-tab--active': activeTab === tab.key }"
@click="switchTab(tab.key)"
> >
<img :src="item.url" alt="" class="w-6 h-6" /> {{ tab.label }}
</span> </button>
</div> </div>
</el-scrollbar>
<el-scrollbar class="emoji-scrollbar">
<transition :name="transitionName" mode="out-in">
<div :key="activeTab" class="emoji-grid">
<div v-for="item in currentEmojiList" :key="item.name">
<el-popover
v-if="item.group === 'yaya'"
placement="top"
trigger="hover"
:offset="10"
:show-arrow="false"
:popper-style="{
height: '150px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}"
>
<el-image :src="item.url" :alt="item.name" class="h-full w-full object-contain" />
<template #reference>
<button
type="button"
class="emoji-item"
:title="item.name"
@click="emit('selectEmoji', item as IEmoji)"
>
<img
:src="item.url"
:alt="item.name"
class="h-6 w-6 object-contain"
:class="{ 'h-8 w-8': item.group === 'yaya' }"
/>
</button>
</template>
</el-popover>
<button
v-else
type="button"
class="emoji-item"
:title="item.name"
@click="emit('selectEmoji', item as IEmoji)"
>
<img
:src="item.url"
:alt="item.name"
class="h-6 w-6 object-contain"
:class="{ 'h-8 w-8': item.group === 'yaya' }"
/>
</button>
</div>
</div>
</transition>
</el-scrollbar>
</div>
</el-popover> </el-popover>
</div> </div>
</template> </template>
<style scoped></style> <style scoped>
.emoji-tabs {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 8px;
padding: 0 6px 8px;
border-bottom: 1px solid #f5f5f5;
}
.emoji-tab {
position: relative;
border: 0;
background: transparent;
color: #9ca3af;
border-radius: 0;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition:
color 0.18s ease,
transform 0.18s ease;
}
.emoji-tab:hover {
color: #f97316;
}
.emoji-tab--active {
color: #ea580c;
transform: none;
}
.emoji-tab--active::after {
content: '';
position: absolute;
left: 0;
bottom: -9px;
width: 100%;
height: 2px;
border-radius: 999px;
background: linear-gradient(90deg, #fb923c 0%, #f97316 100%);
}
.emoji-scrollbar {
height: 188px;
}
.emoji-grid {
display: grid;
grid-template-columns: repeat(8, minmax(0, 1fr));
gap: 6px;
padding: 2px 4px 10px;
}
.emoji-slide-left-enter-active,
.emoji-slide-left-leave-active,
.emoji-slide-right-enter-active,
.emoji-slide-right-leave-active {
transition:
opacity 0.22s ease,
transform 0.22s ease;
}
.emoji-slide-left-enter-from,
.emoji-slide-right-leave-to {
opacity: 0;
transform: translateX(18px);
}
.emoji-slide-left-leave-to,
.emoji-slide-right-enter-from {
opacity: 0;
transform: translateX(-18px);
}
.emoji-slide-left-enter-to,
.emoji-slide-left-leave-from,
.emoji-slide-right-enter-to,
.emoji-slide-right-leave-from {
opacity: 1;
transform: translateX(0);
}
.emoji-item {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 34px;
border: 0;
border-radius: 10px;
background: #fff;
cursor: pointer;
transition:
transform 0.18s ease,
background-color 0.18s ease,
box-shadow 0.18s ease;
}
.emoji-item:hover {
background: linear-gradient(180deg, #fff7ed 0%, #ffedd5 100%);
box-shadow: 0 6px 14px rgb(251 146 60 / 16%);
transform: translateY(-1px);
}
</style>
...@@ -157,18 +157,17 @@ const startUpload = async () => { ...@@ -157,18 +157,17 @@ const startUpload = async () => {
uploadError.value = '' uploadError.value = ''
uploadProgress.value = 0 uploadProgress.value = 0
// 调用你的上传方法 // 先提案获取视频的原信息 用本地blob
const blob = URL.createObjectURL(currentFile.value)
const metadataPromise = getVideoMetadata(blob)
const { promise, cancel } = uploadFileApi(currentFile.value, { const { promise, cancel } = uploadFileApi(currentFile.value, {
onProgress: (progress) => { onProgress: (progress) => {
uploadProgress.value = progress uploadProgress.value = progress
}, },
}) })
cancelUploadController = cancel cancelUploadController = cancel
const data = await promise // 并行等待
const [data, metadata] = await Promise.all([promise, metadataPromise])
// 获取视频元数据
const metadata = await getVideoMetadata(data.filePath)
// 根据你的 API 返回结构调整 // 根据你的 API 返回结构调整
const videoData: VideoInfo = { const videoData: VideoInfo = {
......
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { loginByCode, loginByEmail, refreshTokenApi } from '@/api/login' import { loginByCode, loginByEmail, refreshTokenApi, getChunkApiAuthToken } from '@/api'
import type { LoginResponseDto } from '@/api/login/types' import type { LoginResponseDto } from '@/api/login/types'
import { useStorage } from '@vueuse/core'
/** /**
* 关于用户的store * 关于用户的store
*/ */
export const useUserStore = defineStore('user', () => { export const useUserStore = defineStore('user', () => {
const userInfo = ref(JSON.parse(localStorage.getItem('userInfo') || '{}') as LoginResponseDto) const userInfo = useStorage<LoginResponseDto>('userInfo', {} as LoginResponseDto)
const token = ref(localStorage.getItem('token') || '') const token = useStorage<string>('token', '')
const refreshToken = ref(localStorage.getItem('refreshToken') || '') const refreshToken = useStorage<string>('refreshToken', '')
const chunkApiAuthToken = useStorage<string>('chunkApiAuthToken', '')
// 获取用户信息 // 获取用户信息
const fetchUserInfo = async () => { const fetchUserInfo = async () => {
// { // {
...@@ -41,6 +43,10 @@ export const useUserStore = defineStore('user', () => { ...@@ -41,6 +43,10 @@ export const useUserStore = defineStore('user', () => {
}) => { }) => {
const { data } = await loginByCode({ code, isCodeLogin, cutEmail }) const { data } = await loginByCode({ code, isCodeLogin, cutEmail })
setUserInfoAndToken(data) setUserInfoAndToken(data)
// 同时获取关于分片的token
const token = await getChunkApiAuthToken(data.account)
setChunkApiAuthToken(token)
} }
const getNewToken = async () => { const getNewToken = async () => {
const { data } = await refreshTokenApi(refreshToken.value) const { data } = await refreshTokenApi(refreshToken.value)
...@@ -71,6 +77,10 @@ export const useUserStore = defineStore('user', () => { ...@@ -71,6 +77,10 @@ export const useUserStore = defineStore('user', () => {
// session存一份 // session存一份
localStorage.setItem('token', str) localStorage.setItem('token', str)
} }
const setChunkApiAuthToken = (str: string) => {
chunkApiAuthToken.value = str
localStorage.setItem('chunkApiAuthToken', str)
}
const setRefreshToken = (str: string) => { const setRefreshToken = (str: string) => {
refreshToken.value = str refreshToken.value = str
localStorage.setItem('refreshToken', str) localStorage.setItem('refreshToken', str)
...@@ -82,10 +92,12 @@ export const useUserStore = defineStore('user', () => { ...@@ -82,10 +92,12 @@ export const useUserStore = defineStore('user', () => {
return { return {
userInfo, userInfo,
token, token,
chunkApiAuthToken,
fetchUserInfo, fetchUserInfo,
setUserInfo, setUserInfo,
setToken, setToken,
getUserInfoByCode, getUserInfoByCode,
setChunkApiAuthToken,
refreshToken, refreshToken,
getNewToken, getNewToken,
clearAllUserInfo, clearAllUserInfo,
......
...@@ -2,601 +2,501 @@ ...@@ -2,601 +2,501 @@
{ {
"url": "https://www.emojiall.com/images/60/skype/1f600.png", "url": "https://www.emojiall.com/images/60/skype/1f600.png",
"name": "[face_嘿嘿]", "name": "[face_嘿嘿]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f603.png", "url": "https://www.emojiall.com/images/60/skype/1f603.png",
"name": "[face_哈哈]", "name": "[face_哈哈]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f604.png", "url": "https://www.emojiall.com/images/60/skype/1f604.png",
"name": "[face_大笑]", "name": "[face_大笑]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f601.png", "url": "https://www.emojiall.com/images/60/skype/1f601.png",
"name": "[face_嘻嘻]", "name": "[face_嘻嘻]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f606.png", "url": "https://www.emojiall.com/images/60/skype/1f606.png",
"name": "[face_斜眼笑]", "name": "[face_斜眼笑]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f605.png", "url": "https://www.emojiall.com/images/60/skype/1f605.png",
"name": "[face_苦笑]", "name": "[face_苦笑]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f923.png", "url": "https://www.emojiall.com/images/60/skype/1f923.png",
"name": "[face_笑得满地打滚]", "name": "[face_笑得满地打滚]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f602.png", "url": "https://www.emojiall.com/images/60/skype/1f602.png",
"name": "[face_笑哭了]", "name": "[face_笑哭了]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f642.png", "url": "https://www.emojiall.com/images/60/skype/1f642.png",
"name": "[face_呵呵]", "name": "[face_呵呵]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f643.png", "url": "https://www.emojiall.com/images/60/skype/1f643.png",
"name": "[face_倒脸]", "name": "[face_倒脸]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f609.png", "url": "https://www.emojiall.com/images/60/skype/1f609.png",
"name": "[face_眨眼]", "name": "[face_眨眼]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f60a.png", "url": "https://www.emojiall.com/images/60/skype/1f60a.png",
"name": "[face_羞涩微笑]", "name": "[face_羞涩微笑]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f607.png", "url": "https://www.emojiall.com/images/60/skype/1f607.png",
"name": "[face_微笑天使]", "name": "[face_微笑天使]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f970.png", "url": "https://www.emojiall.com/images/60/skype/1f970.png",
"name": "[face_喜笑颜开]", "name": "[face_喜笑颜开]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f60d.png", "url": "https://www.emojiall.com/images/60/skype/1f60d.png",
"name": "[face_花痴]", "name": "[face_花痴]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f929.png", "url": "https://www.emojiall.com/images/60/skype/1f929.png",
"name": "[face_好崇拜哦]", "name": "[face_好崇拜哦]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f617.png", "url": "https://www.emojiall.com/images/60/skype/1f617.png",
"name": "[face_亲亲]", "name": "[face_亲亲]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f61a.png", "url": "https://www.emojiall.com/images/60/skype/1f61a.png",
"name": "[face_羞涩亲亲]", "name": "[face_羞涩亲亲]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f619.png", "url": "https://www.emojiall.com/images/60/skype/1f619.png",
"name": "[face_微笑亲亲]", "name": "[face_微笑亲亲]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f60b.png", "url": "https://www.emojiall.com/images/60/skype/1f60b.png",
"name": "[face_好吃]", "name": "[face_好吃]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f61b.png", "url": "https://www.emojiall.com/images/60/skype/1f61b.png",
"name": "[face_吐舌]", "name": "[face_吐舌]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f61c.png", "url": "https://www.emojiall.com/images/60/skype/1f61c.png",
"name": "[face_单眼吐舌]", "name": "[face_单眼吐舌]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f92a.png", "url": "https://www.emojiall.com/images/60/skype/1f92a.png",
"name": "[face_滑稽]", "name": "[face_滑稽]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f911.png", "url": "https://www.emojiall.com/images/60/skype/1f911.png",
"name": "[face_发财]", "name": "[face_发财]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f917.png", "url": "https://www.emojiall.com/images/60/skype/1f917.png",
"name": "[face_抱抱]", "name": "[face_抱抱]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f92d.png", "url": "https://www.emojiall.com/images/60/skype/1f92d.png",
"name": "[face_不说]", "name": "[face_不说]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f92b.png", "url": "https://www.emojiall.com/images/60/skype/1f92b.png",
"name": "[face_安静的脸]", "name": "[face_安静的脸]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f914.png", "url": "https://www.emojiall.com/images/60/skype/1f914.png",
"name": "[face_想一想]", "name": "[face_想一想]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f910.png", "url": "https://www.emojiall.com/images/60/skype/1f910.png",
"name": "[face_闭嘴]", "name": "[face_闭嘴]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f928.png", "url": "https://www.emojiall.com/images/60/skype/1f928.png",
"name": "[face_挑眉]", "name": "[face_挑眉]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f610.png", "url": "https://www.emojiall.com/images/60/skype/1f610.png",
"name": "[face_冷漠]", "name": "[face_冷漠]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f611.png", "url": "https://www.emojiall.com/images/60/skype/1f611.png",
"name": "[face_无语]", "name": "[face_无语]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f636.png", "url": "https://www.emojiall.com/images/60/skype/1f636.png",
"name": "[face_沉默]", "name": "[face_沉默]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f60f.png", "url": "https://www.emojiall.com/images/60/skype/1f60f.png",
"name": "[face_得意]", "name": "[face_得意]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f612.png", "url": "https://www.emojiall.com/images/60/skype/1f612.png",
"name": "[face_不高兴]", "name": "[face_不高兴]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f644.png", "url": "https://www.emojiall.com/images/60/skype/1f644.png",
"name": "[face_翻白眼]", "name": "[face_翻白眼]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f62c.png", "url": "https://www.emojiall.com/images/60/skype/1f62c.png",
"name": "[face_龇牙咧嘴]", "name": "[face_龇牙咧嘴]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f925.png", "url": "https://www.emojiall.com/images/60/skype/1f925.png",
"name": "[face_说谎]", "name": "[face_说谎]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f60c.png", "url": "https://www.emojiall.com/images/60/skype/1f60c.png",
"name": "[face_松了口气]", "name": "[face_松了口气]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f614.png", "url": "https://www.emojiall.com/images/60/skype/1f614.png",
"name": "[face_沉思]", "name": "[face_沉思]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f62a.png", "url": "https://www.emojiall.com/images/60/skype/1f62a.png",
"name": "[face_困]", "name": "[face_困]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f634.png", "url": "https://www.emojiall.com/images/60/skype/1f634.png",
"name": "[face_睡着了]", "name": "[face_睡着了]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f637.png", "url": "https://www.emojiall.com/images/60/skype/1f637.png",
"name": "[face_感冒]", "name": "[face_感冒]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f912.png", "url": "https://www.emojiall.com/images/60/skype/1f912.png",
"name": "[face_发烧]", "name": "[face_发烧]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f915.png", "url": "https://www.emojiall.com/images/60/skype/1f915.png",
"name": "[face_受伤]", "name": "[face_受伤]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f922.png", "url": "https://www.emojiall.com/images/60/skype/1f922.png",
"name": "[face_恶心]", "name": "[face_恶心]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f92e.png", "url": "https://www.emojiall.com/images/60/skype/1f92e.png",
"name": "[face_呕吐]", "name": "[face_呕吐]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f975.png", "url": "https://www.emojiall.com/images/60/skype/1f975.png",
"name": "[face_脸发烧]", "name": "[face_脸发烧]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f976.png", "url": "https://www.emojiall.com/images/60/skype/1f976.png",
"name": "[face_冷脸]", "name": "[face_冷脸]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f974.png", "url": "https://www.emojiall.com/images/60/skype/1f974.png",
"name": "[face_头昏眼花]", "name": "[face_头昏眼花]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f92f.png", "url": "https://www.emojiall.com/images/60/skype/1f92f.png",
"name": "[face_爆炸头]", "name": "[face_爆炸头]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f920.png", "url": "https://www.emojiall.com/images/60/skype/1f920.png",
"name": "[face_牛仔帽脸]", "name": "[face_牛仔帽脸]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f973.png", "url": "https://www.emojiall.com/images/60/skype/1f973.png",
"name": "[face_聚会笑脸]", "name": "[face_聚会笑脸]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f60e.png", "url": "https://www.emojiall.com/images/60/skype/1f60e.png",
"name": "[face_墨镜笑脸]", "name": "[face_墨镜笑脸]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f913.png", "url": "https://www.emojiall.com/images/60/skype/1f913.png",
"name": "[face_书呆子脸]", "name": "[face_书呆子脸]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f9d0.png", "url": "https://www.emojiall.com/images/60/skype/1f9d0.png",
"name": "[face_带单片眼镜的脸]", "name": "[face_带单片眼镜的脸]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f615.png", "url": "https://www.emojiall.com/images/60/skype/1f615.png",
"name": "[face_困扰]", "name": "[face_困扰]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f641.png", "url": "https://www.emojiall.com/images/60/skype/1f641.png",
"name": "[face_微微不满]", "name": "[face_微微不满]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/2639-fe0f.png", "url": "https://www.emojiall.com/images/60/skype/2639-fe0f.png",
"name": "[face_不满]", "name": "[face_不满]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f62f.png", "url": "https://www.emojiall.com/images/60/skype/1f62f.png",
"name": "[face_缄默]", "name": "[face_缄默]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f632.png", "url": "https://www.emojiall.com/images/60/skype/1f632.png",
"name": "[face_震惊]", "name": "[face_震惊]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f633.png", "url": "https://www.emojiall.com/images/60/skype/1f633.png",
"name": "[face_脸红]", "name": "[face_脸红]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f97a.png", "url": "https://www.emojiall.com/images/60/skype/1f97a.png",
"name": "[face_恳求的脸]", "name": "[face_恳求的脸]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f626.png", "url": "https://www.emojiall.com/images/60/skype/1f626.png",
"name": "[face_啊]", "name": "[face_啊]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f627.png", "url": "https://www.emojiall.com/images/60/skype/1f627.png",
"name": "[face_极度痛苦]", "name": "[face_极度痛苦]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f628.png", "url": "https://www.emojiall.com/images/60/skype/1f628.png",
"name": "[face_害怕]", "name": "[face_害怕]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f630.png", "url": "https://www.emojiall.com/images/60/skype/1f630.png",
"name": "[face_冷汗]", "name": "[face_冷汗]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f625.png", "url": "https://www.emojiall.com/images/60/skype/1f625.png",
"name": "[face_失望但如释重负]", "name": "[face_失望但如释重负]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f622.png", "url": "https://www.emojiall.com/images/60/skype/1f622.png",
"name": "[face_哭]", "name": "[face_哭]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f62d.png", "url": "https://www.emojiall.com/images/60/skype/1f62d.png",
"name": "[face_放声大哭]", "name": "[face_放声大哭]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f631.png", "url": "https://www.emojiall.com/images/60/skype/1f631.png",
"name": "[face_吓死了]", "name": "[face_吓死了]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f616.png", "url": "https://www.emojiall.com/images/60/skype/1f616.png",
"name": "[face_困惑]", "name": "[face_困惑]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f61e.png", "url": "https://www.emojiall.com/images/60/skype/1f61e.png",
"name": "[face_失望]", "name": "[face_失望]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f613.png", "url": "https://www.emojiall.com/images/60/skype/1f613.png",
"name": "[face_汗]", "name": "[face_汗]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f629.png", "url": "https://www.emojiall.com/images/60/skype/1f629.png",
"name": "[face_累死了]", "name": "[face_累死了]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f62b.png", "url": "https://www.emojiall.com/images/60/skype/1f62b.png",
"name": "[face_累]", "name": "[face_累]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f971.png", "url": "https://www.emojiall.com/images/60/skype/1f971.png",
"name": "[face_打呵欠]", "name": "[face_打呵欠]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f624.png", "url": "https://www.emojiall.com/images/60/skype/1f624.png",
"name": "[face_傲慢]", "name": "[face_傲慢]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f621.png", "url": "https://www.emojiall.com/images/60/skype/1f621.png",
"name": "[face_怒火中烧]", "name": "[face_怒火中烧]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f620.png", "url": "https://www.emojiall.com/images/60/skype/1f620.png",
"name": "[face_生气]", "name": "[face_生气]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f92c.png", "url": "https://www.emojiall.com/images/60/skype/1f92c.png",
"name": "[face_嘴上有符号的脸]", "name": "[face_嘴上有符号的脸]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f608.png", "url": "https://www.emojiall.com/images/60/skype/1f608.png",
"name": "[face_恶魔微笑]", "name": "[face_恶魔微笑]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f47f.png", "url": "https://www.emojiall.com/images/60/skype/1f47f.png",
"name": "[face_生气的恶魔]", "name": "[face_生气的恶魔]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f480.png", "url": "https://www.emojiall.com/images/60/skype/1f480.png",
"name": "[face_头骨]", "name": "[face_头骨]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/2620-fe0f.png", "url": "https://www.emojiall.com/images/60/skype/2620-fe0f.png",
"name": "[face_骷髅]", "name": "[face_骷髅]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f4a9.png", "url": "https://www.emojiall.com/images/60/skype/1f4a9.png",
"name": "[face_大便]", "name": "[face_大便]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f921.png", "url": "https://www.emojiall.com/images/60/skype/1f921.png",
"name": "[face_小丑脸]", "name": "[face_小丑脸]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f479.png", "url": "https://www.emojiall.com/images/60/skype/1f479.png",
"name": "[face_食人魔]", "name": "[face_食人魔]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f47a.png", "url": "https://www.emojiall.com/images/60/skype/1f47a.png",
"name": "[face_小妖精]", "name": "[face_小妖精]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f47b.png", "url": "https://www.emojiall.com/images/60/skype/1f47b.png",
"name": "[face_鬼]", "name": "[face_鬼]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f47d.png", "url": "https://www.emojiall.com/images/60/skype/1f47d.png",
"name": "[face_外星人]", "name": "[face_外星人]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f47e.png", "url": "https://www.emojiall.com/images/60/skype/1f47e.png",
"name": "[face_外星怪物]", "name": "[face_外星怪物]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f916.png", "url": "https://www.emojiall.com/images/60/skype/1f916.png",
"name": "[face_机器人]", "name": "[face_机器人]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f63a.png", "url": "https://www.emojiall.com/images/60/skype/1f63a.png",
"name": "[face_大笑的猫]", "name": "[face_大笑的猫]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f639.png", "url": "https://www.emojiall.com/images/60/skype/1f639.png",
"name": "[face_笑出眼泪的猫]", "name": "[face_笑出眼泪的猫]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f63b.png", "url": "https://www.emojiall.com/images/60/skype/1f63b.png",
"name": "[face_花痴的猫]", "name": "[face_花痴的猫]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f63c.png", "url": "https://www.emojiall.com/images/60/skype/1f63c.png",
"name": "[face_奸笑的猫]", "name": "[face_奸笑的猫]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f63d.png", "url": "https://www.emojiall.com/images/60/skype/1f63d.png",
"name": "[face_亲亲猫]", "name": "[face_亲亲猫]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f640.png", "url": "https://www.emojiall.com/images/60/skype/1f640.png",
"name": "[face_疲倦的猫]", "name": "[face_疲倦的猫]",
"group": "face", "group": "face"
"className": "emoji_small"
}, },
{ {
"url": "https://www.emojiall.com/images/60/skype/1f63f.png", "url": "https://www.emojiall.com/images/60/skype/1f63f.png",
"name": "[face_哭泣的猫]", "name": "[face_哭泣的猫]",
"group": "face", "group": "face"
"className": "emoji_small"
} }
] ]
import emojis from './face.json' import emojis from './face.json'
import yayas from './yaya.json'
type EmojiAsset = {
name: string
url: string
group: string
}
const emojiList: EmojiAsset[] = [...emojis, ...yayas]
// function escapeHTML(str: string) { // function escapeHTML(str: string) {
// return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;') // return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
...@@ -10,18 +19,19 @@ export const parseEmoji = (content: string) => { ...@@ -10,18 +19,19 @@ export const parseEmoji = (content: string) => {
// let html = escapeHTML(content) // let html = escapeHTML(content)
let html = content let html = content
emojis.forEach((item) => { emojiList.forEach((item) => {
const escapedName = item.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') const escapedName = item.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
const reg = new RegExp(escapedName, 'g') const reg = new RegExp(escapedName, 'g')
console.log(item.group)
html = html.replace( html = html.replace(
reg, reg,
`<img `<img
src="${item.url}" src="${item.url}"
alt="${item.name}" alt="${item.name}"
class="${item.className || 'emoji_small'} inline-block align-text-bottom w-6 h-6" class="${item.group === 'yaya' ? 'w-8 h-8' : 'w-6 h-6'}"
/>`, />`,
) )
}) })
......
[
{
"name": "[YAYA_OK]",
"url": "https://soundasia.oss-cn-shenzhen.aliyuncs.com/OA/png/2026/03/30/Common/1774842153149.png",
"group": "yaya"
},
{
"name": "[YAYA_OK1]",
"url": "https://soundasia.oss-cn-shenzhen.aliyuncs.com/OA/png/2026/03/30/Common/1774842183051.png",
"group": "yaya"
},
{
"name": "[YAYA_OKK]",
"url": "https://soundasia.oss-cn-shenzhen.aliyuncs.com/OA/png/2026/03/30/Common/1774842204358.png",
"group": "yaya"
},
{
"name": "[YAYA_拜托]",
"url": "https://soundasia.oss-cn-shenzhen.aliyuncs.com/OA/png/2026/03/30/Common/1774842337245.png",
"group": "yaya"
},
{
"name": "[YAYA_点赞]",
"url": "https://soundasia.oss-cn-shenzhen.aliyuncs.com/OA/png/2026/03/30/Common/1774842370496.png",
"group": "yaya"
},
{
"name": "[YAYA_非常感谢]",
"url": "https://soundasia.oss-cn-shenzhen.aliyuncs.com/OA/png/2026/03/30/Common/1774842403468.png",
"group": "yaya"
},
{
"name": "[YAYA_恭喜]",
"url": "https://soundasia.oss-cn-shenzhen.aliyuncs.com/OA/png/2026/03/30/Common/1774842433720.png",
"group": "yaya"
},
{
"name": "[YAYA_恭喜1]",
"url": "https://soundasia.oss-cn-shenzhen.aliyuncs.com/OA/png/2026/03/30/Common/1774842456906.png",
"group": "yaya"
},
{
"name": "[YAYA_好滴]",
"url": "https://soundasia.oss-cn-shenzhen.aliyuncs.com/OA/png/2026/03/30/Common/1774842482423.png",
"group": "yaya"
},
{
"name": "[YAYA_加油]",
"url": "https://soundasia.oss-cn-shenzhen.aliyuncs.com/OA/png/2026/03/30/Common/1774842512339.png",
"group": "yaya"
},
{
"name": "[YAYA_焦急]",
"url": "https://soundasia.oss-cn-shenzhen.aliyuncs.com/OA/png/2026/03/30/Common/1774842540763.png",
"group": "yaya"
},
{
"name": "[YAYA_开心]",
"url": "https://soundasia.oss-cn-shenzhen.aliyuncs.com/OA/png/2026/03/30/Common/1774842564724.png",
"group": "yaya"
},
{
"name": "[YAYA_哭泣]",
"url": "https://soundasia.oss-cn-shenzhen.aliyuncs.com/OA/png/2026/03/30/Common/1774842593525.png",
"group": "yaya"
},
{
"name": "[YAYA_努力]",
"url": "https://soundasia.oss-cn-shenzhen.aliyuncs.com/OA/png/2026/03/30/Common/1774842629597.png",
"group": "yaya"
},
{
"name": "[YAYA_谢谢]",
"url": "https://soundasia.oss-cn-shenzhen.aliyuncs.com/OA/png/2026/03/30/Common/1774842649971.png",
"group": "yaya"
},
{
"name": "[YAYA_亚声人]",
"url": "https://soundasia.oss-cn-shenzhen.aliyuncs.com/OA/png/2026/03/30/Common/1774842693166.png",
"group": "yaya"
}
]
...@@ -84,9 +84,11 @@ ...@@ -84,9 +84,11 @@
<span class="text-xs text-gray-400 hidden sm:inline">{{ <span class="text-xs text-gray-400 hidden sm:inline">{{
dayjs(item.createTime * 1000).format('YYYY-MM-DD HH:mm') dayjs(item.createTime * 1000).format('YYYY-MM-DD HH:mm')
}}</span> }}</span>
<div class="hidden sm:block w-1 h-1 bg-gray-300 rounded-full mx--1"></div> <template v-if="item.region">
<!-- 地区 --> <div class="hidden sm:block w-1 h-1 bg-gray-300 rounded-full mx--1"></div>
<span class="text-xs text-gray-400 hidden sm:inline">{{ item.region }}</span> <!-- 地区 -->
<span class="text-xs text-gray-400 hidden sm:inline">{{ item.region }}</span>
</template>
<div class="hidden sm:block w-1 h-1 bg-gray-300 rounded-full mx--1"></div> <div class="hidden sm:block w-1 h-1 bg-gray-300 rounded-full mx--1"></div>
<!-- 阅读量 --> <!-- 阅读量 -->
<span class="text-xs text-gray-400 hidden sm:inline">{{ item.viewCount }}阅读</span> <span class="text-xs text-gray-400 hidden sm:inline">{{ item.viewCount }}阅读</span>
......
...@@ -320,6 +320,18 @@ ...@@ -320,6 +320,18 @@
/> />
</el-form-item> </el-form-item>
</div> </div>
<!-- 针对官方账号新增是否显示ip -->
<template v-if="userInfo.isOfficialAccount">
<el-form-item label="IP信息" prop="regionHide">
<el-switch
v-model="form.regionHide"
:active-value="BooleanFlag.YES"
:inactive-value="BooleanFlag.NO"
active-text="隐藏地区"
inactive-text="显示地区"
/>
</el-form-item>
</template>
</el-form> </el-form>
<!-- 抽屉底部按钮 --> <!-- 抽屉底部按钮 -->
...@@ -412,7 +424,7 @@ const [form, resetForm] = useResetData({ ...@@ -412,7 +424,7 @@ const [form, resetForm] = useResetData({
sendType: SendTypeEnum.IMMEDIATE, sendType: SendTypeEnum.IMMEDIATE,
sendTime: '', sendTime: '',
releaseStatus: ReleaseStatusTypeEnum.PUBLISH, releaseStatus: ReleaseStatusTypeEnum.PUBLISH,
regionHide: BooleanFlag.NO,
isPushAll: BooleanFlag.YES, isPushAll: BooleanFlag.YES,
// 推送设置 // 推送设置
pushType: SendTypeEnum.IMMEDIATE, pushType: SendTypeEnum.IMMEDIATE,
...@@ -455,7 +467,7 @@ const rules: Record<string, FormItemRule[]> = { ...@@ -455,7 +467,7 @@ const rules: Record<string, FormItemRule[]> = {
isRecommend: [{ required: true, message: '是否推荐', trigger: 'trigger' }], isRecommend: [{ required: true, message: '是否推荐', trigger: 'trigger' }],
isRelateColleague: [{ required: true, message: '是否同步同事吧', trigger: 'trigger' }], isRelateColleague: [{ required: true, message: '是否同步同事吧', trigger: 'trigger' }],
relateColumnId: [{ required: true, message: '请选择对应的栏目', trigger: 'trigger' }], relateColumnId: [{ required: true, message: '请选择对应的栏目', trigger: 'trigger' }],
regionHide: [{ required: true, message: '请选择是否隐藏IP信息', trigger: 'trigger' }],
// 推送设置 // 推送设置
pushType: [{ required: true, message: '请选择推送类型', trigger: 'trigger' }], pushType: [{ required: true, message: '请选择推送类型', trigger: 'trigger' }],
pushTime: [{ required: true, message: '请选择推送时间', trigger: 'trigger' }], pushTime: [{ required: true, message: '请选择推送时间', trigger: 'trigger' }],
...@@ -614,6 +626,7 @@ onActivated(async () => { ...@@ -614,6 +626,7 @@ onActivated(async () => {
sendTime, sendTime,
tagIdList, tagIdList,
regionHide,
} = data } = data
form.value = { form.value = {
...@@ -626,6 +639,7 @@ onActivated(async () => { ...@@ -626,6 +639,7 @@ onActivated(async () => {
sendType, sendType,
sendTime, sendTime,
id, id,
regionHide,
} }
// 2回显主副标签 // 2回显主副标签
......
...@@ -136,6 +136,23 @@ ...@@ -136,6 +136,23 @@
<!-- 新增推送设置 --> <!-- 新增推送设置 -->
<template v-if="userInfo.isOfficialAccount"> <template v-if="userInfo.isOfficialAccount">
<!-- 是否隐藏地区 -->
<div class="mb-8">
<el-form-item prop="regionHide">
<label class="block text-sm font-semibold text-gray-700 mb-3">
是否隐藏地区
</label>
<div class="w-full flex items-center gap-2">
<el-switch
v-model="form.regionHide"
:active-value="BooleanFlag.YES"
:inactive-value="BooleanFlag.NO"
active-text="隐藏地区"
inactive-text="显示地区"
/>
</div>
</el-form-item>
</div>
<div class="mb-8"> <div class="mb-8">
<el-form-item> <el-form-item>
<label class="block text-sm font-semibold text-gray-700 mb-3"> 推送对象 </label> <label class="block text-sm font-semibold text-gray-700 mb-3"> 推送对象 </label>
...@@ -292,11 +309,12 @@ ...@@ -292,11 +309,12 @@
width="800px" width="800px"
:close-on-click-modal="false" :close-on-click-modal="false"
> >
<!-- 这个用blob是因为 转为canvas 防止跨域污染 --> <!-- 转为canvas 防止跨域污染 crossorigin="anonymous" -->
<div v-if="locationVideoBlolUrl" class="space-y-6"> <div v-if="locationVideoBlolUrl" class="space-y-6">
<!-- 视频预览 --> <!-- 视频预览 -->
<div class="relative"> <div class="relative">
<video <video
crossorigin="anonymous"
ref="videoRef" ref="videoRef"
:src="locationVideoBlolUrl" :src="locationVideoBlolUrl"
class="w-full max-h-96 rounded-lg bg-black" class="w-full max-h-96 rounded-lg bg-black"
...@@ -372,7 +390,7 @@ import SelectTags from '@/components/common/SelectTags/index.vue' ...@@ -372,7 +390,7 @@ import SelectTags from '@/components/common/SelectTags/index.vue'
import type { TagItemDto, AddOrUpdateVideoDto } from '@/api' import type { TagItemDto, AddOrUpdateVideoDto } from '@/api'
import { useVideoStore, useUserStore } from '@/stores' import { useVideoStore, useUserStore } from '@/stores'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { selectDepOrUser, fetchOssBlob } from '@/utils' import { selectDepOrUser } from '@/utils'
import BackButton from '@/components/common/BackButton/index.vue' import BackButton from '@/components/common/BackButton/index.vue'
import { push } from 'notivue' import { push } from 'notivue'
...@@ -404,7 +422,7 @@ const [form, resetData] = useResetData({ ...@@ -404,7 +422,7 @@ const [form, resetData] = useResetData({
faceUrl: '', // 封面URL faceUrl: '', // 封面URL
videoDuration: '', videoDuration: '',
relateColumnId: '' as string | number, relateColumnId: '' as string | number,
regionHide: BooleanFlag.NO,
isPushAll: BooleanFlag.YES, isPushAll: BooleanFlag.YES,
// 推送设置 // 推送设置
pushType: SendTypeEnum.IMMEDIATE, pushType: SendTypeEnum.IMMEDIATE,
...@@ -542,7 +560,7 @@ const rules = { ...@@ -542,7 +560,7 @@ const rules = {
mainTagId: [{ required: true, message: '请选择主标签', trigger: 'change' }], mainTagId: [{ required: true, message: '请选择主标签', trigger: 'change' }],
faceUrl: [{ required: true, message: '请选择视频封面', trigger: 'change' }], faceUrl: [{ required: true, message: '请选择视频封面', trigger: 'change' }],
relateColumnId: [{ required: true, message: '请选择视频栏目', trigger: 'change' }], relateColumnId: [{ required: true, message: '请选择视频栏目', trigger: 'change' }],
regionHide: [{ required: true, message: '请选择是否隐藏地区', trigger: 'trigger' }],
// 推送设置 // 推送设置
pushType: [{ required: true, message: '请选择推送类型', trigger: 'trigger' }], pushType: [{ required: true, message: '请选择推送类型', trigger: 'trigger' }],
pushTime: [{ required: true, message: '请选择推送时间', trigger: 'trigger' }], pushTime: [{ required: true, message: '请选择推送时间', trigger: 'trigger' }],
...@@ -610,19 +628,13 @@ const handleSubmit = async (releaseStatus: ReleaseStatusTypeEnum) => { ...@@ -610,19 +628,13 @@ const handleSubmit = async (releaseStatus: ReleaseStatusTypeEnum) => {
} }
const handleVideoChange = ({ const handleVideoChange = ({
file,
videoDuration, videoDuration,
}: { }: {
file: File file: File
url: string url: string
videoDuration: string videoDuration: string
}) => { }) => {
if (locationVideoBlolUrl.value) { locationVideoBlolUrl.value = form.value.videoUrl
URL.revokeObjectURL(locationVideoBlolUrl.value)
}
if (file) {
locationVideoBlolUrl.value = URL.createObjectURL(file)
}
form.value.videoDuration = videoDuration form.value.videoDuration = videoDuration
} }
...@@ -645,8 +657,9 @@ onDeactivated(() => { ...@@ -645,8 +657,9 @@ onDeactivated(() => {
onActivated(async () => { onActivated(async () => {
if (route.query.id) { if (route.query.id) {
const { data } = await getArticleDetail(route.query.id as string) const { data } = await getArticleDetail(route.query.id as string)
const blobUrl = await fetchOssBlob(data.videoUrl)
locationVideoBlolUrl.value = blobUrl locationVideoBlolUrl.value = data.videoUrl
form.value = { form.value = {
...form.value, ...form.value,
...data, ...data,
......
...@@ -123,8 +123,8 @@ ...@@ -123,8 +123,8 @@
<span class="text-sm text-slate-500 flex items-center gap-1"> <span class="text-sm text-slate-500 flex items-center gap-1">
{{ dayjs(questionDetail.createTime * 1000).format('YYYY-MM-DD HH:mm') }} · {{ dayjs(questionDetail.createTime * 1000).format('YYYY-MM-DD HH:mm') }} ·
<span <span
v-if="questionDetail?.region"
class="text-sm text-slate-500 flex items-center gap-1" class="text-sm text-slate-500 flex items-center gap-1"
v-if="!questionDetail?.isOfficialAccount"
> >
{{ questionDetail.region }} {{ questionDetail.region }}
· ·
...@@ -360,8 +360,8 @@ ...@@ -360,8 +360,8 @@
</div> </div>
<div class="text-[14px] text-slate-500 mb-3 flex items-center gap-3"> <div class="text-[14px] text-slate-500 mb-3 flex items-center gap-3">
发布于 {{ dayjs(answer.createTime * 1000).format('YYYY-MM-DD HH:mm') }} · 发布于 {{ dayjs(answer.createTime * 1000).format('YYYY-MM-DD HH:mm') }}
{{ answer.region }} <span v-if="answer.region">·{{ answer.region }}</span>
</div> </div>
<!-- 底 部吸附操作栏 --> <!-- 底 部吸附操作栏 -->
......
...@@ -82,7 +82,7 @@ ...@@ -82,7 +82,7 @@
<span class="flex items-center"> <span class="flex items-center">
{{ formatNumber(videoDetail?.playCount) }}播放 {{ formatNumber(videoDetail?.playCount) }}播放
</span> </span>
<span v-if="!videoDetail?.isOfficialAccount" class="flex items-center"> <span v-if="videoDetail?.region" class="flex items-center">
·{{ videoDetail?.region }} ·{{ videoDetail?.region }}
</span> </span>
</div> </div>
...@@ -96,6 +96,7 @@ ...@@ -96,6 +96,7 @@
:src="videoDetail?.videoUrl" :src="videoDetail?.videoUrl"
class="aspect-video w-100%" class="aspect-video w-100%"
controls controls
controlsList="nodownload"
@play="handlePlay" @play="handlePlay"
@pause="handlePause" @pause="handlePause"
@timeupdate="handleTimeUpdate" @timeupdate="handleTimeUpdate"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment