diff --git a/package.json b/package.json index 2ff455c..af85f2e 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/Utils.ts b/src/Utils.ts index 1374175..3f84be6 100644 --- a/src/Utils.ts +++ b/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 { diff --git a/src/api/account.ts b/src/api/account.ts index 3b31c4e..9b6b733 100644 --- a/src/api/account.ts +++ b/src/api/account.ts @@ -9,7 +9,6 @@ import { useRoute } from 'vue-router' export const ACCOUNT = ref({} 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}`) }) } } diff --git a/src/api/api-models.ts b/src/api/api-models.ts index 17a567a..f177f0d 100644 --- a/src/api/api-models.ts +++ b/src/api/api-models.ts @@ -3,12 +3,11 @@ export interface APIRoot { message: string data: T } -export interface PaginationResponse { +export interface PaginationResponse extends APIRoot { 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 } diff --git a/src/api/models/forum.ts b/src/api/models/forum.ts new file mode 100644 index 0000000..3bf7373 --- /dev/null +++ b/src/api/models/forum.ts @@ -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 +} diff --git a/src/api/query.ts b/src/api/query.ts index fe9fe1c..720f597 100644 --- a/src/api/query.ts +++ b/src/api/query.ts @@ -19,23 +19,34 @@ export async function QueryPostAPIWithParams( contentType?: string, headers?: [string, string][], ): Promise> { + return await QueryPostAPIWithParamsInternal>(urlString, params, body, contentType, headers) +} +async function QueryPostAPIWithParamsInternal( + 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]) - + return await QueryAPIInternal(url, { + method: 'post', + headers: headers, + body: typeof body === 'string' ? body : JSON.stringify(body), + }) +} +async function QueryAPIInternal(url: URL, init: RequestInit) { try { - const data = await fetch(url, { - method: 'post', - headers: headers, - body: typeof body === 'string' ? body : JSON.stringify(body), - }) - const result = (await data.json()) as APIRoot + 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( params?: any, headers?: [string, string][], ): Promise> { + return await QueryGetAPIInternal>(urlString, params, headers) +} +async function QueryGetAPIInternal(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(), { - method: 'get', - headers: headers, - }) - const result = (await data.json()) as APIRoot - return result - } catch (e) { - console.error(`[GET] API调用失败: ${e}`) - if (!apiFail.value) { - apiFail.value = true - console.log('默认API异常, 切换至故障转移节点') - } - throw e - } + return await QueryAPIInternal(url, { + method: 'get', + headers: headers, + }) } -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(url: string, body?: unknown): Promise>> { - return await QueryPostAPI>(url, body) +export async function QueryPostPaginationAPI(url: string, body?: unknown): Promise> { + return await QueryPostAPIWithParamsInternal>(url, undefined, body) } -export async function QueryGetPaginationAPI( - urlString: string, - params?: unknown, -): Promise>> { - return await QueryGetAPI>(urlString, params) +export async function QueryGetPaginationAPI(urlString: string, params?: unknown): Promise> { + return await QueryGetAPIInternal>(urlString, params) +} +export function GetHeaders(): [string, string][] { + return [['Authorization', `Bearer ${cookie.value}`]] } diff --git a/src/api/user.ts b/src/api/user.ts index 9c48771..28aa203 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -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 }>({}) diff --git a/src/assets/editorDarkMode.css b/src/assets/editorDarkMode.css new file mode 100644 index 0000000..b02de14 --- /dev/null +++ b/src/assets/editorDarkMode.css @@ -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; +} \ No newline at end of file diff --git a/src/assets/forumContentStyle.css b/src/assets/forumContentStyle.css new file mode 100644 index 0000000..2bbd395 --- /dev/null +++ b/src/assets/forumContentStyle.css @@ -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; +} diff --git a/src/components/LiveInfoContainer.vue b/src/components/LiveInfoContainer.vue index 5d1aac3..cb1b101 100644 --- a/src/components/LiveInfoContainer.vue +++ b/src/components/LiveInfoContainer.vue @@ -87,7 +87,7 @@ watch( 已直播: - {{ ((Date.now() - (live.stopAt ?? 0)) / (3600 * 1000)).toFixed(1) }} + {{ ((Date.now() - (live.startAt ?? 0)) / (3600 * 1000)).toFixed(1) }} 时 diff --git a/src/components/TurnstileVerify.vue b/src/components/TurnstileVerify.vue new file mode 100644 index 0000000..04f1e63 --- /dev/null +++ b/src/components/TurnstileVerify.vue @@ -0,0 +1,27 @@ + + + diff --git a/src/components/VEditor.vue b/src/components/VEditor.vue new file mode 100644 index 0000000..b665470 --- /dev/null +++ b/src/components/VEditor.vue @@ -0,0 +1,119 @@ + + + diff --git a/src/components/manage/PointHistoryCard.vue b/src/components/manage/PointHistoryCard.vue index 82a1cb2..7e91d19 100644 --- a/src/components/manage/PointHistoryCard.vue +++ b/src/components/manage/PointHistoryCard.vue @@ -119,7 +119,7 @@ const historyColumn: DataTableColumns = [ h( NTag, { type: 'warning', size: 'tiny', style: { margin: '0' }, bordered: false }, - () => (row.extra?.danmaku.num ?? 1) + '个', + () => (row.count ?? 1) + '个', ), ]) case EventDataTypes.SC: diff --git a/src/components/manage/PointOrderCard.vue b/src/components/manage/PointOrderCard.vue index 9d6bc8c..960a542 100644 --- a/src/components/manage/PointOrderCard.vue +++ b/src/components/manage/PointOrderCard.vue @@ -98,6 +98,17 @@ const orderColumn: DataTableColumns { + return row.instanceOf == 'user' ? row.goods.name : props.goods?.find((g) => g.id == row.goodsId)?.name + }, + }, + { + title: '数量', + key: 'count', + }, { title: '时间', key: 'time', @@ -118,9 +129,12 @@ const orderColumn: DataTableColumns { - return row.status == filterOptionValue - }, + filter: + props.type == 'owner' + ? undefined + : (filterOptionValue: unknown, row: ResponsePointOrder2UserModel | ResponsePointOrder2OwnerModel) => { + return row.status == filterOptionValue + }, filterOptions: [ { label: '等待发货', @@ -151,9 +165,12 @@ const orderColumn: DataTableColumns { - return row.type == filterOptionValue - }, + filter: + props.type == 'owner' + ? undefined + : (filterOptionValue: unknown, row: ResponsePointOrder2UserModel | ResponsePointOrder2OwnerModel) => { + return row.type == filterOptionValue + }, filterOptions: [ { label: '实体礼物', @@ -360,7 +377,10 @@ onMounted(() => { > -