mirror of
https://github.com/Megghy/vtsuru.live.git
synced 2025-12-06 18:36:55 +08:00
particularly complete forum function, add point order export and user delete
This commit is contained in:
@@ -17,6 +17,8 @@
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"@vueuse/core": "^10.7.2",
|
||||
"@vueuse/router": "^10.7.2",
|
||||
"@wangeditor/editor": "^5.1.23",
|
||||
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||
"date-fns": "^3.3.1",
|
||||
"easy-speech": "^2.3.1",
|
||||
"echarts": "^5.5.0",
|
||||
|
||||
13
src/Utils.ts
13
src/Utils.ts
@@ -1,6 +1,8 @@
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import { UploadFileInfo, createDiscreteApi, useOsTheme } from 'naive-ui'
|
||||
import { ThemeType } from './api/api-models'
|
||||
import { computed } from 'vue'
|
||||
import { VTSURU_API_URL } from './data/constants'
|
||||
|
||||
const { message } = createDiscreteApi(['message'])
|
||||
|
||||
@@ -9,10 +11,10 @@ export function NavigateToNewTab(url: string) {
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
const themeType = useStorage('Settings.Theme', ThemeType.Auto)
|
||||
export function isDarkMode(): boolean {
|
||||
export const isDarkMode = computed(() => {
|
||||
if (themeType.value == ThemeType.Auto) return osThemeRef.value === 'dark'
|
||||
else return themeType.value == ThemeType.Dark
|
||||
}
|
||||
})
|
||||
export function copyToClipboard(text: string) {
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(text)
|
||||
@@ -106,6 +108,13 @@ export async function getImageUploadModel(
|
||||
}
|
||||
return result
|
||||
}
|
||||
export function getUserAvatarUrl(userId: number) {
|
||||
return VTSURU_API_URL + 'user-face/' + userId
|
||||
}
|
||||
export function getOUIdAvatarUrl(ouid: string) {
|
||||
return VTSURU_API_URL + 'face/' + ouid
|
||||
}
|
||||
|
||||
export class GuidUtils {
|
||||
// 将数字转换为GUID
|
||||
public static numToGuid(value: number): string {
|
||||
|
||||
@@ -9,7 +9,6 @@ import { useRoute } from 'vue-router'
|
||||
|
||||
export const ACCOUNT = ref<AccountInfo>({} as AccountInfo)
|
||||
export const isLoadingAccount = ref(true)
|
||||
const route = useRoute()
|
||||
|
||||
const { message } = createDiscreteApi(['message'])
|
||||
const cookie = useLocalStorage('JWT_Token', '')
|
||||
@@ -42,6 +41,7 @@ export async function GetSelfAccount() {
|
||||
}
|
||||
export function UpdateAccountLoop() {
|
||||
setInterval(() => {
|
||||
const route = useRoute()
|
||||
if (ACCOUNT.value && route?.name != 'question-display') {
|
||||
// 防止在问题详情页刷新
|
||||
GetSelfAccount()
|
||||
@@ -67,18 +67,14 @@ export async function UpdateFunctionEnable(func: FunctionTypes) {
|
||||
if (ACCOUNT.value) {
|
||||
const oldValue = JSON.parse(JSON.stringify(ACCOUNT.value.settings.enableFunctions))
|
||||
if (ACCOUNT.value?.settings.enableFunctions.includes(func)) {
|
||||
ACCOUNT.value.settings.enableFunctions = ACCOUNT.value.settings.enableFunctions.filter(
|
||||
(f) => f != func,
|
||||
)
|
||||
ACCOUNT.value.settings.enableFunctions = ACCOUNT.value.settings.enableFunctions.filter((f) => f != func)
|
||||
} else {
|
||||
ACCOUNT.value.settings.enableFunctions.push(func)
|
||||
}
|
||||
await SaveEnableFunctions(ACCOUNT.value?.settings.enableFunctions)
|
||||
.then((data) => {
|
||||
if (data.code == 200) {
|
||||
message.success(
|
||||
`已${ACCOUNT.value?.settings.enableFunctions.includes(func) ? '启用' : '禁用'}`,
|
||||
)
|
||||
message.success(`已${ACCOUNT.value?.settings.enableFunctions.includes(func) ? '启用' : '禁用'}`)
|
||||
} else {
|
||||
if (ACCOUNT.value) {
|
||||
ACCOUNT.value.settings.enableFunctions = oldValue
|
||||
@@ -89,9 +85,7 @@ export async function UpdateFunctionEnable(func: FunctionTypes) {
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
message.error(
|
||||
`${ACCOUNT.value?.settings.enableFunctions.includes(func) ? '启用' : '禁用'}失败: ${err}`,
|
||||
)
|
||||
message.error(`${ACCOUNT.value?.settings.enableFunctions.includes(func) ? '启用' : '禁用'}失败: ${err}`)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,11 @@ export interface APIRoot<T> {
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
export interface PaginationResponse<T> {
|
||||
export interface PaginationResponse<T> extends APIRoot<T> {
|
||||
total: number
|
||||
index: number
|
||||
size: number
|
||||
hasMore: boolean
|
||||
datas: T
|
||||
pn: number
|
||||
ps: number
|
||||
more: boolean
|
||||
}
|
||||
export enum IndexTypes {
|
||||
Default,
|
||||
@@ -661,7 +660,9 @@ export interface ResponsePointOrder2OwnerModel {
|
||||
customer: BiliAuthModel
|
||||
address?: AddressInfo
|
||||
goodsId: number
|
||||
count: number
|
||||
createAt: number
|
||||
updateAt: number
|
||||
status: PointOrderStatus
|
||||
|
||||
trackingNumber?: string
|
||||
@@ -692,6 +693,7 @@ export interface ResponsePointHisrotyModel {
|
||||
type: EventDataTypes
|
||||
from: PointFrom
|
||||
createAt: number
|
||||
count: number
|
||||
|
||||
extra?: any
|
||||
}
|
||||
|
||||
122
src/api/models/forum.ts
Normal file
122
src/api/models/forum.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { UserBasicInfo } from '../api-models'
|
||||
|
||||
export enum ForumTopicSortTypes {
|
||||
Time,
|
||||
Comment,
|
||||
Like,
|
||||
}
|
||||
export enum ForumCommentSortTypes {
|
||||
Time,
|
||||
Like,
|
||||
}
|
||||
export enum ForumUserLevels {
|
||||
Guest,
|
||||
User,
|
||||
Member,
|
||||
AuthedMember,
|
||||
Admin,
|
||||
}
|
||||
|
||||
export interface ForumSetting {
|
||||
allowedViewerLevel: ForumUserLevels // Assuming the default value is handled elsewhere
|
||||
allowPost: boolean // Assuming the default value is handled elsewhere
|
||||
allowedPostLevel: ForumUserLevels // Assuming the default value is handled elsewhere
|
||||
requireApply: boolean // Assuming the default value is handled elsewhere
|
||||
requireAuthedToJoin: boolean // Assuming the default value is handled elsewhere
|
||||
sendTopicDelay: number // Assuming the default value is handled elsewhere
|
||||
}
|
||||
export interface ForumUserModel extends UserBasicInfo {
|
||||
isAdmin: boolean
|
||||
}
|
||||
export type ForumModel = {
|
||||
id: number
|
||||
name: string
|
||||
owner: ForumUserModel
|
||||
description: string
|
||||
topicCount: number
|
||||
|
||||
settings: ForumSetting
|
||||
|
||||
admins: ForumUserModel[]
|
||||
members: ForumUserModel[]
|
||||
applying: ForumUserModel[]
|
||||
blackList: ForumUserModel[]
|
||||
|
||||
level: ForumUserLevels
|
||||
isApplied: boolean
|
||||
|
||||
sections: ForumSectionModel[]
|
||||
createAt: number
|
||||
|
||||
isAdmin: boolean
|
||||
}
|
||||
export type ForumSectionModel = {
|
||||
id: number
|
||||
name: string
|
||||
description: string
|
||||
createAt: number
|
||||
}
|
||||
export enum ForumTopicTypes {
|
||||
Default,
|
||||
Vote,
|
||||
}
|
||||
export type ForumTopicSetting = {
|
||||
canReply?: boolean
|
||||
}
|
||||
export interface ForumTopicBaseModel {
|
||||
id: number // Primary and identity fields in C# typically correspond to required fields in TS
|
||||
user: UserBasicInfo
|
||||
section: ForumSectionModel
|
||||
title: string
|
||||
content: string
|
||||
|
||||
latestRepliedBy?: UserBasicInfo
|
||||
repliedAt?: number
|
||||
|
||||
likeCount: number // Assuming the default value is handled elsewhere
|
||||
commentCount: number // Assuming the default value is handled elsewhere
|
||||
viewCount: number // Assuming the default value is handled elsewhere
|
||||
sampleLikedBy: number[]
|
||||
|
||||
createAt: Date // DateTime in C# translates to Date in TS
|
||||
editAt?: Date | null // Nullable DateTime in C# is optional or null in TS
|
||||
|
||||
isLiked: boolean
|
||||
isLocked?: boolean // Assuming the default value is handled elsewhere
|
||||
isPinned?: boolean // Assuming the default value is handled elsewhere
|
||||
isHighlighted?: boolean // Assuming the default value is handled elsewhere
|
||||
}
|
||||
export interface ForumTopicModel extends ForumTopicBaseModel {
|
||||
isLocked?: boolean // Assuming the default value is handled elsewhere
|
||||
isDeleted?: boolean // Assuming the default value is handled elsewhere
|
||||
|
||||
isHidden?: boolean // Assuming the default value is handled elsewhere
|
||||
|
||||
type?: ForumTopicTypes // Assuming the default value is handled elsewhere
|
||||
extraTypeId?: number | null // Nullable int in C# is optional or null in TS
|
||||
likedBy?: number[] // Assuming the default value is handled elsewhere
|
||||
}
|
||||
export interface ForumCommentModel {
|
||||
id: number
|
||||
user: UserBasicInfo
|
||||
content: string
|
||||
replies: ForumReplyModel[]
|
||||
sendAt: Date
|
||||
|
||||
likeCount: number
|
||||
isLiked: boolean
|
||||
}
|
||||
export interface ForumReplyModel {
|
||||
id: number
|
||||
user: UserBasicInfo
|
||||
content: string
|
||||
replyTo?: number
|
||||
sendAt: Date
|
||||
}
|
||||
export interface ForumPostTopicModel {
|
||||
section?: number
|
||||
title: string
|
||||
content: string
|
||||
owner: number
|
||||
type?: ForumTopicTypes
|
||||
}
|
||||
@@ -19,23 +19,34 @@ export async function QueryPostAPIWithParams<T>(
|
||||
contentType?: string,
|
||||
headers?: [string, string][],
|
||||
): Promise<APIRoot<T>> {
|
||||
return await QueryPostAPIWithParamsInternal<APIRoot<T>>(urlString, params, body, contentType, headers)
|
||||
}
|
||||
async function QueryPostAPIWithParamsInternal<T>(
|
||||
urlString: string,
|
||||
params?: any,
|
||||
body?: any,
|
||||
contentType: string = 'application/json',
|
||||
headers: [string, string][] = [],
|
||||
) {
|
||||
const url = new URL(urlString)
|
||||
url.search = getParams(params)
|
||||
headers ??= []
|
||||
if (cookie.value) headers?.push(['Authorization', `Bearer ${cookie.value}`])
|
||||
|
||||
if (contentType) headers?.push(['Content-Type', contentType])
|
||||
|
||||
try {
|
||||
const data = await fetch(url, {
|
||||
return await QueryAPIInternal<T>(url, {
|
||||
method: 'post',
|
||||
headers: headers,
|
||||
body: typeof body === 'string' ? body : JSON.stringify(body),
|
||||
})
|
||||
const result = (await data.json()) as APIRoot<T>
|
||||
}
|
||||
async function QueryAPIInternal<T>(url: URL, init: RequestInit) {
|
||||
try {
|
||||
const data = await fetch(url, init)
|
||||
const result = (await data.json()) as T
|
||||
return result
|
||||
} catch (e) {
|
||||
console.error(`[POST] API调用失败: ${e}`)
|
||||
console.error(`[${init.method}] API调用失败: ${e}`)
|
||||
if (!apiFail.value) {
|
||||
apiFail.value = true
|
||||
console.log('默认API异常, 切换至故障转移节点')
|
||||
@@ -48,30 +59,34 @@ export async function QueryGetAPI<T>(
|
||||
params?: any,
|
||||
headers?: [string, string][],
|
||||
): Promise<APIRoot<T>> {
|
||||
return await QueryGetAPIInternal<APIRoot<T>>(urlString, params, headers)
|
||||
}
|
||||
async function QueryGetAPIInternal<T>(urlString: string, params?: any, headers?: [string, string][]) {
|
||||
const url = new URL(urlString)
|
||||
url.search = getParams(params)
|
||||
if (cookie.value) {
|
||||
headers ??= []
|
||||
if (cookie.value) headers?.push(['Authorization', `Bearer ${cookie.value}`])
|
||||
}
|
||||
try {
|
||||
const data = await fetch(url.toString(), {
|
||||
return await QueryAPIInternal<T>(url, {
|
||||
method: 'get',
|
||||
headers: headers,
|
||||
})
|
||||
const result = (await data.json()) as APIRoot<T>
|
||||
return result
|
||||
} catch (e) {
|
||||
console.error(`[GET] API调用失败: ${e}`)
|
||||
if (!apiFail.value) {
|
||||
apiFail.value = true
|
||||
console.log('默认API异常, 切换至故障转移节点')
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
function getParams(params?: [string, string][]) {
|
||||
function getParams(params: any) {
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
|
||||
if (params) {
|
||||
const keys = Object.keys(params)
|
||||
if (keys.length > 0) {
|
||||
keys.forEach((k) => {
|
||||
if (params[k] == undefined) {
|
||||
delete params[k]
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const resultParams = new URLSearchParams(params)
|
||||
if (urlParams.has('as')) {
|
||||
resultParams.set('as', urlParams.get('as') || '')
|
||||
@@ -81,12 +96,12 @@ function getParams(params?: [string, string][]) {
|
||||
}
|
||||
return resultParams.toString()
|
||||
}
|
||||
export async function QueryPostPaginationAPI<T>(url: string, body?: unknown): Promise<APIRoot<PaginationResponse<T>>> {
|
||||
return await QueryPostAPI<PaginationResponse<T>>(url, body)
|
||||
export async function QueryPostPaginationAPI<T>(url: string, body?: unknown): Promise<PaginationResponse<T>> {
|
||||
return await QueryPostAPIWithParamsInternal<PaginationResponse<T>>(url, undefined, body)
|
||||
}
|
||||
export async function QueryGetPaginationAPI<T>(
|
||||
urlString: string,
|
||||
params?: unknown,
|
||||
): Promise<APIRoot<PaginationResponse<T>>> {
|
||||
return await QueryGetAPI<PaginationResponse<T>>(urlString, params)
|
||||
export async function QueryGetPaginationAPI<T>(urlString: string, params?: unknown): Promise<PaginationResponse<T>> {
|
||||
return await QueryGetAPIInternal<PaginationResponse<T>>(urlString, params)
|
||||
}
|
||||
export function GetHeaders(): [string, string][] {
|
||||
return [['Authorization', `Bearer ${cookie.value}`]]
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { QueryGetAPI } from '@/api/query'
|
||||
import { BASE_API, USER_API_URL, apiFail } from '@/data/constants'
|
||||
import { APIRoot, UserInfo } from './api-models'
|
||||
import { USER_API_URL, apiFail } from '@/data/constants'
|
||||
import { ref } from 'vue'
|
||||
import { useRouteParams } from '@vueuse/router'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { APIRoot, UserInfo } from './api-models'
|
||||
|
||||
export const USERS = ref<{ [id: string]: UserInfo }>({})
|
||||
|
||||
|
||||
63
src/assets/editorDarkMode.css
Normal file
63
src/assets/editorDarkMode.css
Normal file
@@ -0,0 +1,63 @@
|
||||
|
||||
.dark-theme {
|
||||
--w-e-toolbar-active-bg-color: #2c2c2c;
|
||||
transition: all 1s;
|
||||
}
|
||||
.dark-theme {
|
||||
background-color: #333;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.dark-theme .w-e-text-container {
|
||||
background: #333;
|
||||
color: #ccc;
|
||||
}
|
||||
.dark-theme .w-e-bar-item-menus-container{
|
||||
background: #333;
|
||||
color: #ccc;
|
||||
}
|
||||
.dark-theme .w-e-toolbar {
|
||||
background: #444;
|
||||
border-bottom: 1px solid #555;
|
||||
}
|
||||
|
||||
.dark-theme .w-e-menu {
|
||||
color: #fff;
|
||||
/* 更明亮的文字颜色 */
|
||||
}
|
||||
.dark-theme .w-e-bar-item button {
|
||||
color: #fff!important;
|
||||
/* 更明亮的图标颜色 */
|
||||
}
|
||||
.dark-theme .w-e-bar svg {
|
||||
fill: #fff;
|
||||
}
|
||||
.dark-theme .w-e-icon {
|
||||
color: #fff;
|
||||
/* 更明亮的图标颜色 */
|
||||
}
|
||||
|
||||
.dark-theme .w-e-menu.active {
|
||||
color: #409EFF;
|
||||
/* 菜单选中时的颜色 */
|
||||
}
|
||||
|
||||
.dark-theme .w-e-menu-text {
|
||||
color: #fff;
|
||||
/* 更明亮的菜单文字颜色 */
|
||||
}
|
||||
.dark-theme .w-e-bar{
|
||||
background-color: #ffffff00;
|
||||
}
|
||||
.dark-theme .w-e-droplist.w-e-list {
|
||||
background-color: #444;
|
||||
}
|
||||
|
||||
.dark-theme .w-e-list,
|
||||
.dark-theme .w-e-list li a {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.dark-theme .w-e-text a {
|
||||
color: #5dbeff;
|
||||
}
|
||||
53
src/assets/forumContentStyle.css
Normal file
53
src/assets/forumContentStyle.css
Normal file
@@ -0,0 +1,53 @@
|
||||
.editor-content-view {
|
||||
border-radius: 5px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.editor-content-view p,
|
||||
.editor-content-view li {
|
||||
white-space: pre-wrap;
|
||||
/* 保留空格 */
|
||||
}
|
||||
|
||||
.editor-content-view blockquote {
|
||||
border-left: 8px solid #d0e5f2;
|
||||
padding: 10px 10px;
|
||||
margin: 10px 0;
|
||||
background-color: #f1f1f1;
|
||||
}
|
||||
|
||||
.editor-content-view code {
|
||||
font-family: monospace;
|
||||
background-color: #b6b6b679;
|
||||
padding: 3px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.editor-content-view pre > code {
|
||||
display: block;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.editor-content-view table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.editor-content-view td,
|
||||
.editor-content-view th {
|
||||
border: 1px solid #ccc;
|
||||
min-width: 50px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.editor-content-view th {
|
||||
background-color: #f1f1f1;
|
||||
}
|
||||
|
||||
.editor-content-view ul,
|
||||
.editor-content-view ol {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.editor-content-view input[type='checkbox'] {
|
||||
margin-right: 5px;
|
||||
}
|
||||
@@ -87,7 +87,7 @@ watch(
|
||||
</span>
|
||||
<span v-else>
|
||||
已直播:
|
||||
{{ ((Date.now() - (live.stopAt ?? 0)) / (3600 * 1000)).toFixed(1) }}
|
||||
{{ ((Date.now() - (live.startAt ?? 0)) / (3600 * 1000)).toFixed(1) }}
|
||||
时
|
||||
</span>
|
||||
</NPopover>
|
||||
|
||||
27
src/components/TurnstileVerify.vue
Normal file
27
src/components/TurnstileVerify.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import { TURNSTILE_KEY } from '@/data/constants'
|
||||
import { onUnmounted, ref } from 'vue'
|
||||
import { onMounted } from 'vue'
|
||||
|
||||
import VueTurnstile from 'vue-turnstile'
|
||||
const turnstile = ref()
|
||||
|
||||
const token = defineModel<string>('token', {
|
||||
default: '',
|
||||
})
|
||||
onUnmounted(() => {
|
||||
turnstile.value?.remove()
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
reset,
|
||||
})
|
||||
|
||||
function reset() {
|
||||
turnstile.value?.reset()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VueTurnstile ref="turnstile" :site-key="TURNSTILE_KEY" v-model="token" theme="auto" style="text-align: center" />
|
||||
</template>
|
||||
119
src/components/VEditor.vue
Normal file
119
src/components/VEditor.vue
Normal file
@@ -0,0 +1,119 @@
|
||||
<script setup lang="ts">
|
||||
import { isDarkMode } from '@/Utils'
|
||||
import { APIRoot } from '@/api/api-models'
|
||||
import { GetHeaders } from '@/api/query'
|
||||
import '@/assets/editorDarkMode.css'
|
||||
import { BASE_URL, VTSURU_API_URL } from '@/data/constants'
|
||||
import { DomEditor, IEditorConfig, IToolbarConfig } from '@wangeditor/editor'
|
||||
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
|
||||
import '@wangeditor/editor/dist/css/style.css' // 引入 css
|
||||
import { NotificationReactive, useMessage } from 'naive-ui'
|
||||
import { onBeforeUnmount, ref, shallowRef, onMounted } from 'vue'
|
||||
|
||||
type InsertFnType = (url: string, alt: string, href: string) => void
|
||||
|
||||
const props = defineProps({
|
||||
defaultValue: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'default',
|
||||
},
|
||||
maxLength: {
|
||||
type: Number,
|
||||
default: 10000,
|
||||
},
|
||||
})
|
||||
const message = useMessage()
|
||||
|
||||
const editorRef = shallowRef()
|
||||
const toolbar = DomEditor.getToolbar(editorRef.value)
|
||||
const toolbarConfig: Partial<IToolbarConfig> = {
|
||||
excludeKeys: ['group-video', 'group-lineHeight', 'insertImage', 'fullScreen'],
|
||||
}
|
||||
const uploadProgressRef = ref<NotificationReactive>()
|
||||
const editorConfig: Partial<IEditorConfig> = {
|
||||
placeholder: '请输入内容...',
|
||||
maxLength: props.maxLength,
|
||||
|
||||
MENU_CONF: {
|
||||
uploadImage: {
|
||||
maxFileSize: 10 * 1024 * 1024,
|
||||
maxNumberOfFiles: 10,
|
||||
async customUpload(file: File, insertFn: InsertFnType) {
|
||||
const formData = new FormData() //创建一个FormData实例。
|
||||
message.info('图片上传中')
|
||||
formData.append('file', file)
|
||||
const resp = await fetch(VTSURU_API_URL + 'image/upload', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: GetHeaders(),
|
||||
})
|
||||
if (resp.ok) {
|
||||
const data = (await resp.json()) as APIRoot<string>
|
||||
if (data.code == 200) {
|
||||
insertFn(data.data, '', '')
|
||||
} else {
|
||||
message.error('图片上传失败: ' + data.message)
|
||||
}
|
||||
} else {
|
||||
message.error('图片上传失败: ' + resp.statusText)
|
||||
}
|
||||
},
|
||||
onProgress(progress: number) {
|
||||
console.log(progress)
|
||||
},
|
||||
onSuccess(file: File, res: any) {
|
||||
console.log(`${file.name} 上传成功`, res)
|
||||
message.success('图片上传成功')
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const value = defineModel<string>('value')
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
const editor = editorRef.value
|
||||
if (editor == null) return
|
||||
editor.destroy()
|
||||
})
|
||||
onMounted(() => {
|
||||
//editorRef.value?.setHtml(props.defaultValue)
|
||||
})
|
||||
function handleCreated(editor: unknown) {
|
||||
editorRef.value = editor // 记录 editor 实例,重要!
|
||||
}
|
||||
function getText() {
|
||||
return editorRef.value?.getText()
|
||||
}
|
||||
function getHtml() {
|
||||
return editorRef.value?.getText()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
getText,
|
||||
getHtml,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="{ 'dark-theme': isDarkMode }" style="border: 1px solid #ccc">
|
||||
<Toolbar
|
||||
ref="toolbarRef"
|
||||
style="border-bottom: 1px solid #ccc"
|
||||
:editor="editorRef"
|
||||
:defaultConfig="toolbarConfig"
|
||||
:mode="mode"
|
||||
/>
|
||||
<Editor
|
||||
style="height: 500px; overflow-y: hidden"
|
||||
v-model="value"
|
||||
:defaultConfig="editorConfig"
|
||||
:mode="mode"
|
||||
@onCreated="handleCreated"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -119,7 +119,7 @@ const historyColumn: DataTableColumns<ResponsePointHisrotyModel> = [
|
||||
h(
|
||||
NTag,
|
||||
{ type: 'warning', size: 'tiny', style: { margin: '0' }, bordered: false },
|
||||
() => (row.extra?.danmaku.num ?? 1) + '个',
|
||||
() => (row.count ?? 1) + '个',
|
||||
),
|
||||
])
|
||||
case EventDataTypes.SC:
|
||||
|
||||
@@ -98,6 +98,17 @@ const orderColumn: DataTableColumns<ResponsePointOrder2UserModel | ResponsePoint
|
||||
title: '订单号',
|
||||
key: 'id',
|
||||
},
|
||||
{
|
||||
title: '礼物名',
|
||||
key: 'giftName',
|
||||
render: (row: ResponsePointOrder2UserModel | ResponsePointOrder2OwnerModel) => {
|
||||
return row.instanceOf == 'user' ? row.goods.name : props.goods?.find((g) => g.id == row.goodsId)?.name
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '数量',
|
||||
key: 'count',
|
||||
},
|
||||
{
|
||||
title: '时间',
|
||||
key: 'time',
|
||||
@@ -118,7 +129,10 @@ const orderColumn: DataTableColumns<ResponsePointOrder2UserModel | ResponsePoint
|
||||
{
|
||||
title: '订单状态',
|
||||
key: 'status',
|
||||
filter: (filterOptionValue: unknown, row: ResponsePointOrder2UserModel | ResponsePointOrder2OwnerModel) => {
|
||||
filter:
|
||||
props.type == 'owner'
|
||||
? undefined
|
||||
: (filterOptionValue: unknown, row: ResponsePointOrder2UserModel | ResponsePointOrder2OwnerModel) => {
|
||||
return row.status == filterOptionValue
|
||||
},
|
||||
filterOptions: [
|
||||
@@ -151,7 +165,10 @@ const orderColumn: DataTableColumns<ResponsePointOrder2UserModel | ResponsePoint
|
||||
{
|
||||
title: '订单类型',
|
||||
key: 'type',
|
||||
filter: (filterOptionValue: unknown, row: ResponsePointOrder2UserModel | ResponsePointOrder2OwnerModel) => {
|
||||
filter:
|
||||
props.type == 'owner'
|
||||
? undefined
|
||||
: (filterOptionValue: unknown, row: ResponsePointOrder2UserModel | ResponsePointOrder2OwnerModel) => {
|
||||
return row.type == filterOptionValue
|
||||
},
|
||||
filterOptions: [
|
||||
@@ -360,7 +377,10 @@ onMounted(() => {
|
||||
></iframe>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else-if="orderDetail.instanceOf == 'owner'">
|
||||
<template v-else-if="orderDetail.instanceOf == 'owner'"
|
||||
><NFlex justify="center">
|
||||
<PointGoodsItem style="max-width: 300px" :goods="currentGoods" />
|
||||
</NFlex>
|
||||
<NDivider> 设置订单状态 </NDivider>
|
||||
<NFlex justify="center" style="width: 100%">
|
||||
<NSteps
|
||||
|
||||
@@ -13,9 +13,11 @@ export const IMGUR_URL = FILE_BASE_URL + '/imgur/'
|
||||
export const THINGS_URL = FILE_BASE_URL + '/things/'
|
||||
export const apiFail = ref(false)
|
||||
|
||||
export const BASE_API = {
|
||||
toString: () =>
|
||||
(process.env.NODE_ENV === 'development' ? debugAPI : apiFail.value ? failoverAPI : releseAPI) + 'api/',
|
||||
export const BASE_URL = {
|
||||
toString: () => (process.env.NODE_ENV === 'development' ? debugAPI : apiFail.value ? failoverAPI : releseAPI),
|
||||
}
|
||||
export const BASE_API_URL = {
|
||||
toString: () => BASE_URL + 'api/',
|
||||
}
|
||||
export const FETCH_API = 'https://fetch.vtsuru.live/'
|
||||
export const BASE_HUB_URL = {
|
||||
@@ -25,26 +27,27 @@ export const BASE_HUB_URL = {
|
||||
|
||||
export const TURNSTILE_KEY = '0x4AAAAAAAETUSAKbds019h0'
|
||||
|
||||
export const USER_API_URL = { toString: () => `${BASE_API}user/` }
|
||||
export const ACCOUNT_API_URL = { toString: () => `${BASE_API}account/` }
|
||||
export const BILI_API_URL = { toString: () => `${BASE_API}bili/` }
|
||||
export const SONG_API_URL = { toString: () => `${BASE_API}song-list/` }
|
||||
export const NOTIFACTION_API_URL = { toString: () => `${BASE_API}notifaction/` }
|
||||
export const QUESTION_API_URL = { toString: () => `${BASE_API}qa/` }
|
||||
export const LOTTERY_API_URL = { toString: () => `${BASE_API}lottery/` }
|
||||
export const HISTORY_API_URL = { toString: () => `${BASE_API}history/` }
|
||||
export const SCHEDULE_API_URL = { toString: () => `${BASE_API}schedule/` }
|
||||
export const VIDEO_COLLECT_API_URL = { toString: () => `${BASE_API}video-collect/` }
|
||||
export const OPEN_LIVE_API_URL = { toString: () => `${BASE_API}open-live/` }
|
||||
export const SONG_REQUEST_API_URL = { toString: () => `${BASE_API}live-request/` }
|
||||
export const QUEUE_API_URL = { toString: () => `${BASE_API}queue/` }
|
||||
export const EVENT_API_URL = { toString: () => `${BASE_API}event/` }
|
||||
export const LIVE_API_URL = { toString: () => `${BASE_API}live/` }
|
||||
export const FEEDBACK_API_URL = { toString: () => `${BASE_API}feedback/` }
|
||||
export const MUSIC_REQUEST_API_URL = { toString: () => `${BASE_API}music-request/` }
|
||||
export const VTSURU_API_URL = { toString: () => `${BASE_API}vtsuru/` }
|
||||
export const POINT_API_URL = { toString: () => `${BASE_API}point/` }
|
||||
export const BILI_AUTH_API_URL = { toString: () => `${BASE_API}bili-auth/` }
|
||||
export const USER_API_URL = { toString: () => `${BASE_API_URL}user/` }
|
||||
export const ACCOUNT_API_URL = { toString: () => `${BASE_API_URL}account/` }
|
||||
export const BILI_API_URL = { toString: () => `${BASE_API_URL}bili/` }
|
||||
export const SONG_API_URL = { toString: () => `${BASE_API_URL}song-list/` }
|
||||
export const NOTIFACTION_API_URL = { toString: () => `${BASE_API_URL}notifaction/` }
|
||||
export const QUESTION_API_URL = { toString: () => `${BASE_API_URL}qa/` }
|
||||
export const LOTTERY_API_URL = { toString: () => `${BASE_API_URL}lottery/` }
|
||||
export const HISTORY_API_URL = { toString: () => `${BASE_API_URL}history/` }
|
||||
export const SCHEDULE_API_URL = { toString: () => `${BASE_API_URL}schedule/` }
|
||||
export const VIDEO_COLLECT_API_URL = { toString: () => `${BASE_API_URL}video-collect/` }
|
||||
export const OPEN_LIVE_API_URL = { toString: () => `${BASE_API_URL}open-live/` }
|
||||
export const SONG_REQUEST_API_URL = { toString: () => `${BASE_API_URL}live-request/` }
|
||||
export const QUEUE_API_URL = { toString: () => `${BASE_API_URL}queue/` }
|
||||
export const EVENT_API_URL = { toString: () => `${BASE_API_URL}event/` }
|
||||
export const LIVE_API_URL = { toString: () => `${BASE_API_URL}live/` }
|
||||
export const FEEDBACK_API_URL = { toString: () => `${BASE_API_URL}feedback/` }
|
||||
export const MUSIC_REQUEST_API_URL = { toString: () => `${BASE_API_URL}music-request/` }
|
||||
export const VTSURU_API_URL = { toString: () => `${BASE_API_URL}vtsuru/` }
|
||||
export const POINT_API_URL = { toString: () => `${BASE_API_URL}point/` }
|
||||
export const BILI_AUTH_API_URL = { toString: () => `${BASE_API_URL}bili-auth/` }
|
||||
export const FORUM_API_URL = { toString: () => `${BASE_API_URL}forum/` }
|
||||
|
||||
export const ScheduleTemplateMap = {
|
||||
'': {
|
||||
|
||||
24
src/document/EnableForumAgreement.md
Normal file
24
src/document/EnableForumAgreement.md
Normal file
@@ -0,0 +1,24 @@
|
||||
以下是您需要遵守的规则和条件,只有在您同意并接受所有条款后,您才能获准在本站开通粉丝讨论区功能。
|
||||
|
||||
1. **用户行为规则**
|
||||
|
||||
用户必须過適當和尊重的言語交换观点和想法。禁止在讨论区发布任何色情、恶心、仇恨、歧视、威胁或骚扰的内容。此外,禁止发布任何可能侵犯他人知识产权、隐私权或其他权利的内容。
|
||||
|
||||
2. **内容所有权**
|
||||
|
||||
发布在讨论区中的所有内容,包括文字、图片、视频等,版权均属于原作者。用户需保证拥有发布内容的全部权利或已取得必要许可。
|
||||
|
||||
3. **隐私政策**
|
||||
|
||||
我们维护所有注册用户的隐私,将严格遵守我们的隐私政策。严禁未经许可收集、发布或使用其他用户的个人信息。
|
||||
|
||||
4. **违规处理**
|
||||
|
||||
如果发现用户违反本协议,我们将保留采取包括但不限于删除内容、禁言、封号等一切必要行动的权利。
|
||||
|
||||
5. **免责声明**
|
||||
|
||||
本站对涉及讨论区的任何争议和纠纷不承担任何责任。
|
||||
|
||||
在进行任何活动之前,确保您理解并遵守这些规则。您确定开始使用我们的随附服务,即表示您已经阅读、理解并同意接受本协议的条款和条件。
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { QueryGetAPI } from '@/api/query'
|
||||
import { BASE_API, apiFail } from '@/data/constants'
|
||||
import { BASE_API_URL, apiFail } from '@/data/constants'
|
||||
import EasySpeech from 'easy-speech'
|
||||
import { NButton, NFlex, NText, createDiscreteApi } from 'naive-ui'
|
||||
import { createPinia } from 'pinia'
|
||||
@@ -16,13 +16,11 @@ const pinia = createPinia()
|
||||
const app = createApp(App)
|
||||
app.use(router).use(pinia).mount('#app')
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
let currentVersion: string
|
||||
let isHaveNewVersion = false
|
||||
|
||||
const { notification } = createDiscreteApi(['notification'])
|
||||
QueryGetAPI<string>(BASE_API + 'vtsuru/version')
|
||||
QueryGetAPI<string>(BASE_API_URL + 'vtsuru/version')
|
||||
.then((version) => {
|
||||
if (version.code == 200) {
|
||||
currentVersion = version.data
|
||||
@@ -45,12 +43,13 @@ QueryGetAPI<string>(BASE_API + 'vtsuru/version')
|
||||
if (isHaveNewVersion) {
|
||||
return
|
||||
}
|
||||
QueryGetAPI<string>(BASE_API + 'vtsuru/version').then((keepCheckData) => {
|
||||
QueryGetAPI<string>(BASE_API_URL + 'vtsuru/version').then((keepCheckData) => {
|
||||
if (keepCheckData.code == 200 && keepCheckData.data != currentVersion) {
|
||||
isHaveNewVersion = true
|
||||
currentVersion = version.data
|
||||
localStorage.setItem('Version', currentVersion)
|
||||
|
||||
const route = useRoute()
|
||||
if (!route.path.startsWith('/obs')) {
|
||||
const n = notification.info({
|
||||
title: '发现新的版本更新',
|
||||
|
||||
@@ -166,5 +166,13 @@ export default //管理页面
|
||||
title: '积分',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'forum',
|
||||
name: 'manage-forum',
|
||||
component: () => import('@/views/manage/ForumManage.vue'),
|
||||
meta: {
|
||||
title: '粉丝讨论区',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -64,4 +64,22 @@ export default [
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'forum/topic/:topicId',
|
||||
name: 'user-forum-topic-detail',
|
||||
component: () => import('@/views/view/forumViews/ForumTopicDetail.vue'),
|
||||
meta: {
|
||||
title: '帖子详情',
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'forum',
|
||||
name: 'user-forum',
|
||||
component: () => import('@/views/view/forumViews/ForumView.vue'),
|
||||
meta: {
|
||||
title: '讨论区',
|
||||
keepAlive: true,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
@@ -21,6 +21,7 @@ export const useAuthStore = defineStore('BiliAuth', () => {
|
||||
|
||||
const isLoading = ref(false)
|
||||
const isAuthed = computed(() => currentToken.value != null && currentToken.value.length > 0)
|
||||
const isInvalid = ref(false)
|
||||
|
||||
async function setCurrentAuth(token: string) {
|
||||
if (!token) {
|
||||
@@ -58,9 +59,12 @@ export const useAuthStore = defineStore('BiliAuth', () => {
|
||||
})
|
||||
console.log('添加新的认证账户: ' + biliAuth.value.userId)
|
||||
}
|
||||
isInvalid.value = false
|
||||
return true
|
||||
} else {
|
||||
console.error('[bili-auth] 无法获取 Bilibili 认证信息: ' + data.message)
|
||||
isInvalid.value = true
|
||||
logout()
|
||||
//message.error('无法获取 Bilibili 认证信息: ' + data.message)
|
||||
}
|
||||
})
|
||||
@@ -133,6 +137,7 @@ export const useAuthStore = defineStore('BiliAuth', () => {
|
||||
biliTokens,
|
||||
isLoading,
|
||||
isAuthed,
|
||||
isInvalid,
|
||||
currentToken,
|
||||
getAuthInfo,
|
||||
QueryBiliAuthGetAPI,
|
||||
|
||||
402
src/store/useForumStore.ts
Normal file
402
src/store/useForumStore.ts
Normal file
@@ -0,0 +1,402 @@
|
||||
import {
|
||||
ForumCommentModel,
|
||||
ForumCommentSortTypes,
|
||||
ForumModel,
|
||||
ForumPostTopicModel,
|
||||
ForumReplyModel,
|
||||
ForumTopicBaseModel,
|
||||
ForumTopicModel,
|
||||
ForumTopicSortTypes,
|
||||
} from '@/api/models/forum'
|
||||
import { QueryGetAPI, QueryGetPaginationAPI, QueryPostAPI } from '@/api/query'
|
||||
import { FORUM_API_URL } from '@/data/constants'
|
||||
import { createDiscreteApi } from 'naive-ui'
|
||||
import { MessageApiInjection } from 'naive-ui/es/message/src/MessageProvider'
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export const useForumStore = defineStore('forum', () => {
|
||||
const { message } = createDiscreteApi(['message'])
|
||||
|
||||
const isLoading = ref(false)
|
||||
const isLikeLoading = ref(false)
|
||||
|
||||
const replyingComment = ref<ForumCommentModel>()
|
||||
const replyingReply = ref<ForumReplyModel>()
|
||||
const showReplyModal = ref(false)
|
||||
|
||||
async function GetForumInfo(owner: number) {
|
||||
try {
|
||||
isLoading.value = true
|
||||
const data = await QueryGetAPI<ForumModel>(FORUM_API_URL + 'get-forum', { owner: owner })
|
||||
if (data.code == 200) {
|
||||
return data.data
|
||||
} else if (data.code != 404) {
|
||||
message?.error('无法获取数据: ' + data.message)
|
||||
return undefined
|
||||
}
|
||||
} catch (err) {
|
||||
message?.error('无法获取数据: ' + err)
|
||||
return undefined
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
async function GetManagedForums() {
|
||||
try {
|
||||
isLoading.value = true
|
||||
const data = await QueryGetAPI<ForumModel[]>(FORUM_API_URL + 'get-managed-forums')
|
||||
if (data.code == 200) {
|
||||
return data.data
|
||||
} else {
|
||||
message?.error('无法获取数据: ' + data.message)
|
||||
return undefined
|
||||
}
|
||||
} catch (err) {
|
||||
message?.error('无法获取数据: ' + err)
|
||||
return undefined
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
async function GetTopics(
|
||||
owner: number,
|
||||
pn: number,
|
||||
ps: number,
|
||||
sort: ForumTopicSortTypes,
|
||||
section?: number,
|
||||
message?: MessageApiInjection,
|
||||
) {
|
||||
try {
|
||||
isLoading.value = true
|
||||
const data = await QueryGetPaginationAPI<ForumTopicBaseModel[]>(FORUM_API_URL + 'get-topics', {
|
||||
owner,
|
||||
pageSize: ps,
|
||||
page: pn,
|
||||
sort,
|
||||
section: section,
|
||||
})
|
||||
if (data.code == 200) {
|
||||
return {
|
||||
data: data.data,
|
||||
total: data.total,
|
||||
more: data.more,
|
||||
}
|
||||
} else {
|
||||
message?.error('无法获取数据: ' + data.message)
|
||||
console.error('无法获取数据: ' + data.message)
|
||||
return undefined
|
||||
}
|
||||
} catch (err) {
|
||||
message?.error('无法获取数据: ' + err)
|
||||
console.error('无法获取数据: ' + err)
|
||||
return undefined
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
async function GetTopicDetail(topic: number) {
|
||||
try {
|
||||
isLoading.value = true
|
||||
const data = await QueryGetAPI<ForumTopicModel>(FORUM_API_URL + 'get-topic', { topic: topic })
|
||||
if (data.code == 200) {
|
||||
return data.data
|
||||
} else {
|
||||
message?.error('无法获取数据: ' + data.message)
|
||||
console.error('无法获取数据: ' + data.message)
|
||||
return undefined
|
||||
}
|
||||
} catch (err) {
|
||||
message?.error('无法获取数据: ' + err)
|
||||
console.error('无法获取数据: ' + err)
|
||||
return undefined
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
async function GetComments(topic: number, pn: number, ps: number, sort: ForumCommentSortTypes) {
|
||||
try {
|
||||
isLoading.value = true
|
||||
const data = await QueryGetPaginationAPI<ForumCommentModel[]>(FORUM_API_URL + 'get-comments', {
|
||||
topic,
|
||||
pageSize: ps,
|
||||
page: pn,
|
||||
sort,
|
||||
})
|
||||
if (data.code == 200) {
|
||||
return data.data
|
||||
} else {
|
||||
console.error('无法获取数据: ' + data.message)
|
||||
message?.error('无法获取数据: ' + data.message)
|
||||
return undefined
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('无法获取数据: ' + err)
|
||||
message?.error('无法获取数据: ' + err)
|
||||
return undefined
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
async function ApplyToForum(owner: number) {
|
||||
try {
|
||||
isLoading.value = true
|
||||
const data = await QueryGetAPI<ForumModel>(FORUM_API_URL + 'apply', { owner: owner })
|
||||
if (data.code == 200) {
|
||||
message?.success('已提交申请, 等待管理员审核')
|
||||
return true
|
||||
} else {
|
||||
message?.error('无法获取数据: ' + data.message)
|
||||
console.error('无法获取数据: ' + data.message)
|
||||
return false
|
||||
}
|
||||
} catch (err) {
|
||||
message?.error('无法获取数据: ' + err)
|
||||
console.error('无法获取数据: ' + err)
|
||||
return false
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function PostTopic(topic: ForumPostTopicModel, token: string) {
|
||||
try {
|
||||
isLoading.value = true
|
||||
const data = await QueryPostAPI<ForumTopicModel>(FORUM_API_URL + 'post-topic', topic, [['Turnstile', token]])
|
||||
if (data.code == 200) {
|
||||
message?.success('发布成功')
|
||||
return data.data
|
||||
} else {
|
||||
message?.error('发布失败: ' + data.message)
|
||||
console.error('发布失败: ' + data.message)
|
||||
return undefined
|
||||
}
|
||||
} catch (err) {
|
||||
message?.error('发布失败: ' + err)
|
||||
console.error('发布失败: ' + err)
|
||||
return undefined
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
async function PostComment(model: { topic: number; content: string }, token: string) {
|
||||
try {
|
||||
isLoading.value = true
|
||||
const data = await QueryPostAPI<ForumCommentModel>(FORUM_API_URL + 'post-comment', model, [['Turnstile', token]])
|
||||
if (data.code == 200) {
|
||||
message?.success('评论成功')
|
||||
return data.data
|
||||
} else {
|
||||
message?.error('评论失败: ' + data.message)
|
||||
console.error('评论失败: ' + data.message)
|
||||
return undefined
|
||||
}
|
||||
} catch (err) {
|
||||
message?.error('评论失败: ' + err)
|
||||
console.error('评论失败: ' + err)
|
||||
return undefined
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
async function PostReply(model: { comment: number; content: string; replyTo?: number }, token: string) {
|
||||
try {
|
||||
isLoading.value = true
|
||||
const data = await QueryPostAPI<ForumCommentModel>(FORUM_API_URL + 'post-reply', model, [['Turnstile', token]])
|
||||
if (data.code == 200) {
|
||||
message?.success('评论成功')
|
||||
return data.data
|
||||
} else {
|
||||
message?.error('评论失败: ' + data.message)
|
||||
console.error('评论失败: ' + data.message)
|
||||
return undefined
|
||||
}
|
||||
} catch (err) {
|
||||
message?.error('评论失败: ' + err)
|
||||
console.error('评论失败: ' + err)
|
||||
return undefined
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
async function LikeTopic(topic: number, like: boolean) {
|
||||
try {
|
||||
isLikeLoading.value = true
|
||||
const data = await QueryGetAPI(FORUM_API_URL + 'like-topic', { topic: topic, like: like })
|
||||
if (data.code == 200) {
|
||||
//message?.success('已点赞')
|
||||
return true
|
||||
} else {
|
||||
message?.error('点赞失败: ' + data.message)
|
||||
console.error('点赞失败: ' + data.message)
|
||||
return false
|
||||
}
|
||||
} catch (err) {
|
||||
message?.error('点赞失败: ' + err)
|
||||
console.error('点赞失败: ' + err)
|
||||
return false
|
||||
} finally {
|
||||
isLikeLoading.value = false
|
||||
}
|
||||
}
|
||||
async function LikeComment(comment: number, like: boolean) {
|
||||
try {
|
||||
isLikeLoading.value = true
|
||||
const data = await QueryGetAPI(FORUM_API_URL + 'like-comment', { comment: comment, like: like })
|
||||
if (data.code == 200) {
|
||||
//message?.success('已点赞')
|
||||
return true
|
||||
} else {
|
||||
message?.error('点赞失败: ' + data.message)
|
||||
console.error('点赞失败: ' + data.message)
|
||||
return false
|
||||
}
|
||||
} catch (err) {
|
||||
message?.error('点赞失败: ' + err)
|
||||
console.error('点赞失败: ' + err)
|
||||
return false
|
||||
} finally {
|
||||
isLikeLoading.value = false
|
||||
}
|
||||
}
|
||||
async function SetReplyingComment(
|
||||
comment: ForumCommentModel | undefined = undefined,
|
||||
reply: ForumReplyModel | undefined = undefined,
|
||||
) {
|
||||
if (!comment) {
|
||||
replyingComment.value = undefined
|
||||
replyingReply.value = undefined
|
||||
showReplyModal.value = false
|
||||
return
|
||||
}
|
||||
replyingComment.value = comment
|
||||
replyingReply.value = reply
|
||||
showReplyModal.value = true
|
||||
}
|
||||
|
||||
async function SetTopicTop(topic: number, top: boolean) {
|
||||
try {
|
||||
isLoading.value = true
|
||||
const data = await QueryGetAPI(FORUM_API_URL + 'manage/set-topic-top', { topic: topic, top: top })
|
||||
if (data.code == 200) {
|
||||
message?.success('完成')
|
||||
return true
|
||||
} else {
|
||||
message?.error('操作失败: ' + data.message)
|
||||
console.error('操作失败: ' + data.message)
|
||||
return false
|
||||
}
|
||||
} catch (err) {
|
||||
message?.error('操作失败: ' + err)
|
||||
console.error('操作失败: ' + err)
|
||||
return false
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
async function DelTopic(topic: number) {
|
||||
try {
|
||||
isLoading.value = true
|
||||
const data = await QueryGetAPI(FORUM_API_URL + 'manage/delete-topic', { topic: topic })
|
||||
if (data.code == 200) {
|
||||
message?.success('删除成功')
|
||||
return true
|
||||
} else {
|
||||
message?.error('删除失败: ' + data.message)
|
||||
console.error('删除失败: ' + data.message)
|
||||
return false
|
||||
}
|
||||
} catch (err) {
|
||||
message?.error('删除失败: ' + err)
|
||||
console.error('删除失败: ' + err)
|
||||
return false
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
async function DelComment(comment: number) {
|
||||
try {
|
||||
isLoading.value = true
|
||||
const data = await QueryGetAPI(FORUM_API_URL + 'manage/delete-comment', { comment: comment })
|
||||
if (data.code == 200) {
|
||||
message?.success('删除成功')
|
||||
return true
|
||||
} else {
|
||||
message?.error('删除失败: ' + data.message)
|
||||
console.error('删除失败: ' + data.message)
|
||||
return false
|
||||
}
|
||||
} catch (err) {
|
||||
message?.error('删除失败: ' + err)
|
||||
console.error('删除失败: ' + err)
|
||||
return false
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
async function DelReply(reply: number) {
|
||||
try {
|
||||
isLoading.value = true
|
||||
const data = await QueryGetAPI(FORUM_API_URL + 'manage/delete-reply', { reply: reply })
|
||||
if (data.code == 200) {
|
||||
message?.success('删除成功')
|
||||
return true
|
||||
} else {
|
||||
message?.error('删除失败: ' + data.message)
|
||||
console.error('删除失败: ' + data.message)
|
||||
return false
|
||||
}
|
||||
} catch (err) {
|
||||
message?.error('删除失败: ' + err)
|
||||
console.error('删除失败: ' + err)
|
||||
return false
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
async function ConfirmApply(owner: number, id: number) {
|
||||
try {
|
||||
isLoading.value = true
|
||||
const data = await QueryGetAPI(FORUM_API_URL + 'manage/confirm-apply', { owner: owner, id: id })
|
||||
if (data.code == 200) {
|
||||
message?.success('已通过申请')
|
||||
return true
|
||||
} else {
|
||||
message?.error('确认失败: ' + data.message)
|
||||
console.error('确认失败: ' + data.message)
|
||||
return false
|
||||
}
|
||||
} catch (err) {
|
||||
message?.error('确认失败: ' + err)
|
||||
console.error('确认失败: ' + err)
|
||||
return false
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
GetForumInfo,
|
||||
GetManagedForums,
|
||||
GetTopics,
|
||||
GetTopicDetail,
|
||||
GetComments,
|
||||
ApplyToForum,
|
||||
PostTopic,
|
||||
PostComment,
|
||||
PostReply,
|
||||
LikeTopic,
|
||||
LikeComment,
|
||||
SetReplyingComment,
|
||||
SetTopicTop,
|
||||
DelTopic,
|
||||
DelComment,
|
||||
DelReply,
|
||||
ConfirmApply,
|
||||
isLoading,
|
||||
isLikeLoading,
|
||||
replyingComment,
|
||||
replyingReply,
|
||||
showReplyModal,
|
||||
}
|
||||
})
|
||||
49
src/store/usePointStore.ts
Normal file
49
src/store/usePointStore.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { ResponsePointGoodModel } from "@/api/api-models";
|
||||
import { QueryGetAPI } from "@/api/query";
|
||||
import { POINT_API_URL } from "@/data/constants";
|
||||
import { MessageApiInjection } from "naive-ui/es/message/src/MessageProvider";
|
||||
import { defineStore } from "pinia";
|
||||
import { useAuthStore } from "./useAuthStore";
|
||||
|
||||
export const usePointStore = defineStore('point', () => {
|
||||
const useAuth = useAuthStore()
|
||||
|
||||
async function GetSpecificPoint(id: number) {
|
||||
try {
|
||||
const data = await useAuth.QueryBiliAuthGetAPI<number>(POINT_API_URL + 'user/get-point', { id: id })
|
||||
if (data.code == 200) {
|
||||
return data.data
|
||||
} else {
|
||||
console.error('[point] 无法获取在指定直播间拥有的积分: ' + data.message)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[point] 无法获取在指定直播间拥有的积分: ' + err)
|
||||
}
|
||||
return null
|
||||
}
|
||||
async function GetGoods(id: number | undefined = undefined, message?: MessageApiInjection) {
|
||||
if (!id) {
|
||||
return []
|
||||
}
|
||||
try {
|
||||
const resp = await QueryGetAPI<ResponsePointGoodModel[]>(POINT_API_URL + 'get-goods', {
|
||||
id: id,
|
||||
})
|
||||
if (resp.code == 200) {
|
||||
return resp.data
|
||||
} else {
|
||||
message?.error('无法获取数据: ' + resp.message)
|
||||
console.error('无法获取数据: ' + resp.message)
|
||||
}
|
||||
} catch (err) {
|
||||
message?.error('无法获取数据: ' + err)
|
||||
console.error('无法获取数据: ' + err)
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
return {
|
||||
GetSpecificPoint,
|
||||
GetGoods
|
||||
}
|
||||
})
|
||||
@@ -36,6 +36,7 @@ import { NButton, NCard, NDivider, NLayoutContent, NSpace, NText, NTimeline, NTi
|
||||
</NSpace>
|
||||
<NDivider title-placement="left"> 更新日志 </NDivider>
|
||||
<NTimeline>
|
||||
<NTimelineItem type="info" title="功能更新" content="积分订单添加导出功能, 允许删除积分用户" time="2024-3-22" />
|
||||
<NTimelineItem type="info" title="功能更新" content="1. 点歌(歌势) 修改为点播 2. 棉花糖支持创建话题(标签) 3. 一些bug修复" time="2024-3-12" />
|
||||
<NTimelineItem type="info" title="功能更新" content="棉花糖添加展示页面" time="2024-2-20" />
|
||||
<NTimelineItem type="info" title="功能更新" content="歌单新增从文件导入" time="2024-2-10" />
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
Live24Filled,
|
||||
Lottery24Filled,
|
||||
PeopleQueue24Filled,
|
||||
Person48Filled,
|
||||
PersonFeedback24Filled,
|
||||
TabletSpeaker24Filled,
|
||||
VehicleShip24Filled,
|
||||
@@ -463,7 +464,7 @@ onMounted(() => {
|
||||
<template #extra>
|
||||
<NSpace align="center" justify="center">
|
||||
<NSwitch
|
||||
:default-value="!isDarkMode()"
|
||||
:default-value="!isDarkMode"
|
||||
@update:value="
|
||||
(value: string & number & boolean) => (themeType = value ? ThemeType.Light : ThemeType.Dark)
|
||||
"
|
||||
@@ -481,7 +482,7 @@ onMounted(() => {
|
||||
type="primary"
|
||||
@click="$router.push({ name: 'user-index', params: { id: accountInfo?.name } })"
|
||||
>
|
||||
回到主页
|
||||
回到展示页
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</template>
|
||||
@@ -520,7 +521,7 @@ onMounted(() => {
|
||||
</NSpace>
|
||||
<NButton v-if="accountInfo.biliUserAuthInfo" @click="gotoAuthPage()" type="info" secondary>
|
||||
<template #icon>
|
||||
<NIcon :component="BrowsersOutline" />
|
||||
<NIcon :component="Person48Filled" />
|
||||
</template>
|
||||
<template v-if="width >= 180"> 认证用户主页 </template>
|
||||
</NButton>
|
||||
|
||||
@@ -154,7 +154,7 @@ onUnmounted(() => {
|
||||
{{ client?.roomAuthInfo.value ? `已连接 | ${client.roomAuthInfo.value?.anchor_info.uname}` : '未连接' }}
|
||||
</NTag>
|
||||
<NSwitch
|
||||
:default-value="!isDarkMode()"
|
||||
:default-value="!isDarkMode"
|
||||
@update:value="
|
||||
(value: string & number & boolean) => (themeType = value ? ThemeType.Light : ThemeType.Dark)
|
||||
"
|
||||
@@ -198,7 +198,7 @@ onUnmounted(() => {
|
||||
round
|
||||
bordered
|
||||
:style="{
|
||||
boxShadow: isDarkMode() ? 'rgb(195 192 192 / 35%) 0px 0px 8px' : '0 2px 3px rgba(0, 0, 0, 0.1)',
|
||||
boxShadow: isDarkMode ? 'rgb(195 192 192 / 35%) 0px 0px 8px' : '0 2px 3px rgba(0, 0, 0, 0.1)',
|
||||
}"
|
||||
/>
|
||||
<NEllipsis v-if="width > 100" style="max-width: 100%">
|
||||
|
||||
@@ -7,7 +7,13 @@ import { useUser } from '@/api/user'
|
||||
import RegisterAndLogin from '@/components/RegisterAndLogin.vue'
|
||||
import { FETCH_API } from '@/data/constants'
|
||||
import { useAuthStore } from '@/store/useAuthStore'
|
||||
import { BookCoins20Filled, CalendarClock24Filled, VideoAdd20Filled } from '@vicons/fluent'
|
||||
import {
|
||||
BookCoins20Filled,
|
||||
CalendarClock24Filled,
|
||||
Person48Filled,
|
||||
VideoAdd20Filled,
|
||||
WindowWrench20Filled,
|
||||
} from '@vicons/fluent'
|
||||
import { Chatbox, Home, Moon, MusicalNote, Sunny } from '@vicons/ionicons5'
|
||||
import { useElementSize, useStorage } from '@vueuse/core'
|
||||
import {
|
||||
@@ -51,6 +57,7 @@ const notfount = ref(false)
|
||||
const registerAndLoginModalVisiable = ref(false)
|
||||
const sider = ref()
|
||||
const { width } = useElementSize(sider)
|
||||
const windowWidth = window.innerWidth
|
||||
|
||||
function renderIcon(icon: unknown) {
|
||||
return () => h(NIcon, null, { default: () => h(icon as any) })
|
||||
@@ -73,11 +80,12 @@ function gotoAuthPage() {
|
||||
message.error('你尚未进行 Bilibili 认证, 请前往面板进行认证和绑定')
|
||||
return
|
||||
}
|
||||
useAuthStore()
|
||||
/*useAuthStore()
|
||||
.setCurrentAuth(accountInfo.value?.biliUserAuthInfo.token)
|
||||
.then(() => {
|
||||
NavigateToNewTab('/bili-user')
|
||||
})
|
||||
})*/
|
||||
NavigateToNewTab('/bili-user')
|
||||
}
|
||||
onMounted(async () => {
|
||||
userInfo.value = await useUser(id.value?.toString())
|
||||
@@ -193,7 +201,7 @@ onMounted(async () => {
|
||||
<template #extra>
|
||||
<NSpace align="center">
|
||||
<NSwitch
|
||||
:default-value="!isDarkMode()"
|
||||
:default-value="!isDarkMode"
|
||||
@update:value="
|
||||
(value: string & number & boolean) => (themeType = value ? ThemeType.Light : ThemeType.Dark)
|
||||
"
|
||||
@@ -211,11 +219,16 @@ onMounted(async () => {
|
||||
v-if="useAuth.isAuthed || accountInfo.biliUserAuthInfo"
|
||||
style="right: 0px; position: relative"
|
||||
type="primary"
|
||||
@click="gotoAuthPage"
|
||||
tag="a"
|
||||
href="/bili-user"
|
||||
target="_blank"
|
||||
size="small"
|
||||
secondary
|
||||
>
|
||||
认证用户中心
|
||||
<template #icon>
|
||||
<NIcon :component="Person48Filled" />
|
||||
</template>
|
||||
<span v-if="windowWidth >= 768"> 认证用户中心 </span>
|
||||
</NButton>
|
||||
<NButton
|
||||
style="right: 0px; position: relative"
|
||||
@@ -223,7 +236,10 @@ onMounted(async () => {
|
||||
@click="$router.push({ name: 'manage-index' })"
|
||||
size="small"
|
||||
>
|
||||
个人中心
|
||||
<template #icon>
|
||||
<NIcon :component="WindowWrench20Filled" />
|
||||
</template>
|
||||
<span v-if="windowWidth >= 768"> 主播后台 </span>
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</template>
|
||||
@@ -265,7 +281,7 @@ onMounted(async () => {
|
||||
round
|
||||
bordered
|
||||
:style="{
|
||||
boxShadow: isDarkMode() ? 'rgb(195 192 192 / 35%) 0px 0px 8px' : '0 2px 3px rgba(0, 0, 0, 0.1)',
|
||||
boxShadow: isDarkMode ? 'rgb(195 192 192 / 35%) 0px 0px 8px' : '0 2px 3px rgba(0, 0, 0, 0.1)',
|
||||
}"
|
||||
/>
|
||||
<NEllipsis v-if="width > 100" style="max-width: 100%">
|
||||
@@ -295,7 +311,7 @@ onMounted(async () => {
|
||||
<NLayout style="height: 100%">
|
||||
<div
|
||||
class="viewer-page-content"
|
||||
:style="`box-shadow:${isDarkMode() ? 'rgb(28 28 28 / 9%) 5px 5px 6px inset, rgba(139, 139, 139, 0.09) -5px -5px 6px inset' : 'inset 5px 5px 6px #8b8b8b17, inset -5px -5px 6px #8b8b8b17;'}`"
|
||||
:style="`box-shadow:${isDarkMode ? 'rgb(28 28 28 / 9%) 5px 5px 6px inset, rgba(139, 139, 139, 0.09) -5px -5px 6px inset' : 'inset 5px 5px 6px #8b8b8b17, inset -5px -5px 6px #8b8b8b17;'}`"
|
||||
>
|
||||
<RouterView v-if="userInfo" v-slot="{ Component }">
|
||||
<KeepAlive>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { isDarkMode } from '@/Utils'
|
||||
import { useAccount } from '@/api/account'
|
||||
import { QueryGetAPI } from '@/api/query'
|
||||
import EventFetcherStatusCard from '@/components/EventFetcherStatusCard.vue'
|
||||
import { AVATAR_URL, BASE_API, EVENT_API_URL } from '@/data/constants'
|
||||
import { AVATAR_URL, BASE_API_URL, EVENT_API_URL } from '@/data/constants'
|
||||
import { Grid28Filled, List16Filled } from '@vicons/fluent'
|
||||
import { format } from 'date-fns'
|
||||
import { saveAs } from 'file-saver'
|
||||
@@ -253,7 +253,7 @@ function objectsToCSV(arr: any[]) {
|
||||
:color="{
|
||||
color: selectedType == EventType.Guard ? GetGuardColor(item.price) : GetSCColor(item.price),
|
||||
textColor: 'white',
|
||||
borderColor: isDarkMode() ? 'white' : '#00000000',
|
||||
borderColor: isDarkMode ? 'white' : '#00000000',
|
||||
}"
|
||||
>
|
||||
{{ item.price }}
|
||||
|
||||
249
src/views/manage/ForumManage.vue
Normal file
249
src/views/manage/ForumManage.vue
Normal file
@@ -0,0 +1,249 @@
|
||||
<script setup lang="ts">
|
||||
import { useAccount } from '@/api/account'
|
||||
import { QueryPostAPI } from '@/api/query'
|
||||
import { useForumStore } from '@/store/useForumStore'
|
||||
import {
|
||||
DataTableColumns,
|
||||
NAlert,
|
||||
NButton,
|
||||
NCard,
|
||||
NCheckbox,
|
||||
NDataTable,
|
||||
NDescriptions,
|
||||
NDescriptionsItem,
|
||||
NDivider,
|
||||
NFlex,
|
||||
NInput,
|
||||
NInputGroup,
|
||||
NInputGroupLabel,
|
||||
NModal,
|
||||
NSelect,
|
||||
NSpin,
|
||||
NTabPane,
|
||||
NTabs,
|
||||
NTag,
|
||||
NTime,
|
||||
useMessage,
|
||||
} from 'naive-ui'
|
||||
import { h, ref } from 'vue'
|
||||
import { ForumModel, ForumUserModel } from '@/api/models/forum'
|
||||
import { FORUM_API_URL } from '@/data/constants'
|
||||
// @ts-ignore
|
||||
import Agreement from '@/document/EnableForumAgreement.md'
|
||||
import { UserBasicInfo } from '@/api/api-models'
|
||||
|
||||
const useForum = useForumStore()
|
||||
const accountInfo = useAccount()
|
||||
const message = useMessage()
|
||||
|
||||
const managedForums = ref((await useForum.GetManagedForums()) ?? [])
|
||||
const currentForum = ref((await useForum.GetForumInfo(accountInfo.value.id)) ?? ({} as ForumModel))
|
||||
const selectedForum = ref(accountInfo.value.id)
|
||||
|
||||
const readedAgreement = ref(false)
|
||||
const showAgreement = ref(false)
|
||||
const create_Name = ref('')
|
||||
const create_Description = ref('')
|
||||
|
||||
const paginationSetting = { defaultPageSize: 20, showSizePicker: true, pageSizes: [20, 50, 100] }
|
||||
|
||||
async function createForum() {
|
||||
if (!readedAgreement.value) {
|
||||
message.warning('请先阅读并同意服务协议')
|
||||
return
|
||||
}
|
||||
if (!create_Name.value) {
|
||||
message.warning('请输入名称')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const data = await QueryPostAPI<ForumModel>(FORUM_API_URL + 'create', {
|
||||
name: create_Name.value,
|
||||
})
|
||||
if (data.code == 200) {
|
||||
message.success('创建成功')
|
||||
currentForum.value = data.data
|
||||
} else {
|
||||
message.error('创建失败:' + data.message)
|
||||
console.error(data.message)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
message.error('创建失败:' + err)
|
||||
}
|
||||
}
|
||||
async function SwitchForum(owner: number) {
|
||||
currentForum.value = (await useForum.GetForumInfo(owner)) ?? ({} as ForumModel)
|
||||
}
|
||||
const defaultColumns: DataTableColumns<ForumUserModel> = [
|
||||
{
|
||||
title: '名称',
|
||||
key: 'name',
|
||||
},
|
||||
{
|
||||
title: 'B站账号',
|
||||
key: 'biliId',
|
||||
render(row) {
|
||||
return h(NTag, { type: row.isBiliAuthed ? 'success' : 'warning' }, () => (row.isBiliAuthed ? '已绑定' : '未绑定'))
|
||||
},
|
||||
},
|
||||
]
|
||||
const applyingColumns: DataTableColumns<ForumUserModel> = [
|
||||
...defaultColumns,
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
render(row) {
|
||||
return h(
|
||||
NButton,
|
||||
{
|
||||
text: true,
|
||||
type: 'success',
|
||||
onClick: () =>
|
||||
useForum.ConfirmApply(currentForum.value.owner.id, row.id).then((success) => {
|
||||
if (success) message.success('操作成功')
|
||||
currentForum.value.applying = currentForum.value.applying.filter((u) => u.id != row.id)
|
||||
}),
|
||||
},
|
||||
{ default: () => '通过申请' },
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
const memberColumns: DataTableColumns<ForumUserModel> = [
|
||||
...defaultColumns,
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
render(row) {
|
||||
return h(
|
||||
NButton,
|
||||
{
|
||||
text: true,
|
||||
type: 'success',
|
||||
onClick: () =>
|
||||
useForum.ConfirmApply(currentForum.value.owner.id, row.id).then((success) => {
|
||||
if (success) message.success('操作成功')
|
||||
currentForum.value.applying = currentForum.value.applying.filter((u) => u.id != row.id)
|
||||
}),
|
||||
},
|
||||
{ default: () => '通过申请' },
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
const banColumns: DataTableColumns<ForumUserModel> = [
|
||||
...defaultColumns,
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
render(row) {
|
||||
return h(
|
||||
NButton,
|
||||
{
|
||||
text: true,
|
||||
type: 'success',
|
||||
onClick: () =>
|
||||
useForum.ConfirmApply(currentForum.value.owner.id, row.id).then((success) => {
|
||||
if (success) message.success('操作成功')
|
||||
currentForum.value.applying = currentForum.value.applying.filter((u) => u.id != row.id)
|
||||
}),
|
||||
},
|
||||
{ default: () => '通过申请' },
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NCard v-if="!currentForum.name" size="small" title="啊哦">
|
||||
<NAlert type="error"> 你尚未创建粉丝讨论区 </NAlert>
|
||||
<NDivider />
|
||||
<NFlex justify="center">
|
||||
<NFlex vertical>
|
||||
<NButton @click="createForum" size="large" type="primary"> 创建粉丝讨论区 </NButton>
|
||||
<NInputGroup>
|
||||
<NInputGroupLabel> 名称 </NInputGroupLabel>
|
||||
<NInput v-model:value="create_Name" placeholder="就是名称" maxlength="20" minlength="1" show-count />
|
||||
</NInputGroup>
|
||||
|
||||
<NInput
|
||||
v-model:value="create_Description"
|
||||
placeholder="(可选) 公告/描述"
|
||||
maxlength="200"
|
||||
show-count
|
||||
type="textarea"
|
||||
/>
|
||||
<NCheckbox v-model:checked="readedAgreement">
|
||||
已阅读并同意 <NButton @click="showAgreement = true" text type="info"> 服务协议 </NButton>
|
||||
</NCheckbox>
|
||||
</NFlex>
|
||||
</NFlex>
|
||||
</NCard>
|
||||
<template v-else>
|
||||
<NSpin :show="useForum.isLoading">
|
||||
<NSelect
|
||||
v-model:value="selectedForum"
|
||||
:options="
|
||||
managedForums.map((f) => ({
|
||||
label: (f.owner.id == accountInfo.id ? '[我的] ' : '') + f.name + ` (${f.owner.name})`,
|
||||
value: f.owner.id,
|
||||
}))
|
||||
"
|
||||
@update:value="(v) => SwitchForum(v)"
|
||||
>
|
||||
<template #header>
|
||||
<NButton @click="SwitchForum(accountInfo.id)" size="small" type="primary"> 我的粉丝讨论区 </NButton>
|
||||
</template>
|
||||
</NSelect>
|
||||
<NDivider />
|
||||
<NTabs animated v-bind:key="selectedForum" type="segment">
|
||||
<NTabPane tab="信息" name="info">
|
||||
<NDescriptions bordered size="small">
|
||||
<NDescriptionsItem label="名称"> {{ currentForum.name }} </NDescriptionsItem>
|
||||
<NDescriptionsItem label="公告"> {{ currentForum.description ?? '无' }} </NDescriptionsItem>
|
||||
<NDescriptionsItem label="创建者"> {{ currentForum.owner.name }} </NDescriptionsItem>
|
||||
<NDescriptionsItem label="创建时间"> <NTime :time="currentForum.createAt" /> </NDescriptionsItem>
|
||||
<NDescriptionsItem label="帖子数量"> {{ currentForum.topicCount }} </NDescriptionsItem>
|
||||
<NDescriptionsItem v-if="currentForum.settings.requireApply" label="成员数量">
|
||||
{{ currentForum.members?.length ?? 0 }}
|
||||
</NDescriptionsItem>
|
||||
<NDescriptionsItem label="管理员数量"> {{ currentForum.admins?.length ?? 0 }} </NDescriptionsItem>
|
||||
</NDescriptions>
|
||||
<NDivider> 设置 </NDivider>
|
||||
</NTabPane>
|
||||
<NTabPane tab="成员" name="member">
|
||||
<NDivider> 申请 </NDivider>
|
||||
<NDataTable
|
||||
:columns="applyingColumns"
|
||||
:data="currentForum.applying"
|
||||
:pagination="paginationSetting"
|
||||
/>
|
||||
<template v-if="currentForum.settings.requireApply">
|
||||
<NDivider> 成员 </NDivider>
|
||||
<NDataTable
|
||||
:columns="memberColumns"
|
||||
:data="currentForum.members.sort((a, b) => (a.isAdmin ? 1 : 0) - (b.isAdmin ? 1 : 0))"
|
||||
:pagination="paginationSetting"
|
||||
/>
|
||||
</template>
|
||||
<NDivider> 封禁用户 </NDivider>
|
||||
<NDataTable
|
||||
:columns="banColumns"
|
||||
:data="currentForum.blackList"
|
||||
:pagination="paginationSetting"
|
||||
/>
|
||||
</NTabPane>
|
||||
</NTabs>
|
||||
</NSpin>
|
||||
</template>
|
||||
<NModal
|
||||
v-model:show="showAgreement"
|
||||
preset="card"
|
||||
title="开通粉丝讨论区用户协议"
|
||||
style="width: 600px; max-width: 90vw"
|
||||
>
|
||||
<Agreement />
|
||||
</NModal>
|
||||
</template>
|
||||
@@ -2,6 +2,7 @@
|
||||
import { useAccount } from '@/api/account'
|
||||
import { ResponseLiveInfoModel } from '@/api/api-models'
|
||||
import { QueryGetAPI } from '@/api/query'
|
||||
import EventFetcherStatusCard from '@/components/EventFetcherStatusCard.vue'
|
||||
import LiveInfoContainer from '@/components/LiveInfoContainer.vue'
|
||||
import { LIVE_API_URL } from '@/data/constants'
|
||||
import { NAlert, NButton, NDivider, NList, NListItem, NPagination, NSpace, useMessage } from 'naive-ui'
|
||||
@@ -43,7 +44,6 @@ function OnClickCover(live: ResponseLiveInfoModel) {
|
||||
|
||||
<template>
|
||||
<NSpace vertical>
|
||||
<NAlert type="warning"> 测试功能, 尚不稳定 </NAlert>
|
||||
<NAlert type="error" title="2024.2.26">
|
||||
近期逸站对开放平台直播弹幕流进行了极为严格的限制, 目前本站服务器只能连接个位数的直播间, 这使得在不使用
|
||||
<NButton tag="a" href="https://www.yuque.com/megghy/dez70g/vfvcyv3024xvaa1p" target="_blank" type="primary" text>
|
||||
@@ -57,6 +57,8 @@ function OnClickCover(live: ResponseLiveInfoModel) {
|
||||
</NButton>
|
||||
, 否则只能记录直播的时间而不包含弹幕
|
||||
</NAlert>
|
||||
|
||||
<EventFetcherStatusCard />
|
||||
</NSpace>
|
||||
<NDivider />
|
||||
<NAlert v-if="accountInfo?.isBiliVerified != true" type="info"> 尚未进行Bilibili认证 </NAlert>
|
||||
|
||||
@@ -160,7 +160,10 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<NSpace align="center">
|
||||
<NAlert type="info" style="max-width: 200px">
|
||||
<NAlert
|
||||
:type="accountInfo.settings.enableFunctions.includes(FunctionTypes.QuestionBox) ? 'success' : 'warning'"
|
||||
style="max-width: 200px"
|
||||
>
|
||||
启用提问箱
|
||||
<NDivider vertical />
|
||||
<NSwitch
|
||||
|
||||
@@ -262,7 +262,10 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<NSpace align="center">
|
||||
<NAlert type="info" style="max-width: 200px">
|
||||
<NAlert
|
||||
:type="accountInfo.settings.enableFunctions.includes(FunctionTypes.Schedule) ? 'success' : 'warning'"
|
||||
style="max-width: 200px"
|
||||
>
|
||||
启用日程表
|
||||
<NDivider vertical />
|
||||
<NSwitch
|
||||
|
||||
@@ -571,7 +571,10 @@ onMounted(async () => {
|
||||
|
||||
<template>
|
||||
<NSpace align="center">
|
||||
<NAlert type="info" style="max-width: 200px">
|
||||
<NAlert
|
||||
:type="accountInfo.settings.enableFunctions.includes(FunctionTypes.SongList) ? 'success' : 'warning'"
|
||||
style="max-width: 200px"
|
||||
>
|
||||
启用歌单
|
||||
<NDivider vertical />
|
||||
<NSwitch
|
||||
|
||||
@@ -127,7 +127,11 @@ function createTable() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NAlert type="info" v-if="accountInfo.id">
|
||||
<NAlert
|
||||
:type="accountInfo.settings.enableFunctions.includes(FunctionTypes.VideoCollect) ? 'success' : 'warning'"
|
||||
v-if="accountInfo.id"
|
||||
style="max-width: 300px"
|
||||
>
|
||||
在个人主页展示进行中的征集表
|
||||
<NSwitch
|
||||
:value="accountInfo.settings.enableFunctions.includes(FunctionTypes.VideoCollect)"
|
||||
|
||||
@@ -337,7 +337,14 @@ onMounted(() => {})
|
||||
|
||||
<template>
|
||||
<NFlex>
|
||||
<NAlert type="info" style="min-width: 400px">
|
||||
<NAlert
|
||||
:type="
|
||||
accountInfo.settings.enableFunctions.includes(FunctionTypes.Point) && accountInfo.eventFetcherState.online
|
||||
? 'success'
|
||||
: 'warning'
|
||||
"
|
||||
style="min-width: 400px"
|
||||
>
|
||||
启用
|
||||
<NButton text type="primary" tag="a" href="https://www.yuque.com/megghy/dez70g/ohulp2torghlqqn8" target="_blank">
|
||||
积分系统
|
||||
@@ -369,6 +376,9 @@ onMounted(() => {})
|
||||
<NTabPane name="goods" tab="礼物">
|
||||
<NFlex>
|
||||
<NButton type="primary" @click="onModalOpen"> 添加礼物 </NButton>
|
||||
<NButton @click="$router.push({ name: 'user-goods', params: { id: accountInfo?.name } })" secondary>
|
||||
前往展示页
|
||||
</NButton>
|
||||
</NFlex>
|
||||
<NDivider />
|
||||
<NEmpty v-if="goods.filter((g) => g.status != GoodsStatus.Discontinued).length == 0" description="暂无礼物" />
|
||||
|
||||
@@ -1,21 +1,47 @@
|
||||
<script setup lang="ts">
|
||||
import { ResponsePointGoodModel, ResponsePointOrder2OwnerModel } from '@/api/api-models'
|
||||
import { useAccount } from '@/api/account'
|
||||
import { GoodsTypes, PointOrderStatus, ResponsePointGoodModel, ResponsePointOrder2OwnerModel } from '@/api/api-models'
|
||||
import { QueryGetAPI } from '@/api/query'
|
||||
import PointOrderCard from '@/components/manage/PointOrderCard.vue'
|
||||
import { POINT_API_URL } from '@/data/constants'
|
||||
import { NEmpty, useMessage } from 'naive-ui'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { objectsToCSV } from '@/Utils'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import { format } from 'date-fns'
|
||||
import { saveAs } from 'file-saver'
|
||||
import { NButton, NCard, NCheckbox, NDivider, NEmpty, NFlex, NSelect, NSpin, useMessage } from 'naive-ui'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
type OrderFilterSettings = {
|
||||
type?: GoodsTypes
|
||||
status?: PointOrderStatus
|
||||
onlyRequireShippingInfo: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
goods: ResponsePointGoodModel[]
|
||||
}>()
|
||||
const defaultSettings = {
|
||||
onlyRequireShippingInfo: false,
|
||||
} as OrderFilterSettings
|
||||
const filterSettings = useStorage<OrderFilterSettings>('Setting.Point.OrderFilter', defaultSettings)
|
||||
|
||||
const message = useMessage()
|
||||
const accountInfo = useAccount()
|
||||
|
||||
const orders = ref<ResponsePointOrder2OwnerModel[]>([])
|
||||
const filteredOrders = computed(() => {
|
||||
return orders.value.filter((o) => {
|
||||
if (filterSettings.value.type != undefined && o.type !== filterSettings.value.type) return false
|
||||
if (filterSettings.value.status != undefined && o.status !== filterSettings.value.status) return false
|
||||
if (filterSettings.value.onlyRequireShippingInfo && o.trackingNumber) return false
|
||||
return true
|
||||
})
|
||||
})
|
||||
const isLoading = ref(false)
|
||||
|
||||
async function getOrders() {
|
||||
try {
|
||||
isLoading.value = true
|
||||
const data = await QueryGetAPI<ResponsePointOrder2OwnerModel[]>(POINT_API_URL + 'get-orders')
|
||||
if (data.code == 200) {
|
||||
return data.data
|
||||
@@ -25,9 +51,53 @@ async function getOrders() {
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
message.error('获取订单失败: ' + err)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
return []
|
||||
}
|
||||
const statusText = {
|
||||
[PointOrderStatus.Completed]: '已完成',
|
||||
[PointOrderStatus.Pending]: '等待发货',
|
||||
[PointOrderStatus.Shipped]: '已发货',
|
||||
}
|
||||
function exportData() {
|
||||
const text = objectsToCSV(
|
||||
filteredOrders.value.map((s) => {
|
||||
const gift = props.goods.find((g) => g.id == s.goodsId)
|
||||
return {
|
||||
订单号: s.id,
|
||||
订单类型: s.type == GoodsTypes.Physical ? '实体' : '虚拟',
|
||||
订单状态: statusText[s.status],
|
||||
用户名: s.customer.name ?? '未知',
|
||||
用户UID: s.customer.userId,
|
||||
联系人: s.address?.name,
|
||||
联系电话: s.address?.phone,
|
||||
地址: s.address
|
||||
? `${s.address?.province}省${s.address?.city}市${s.address?.district}区${s.address?.street}街道${s.address?.address}`
|
||||
: '无',
|
||||
礼物名: gift?.name ?? '已删除',
|
||||
礼物数量: s.count,
|
||||
礼物单价: gift?.price,
|
||||
礼物总价: s.point,
|
||||
快递公司: s.expressCompany,
|
||||
快递单号: s.trackingNumber,
|
||||
创建时间: format(s.createAt, 'yyyy-MM-dd HH:mm:ss'),
|
||||
更新时间: s.updateAt ? format(s.updateAt, 'yyyy-MM-dd HH:mm:ss') : '未更新',
|
||||
}
|
||||
}),
|
||||
)
|
||||
const BOM = new Uint8Array([0xef, 0xbb, 0xbf])
|
||||
const utf8encoder = new TextEncoder()
|
||||
const utf8array = utf8encoder.encode(text)
|
||||
saveAs(
|
||||
new Blob([BOM, utf8array], { type: 'text/csv;charset=utf-8;' }),
|
||||
`积分订单_${format(Date.now(), 'yyyy-MM-dd HH:mm:ss')}_${accountInfo.value?.name}_.csv`,
|
||||
)
|
||||
}
|
||||
async function refresh() {
|
||||
orders.value = await getOrders()
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
orders.value = await getOrders()
|
||||
@@ -35,6 +105,48 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NEmpty v-if="orders.length == 0" description="暂无订单"></NEmpty>
|
||||
<PointOrderCard v-else :order="orders" :goods="goods" type="owner" />
|
||||
<NSpin :show="isLoading">
|
||||
<NEmpty v-if="filteredOrders.length == 0" description="暂无订单"></NEmpty>
|
||||
<template v-else>
|
||||
<br />
|
||||
<NFlex>
|
||||
<NButton @click="refresh">刷新</NButton>
|
||||
<NButton @click="exportData" secondary type="info">导出数据</NButton>
|
||||
</NFlex>
|
||||
<NDivider />
|
||||
<NCard size="small" title="筛选订单">
|
||||
<template #header-extra>
|
||||
<NButton @click="filterSettings = JSON.parse(JSON.stringify(defaultSettings))" size="small" type="warning">
|
||||
重置
|
||||
</NButton>
|
||||
</template>
|
||||
<NFlex align="center">
|
||||
<NSelect
|
||||
v-model:value="filterSettings.type"
|
||||
:options="[
|
||||
{ label: '实体', value: GoodsTypes.Physical },
|
||||
{ label: '虚拟', value: GoodsTypes.Virtual },
|
||||
]"
|
||||
clearable
|
||||
placeholder="订单类型"
|
||||
style="width: 150px"
|
||||
/>
|
||||
<NSelect
|
||||
v-model:value="filterSettings.status"
|
||||
:options="[
|
||||
{ label: '已完成', value: PointOrderStatus.Completed },
|
||||
{ label: '等待发货', value: PointOrderStatus.Pending },
|
||||
{ label: '已发货', value: PointOrderStatus.Shipped },
|
||||
]"
|
||||
placeholder="订单状态"
|
||||
clearable
|
||||
style="width: 150px"
|
||||
/>
|
||||
<NCheckbox v-model:checked="filterSettings.onlyRequireShippingInfo" label="仅包含未填写快递单号的订单" />
|
||||
</NFlex>
|
||||
</NCard>
|
||||
<NDivider />
|
||||
<PointOrderCard :order="filteredOrders" :goods="goods" type="owner" />
|
||||
</template>
|
||||
</NSpin>
|
||||
</template>
|
||||
|
||||
@@ -111,6 +111,14 @@ async function updateGift() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NAlert v-if="!accountInfo.eventFetcherState.online" type="warning">
|
||||
由于你尚未部署
|
||||
<NButton text type="primary" tag="a" href="https://www.yuque.com/megghy/dez70g/vfvcyv3024xvaa1p" target="_blank">
|
||||
VtsuruEventFetcher
|
||||
</NButton>
|
||||
, 以下选项设置了也没用
|
||||
</NAlert>
|
||||
<br />
|
||||
<NAlert type="info"> 积分总是最多保留两位小数, 四舍五入 </NAlert>
|
||||
<NDivider> 常用 </NDivider>
|
||||
<NSpin :show="isLoading">
|
||||
|
||||
@@ -133,6 +133,22 @@ const column: DataTableColumns<ResponsePointUserModel> = [
|
||||
},
|
||||
{ default: () => '详情' },
|
||||
),
|
||||
h(
|
||||
NPopconfirm,
|
||||
{ onPositiveClick: () => deleteUser(row) },
|
||||
{
|
||||
default: '确定要删除这个用户吗?记录将无法恢复',
|
||||
trigger: () =>
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
type: 'error',
|
||||
size: 'small',
|
||||
},
|
||||
{ default: () => '删除' },
|
||||
),
|
||||
},
|
||||
),
|
||||
])
|
||||
},
|
||||
},
|
||||
@@ -176,7 +192,9 @@ async function givePoint() {
|
||||
if (data.code == 200) {
|
||||
message.success('添加成功')
|
||||
showGivePointModal.value = false
|
||||
await refresh()
|
||||
setTimeout(() => {
|
||||
refresh()
|
||||
}, 1500)
|
||||
|
||||
addPointCount.value = 0
|
||||
addPointReason.value = undefined
|
||||
@@ -186,6 +204,37 @@ async function givePoint() {
|
||||
}
|
||||
} catch (err) {
|
||||
message.error('添加失败: ' + err)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
async function deleteUser(user: ResponsePointUserModel) {
|
||||
isLoading.value = true
|
||||
try {
|
||||
const data = await QueryGetAPI(
|
||||
POINT_API_URL + 'delete-user',
|
||||
user.isAuthed
|
||||
? {
|
||||
authId: user.info.id,
|
||||
}
|
||||
: user.info.userId
|
||||
? {
|
||||
uId: user.info.userId,
|
||||
}
|
||||
: {
|
||||
uId: user.info.openId,
|
||||
},
|
||||
)
|
||||
if (data.code == 200) {
|
||||
message.success('已删除')
|
||||
users.value = users.value.filter((u) => u != user)
|
||||
} else {
|
||||
message.error('删除失败: ' + data.message)
|
||||
}
|
||||
} catch (err) {
|
||||
message.error('删除失败: ' + err)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,6 +264,7 @@ onMounted(async () => {
|
||||
<NCheckbox v-model:checked="settings.onlyAuthed"> 只显示已认证用户 </NCheckbox>
|
||||
</NFlex>
|
||||
</NCard>
|
||||
<NDivider />
|
||||
<template v-if="filteredUsers.length == 0">
|
||||
<NDivider />
|
||||
<NEmpty :description="settings.onlyAuthed ? '没有已认证的用户' : '没有用户'" />
|
||||
|
||||
@@ -747,7 +747,10 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NAlert type="info" v-if="accountInfo.id">
|
||||
<NAlert
|
||||
:type="accountInfo.settings.enableFunctions.includes(FunctionTypes.SongRequest) ? 'success' : 'warning'"
|
||||
v-if="accountInfo.id"
|
||||
>
|
||||
启用弹幕点播功能
|
||||
<NSwitch
|
||||
:value="accountInfo?.settings.enableFunctions.includes(FunctionTypes.SongRequest)"
|
||||
|
||||
@@ -749,7 +749,10 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NAlert type="info" v-if="accountInfo.id">
|
||||
<NAlert
|
||||
:type="accountInfo.settings.enableFunctions.includes(FunctionTypes.Queue) ? 'success' : 'warning'"
|
||||
v-if="accountInfo.id"
|
||||
>
|
||||
启用弹幕队列功能
|
||||
<NSwitch
|
||||
:value="accountInfo?.settings.enableFunctions.includes(FunctionTypes.Queue)"
|
||||
|
||||
@@ -199,7 +199,10 @@ onMounted(async () => {
|
||||
<NCard v-else style="max-width: 600px" embedded hoverable>
|
||||
<template #header> 你好, {{ useAuth.biliAuth.name }} </template>
|
||||
<template #header-extra>
|
||||
<NFlex>
|
||||
<NButton type="info" @click="gotoAuthPage" secondary size="small"> 前往认证用户中心 </NButton>
|
||||
<NButton @click="NavigateToNewTab('/bili-user#settings')" secondary size="small"> 切换账号 </NButton>
|
||||
</NFlex>
|
||||
</template>
|
||||
<NText> 你在 {{ userInfo.extra?.streamerInfo?.name ?? userInfo.name }} 的直播间的积分为 {{ currentPoint }} </NText>
|
||||
</NCard>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ResponsePointOrder2UserModel } from '@/api/api-models'
|
||||
import PointOrderCard from '@/components/manage/PointOrderCard.vue'
|
||||
import { POINT_API_URL } from '@/data/constants'
|
||||
import { useAuthStore } from '@/store/useAuthStore'
|
||||
import { NEmpty, useMessage } from 'naive-ui'
|
||||
import { NEmpty, NSpin, useMessage } from 'naive-ui'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
const message = useMessage()
|
||||
@@ -24,8 +24,9 @@ async function getOrders() {
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
message.error('获取订单失败: ' + err)
|
||||
}
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
@@ -35,6 +36,8 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NEmpty v-if="orders.length == 0" description="暂无订单"></NEmpty>
|
||||
<NSpin :show="isLoading">
|
||||
<NEmpty v-if="orders.length == 0" description="暂无订单" />
|
||||
<PointOrderCard v-else :order="orders" :loading="isLoading" type="user" />
|
||||
</NSpin>
|
||||
</template>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ResponsePointHisrotyModel } from '@/api/api-models'
|
||||
import PointHistoryCard from '@/components/manage/PointHistoryCard.vue'
|
||||
import { POINT_API_URL } from '@/data/constants'
|
||||
import { useAuthStore } from '@/store/useAuthStore'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import { NSpin, useMessage } from 'naive-ui'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
const message = useMessage()
|
||||
@@ -26,6 +26,8 @@ async function getHistories() {
|
||||
} catch (err) {
|
||||
message.error('获取积分历史失败: ' + err)
|
||||
console.error(err)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
return []
|
||||
}
|
||||
@@ -36,5 +38,7 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NSpin :show="isLoading">
|
||||
<PointHistoryCard :histories="history" />
|
||||
</NSpin>
|
||||
</template>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { POINT_API_URL } from '@/data/constants'
|
||||
import { useAuthStore } from '@/store/useAuthStore'
|
||||
import { useRouteHash } from '@vueuse/router'
|
||||
import {
|
||||
NAlert,
|
||||
NButton,
|
||||
NCard,
|
||||
NDataTable,
|
||||
@@ -123,11 +124,12 @@ onMounted(async () => {
|
||||
|
||||
<template>
|
||||
<NLayout>
|
||||
<NSpin v-if="!biliAuth.id && useAuth.currentToken" :show="useAuth.isLoading" />
|
||||
<NSpin v-if="useAuth.isLoading && useAuth.currentToken" :show="useAuth.isLoading" />
|
||||
<NLayoutContent
|
||||
v-else-if="!useAuth.currentToken && useAuth.biliTokens.length > 0"
|
||||
v-else-if="(!useAuth.currentToken && useAuth.biliTokens.length > 0) || useAuth.isInvalid"
|
||||
style="height: 100vh; padding: 50px"
|
||||
>
|
||||
<NAlert v-if="useAuth.isInvalid" type="error"> 当前登录的 Bilibili 账号已失效 </NAlert>
|
||||
<NCard title="选择B站账号" embedded>
|
||||
<template #header-extra>
|
||||
<NButton type="primary" @click="$router.push({ name: 'bili-auth' })" size="small" secondary
|
||||
@@ -142,6 +144,7 @@ onMounted(async () => {
|
||||
</NCard>
|
||||
</NLayoutContent>
|
||||
<NLayoutContent v-else-if="!useAuth.currentToken" style="height: 100vh">
|
||||
<NAlert v-if="useAuth.isInvalid" type="error"> 当前登录的 Bilibili 账号已失效 </NAlert>
|
||||
<NResult status="error" title="你还未进行过B站账户验证" description="请先进行认证" style="padding-top: 64px">
|
||||
<template #footer>
|
||||
<NButton type="primary" @click="$router.push({ name: 'bili-auth' })">去认证</NButton>
|
||||
@@ -156,7 +159,7 @@ onMounted(async () => {
|
||||
</NLayoutHeader>
|
||||
<NLayoutContent content-style="padding: 24px;">
|
||||
<NFlex align="center" justify="center">
|
||||
<div style="max-width: 95vw; width: 900px">
|
||||
<div style="max-width: 95vw; width: 1200px">
|
||||
<NCard title="我的信息">
|
||||
<NDescriptions label-placement="left" bordered size="small">
|
||||
<NDescriptionsItem label="用户名">
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
NSelect,
|
||||
NSpin,
|
||||
NTag,
|
||||
NText,
|
||||
useMessage,
|
||||
} from 'naive-ui'
|
||||
import { computed, ref } from 'vue'
|
||||
@@ -284,7 +285,7 @@ function logout() {
|
||||
<NListItem v-for="item in useAuth.biliTokens" :key="item.token" @click="switchAuth(item.token)">
|
||||
<NFlex align="center">
|
||||
<NTag v-if="useAuth.biliToken == item.token" type="info"> 当前账号 </NTag>
|
||||
{{ item.uId }}
|
||||
{{ item.name }} <NDivider vertical style="margin: 0" /><NText depth="3"> {{ item.uId }} </NText>
|
||||
</NFlex>
|
||||
</NListItem>
|
||||
</NList>
|
||||
|
||||
@@ -247,7 +247,7 @@ onUnmounted(() => {
|
||||
<NImage v-if="item.question.image" :src="item.question.image" height="100" lazy />
|
||||
</NCard>
|
||||
<template v-if="item.answer" #footer>
|
||||
<NSpace align="center" :size="6">
|
||||
<NSpace align="center" :size="6" :wrap="false">
|
||||
<NAvatar :src="biliInfo.face + '@64w'" circle :size="45" :img-props="{ referrerpolicy: 'no-referrer' }" />
|
||||
<NDivider vertical />
|
||||
<NText style="font-size: 16px">
|
||||
|
||||
104
src/views/view/forumViews/ForumCommentItem.vue
Normal file
104
src/views/view/forumViews/ForumCommentItem.vue
Normal file
@@ -0,0 +1,104 @@
|
||||
<script setup lang="ts">
|
||||
import { useAccount } from '@/api/account'
|
||||
import { ForumCommentModel, ForumTopicModel } from '@/api/models/forum'
|
||||
import { VTSURU_API_URL } from '@/data/constants'
|
||||
import { useForumStore } from '@/store/useForumStore'
|
||||
import { ArrowReply16Filled } from '@vicons/fluent'
|
||||
import { Heart, HeartOutline } from '@vicons/ionicons5'
|
||||
import { NAvatar, NButton, NCard, NDivider, NFlex, NIcon, NText, NTime, NTooltip } from 'naive-ui'
|
||||
import ForumReplyItem from './ForumReplyItem.vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
item: ForumCommentModel
|
||||
topic: ForumTopicModel
|
||||
}>()
|
||||
|
||||
const useForum = useForumStore()
|
||||
const accountInfo = useAccount()
|
||||
|
||||
const canOprate = computed(() => {
|
||||
return !props.topic.isLocked && accountInfo.value.id > 0
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NFlex>
|
||||
<NAvatar
|
||||
:src="VTSURU_API_URL + 'user-face/' + item.user.id + '?size=64'"
|
||||
:img-props="{ referrerpolicy: 'no-referrer' }"
|
||||
/>
|
||||
<NFlex vertical style="flex: 1" :size="2">
|
||||
<NFlex>
|
||||
<NText>
|
||||
{{ item.user.name }}
|
||||
</NText>
|
||||
<NText depth="3">
|
||||
<NTooltip>
|
||||
<template #trigger>
|
||||
<NTime :time="item.sendAt" type="relative" />
|
||||
</template>
|
||||
<NTime :time="item.sendAt" />
|
||||
</NTooltip>
|
||||
</NText>
|
||||
</NFlex>
|
||||
<div class="editor-content-view" v-html="item.content"></div>
|
||||
<NDivider style="margin: 0" />
|
||||
<NFlex>
|
||||
<NTooltip>
|
||||
<template #trigger>
|
||||
<NButton
|
||||
size="small"
|
||||
@click="
|
||||
useForum.LikeComment(item.id, !item.isLiked).then((success) => {
|
||||
if (success) {
|
||||
item.isLiked = !item.isLiked
|
||||
item.likeCount += item.isLiked ? 1 : -1
|
||||
}
|
||||
})
|
||||
"
|
||||
text
|
||||
:loading="useForum.isLikeLoading"
|
||||
:disabled="!canOprate"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon :component="item.isLiked ? Heart : HeartOutline" :color="item.isLiked ? '#dd484f' : ''" />
|
||||
</template>
|
||||
{{ item.likeCount }}
|
||||
</NButton>
|
||||
</template>
|
||||
点赞
|
||||
</NTooltip>
|
||||
<NTooltip>
|
||||
<template #trigger>
|
||||
<NButton
|
||||
size="small"
|
||||
@click="useForum.SetReplyingComment(item)"
|
||||
text
|
||||
:disabled="!canOprate"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon :component="ArrowReply16Filled" />
|
||||
</template>
|
||||
{{ item.replies.length }}
|
||||
</NButton>
|
||||
</template>
|
||||
回复
|
||||
</NTooltip>
|
||||
</NFlex>
|
||||
<NCard v-if="item.replies.length > 0" size="small">
|
||||
<NFlex vertical>
|
||||
<ForumReplyItem
|
||||
v-for="reply in item.replies"
|
||||
:key="reply.id"
|
||||
:item="reply"
|
||||
:comment="item"
|
||||
:topic="topic"
|
||||
showReplyButton
|
||||
:reply-to="reply.replyTo ? item.replies.find((r) => r.id === reply.replyTo) : undefined"
|
||||
/>
|
||||
</NFlex>
|
||||
</NCard>
|
||||
</NFlex>
|
||||
</NFlex>
|
||||
</template>
|
||||
101
src/views/view/forumViews/ForumPreviewItem.vue
Normal file
101
src/views/view/forumViews/ForumPreviewItem.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<script setup lang="ts">
|
||||
import { ForumModel, ForumTopicBaseModel } from '@/api/models/forum'
|
||||
import { useForumStore } from '@/store/useForumStore'
|
||||
import { ArrowReply24Filled, Chat24Regular, MoreVertical24Filled, Star24Filled } from '@vicons/fluent'
|
||||
import { NButton, NDropdown, NFlex, NIcon, NTag, NText, NTime, NTooltip, useDialog } from 'naive-ui'
|
||||
|
||||
const props = defineProps<{
|
||||
item: ForumTopicBaseModel
|
||||
forum: ForumModel
|
||||
}>()
|
||||
|
||||
const useForum = useForumStore()
|
||||
const dialog = useDialog()
|
||||
|
||||
function onDropdownSelect(key: string) {
|
||||
switch (key) {
|
||||
case 'delete':
|
||||
dialog.warning({
|
||||
title: '警告',
|
||||
content: '确定要删除这条话题吗?',
|
||||
positiveText: '确定',
|
||||
negativeText: '再想想',
|
||||
onPositiveClick: () => {
|
||||
useForum.DelTopic(props.item.id).then((success) => {
|
||||
if (success) {
|
||||
setTimeout(() => {
|
||||
window.location.reload()
|
||||
}, 1000)
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
break
|
||||
case 'top':
|
||||
dialog.info({
|
||||
title: '问',
|
||||
content: `确定要${props.item.isPinned ? '取消' : ''}置顶这条话题吗?`,
|
||||
positiveText: '确定',
|
||||
negativeText: '再想想',
|
||||
onPositiveClick: () => {
|
||||
useForum.SetTopicTop(props.item.id, !props.item.isPinned).then((success) => {
|
||||
if (success) {
|
||||
props.item.isPinned = !props.item.isPinned
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NFlex align="center">
|
||||
<NFlex align="center" :wrap="false">
|
||||
<NTag v-if="item.isPinned" size="small" round>
|
||||
<NIcon :component="Star24Filled" color="#dba913" />
|
||||
</NTag>
|
||||
<NTag size="small" style="color: gray">
|
||||
<template #icon>
|
||||
<NIcon :component="Chat24Regular" />
|
||||
</template>
|
||||
{{ item.commentCount }}
|
||||
</NTag>
|
||||
<NText style="font-size: large">
|
||||
{{ item.title }}
|
||||
</NText>
|
||||
</NFlex>
|
||||
<NFlex style="flex: 1; color: gray; font-size: small" justify="end" align="center">
|
||||
<template v-if="item.latestRepliedBy">
|
||||
<span>
|
||||
<NIcon :component="ArrowReply24Filled" size="15" />
|
||||
@{{ item.latestRepliedBy.name }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-else> @{{ item.user?.name }} 发布于 </template>
|
||||
<NTooltip>
|
||||
<template #trigger>
|
||||
<NTime :time="item.createAt" type="relative" />
|
||||
</template>
|
||||
<NTime :time="item.createAt" />
|
||||
</NTooltip>
|
||||
<NDropdown
|
||||
v-if="forum.isAdmin"
|
||||
:options="[
|
||||
{ label: '删除', key: 'delete' },
|
||||
{ label: item.isPinned ? '取消置顶' : '置顶', key: 'top' },
|
||||
]"
|
||||
trigger="hover"
|
||||
@select="onDropdownSelect"
|
||||
>
|
||||
<NButton text>
|
||||
<template #icon>
|
||||
<NIcon :component="MoreVertical24Filled" />
|
||||
</template>
|
||||
</NButton>
|
||||
</NDropdown>
|
||||
</NFlex>
|
||||
</NFlex>
|
||||
</template>
|
||||
82
src/views/view/forumViews/ForumReplyItem.vue
Normal file
82
src/views/view/forumViews/ForumReplyItem.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<script setup lang="ts">
|
||||
import { getUserAvatarUrl } from '@/Utils'
|
||||
import { useAccount } from '@/api/account'
|
||||
import { ForumCommentModel, ForumReplyModel, ForumTopicModel } from '@/api/models/forum'
|
||||
import { useForumStore } from '@/store/useForumStore'
|
||||
import { ArrowReply16Filled } from '@vicons/fluent'
|
||||
import { NAvatar, NButton, NCard, NFlex, NIcon, NText, NTime, NTooltip } from 'naive-ui'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
item: ForumReplyModel
|
||||
replyTo?: ForumReplyModel
|
||||
comment: ForumCommentModel
|
||||
topic: ForumTopicModel
|
||||
showReplyButton?: boolean
|
||||
}>()
|
||||
|
||||
const useForum = useForumStore()
|
||||
const accountInfo = useAccount()
|
||||
|
||||
const canOprate = computed(() => {
|
||||
return !props.topic.isLocked && accountInfo.value.id > 0
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NFlex align="center" class="forum-reply-item">
|
||||
<NFlex :wrap="false" align="center">
|
||||
<NTooltip v-if="replyTo">
|
||||
<template #trigger>
|
||||
<NIcon :component="ArrowReply16Filled" />
|
||||
</template>
|
||||
<ForumReplyItem :item="replyTo" :comment="comment" :topic="topic" :show-reply-button="false" />
|
||||
</NTooltip>
|
||||
<NAvatar
|
||||
:src="getUserAvatarUrl(item.user.id)"
|
||||
:img-props="{ referrerpolicy: 'no-referrer' }"
|
||||
size="small"
|
||||
round
|
||||
style="margin-top: -3px; min-width: 28px; min-height: 28px"
|
||||
/>
|
||||
<NText strong depth="3" style="white-space: nowrap">
|
||||
{{ item.user.name }}
|
||||
</NText>
|
||||
</NFlex>
|
||||
{{ item.content }}
|
||||
<NFlex justify="end" align="center" :wrap="false" style="flex: 1">
|
||||
<NTooltip>
|
||||
<template #trigger>
|
||||
<NText depth="3" style="font-size: small; min-width: 50px">
|
||||
<NTime :time="item.sendAt" type="relative" />
|
||||
</NText>
|
||||
</template>
|
||||
<NTime :time="item.sendAt" />
|
||||
</NTooltip>
|
||||
<NTooltip v-if="showReplyButton">
|
||||
<template #trigger>
|
||||
<NButton
|
||||
size="tiny"
|
||||
@click="useForum.SetReplyingComment(comment, item)"
|
||||
round
|
||||
secondary
|
||||
:disabled="!canOprate"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon :component="ArrowReply16Filled" />
|
||||
</template>
|
||||
</NButton>
|
||||
</template>
|
||||
回复这条回复
|
||||
</NTooltip>
|
||||
</NFlex>
|
||||
</NFlex>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@media screen and (min-width: 900px) {
|
||||
.forum-reply-item {
|
||||
flex-wrap: nowrap !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
287
src/views/view/forumViews/ForumTopicDetail.vue
Normal file
287
src/views/view/forumViews/ForumTopicDetail.vue
Normal file
@@ -0,0 +1,287 @@
|
||||
<script setup lang="ts">
|
||||
import { getUserAvatarUrl } from '@/Utils'
|
||||
import { UserInfo } from '@/api/api-models'
|
||||
import { ForumCommentModel, ForumCommentSortTypes, ForumTopicModel } from '@/api/models/forum'
|
||||
import '@/assets/forumContentStyle.css'
|
||||
import TurnstileVerify from '@/components/TurnstileVerify.vue'
|
||||
import VEditor from '@/components/VEditor.vue'
|
||||
import { VTSURU_API_URL } from '@/data/constants'
|
||||
import { useForumStore } from '@/store/useForumStore'
|
||||
import { ArrowCircleLeft12Filled, ArrowCircleLeft12Regular, Comment24Regular, Eye24Regular } from '@vicons/fluent'
|
||||
import { Heart, HeartOutline } from '@vicons/ionicons5'
|
||||
import {
|
||||
NAvatar,
|
||||
NAvatarGroup,
|
||||
NBackTop,
|
||||
NBadge,
|
||||
NButton,
|
||||
NCard,
|
||||
NDivider,
|
||||
NEllipsis,
|
||||
NEmpty,
|
||||
NFlex,
|
||||
NIcon,
|
||||
NInput,
|
||||
NList,
|
||||
NListItem,
|
||||
NModal,
|
||||
NText,
|
||||
NTime,
|
||||
NTooltip,
|
||||
useMessage,
|
||||
} from 'naive-ui'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import ForumCommentItem from './ForumCommentItem.vue'
|
||||
import ForumReplyItem from './ForumReplyItem.vue'
|
||||
import { useAccount } from '@/api/account'
|
||||
|
||||
type PostCommentModel = {
|
||||
content: string
|
||||
topic: number
|
||||
}
|
||||
type PostReplyModel = {
|
||||
content: string
|
||||
comment: number
|
||||
replyTo?: number
|
||||
}
|
||||
|
||||
const { biliInfo, userInfo } = defineProps<{
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
biliInfo: any | undefined
|
||||
userInfo: UserInfo | undefined
|
||||
}>()
|
||||
|
||||
const route = useRoute()
|
||||
const message = useMessage()
|
||||
const accountInfo = useAccount()
|
||||
|
||||
const topicId = ref(-1)
|
||||
const useForum = useForumStore()
|
||||
|
||||
const token = ref('')
|
||||
const turnstile = ref()
|
||||
const editorRef = ref()
|
||||
|
||||
const showCommentModal = ref(false)
|
||||
const currentCommentContent = ref<PostCommentModel>({} as PostCommentModel)
|
||||
|
||||
const currentReplyContent = ref<PostReplyModel>({} as PostReplyModel)
|
||||
|
||||
const topic = ref<ForumTopicModel>({ id: -1 } as ForumTopicModel)
|
||||
const comments = ref<ForumCommentModel[]>([])
|
||||
const ps = ref(20)
|
||||
const pn = ref(0)
|
||||
const sort = ref(ForumCommentSortTypes.Time)
|
||||
|
||||
const canOprate = computed(() => {
|
||||
return !topic.value.isLocked && accountInfo.value.id > 0
|
||||
})
|
||||
|
||||
async function postComment() {
|
||||
if (!topic.value.id) return
|
||||
if (!currentCommentContent.value.content) {
|
||||
message.error('评论内容不能为空')
|
||||
return
|
||||
}
|
||||
currentCommentContent.value.topic = topic.value.id
|
||||
useForum
|
||||
.PostComment(currentCommentContent.value, token.value)
|
||||
.then(async (comment) => {
|
||||
if (comment) {
|
||||
comments.value = (await useForum.GetComments(topic.value.id, pn.value, ps.value, sort.value)) ?? []
|
||||
currentCommentContent.value = {} as PostCommentModel
|
||||
showCommentModal.value = false
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
turnstile.value?.reset()
|
||||
})
|
||||
}
|
||||
async function postReply() {
|
||||
if (!topic.value.id) return
|
||||
if (!currentReplyContent.value.content) {
|
||||
message.error('回复内容不能为空')
|
||||
return
|
||||
}
|
||||
currentReplyContent.value.comment = useForum.replyingComment?.id ?? -1
|
||||
currentReplyContent.value.replyTo = useForum.replyingReply?.id
|
||||
useForum
|
||||
.PostReply(currentReplyContent.value, token.value)
|
||||
.then(async (comment) => {
|
||||
if (comment) {
|
||||
comments.value = (await useForum.GetComments(topic.value.id, pn.value, ps.value, sort.value)) ?? []
|
||||
currentReplyContent.value = {} as PostReplyModel
|
||||
useForum.SetReplyingComment()
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
turnstile.value?.reset()
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (route.params.topicId) {
|
||||
topicId.value = route.params.topicId as unknown as number
|
||||
topic.value = (await useForum.GetTopicDetail(topicId.value)) ?? ({ id: -1 } as ForumTopicModel)
|
||||
comments.value = (await useForum.GetComments(topicId.value, pn.value, ps.value, sort.value)) ?? []
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="!topic.id"> </template>
|
||||
<template v-else>
|
||||
<div size="small" embedded style="max-width: 1500px; margin: 0 auto">
|
||||
<NBackTop />
|
||||
<NBadge class="back-forum-badge" style="width: 100%; left: 0" type="info" :offset="[3, 3]">
|
||||
<NCard size="small">
|
||||
<NText style="font-size: large; font-weight: bold; text-align: center; width: 100%">
|
||||
<NEllipsis style="width: 100%">
|
||||
{{ topic.title }}
|
||||
</NEllipsis>
|
||||
</NText>
|
||||
</NCard>
|
||||
<template #value>
|
||||
<NTooltip>
|
||||
<template #trigger>
|
||||
<NButton text @click="() => $router.push({ name: 'user-forum', params: { id: userInfo?.name } })">
|
||||
<template #icon>
|
||||
<NIcon :component="ArrowCircleLeft12Regular" color="white" />
|
||||
</template>
|
||||
</NButton>
|
||||
</template>
|
||||
返回
|
||||
</NTooltip>
|
||||
</template>
|
||||
</NBadge>
|
||||
<NCard content-style="padding: 0 12px 0 12px;" embedded>
|
||||
<template #header>
|
||||
<NFlex align="center" :size="5">
|
||||
<NAvatar
|
||||
:src="VTSURU_API_URL + 'user-face/' + topic?.user?.id + '?size=64'"
|
||||
:img-props="{ referrerpolicy: 'no-referrer' }"
|
||||
/>
|
||||
<NDivider vertical />
|
||||
{{ topic.user?.name }}
|
||||
</NFlex>
|
||||
</template>
|
||||
<template #header-extra>
|
||||
<NTooltip>
|
||||
<template #trigger>
|
||||
<NText depth="3">
|
||||
<NTime :time="topic.createAt" type="relative" />
|
||||
</NText>
|
||||
</template>
|
||||
<NTime :time="topic.createAt" />
|
||||
</NTooltip>
|
||||
</template>
|
||||
<template #footer>
|
||||
<NAvatarGroup
|
||||
:size="30"
|
||||
:options="topic.sampleLikedBy?.map((u) => ({ src: getUserAvatarUrl(u) })) ?? []"
|
||||
:img-props="{ referrerpolicy: 'no-referrer' }"
|
||||
/>
|
||||
<NDivider style="margin: 5px 0 10px 0" />
|
||||
<NFlex>
|
||||
<NTooltip>
|
||||
<template #trigger>
|
||||
<NButton size="small" :bordered="topic.isLiked" text>
|
||||
<template #icon>
|
||||
<NIcon :component="Eye24Regular" />
|
||||
</template>
|
||||
{{ topic.viewCount }}
|
||||
</NButton>
|
||||
</template>
|
||||
浏览
|
||||
</NTooltip>
|
||||
<NTooltip>
|
||||
<template #trigger>
|
||||
<NButton
|
||||
size="small"
|
||||
@click="
|
||||
useForum.LikeTopic(topic.id, !topic.isLiked).then((success) => {
|
||||
if (success) {
|
||||
topic.isLiked = !topic.isLiked
|
||||
topic.likeCount += topic.isLiked ? 1 : -1
|
||||
}
|
||||
})
|
||||
"
|
||||
:bordered="topic.isLiked"
|
||||
secondary
|
||||
:type="topic.isLiked ? 'primary' : 'default'"
|
||||
:loading="useForum.isLikeLoading"
|
||||
:disabled="!canOprate"
|
||||
>
|
||||
<template #icon>
|
||||
<NIcon :component="topic.isLiked ? Heart : HeartOutline" :color="topic.isLiked ? '#dd484f' : ''" />
|
||||
</template>
|
||||
{{ topic.likeCount }}
|
||||
</NButton>
|
||||
</template>
|
||||
点赞
|
||||
</NTooltip>
|
||||
<NTooltip>
|
||||
<template #trigger>
|
||||
<NButton size="small" @click="showCommentModal = true" secondary :disabled="!canOprate">
|
||||
<template #icon>
|
||||
<NIcon :component="Comment24Regular" />
|
||||
</template>
|
||||
{{ topic.commentCount }}
|
||||
</NButton>
|
||||
</template>
|
||||
评论
|
||||
</NTooltip>
|
||||
</NFlex>
|
||||
</template>
|
||||
<div class="editor-content-view" v-html="topic.content"></div>
|
||||
</NCard>
|
||||
<NDivider>
|
||||
<NButton @click="showCommentModal = true" type="primary" :disabled="!canOprate">发送评论</NButton>
|
||||
</NDivider>
|
||||
<NEmpty v-if="comments.length === 0" description="暂无评论" />
|
||||
<NList v-else hoverable bordered size="small">
|
||||
<NListItem v-for="item in comments" :key="item.id">
|
||||
<ForumCommentItem :item="item" :topic="topic" />
|
||||
</NListItem>
|
||||
</NList>
|
||||
<NDivider />
|
||||
</div>
|
||||
</template>
|
||||
<NModal v-model:show="showCommentModal" preset="card" style="width: 1000px; max-width: 90vw; height: auto">
|
||||
<template #header> 发送评论 </template>
|
||||
<VEditor v-model:value="currentCommentContent.content" :max-length="1111" ref="editorRef" />
|
||||
<NButton type="primary" @click="postComment" :loading="!token || useForum.isLoading"> 发布 </NButton>
|
||||
</NModal>
|
||||
<NModal v-model:show="useForum.showReplyModal" preset="card" style="width: 1000px; max-width: 90vw; height: auto">
|
||||
<template #header> 发送回复 </template>
|
||||
<template v-if="useForum.replyingReply">
|
||||
<NCard size="small" title="正在回复" embedded>
|
||||
<ForumReplyItem
|
||||
v-if="useForum.replyingReply && useForum.replyingComment"
|
||||
:item="useForum.replyingReply"
|
||||
:comment="useForum.replyingComment"
|
||||
:topic="topic"
|
||||
:show-reply-button="false"
|
||||
/>
|
||||
</NCard>
|
||||
<NDivider />
|
||||
</template>
|
||||
<NInput
|
||||
v-model:value="currentReplyContent.content"
|
||||
type="textarea"
|
||||
placeholder="回复内容"
|
||||
maxlength="233"
|
||||
show-count
|
||||
/>
|
||||
<NDivider />
|
||||
<NButton type="primary" @click="postReply" :loading="!token || useForum.isLoading"> 发布 </NButton>
|
||||
</NModal>
|
||||
<TurnstileVerify ref="turnstile" v-model="token" />
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.n-badge-sup {
|
||||
left: 0 !important;
|
||||
}
|
||||
</style>
|
||||
163
src/views/view/forumViews/ForumView.vue
Normal file
163
src/views/view/forumViews/ForumView.vue
Normal file
@@ -0,0 +1,163 @@
|
||||
<script setup lang="ts">
|
||||
import { UserInfo } from '@/api/api-models'
|
||||
import { ForumPostTopicModel, ForumTopicBaseModel, ForumTopicSortTypes, ForumUserLevels } from '@/api/models/forum'
|
||||
import TurnstileVerify from '@/components/TurnstileVerify.vue'
|
||||
import VEditor from '@/components/VEditor.vue'
|
||||
import { TURNSTILE_KEY } from '@/data/constants'
|
||||
import { useForumStore } from '@/store/useForumStore'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import {
|
||||
NAlert,
|
||||
NButton,
|
||||
NCard,
|
||||
NDivider,
|
||||
NFlex,
|
||||
NInput,
|
||||
NList,
|
||||
NListItem,
|
||||
NModal,
|
||||
NText,
|
||||
NTime,
|
||||
useMessage,
|
||||
} from 'naive-ui'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import VueTurnstile from 'vue-turnstile'
|
||||
import ForumPreviewItem from './ForumPreviewItem.vue'
|
||||
import ForumCommentItem from './ForumCommentItem.vue'
|
||||
|
||||
const { biliInfo, userInfo } = defineProps<{
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
biliInfo: any | undefined
|
||||
userInfo: UserInfo | undefined
|
||||
}>()
|
||||
const token = ref('')
|
||||
const turnstile = ref()
|
||||
const editor = ref()
|
||||
|
||||
const postTopicBackup = useStorage<{ [key: number]: ForumPostTopicModel }>('Forum.PostTopic', {})
|
||||
const showPostTopicModal = ref(false)
|
||||
const currentPostTopicModel = ref<ForumPostTopicModel>({} as ForumPostTopicModel)
|
||||
const lastBackupTopic = ref(Date.now())
|
||||
|
||||
const useForum = useForumStore()
|
||||
const message = useMessage()
|
||||
const ps = ref(20)
|
||||
const pn = ref(0)
|
||||
const sort = ref(ForumTopicSortTypes.Time)
|
||||
|
||||
const forumInfo = ref(await useForum.GetForumInfo(userInfo?.id ?? -1))
|
||||
const topics = ref<{ data: ForumTopicBaseModel[]; total: number; more: boolean } | undefined>({
|
||||
data: [],
|
||||
total: 0,
|
||||
more: false,
|
||||
})
|
||||
|
||||
async function ApplyToForum() {
|
||||
if (!forumInfo.value) return
|
||||
if (await useForum.ApplyToForum(forumInfo.value.owner.id ?? -1)) {
|
||||
forumInfo.value.isApplied = true
|
||||
}
|
||||
}
|
||||
function backupTopic() {
|
||||
if (!showPostTopicModal.value) {
|
||||
return
|
||||
}
|
||||
postTopicBackup.value[forumInfo.value?.owner.id ?? -1] = currentPostTopicModel.value
|
||||
lastBackupTopic.value = Date.now()
|
||||
}
|
||||
function postTopic() {
|
||||
currentPostTopicModel.value.owner = forumInfo.value?.owner.id ?? -1
|
||||
useForum
|
||||
.PostTopic(currentPostTopicModel.value, token.value)
|
||||
.then(async (topic) => {
|
||||
if (topic) {
|
||||
currentPostTopicModel.value = {} as ForumPostTopicModel
|
||||
delete postTopicBackup.value[forumInfo.value?.owner.id ?? -1]
|
||||
showPostTopicModal.value = false
|
||||
topics.value = await useForum.GetTopics(forumInfo.value?.owner.id ?? -1, ps.value, pn.value, sort.value)
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
turnstile.value?.reset()
|
||||
})
|
||||
}
|
||||
|
||||
let timer: any
|
||||
onMounted(async () => {
|
||||
if (forumInfo.value) {
|
||||
topics.value = await useForum.GetTopics(forumInfo.value.owner.id ?? -1, ps.value, pn.value, sort.value)
|
||||
if (postTopicBackup.value[forumInfo.value.owner.id ?? -1]) {
|
||||
currentPostTopicModel.value = postTopicBackup.value[forumInfo.value.owner.id ?? -1]
|
||||
}
|
||||
timer = setInterval(async () => {
|
||||
backupTopic()
|
||||
}, 10000)
|
||||
}
|
||||
})
|
||||
onUnmounted(() => {
|
||||
clearInterval(timer)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NAlert v-if="!forumInfo" type="error"> 用户未创建粉丝讨论区 </NAlert>
|
||||
<NCard
|
||||
v-else-if="
|
||||
(forumInfo.level < ForumUserLevels.Member && forumInfo.settings.requireApply) ||
|
||||
forumInfo.settings.allowedViewerLevel > forumInfo.level
|
||||
"
|
||||
>
|
||||
<NAlert type="warning"> 你需要成为成员才能访问 </NAlert>
|
||||
<NAlert v-if="forumInfo.isApplied" type="success"> 已申请, 正在等待管理员审核 </NAlert>
|
||||
<NCard v-else title="加入">
|
||||
加入 {{ forumInfo.name }}
|
||||
<NButton type="primary" @click="ApplyToForum" :loading="useForum.isLoading">
|
||||
{{ forumInfo.settings.requireApply ? '申请' : '' }}加入
|
||||
</NButton>
|
||||
</NCard>
|
||||
</NCard>
|
||||
<template v-else>
|
||||
<NFlex vertical>
|
||||
<NCard size="small">
|
||||
<template #header>
|
||||
<NFlex justify="center">
|
||||
<NText style="font-size: large">{{ forumInfo.name }}</NText>
|
||||
</NFlex>
|
||||
</template>
|
||||
</NCard>
|
||||
<NFlex>
|
||||
<NCard style="max-width: 300px">
|
||||
<NFlex vertical>
|
||||
<NButton @click="showPostTopicModal = true"> 发布话题 </NButton>
|
||||
</NFlex>
|
||||
</NCard>
|
||||
<NList bordered style="flex: 1" size="small" hoverable clickable>
|
||||
<NListItem v-for="item in topics?.data ?? []" :key="item.id">
|
||||
<a :href="`${$route.path}/topic/${item.id}`" target="_blank">
|
||||
<ForumPreviewItem :item="item" :forum="forumInfo" />
|
||||
</a>
|
||||
</NListItem>
|
||||
</NList>
|
||||
</NFlex>
|
||||
</NFlex>
|
||||
<NModal preset="card" v-model:show="showPostTopicModal" style="width: 800px; max-width: 95%">
|
||||
<template #header>
|
||||
发布话题
|
||||
<NDivider vertical />
|
||||
<NText depth="3" style="font-size: small"> 保存于 <NTime :time="lastBackupTopic" format="HH:mm:ss" /> </NText>
|
||||
</template>
|
||||
<NFlex vertical>
|
||||
<NInput v-model:value="currentPostTopicModel.title" placeholder="标题" />
|
||||
<VEditor v-model:value="currentPostTopicModel.content" :max-length="2333" ref="editor" />
|
||||
<NButton type="primary" @click="postTopic" :loading="!token || useForum.isLoading"> 发布 </NButton>
|
||||
</NFlex>
|
||||
</NModal>
|
||||
<TurnstileVerify ref="turnstile" v-model="token" />
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
@@ -60,7 +60,7 @@ export const Config: TemplateConfig<ConfigType> = {
|
||||
:img-props="{
|
||||
referrerpolicy: 'no-referrer',
|
||||
}"
|
||||
:style="{ boxShadow: isDarkMode() ? 'rgb(195 192 192 / 35%) 0px 5px 20px' : '0 5px 15px rgba(0, 0, 0, 0.2)' }"
|
||||
:style="{ boxShadow: isDarkMode ? 'rgb(195 192 192 / 35%) 0px 5px 20px' : '0 5px 15px rgba(0, 0, 0, 0.2)' }"
|
||||
/>
|
||||
<NSpace align="baseline" justify="center">
|
||||
<NText strong style="font-size: 32px"> {{ biliInfo?.name }} </NText>
|
||||
|
||||
524
yarn.lock
524
yarn.lock
@@ -334,6 +334,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/runtime@npm:^7.12.0":
|
||||
version: 7.24.0
|
||||
resolution: "@babel/runtime@npm:7.24.0"
|
||||
dependencies:
|
||||
regenerator-runtime: "npm:^0.14.0"
|
||||
checksum: 3495eed727bf4a4f84c35bb51ab53317ae38f4bbc3b1d0a8303751f9dfa0ce6f5fb2afced72b76c3dd0d8bb2ccb84787559a4dee9886291a36b26f02f0f759b4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/runtime@npm:^7.21.0":
|
||||
version: 7.23.6
|
||||
resolution: "@babel/runtime@npm:7.23.6"
|
||||
@@ -961,6 +970,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@transloadit/prettier-bytes@npm:0.0.7":
|
||||
version: 0.0.7
|
||||
resolution: "@transloadit/prettier-bytes@npm:0.0.7"
|
||||
checksum: cb9e6ef84298300f6b6cb4e64ebb44927a9350476e2122a1a07b652639c0ab44feb4d3e11a7b06d093c92bf2b17485d3f715f8ea6388f6f2d5cf2bd59eddbd6c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@trysound/sax@npm:0.2.0":
|
||||
version: 0.2.0
|
||||
resolution: "@trysound/sax@npm:0.2.0"
|
||||
@@ -985,6 +1001,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/event-emitter@npm:^0.3.3":
|
||||
version: 0.3.5
|
||||
resolution: "@types/event-emitter@npm:0.3.5"
|
||||
checksum: 3ca5039c08376397ade2deac33b5e439dd20742599fc71005330328c4da7218906c4b9a2e01b9a1f868455fc0060f93eb4d51d0d3df8e101bb9a24eace5dcc51
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/json-schema@npm:*, @types/json-schema@npm:^7.0.12":
|
||||
version: 7.0.15
|
||||
resolution: "@types/json-schema@npm:7.0.15"
|
||||
@@ -1335,6 +1358,61 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@uppy/companion-client@npm:^2.2.2":
|
||||
version: 2.2.2
|
||||
resolution: "@uppy/companion-client@npm:2.2.2"
|
||||
dependencies:
|
||||
"@uppy/utils": "npm:^4.1.2"
|
||||
namespace-emitter: "npm:^2.0.1"
|
||||
checksum: ce9cec2c59ad5dcd0a7200f7d2efd01b7250f31c66593df1f2d1af9cce2d96a6f2a265ec891742ae30a0aae44b1337a619bd1f7f60912ddf23a56fb81ebb4791
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@uppy/core@npm:^2.1.1":
|
||||
version: 2.3.4
|
||||
resolution: "@uppy/core@npm:2.3.4"
|
||||
dependencies:
|
||||
"@transloadit/prettier-bytes": "npm:0.0.7"
|
||||
"@uppy/store-default": "npm:^2.1.1"
|
||||
"@uppy/utils": "npm:^4.1.3"
|
||||
lodash.throttle: "npm:^4.1.1"
|
||||
mime-match: "npm:^1.0.2"
|
||||
namespace-emitter: "npm:^2.0.1"
|
||||
nanoid: "npm:^3.1.25"
|
||||
preact: "npm:^10.5.13"
|
||||
checksum: 68aeb5ce28934c3f599a53814f7b3690edd086e152f3df88feff7adc3cbb16c45cf3b6430e759b07540068556aecc7e1e944b5cd5290bac6f0a0f840bb327b17
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@uppy/store-default@npm:^2.1.1":
|
||||
version: 2.1.1
|
||||
resolution: "@uppy/store-default@npm:2.1.1"
|
||||
checksum: 7dc350cb58a5b91a75a302b741284f6a8eb168df15d8d497cf658a053fa8bd7a3e6e2670ba4384e9725e906ab46d21b823a651a21777b462a7dc338e56a447bc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@uppy/utils@npm:^4.1.2, @uppy/utils@npm:^4.1.3":
|
||||
version: 4.1.3
|
||||
resolution: "@uppy/utils@npm:4.1.3"
|
||||
dependencies:
|
||||
lodash.throttle: "npm:^4.1.1"
|
||||
checksum: d7e2c4c3d8f41d489c5c89baed4b64b69c3fe3acef2dded566dd33b9155ac21666db218c4337875513ec932b898d39fcc4fd07240aa0ac1d1b426779b0506b93
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@uppy/xhr-upload@npm:^2.0.3":
|
||||
version: 2.1.3
|
||||
resolution: "@uppy/xhr-upload@npm:2.1.3"
|
||||
dependencies:
|
||||
"@uppy/companion-client": "npm:^2.2.2"
|
||||
"@uppy/utils": "npm:^4.1.2"
|
||||
nanoid: "npm:^3.1.25"
|
||||
peerDependencies:
|
||||
"@uppy/core": ^2.3.3
|
||||
checksum: 999fc6b852d4765785909f3bf4f6fc7b258e83ba91f66de956f51c0fe2eb51eea4097ed881d5450fa9a3b2d275a159694fc40a65fff86d9867730967fdf283d3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vicons/fluent@npm:^0.12.0":
|
||||
version: 0.12.0
|
||||
resolution: "@vicons/fluent@npm:0.12.0"
|
||||
@@ -1563,6 +1641,162 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@wangeditor/basic-modules@npm:^1.1.7":
|
||||
version: 1.1.7
|
||||
resolution: "@wangeditor/basic-modules@npm:1.1.7"
|
||||
dependencies:
|
||||
is-url: "npm:^1.2.4"
|
||||
peerDependencies:
|
||||
"@wangeditor/core": 1.x
|
||||
dom7: ^3.0.0
|
||||
lodash.throttle: ^4.1.1
|
||||
nanoid: ^3.2.0
|
||||
slate: ^0.72.0
|
||||
snabbdom: ^3.1.0
|
||||
checksum: eb5f17897570feb9523fc9777658779b66ac251c358db5d0231a3d57796bd31cc8a5b8ebd99af6423c1fa9a4149a9258b82409204bd154654ca045874421fa21
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@wangeditor/code-highlight@npm:^1.0.3":
|
||||
version: 1.0.3
|
||||
resolution: "@wangeditor/code-highlight@npm:1.0.3"
|
||||
dependencies:
|
||||
prismjs: "npm:^1.23.0"
|
||||
peerDependencies:
|
||||
"@wangeditor/core": 1.x
|
||||
dom7: ^3.0.0
|
||||
slate: ^0.72.0
|
||||
snabbdom: ^3.1.0
|
||||
checksum: c27a0346991e8bbda46e28dae67b941e340ab03dea646be20fa706c52887860e19c8646193a9ca3d6143a3a54c36700ed7280d9a27ce60e306b0b3319dc467de
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@wangeditor/core@npm:^1.1.19":
|
||||
version: 1.1.19
|
||||
resolution: "@wangeditor/core@npm:1.1.19"
|
||||
dependencies:
|
||||
"@types/event-emitter": "npm:^0.3.3"
|
||||
event-emitter: "npm:^0.3.5"
|
||||
html-void-elements: "npm:^2.0.0"
|
||||
i18next: "npm:^20.4.0"
|
||||
scroll-into-view-if-needed: "npm:^2.2.28"
|
||||
slate-history: "npm:^0.66.0"
|
||||
peerDependencies:
|
||||
"@uppy/core": ^2.1.1
|
||||
"@uppy/xhr-upload": ^2.0.3
|
||||
dom7: ^3.0.0
|
||||
is-hotkey: ^0.2.0
|
||||
lodash.camelcase: ^4.3.0
|
||||
lodash.clonedeep: ^4.5.0
|
||||
lodash.debounce: ^4.0.8
|
||||
lodash.foreach: ^4.5.0
|
||||
lodash.isequal: ^4.5.0
|
||||
lodash.throttle: ^4.1.1
|
||||
lodash.toarray: ^4.4.0
|
||||
nanoid: ^3.2.0
|
||||
slate: ^0.72.0
|
||||
snabbdom: ^3.1.0
|
||||
checksum: 6211ecd6f2cdb517bc17cf60ed867a55e37991f3ee6c9fa344c299fe592f25ec7eefdb1a65753747fe2fc677867f713891189a50aa9bb4e8fc3aa04124b4c2ed
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@wangeditor/editor-for-vue@npm:^5.1.12":
|
||||
version: 5.1.12
|
||||
resolution: "@wangeditor/editor-for-vue@npm:5.1.12"
|
||||
peerDependencies:
|
||||
"@wangeditor/editor": ">=5.1.0"
|
||||
vue: ^3.0.5
|
||||
checksum: c6bb0665f9370ba8684789ec7d292fdc563361429e1aaa81cd6aa38900897ba844efd965611c3ac2a3bbb8960d11c783f246d0349e5f3fdb1d9cf2e0668c1fba
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@wangeditor/editor@npm:^5.1.23":
|
||||
version: 5.1.23
|
||||
resolution: "@wangeditor/editor@npm:5.1.23"
|
||||
dependencies:
|
||||
"@uppy/core": "npm:^2.1.1"
|
||||
"@uppy/xhr-upload": "npm:^2.0.3"
|
||||
"@wangeditor/basic-modules": "npm:^1.1.7"
|
||||
"@wangeditor/code-highlight": "npm:^1.0.3"
|
||||
"@wangeditor/core": "npm:^1.1.19"
|
||||
"@wangeditor/list-module": "npm:^1.0.5"
|
||||
"@wangeditor/table-module": "npm:^1.1.4"
|
||||
"@wangeditor/upload-image-module": "npm:^1.0.2"
|
||||
"@wangeditor/video-module": "npm:^1.1.4"
|
||||
dom7: "npm:^3.0.0"
|
||||
is-hotkey: "npm:^0.2.0"
|
||||
lodash.camelcase: "npm:^4.3.0"
|
||||
lodash.clonedeep: "npm:^4.5.0"
|
||||
lodash.debounce: "npm:^4.0.8"
|
||||
lodash.foreach: "npm:^4.5.0"
|
||||
lodash.isequal: "npm:^4.5.0"
|
||||
lodash.throttle: "npm:^4.1.1"
|
||||
lodash.toarray: "npm:^4.4.0"
|
||||
nanoid: "npm:^3.2.0"
|
||||
slate: "npm:^0.72.0"
|
||||
snabbdom: "npm:^3.1.0"
|
||||
checksum: 465e726bea84c4d70e269c47840d2652b6c400413a33d6acf08e4b9afc3392cef4d281420b334baf9d745494902ee9182b824fde1a23e4a878e0d1719a8d2403
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@wangeditor/list-module@npm:^1.0.5":
|
||||
version: 1.0.5
|
||||
resolution: "@wangeditor/list-module@npm:1.0.5"
|
||||
peerDependencies:
|
||||
"@wangeditor/core": 1.x
|
||||
dom7: ^3.0.0
|
||||
slate: ^0.72.0
|
||||
snabbdom: ^3.1.0
|
||||
checksum: b68e2f2d7c91b00453df3f1c98d1989675a084faa3706fedf5f7df0e1394b85eb2f405ebead747e5b7bd826ab18761ea79e736d6a9987e3665fd076658aa9fc0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@wangeditor/table-module@npm:^1.1.4":
|
||||
version: 1.1.4
|
||||
resolution: "@wangeditor/table-module@npm:1.1.4"
|
||||
peerDependencies:
|
||||
"@wangeditor/core": 1.x
|
||||
dom7: ^3.0.0
|
||||
lodash.isequal: ^4.5.0
|
||||
lodash.throttle: ^4.1.1
|
||||
nanoid: ^3.2.0
|
||||
slate: ^0.72.0
|
||||
snabbdom: ^3.1.0
|
||||
checksum: ac54dc52353d8ada4de917953055d55b907cc5e747e70a914cd118570d5311da0f08abfabe7ec61d81bf60dfabf2d985d0fcde856d9bcb89ec56a33e2461fd64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@wangeditor/upload-image-module@npm:^1.0.2":
|
||||
version: 1.0.2
|
||||
resolution: "@wangeditor/upload-image-module@npm:1.0.2"
|
||||
peerDependencies:
|
||||
"@uppy/core": ^2.0.3
|
||||
"@uppy/xhr-upload": ^2.0.3
|
||||
"@wangeditor/basic-modules": 1.x
|
||||
"@wangeditor/core": 1.x
|
||||
dom7: ^3.0.0
|
||||
lodash.foreach: ^4.5.0
|
||||
slate: ^0.72.0
|
||||
snabbdom: ^3.1.0
|
||||
checksum: b94996e89438a2d0a25603736147edecb746356ec0466e1a2331e27d1d83ca5a37f31828c6edc2791447f30c8744b99aa265ed771e9e8b00414488be6c537684
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@wangeditor/video-module@npm:^1.1.4":
|
||||
version: 1.1.4
|
||||
resolution: "@wangeditor/video-module@npm:1.1.4"
|
||||
peerDependencies:
|
||||
"@uppy/core": ^2.1.4
|
||||
"@uppy/xhr-upload": ^2.0.7
|
||||
"@wangeditor/core": 1.x
|
||||
dom7: ^3.0.0
|
||||
nanoid: ^3.2.0
|
||||
slate: ^0.72.0
|
||||
snabbdom: ^3.1.0
|
||||
checksum: 9806c4d41c994b144fc50748c14d1c380c419797d4a231970719147598dab52c5fe671d09a056c0c5e7fb1f6fafd22e414558cc3a055d4e9948abae53c790647
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"abbrev@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "abbrev@npm:2.0.0"
|
||||
@@ -2061,6 +2295,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"compute-scroll-into-view@npm:^1.0.20":
|
||||
version: 1.0.20
|
||||
resolution: "compute-scroll-into-view@npm:1.0.20"
|
||||
checksum: 19034322590bfce59cb6939b3603e7aaf6f0d4128b8627bbc136e71c8714905e2f8bf2ba0cb7f153c6e8cdb8ad907ffd6d0188ccc7625dc05790a59ae6a81f01
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"concat-map@npm:0.0.1":
|
||||
version: 0.0.1
|
||||
resolution: "concat-map@npm:0.0.1"
|
||||
@@ -2193,6 +2434,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"d@npm:1, d@npm:^1.0.1, d@npm:^1.0.2":
|
||||
version: 1.0.2
|
||||
resolution: "d@npm:1.0.2"
|
||||
dependencies:
|
||||
es5-ext: "npm:^0.10.64"
|
||||
type: "npm:^2.7.2"
|
||||
checksum: 3e6ede10cd3b77586c47da48423b62bed161bf1a48bdbcc94d87263522e22f5dfb0e678a6dba5323fdc14c5d8612b7f7eb9e7d9e37b2e2d67a7bf9f116dabe5a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"date-fns-tz@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "date-fns-tz@npm:2.0.0"
|
||||
@@ -2306,6 +2557,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"dom7@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "dom7@npm:3.0.0"
|
||||
dependencies:
|
||||
ssr-window: "npm:^3.0.0-alpha.1"
|
||||
checksum: c64c64e8ed99a6e1e9a8754b37c8effa5e5e3f192b275e7853cb06bf3c670fd6595fc61c87e75553773f350a04b9d45e2964c9b7b62b8fec867766c1a3b6df41
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"domelementtype@npm:^2.3.0":
|
||||
version: 2.3.0
|
||||
resolution: "domelementtype@npm:2.3.0"
|
||||
@@ -2493,6 +2753,39 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"es5-ext@npm:^0.10.35, es5-ext@npm:^0.10.62, es5-ext@npm:^0.10.64, es5-ext@npm:~0.10.14":
|
||||
version: 0.10.64
|
||||
resolution: "es5-ext@npm:0.10.64"
|
||||
dependencies:
|
||||
es6-iterator: "npm:^2.0.3"
|
||||
es6-symbol: "npm:^3.1.3"
|
||||
esniff: "npm:^2.0.1"
|
||||
next-tick: "npm:^1.1.0"
|
||||
checksum: 4459b6ae216f3c615db086e02437bdfde851515a101577fd61b19f9b3c1ad924bab4d197981eb7f0ccb915f643f2fc10ff76b97a680e96cbb572d15a27acd9a3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"es6-iterator@npm:^2.0.3":
|
||||
version: 2.0.3
|
||||
resolution: "es6-iterator@npm:2.0.3"
|
||||
dependencies:
|
||||
d: "npm:1"
|
||||
es5-ext: "npm:^0.10.35"
|
||||
es6-symbol: "npm:^3.1.1"
|
||||
checksum: 91f20b799dba28fb05bf623c31857fc1524a0f1c444903beccaf8929ad196c8c9ded233e5ac7214fc63a92b3f25b64b7f2737fcca8b1f92d2d96cf3ac902f5d8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"es6-symbol@npm:^3.1.1, es6-symbol@npm:^3.1.3":
|
||||
version: 3.1.4
|
||||
resolution: "es6-symbol@npm:3.1.4"
|
||||
dependencies:
|
||||
d: "npm:^1.0.2"
|
||||
ext: "npm:^1.7.0"
|
||||
checksum: 777bf3388db5d7919e09a0fd175aa5b8a62385b17cb2227b7a137680cba62b4d9f6193319a102642aa23d5840d38a62e4784f19cfa5be4a2210a3f0e9b23d15d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"esbuild@npm:^0.19.3":
|
||||
version: 0.19.10
|
||||
resolution: "esbuild@npm:0.19.10"
|
||||
@@ -2771,6 +3064,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"esniff@npm:^2.0.1":
|
||||
version: 2.0.1
|
||||
resolution: "esniff@npm:2.0.1"
|
||||
dependencies:
|
||||
d: "npm:^1.0.1"
|
||||
es5-ext: "npm:^0.10.62"
|
||||
event-emitter: "npm:^0.3.5"
|
||||
type: "npm:^2.7.2"
|
||||
checksum: 7efd8d44ac20e5db8cb0ca77eb65eca60628b2d0f3a1030bcb05e71cc40e6e2935c47b87dba3c733db12925aa5b897f8e0e7a567a2c274206f184da676ea2e65
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"espree@npm:^10.0.1":
|
||||
version: 10.0.1
|
||||
resolution: "espree@npm:10.0.1"
|
||||
@@ -2842,6 +3147,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"event-emitter@npm:^0.3.5":
|
||||
version: 0.3.5
|
||||
resolution: "event-emitter@npm:0.3.5"
|
||||
dependencies:
|
||||
d: "npm:1"
|
||||
es5-ext: "npm:~0.10.14"
|
||||
checksum: 75082fa8ffb3929766d0f0a063bfd6046bd2a80bea2666ebaa0cfd6f4a9116be6647c15667bea77222afc12f5b4071b68d393cf39fdaa0e8e81eda006160aff0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"event-target-shim@npm:^5.0.0":
|
||||
version: 5.0.1
|
||||
resolution: "event-target-shim@npm:5.0.1"
|
||||
@@ -2877,6 +3192,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ext@npm:^1.7.0":
|
||||
version: 1.7.0
|
||||
resolution: "ext@npm:1.7.0"
|
||||
dependencies:
|
||||
type: "npm:^2.7.2"
|
||||
checksum: a8e5f34e12214e9eee3a4af3b5c9d05ba048f28996450975b369fc86e5d0ef13b6df0615f892f5396a9c65d616213c25ec5b0ad17ef42eac4a500512a19da6c7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"extend-shallow@npm:^2.0.1":
|
||||
version: 2.0.1
|
||||
resolution: "extend-shallow@npm:2.0.1"
|
||||
@@ -3367,6 +3691,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"html-void-elements@npm:^2.0.0":
|
||||
version: 2.0.1
|
||||
resolution: "html-void-elements@npm:2.0.1"
|
||||
checksum: 1079c9e9fdb3b6a2481f2a282098a0183f3d45bf2b9d76c7dfc1671ee1857d7bacdd04fd8c6e2418f5ff550c30cabf97a010fe31ec402d0c89189807b48e6d79
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"html2canvas@npm:^1.4.1":
|
||||
version: 1.4.1
|
||||
resolution: "html2canvas@npm:1.4.1"
|
||||
@@ -3404,6 +3735,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"i18next@npm:^20.4.0":
|
||||
version: 20.6.1
|
||||
resolution: "i18next@npm:20.6.1"
|
||||
dependencies:
|
||||
"@babel/runtime": "npm:^7.12.0"
|
||||
checksum: f34b58a2b6cfb1b3337454610878ab27e4aeba58f5a7d1f970d9c550a8239fcbd24951cb3072dab8e7bdffbdc026248681b37507665d256af177b8ca6ec7b1a8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"iconv-lite@npm:^0.6.2":
|
||||
version: 0.6.3
|
||||
resolution: "iconv-lite@npm:0.6.3"
|
||||
@@ -3427,6 +3767,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"immer@npm:^9.0.6":
|
||||
version: 9.0.21
|
||||
resolution: "immer@npm:9.0.21"
|
||||
checksum: 03ea3ed5d4d72e8bd428df4a38ad7e483ea8308e9a113d3b42e0ea2cc0cc38340eb0a6aca69592abbbf047c685dbda04e3d34bf2ff438ab57339ed0a34cc0a05
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"import-fresh@npm:^3.2.1":
|
||||
version: 3.3.0
|
||||
resolution: "import-fresh@npm:3.3.0"
|
||||
@@ -3580,6 +3927,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-hotkey@npm:^0.2.0":
|
||||
version: 0.2.0
|
||||
resolution: "is-hotkey@npm:0.2.0"
|
||||
checksum: d6589159b641014e26c0999ad261ca191e4cb7192e7db32dc849e69df416699b5ec78ad2a2192c169e7b3838847e48b0e1fc76f89f61946fad3036b23cfcd9b4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-lambda@npm:^1.0.1":
|
||||
version: 1.0.1
|
||||
resolution: "is-lambda@npm:1.0.1"
|
||||
@@ -3617,6 +3971,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-plain-object@npm:^5.0.0":
|
||||
version: 5.0.0
|
||||
resolution: "is-plain-object@npm:5.0.0"
|
||||
checksum: 893e42bad832aae3511c71fd61c0bf61aa3a6d853061c62a307261842727d0d25f761ce9379f7ba7226d6179db2a3157efa918e7fe26360f3bf0842d9f28942c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-regex@npm:^1.1.4":
|
||||
version: 1.1.4
|
||||
resolution: "is-regex@npm:1.1.4"
|
||||
@@ -3663,6 +4024,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-url@npm:^1.2.4":
|
||||
version: 1.2.4
|
||||
resolution: "is-url@npm:1.2.4"
|
||||
checksum: 0157a79874f8f95fdd63540e3f38c8583c2ef572661cd0693cda80ae3e42dfe8e9a4a972ec1b827f861d9a9acf75b37f7d58a37f94a8a053259642912c252bc3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-weakref@npm:^1.0.2":
|
||||
version: 1.0.2
|
||||
resolution: "is-weakref@npm:1.0.2"
|
||||
@@ -3862,6 +4230,41 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lodash.camelcase@npm:^4.3.0":
|
||||
version: 4.3.0
|
||||
resolution: "lodash.camelcase@npm:4.3.0"
|
||||
checksum: fcba15d21a458076dd309fce6b1b4bf611d84a0ec252cb92447c948c533ac250b95d2e00955801ebc367e5af5ed288b996d75d37d2035260a937008e14eaf432
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lodash.clonedeep@npm:^4.5.0":
|
||||
version: 4.5.0
|
||||
resolution: "lodash.clonedeep@npm:4.5.0"
|
||||
checksum: 2caf0e4808f319d761d2939ee0642fa6867a4bbf2cfce43276698828380756b99d4c4fa226d881655e6ac298dd453fe12a5ec8ba49861777759494c534936985
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lodash.debounce@npm:^4.0.8":
|
||||
version: 4.0.8
|
||||
resolution: "lodash.debounce@npm:4.0.8"
|
||||
checksum: 762998a63e095412b6099b8290903e0a8ddcb353ac6e2e0f2d7e7d03abd4275fe3c689d88960eb90b0dde4f177554d51a690f22a343932ecbc50a5d111849987
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lodash.foreach@npm:^4.5.0":
|
||||
version: 4.5.0
|
||||
resolution: "lodash.foreach@npm:4.5.0"
|
||||
checksum: bd9cc83e87e805b21058ce6cf718dd22db137c7ca08eddbd608549db59989911c571b7195707f615cb37f27bb4f9a9fa9980778940d768c24095f5a04b244c84
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lodash.isequal@npm:^4.5.0":
|
||||
version: 4.5.0
|
||||
resolution: "lodash.isequal@npm:4.5.0"
|
||||
checksum: dfdb2356db19631a4b445d5f37868a095e2402292d59539a987f134a8778c62a2810c2452d11ae9e6dcac71fc9de40a6fedcb20e2952a15b431ad8b29e50e28f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lodash.merge@npm:^4.6.2":
|
||||
version: 4.6.2
|
||||
resolution: "lodash.merge@npm:4.6.2"
|
||||
@@ -3869,6 +4272,20 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lodash.throttle@npm:^4.1.1":
|
||||
version: 4.1.1
|
||||
resolution: "lodash.throttle@npm:4.1.1"
|
||||
checksum: 14628013e9e7f65ac904fc82fd8ecb0e55a9c4c2416434b1dd9cf64ae70a8937f0b15376a39a68248530adc64887ed0fe2b75204b2c9ec3eea1cb2d66ddd125d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lodash.toarray@npm:^4.4.0":
|
||||
version: 4.4.0
|
||||
resolution: "lodash.toarray@npm:4.4.0"
|
||||
checksum: 6ad3042f85f8a29e03b54547ab34aa811aa9478dea3086f3e4b1486c23a2bb28b25e8869df85187b1e12a2a6327dc5f738809a8ff28f1286d1ade96c796394bf
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lodash@npm:^4.17.21":
|
||||
version: 4.17.21
|
||||
resolution: "lodash@npm:4.17.21"
|
||||
@@ -3990,6 +4407,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mime-match@npm:^1.0.2":
|
||||
version: 1.0.2
|
||||
resolution: "mime-match@npm:1.0.2"
|
||||
dependencies:
|
||||
wildcard: "npm:^1.1.0"
|
||||
checksum: f7f465246abe7798ed4b2072d9474fe86c8bbee6ea6169abb22eceab4a9752b0393272be3efd8620d3214d421a55420663416a1b4e728daba0f431da55030309
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"minimatch@npm:9.0.3, minimatch@npm:^9.0.1":
|
||||
version: 9.0.3
|
||||
resolution: "minimatch@npm:9.0.3"
|
||||
@@ -4186,7 +4612,14 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"nanoid@npm:^3.3.7":
|
||||
"namespace-emitter@npm:^2.0.1":
|
||||
version: 2.0.1
|
||||
resolution: "namespace-emitter@npm:2.0.1"
|
||||
checksum: 847fdc3cc68fc9ba0f8959f240bf530f5c0b33c679079aa7756a39954cfef13f397bb4e4209387032a73f10a73684c75df7c6f60794e03c2b5786eda57bea756
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"nanoid@npm:^3.1.25, nanoid@npm:^3.2.0, nanoid@npm:^3.3.7":
|
||||
version: 3.3.7
|
||||
resolution: "nanoid@npm:3.3.7"
|
||||
bin:
|
||||
@@ -4209,6 +4642,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"next-tick@npm:^1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "next-tick@npm:1.1.0"
|
||||
checksum: 3ba80dd805fcb336b4f52e010992f3e6175869c8d88bf4ff0a81d5d66e6049f89993463b28211613e58a6b7fe93ff5ccbba0da18d4fa574b96289e8f0b577f28
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-fetch@npm:^2.6.7":
|
||||
version: 2.7.0
|
||||
resolution: "node-fetch@npm:2.7.0"
|
||||
@@ -4513,6 +4953,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"preact@npm:^10.5.13":
|
||||
version: 10.19.6
|
||||
resolution: "preact@npm:10.19.6"
|
||||
checksum: 305c63bc59f9a081185fea8ee9a43c96f69af58e50692228d0e566eacd69bac009f2fb9d4ebfa2bcfcfd9762c5318a7f1ccd1d5223ab8764e3f7e14386bce626
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"prelude-ls@npm:^1.2.1":
|
||||
version: 1.2.1
|
||||
resolution: "prelude-ls@npm:1.2.1"
|
||||
@@ -4538,6 +4985,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"prismjs@npm:^1.23.0":
|
||||
version: 1.29.0
|
||||
resolution: "prismjs@npm:1.29.0"
|
||||
checksum: d906c4c4d01b446db549b4f57f72d5d7e6ccaca04ecc670fb85cea4d4b1acc1283e945a9cbc3d81819084a699b382f970e02f9d1378e14af9808d366d9ed7ec6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"proc-log@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "proc-log@npm:3.0.0"
|
||||
@@ -4853,6 +5307,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"scroll-into-view-if-needed@npm:^2.2.28":
|
||||
version: 2.2.31
|
||||
resolution: "scroll-into-view-if-needed@npm:2.2.31"
|
||||
dependencies:
|
||||
compute-scroll-into-view: "npm:^1.0.20"
|
||||
checksum: d44c518479505e37ab5b8b4a5aef9130edd8745f8ba9ca291ff0d8358bc89b63da8c30434f35c097384e455702bfe4acbe8b82dfb8b860a971adcae084c5b2f7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"section-matter@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "section-matter@npm:1.0.0"
|
||||
@@ -4961,6 +5424,28 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"slate-history@npm:^0.66.0":
|
||||
version: 0.66.0
|
||||
resolution: "slate-history@npm:0.66.0"
|
||||
dependencies:
|
||||
is-plain-object: "npm:^5.0.0"
|
||||
peerDependencies:
|
||||
slate: ">=0.65.3"
|
||||
checksum: 132e79a072072768cbba36fbf37348f3398d1efeb6a0695c2bd641a9b00aad281889661053c6fcafb4b03f64a3e5285b6d9f4ae8d70b49212e05a9a56309a042
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"slate@npm:^0.72.0":
|
||||
version: 0.72.8
|
||||
resolution: "slate@npm:0.72.8"
|
||||
dependencies:
|
||||
immer: "npm:^9.0.6"
|
||||
is-plain-object: "npm:^5.0.0"
|
||||
tiny-warning: "npm:^1.0.3"
|
||||
checksum: 589d3bd94a4f7ae7dec180fa8148d5f2fff6b9569d7b3f1a81150e1e53555b394e50e99a28b925890759d7d08ee539fcaf0b8101e0fdfa4cd6cd9d5a63815af2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"smart-buffer@npm:^4.2.0":
|
||||
version: 4.2.0
|
||||
resolution: "smart-buffer@npm:4.2.0"
|
||||
@@ -4968,6 +5453,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"snabbdom@npm:^3.1.0":
|
||||
version: 3.6.2
|
||||
resolution: "snabbdom@npm:3.6.2"
|
||||
checksum: 5b6763128458fde2ec3c32d3753650db7ab2a66ebc233696e201c65d41e976fdce038857056ccdf12c23540fd3745bbdf48c4819f061878d7546c2a5fcd9b1ca
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"socks-proxy-agent@npm:^8.0.1":
|
||||
version: 8.0.2
|
||||
resolution: "socks-proxy-agent@npm:8.0.2"
|
||||
@@ -5019,6 +5511,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ssr-window@npm:^3.0.0-alpha.1":
|
||||
version: 3.0.0
|
||||
resolution: "ssr-window@npm:3.0.0"
|
||||
checksum: d60efa4bf1e8de03c2410ba81976dfec02a084a44a8f952d9cac794e806e87dc7dcd6033f26fdd35430851bd358f39e8d9625404683b8a1792b46711fc1eb382
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ssri@npm:^10.0.0":
|
||||
version: 10.0.5
|
||||
resolution: "ssri@npm:10.0.5"
|
||||
@@ -5252,6 +5751,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tiny-warning@npm:^1.0.3":
|
||||
version: 1.0.3
|
||||
resolution: "tiny-warning@npm:1.0.3"
|
||||
checksum: ef8531f581b30342f29670cb41ca248001c6fd7975ce22122bd59b8d62b4fc84ad4207ee7faa95cde982fa3357cd8f4be650142abc22805538c3b1392d7084fa
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"to-fast-properties@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "to-fast-properties@npm:2.0.0"
|
||||
@@ -5355,6 +5861,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"type@npm:^2.7.2":
|
||||
version: 2.7.2
|
||||
resolution: "type@npm:2.7.2"
|
||||
checksum: 84c2382788fe24e0bc3d64c0c181820048f672b0f06316aa9c7bdb373f8a09f8b5404f4e856bc4539fb931f2f08f2adc4c53f6c08c9c0314505d70c29a1289e1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"typed-array-buffer@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "typed-array-buffer@npm:1.0.0"
|
||||
@@ -5653,6 +6166,8 @@ __metadata:
|
||||
"@vue/eslint-config-typescript": "npm:^12.0.0"
|
||||
"@vueuse/core": "npm:^10.7.2"
|
||||
"@vueuse/router": "npm:^10.7.2"
|
||||
"@wangeditor/editor": "npm:^5.1.23"
|
||||
"@wangeditor/editor-for-vue": "npm:^5.1.12"
|
||||
date-fns: "npm:^3.3.1"
|
||||
easy-speech: "npm:^2.3.1"
|
||||
echarts: "npm:^5.5.0"
|
||||
@@ -5961,6 +6476,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"wildcard@npm:^1.1.0":
|
||||
version: 1.1.2
|
||||
resolution: "wildcard@npm:1.1.2"
|
||||
checksum: 4051a90884f60c2e7eed81811cbb21b2de37986c0f2f82f081b2a43b9693d5d240e80a98ede68a6fa3fb0779bcdf26720e954a890de26ae4e73ee5f04e233649
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"wmf@npm:~1.0.1":
|
||||
version: 1.0.2
|
||||
resolution: "wmf@npm:1.0.2"
|
||||
|
||||
Reference in New Issue
Block a user