chore: 更新依赖, 支持日程表单日多日程

This commit is contained in:
Megghy
2025-09-30 09:48:57 +08:00
parent 7c516559f1
commit 6fd046adcd
15 changed files with 1307 additions and 482 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -1,28 +1,75 @@
import oxlint from 'eslint-plugin-oxlint'; import antfu from '@antfu/eslint-config'
import vue from 'eslint-plugin-vue';
import ts from 'typescript-eslint';
// `VueVine()` 返回一个 ESLint flat config
import VueVine from '@vue-vine/eslint-config' import VueVine from '@vue-vine/eslint-config'
export default [ export default antfu(
{ {
languageOptions: { // 项目类型: app (默认) 或 lib
ecmaVersion: 'latest', type: 'app',
// 启用 TypeScript 支持 (自动检测)
typescript: {
tsconfigPath: 'tsconfig.json',
}, },
// 启用 Vue 支持 (自动检测)
vue: true,
// 启用格式化规则
stylistic: {
indent: 2,
quotes: 'single',
semi: false,
},
// 禁用某些文件类型的支持
jsonc: true,
yaml: true,
markdown: true,
// 忽略的文件
ignores: [
'**/node_modules',
'**/dist',
'**/output',
'**/.vitepress/cache',
'**/.nuxt',
'**/.next',
'**/.vercel',
'**/.changeset',
'**/.idea',
'**/.cache',
'**/.output',
'**/.vite-inspect',
'**/CHANGELOG*.md',
'**/*.min.*',
'**/LICENSE*',
'**/__snapshots__',
'**/auto-import?(s).d.ts',
'**/components.d.ts',
],
}, },
...vue.configs['flat/recommended'],
{ {
// files: ['*.vue', '**/*.vue'], // 自定义规则
languageOptions: {
parserOptions: {
parser: ts.parser,
},
},
rules: { rules: {
"vue/no-mutating-props": "off", // Vue 相关规则
'vue/multi-word-component-names': 'off',
'vue/no-mutating-props': 'off',
'vue/no-v-html': 'off',
'vue/require-default-prop': 'off',
// TypeScript 相关规则
'ts/no-explicit-any': 'off',
'ts/ban-ts-comment': 'off',
// 通用规则
'no-console': 'off',
'unused-imports/no-unused-vars': 'warn',
// 关闭一些过于严格的规则
'antfu/if-newline': 'off',
'style/brace-style': ['error', '1tbs'],
}, },
}, },
// 集成 VueVine 配置
...VueVine(), ...VueVine(),
...oxlint.configs['flat/recommended'], // oxlint should be the last one )
]

View File

@@ -13,84 +13,82 @@
"@guolao/vue-monaco-editor": "^1.5.5", "@guolao/vue-monaco-editor": "^1.5.5",
"@hyperdx/browser": "^0.21.2", "@hyperdx/browser": "^0.21.2",
"@hyperdx/cli": "^0.1.0", "@hyperdx/cli": "^0.1.0",
"@microsoft/signalr": "^8.0.7", "@microsoft/signalr": "^9.0.6",
"@microsoft/signalr-protocol-msgpack": "^8.0.7", "@microsoft/signalr-protocol-msgpack": "^9.0.6",
"@mixer/postmessage-rpc": "^1.1.4", "@mixer/postmessage-rpc": "^1.1.4",
"@oneidentity/zstd-js": "^1.0.3", "@oneidentity/zstd-js": "^1.0.3",
"@tauri-apps/api": "^2.5.0", "@tauri-apps/api": "^2.8.0",
"@tauri-apps/plugin-autostart": "^2.3.0", "@tauri-apps/plugin-autostart": "^2.5.0",
"@tauri-apps/plugin-http": "^2.4.4", "@tauri-apps/plugin-http": "^2.5.2",
"@tauri-apps/plugin-log": "^2.4.0", "@tauri-apps/plugin-log": "^2.7.0",
"@tauri-apps/plugin-notification": "^2.2.2", "@tauri-apps/plugin-notification": "^2.3.1",
"@tauri-apps/plugin-opener": "^2.2.7", "@tauri-apps/plugin-opener": "^2.5.0",
"@tauri-apps/plugin-os": "^2.2.1", "@tauri-apps/plugin-os": "^2.3.1",
"@tauri-apps/plugin-process": "^2.2.1", "@tauri-apps/plugin-process": "^2.3.0",
"@tauri-apps/plugin-store": "^2.2.0", "@tauri-apps/plugin-store": "^2.4.0",
"@tauri-apps/plugin-updater": "^2.7.1", "@tauri-apps/plugin-updater": "^2.9.0",
"@types/crypto-js": "^4.2.2", "@types/crypto-js": "^4.2.2",
"@types/md5": "^2.3.5", "@types/md5": "^2.3.5",
"@types/vue-cropperjs": "^4.1.6", "@types/vue-cropperjs": "^4.1.6",
"@vicons/fluent": "^0.13.0", "@vicons/fluent": "^0.13.0",
"@vitejs/plugin-vue": "^5.2.4", "@vitejs/plugin-vue": "^6.0.1",
"@vueuse/core": "^13.3.0", "@vueuse/core": "^13.9.0",
"@vueuse/integrations": "^13.3.0", "@vueuse/integrations": "^13.9.0",
"@vueuse/router": "^13.3.0", "@vueuse/router": "^13.9.0",
"@wangeditor/editor": "^5.1.23", "@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12", "@wangeditor/editor-for-vue": "^5.1.12",
"bilibili-live-ws": "^6.3.1", "bilibili-live-ws": "^6.3.1",
"cropperjs": "^2.0.0", "cropperjs": "^2.0.1",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"easy-speech": "^2.4.0", "easy-speech": "^2.4.0",
"echarts": "^5.6.0", "echarts": "^6.0.0",
"eslint-plugin-oxlint": "^0.16.12",
"fast-xml-parser": "^5.2.5", "fast-xml-parser": "^5.2.5",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"grapheme-splitter": "^1.0.4", "grapheme-splitter": "^1.0.4",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"jszip": "^3.10.1",
"idb-keyval": "^6.2.2", "idb-keyval": "^6.2.2",
"linqts": "^2.0.0", "jszip": "^3.10.1",
"linqts": "^3.2.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"md5": "^2.3.0", "md5": "^2.3.0",
"mitt": "^3.0.1", "mitt": "^3.0.1",
"monaco-editor": "^0.52.2", "monaco-editor": "^0.53.0",
"naive-ui": "^2.41.1", "naive-ui": "^2.43.1",
"nanoid": "^5.1.5", "nanoid": "^5.1.6",
"peerjs": "^1.5.5", "peerjs": "^1.5.5",
"pinia": "^3.0.3", "pinia": "^3.0.3",
"qrcode.vue": "^3.6.0", "qrcode.vue": "^3.6.0",
"unplugin-auto-import": "^19.3.0", "unplugin-auto-import": "^20.2.0",
"unplugin-vue-components": "^28.7.0", "unplugin-vue-components": "^29.1.0",
"unplugin-vue-markdown": "^28.3.1", "unplugin-vue-markdown": "^29.2.0",
"uuid": "^11.1.0", "uuid": "^13.0.0",
"vite": "6.3.4", "vite": "7.1.7",
"vite-plugin-oxlint": "^1.3.3",
"vite-svg-loader": "^5.1.0", "vite-svg-loader": "^5.1.0",
"vue": "3.5.13", "vue": "3.5.22",
"vue-cropperjs": "^5.0.0", "vue-cropperjs": "^5.0.0",
"vue-echarts": "^7.0.3", "vue-echarts": "^8.0.0",
"vue-request": "^2.0.4", "vue-request": "^2.0.4",
"vue-router": "^4.5.1", "vue-router": "^4.5.1",
"vue-turnstile": "^1.0.11", "vue-turnstile": "^1.0.11",
"vue3-aplayer": "^1.7.3", "vue3-aplayer": "^1.7.3",
"vue3-marquee": "^4.2.2", "vue3-marquee": "^4.2.2",
"vueuc": "^0.4.64", "vueuc": "^0.4.65",
"worker-timers": "^8.0.22", "worker-timers": "^8.0.25",
"xlsx": "^0.18.5" "xlsx": "^0.18.5"
}, },
"devDependencies": { "devDependencies": {
"@types/bun": "^1.2.16", "@antfu/eslint-config": "^5.4.1",
"@types/bun": "^1.2.23",
"@types/file-saver": "^2.0.7", "@types/file-saver": "^2.0.7",
"@types/jszip": "^3.4.1", "@types/jszip": "^3.4.1",
"@types/uuid": "^10.0.0", "@types/uuid": "^11.0.0",
"@vicons/ionicons5": "^0.13.0", "@vicons/ionicons5": "^0.13.0",
"@vitejs/plugin-vue-jsx": "^4.2.0", "@vitejs/plugin-vue-jsx": "^5.1.1",
"@vue-vine/eslint-config": "^0.2.20", "@vue-vine/eslint-config": "^1.1.9",
"eslint": "^9.29.0", "eslint": "^9.36.0",
"eslint-plugin-vue": "^10.2.0",
"stylus": "^0.64.0", "stylus": "^0.64.0",
"typescript": "^5.9.0-dev.20250614", "typescript": "^5.9.2",
"vue-vine": "^0.4.4" "vue-vine": "^1.7.6"
} }
} }

View File

@@ -413,13 +413,22 @@ export interface LotteryUserCardInfo {
export interface ScheduleWeekInfo { export interface ScheduleWeekInfo {
year: number year: number
week: number week: number
days: ScheduleDayInfo[] days: ScheduleDayInfo[][]
} }
export interface ScheduleDayInfo { export interface ScheduleDayInfo {
title: string | null title: string | null
tag: string | null tag: string | null
tagColor: string | null tagColor: string | null
time: string | null time: string | null
id: string | null
}
export interface BatchScheduleRequest {
startYear: number
startWeek: number
count: number
dayOfWeek: number
schedule: ScheduleDayInfo
} }
export enum ThemeType { export enum ThemeType {
Auto = 'auto', Auto = 'auto',

View File

@@ -115,6 +115,7 @@ declare global {
const getActivePinia: typeof import('pinia')['getActivePinia'] const getActivePinia: typeof import('pinia')['getActivePinia']
const getCurrentInstance: typeof import('vue')['getCurrentInstance'] const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope'] const getCurrentScope: typeof import('vue')['getCurrentScope']
const getCurrentWatcher: typeof import('vue')['getCurrentWatcher']
const getDate: typeof import('date-fns')['getDate'] const getDate: typeof import('date-fns')['getDate']
const getDay: typeof import('date-fns')['getDay'] const getDay: typeof import('date-fns')['getDay']
const getDayOfYear: typeof import('date-fns')['getDayOfYear'] const getDayOfYear: typeof import('date-fns')['getDayOfYear']
@@ -179,6 +180,7 @@ declare global {
const isSameWeek: typeof import('date-fns')['isSameWeek'] const isSameWeek: typeof import('date-fns')['isSameWeek']
const isSameYear: typeof import('date-fns')['isSameYear'] const isSameYear: typeof import('date-fns')['isSameYear']
const isSaturday: typeof import('date-fns')['isSaturday'] const isSaturday: typeof import('date-fns')['isSaturday']
const isShallow: typeof import('vue')['isShallow']
const isSunday: typeof import('date-fns')['isSunday'] const isSunday: typeof import('date-fns')['isSunday']
const isThisHour: typeof import('date-fns')['isThisHour'] const isThisHour: typeof import('date-fns')['isThisHour']
const isThisISOWeek: typeof import('date-fns')['isThisISOWeek'] const isThisISOWeek: typeof import('date-fns')['isThisISOWeek']
@@ -507,6 +509,7 @@ declare global {
const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn'] const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn']
const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory'] const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory']
const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo'] const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo']
const useTimeAgoIntl: typeof import('@vueuse/core')['useTimeAgoIntl']
const useTimeout: typeof import('@vueuse/core')['useTimeout'] const useTimeout: typeof import('@vueuse/core')['useTimeout']
const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn'] const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn']
const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll'] const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
@@ -553,6 +556,6 @@ declare global {
// for type re-export // for type re-export
declare global { declare global {
// @ts-ignore // @ts-ignore
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue' export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue') import('vue')
} }

14
src/components.d.ts vendored
View File

@@ -19,27 +19,14 @@ declare module 'vue' {
LiveInfoContainer: typeof import('./components/LiveInfoContainer.vue')['default'] LiveInfoContainer: typeof import('./components/LiveInfoContainer.vue')['default']
MonacoEditorComponent: typeof import('./components/MonacoEditorComponent.vue')['default'] MonacoEditorComponent: typeof import('./components/MonacoEditorComponent.vue')['default']
NAlert: typeof import('naive-ui')['NAlert'] NAlert: typeof import('naive-ui')['NAlert']
NAvatar: typeof import('naive-ui')['NAvatar']
NButton: typeof import('naive-ui')['NButton']
NCard: typeof import('naive-ui')['NCard'] NCard: typeof import('naive-ui')['NCard']
NDescriptionsItem: typeof import('naive-ui')['NDescriptionsItem']
NEllipsis: typeof import('naive-ui')['NEllipsis']
NEmpty: typeof import('naive-ui')['NEmpty']
NFlex: typeof import('naive-ui')['NFlex'] NFlex: typeof import('naive-ui')['NFlex']
NFormItemGi: typeof import('naive-ui')['NFormItemGi'] NFormItemGi: typeof import('naive-ui')['NFormItemGi']
NGridItem: typeof import('naive-ui')['NGridItem'] NGridItem: typeof import('naive-ui')['NGridItem']
NIcon: typeof import('naive-ui')['NIcon'] NIcon: typeof import('naive-ui')['NIcon']
NImage: typeof import('naive-ui')['NImage']
NInputGroupLabel: typeof import('naive-ui')['NInputGroupLabel']
NInputNumber: typeof import('naive-ui')['NInputNumber']
NModal: typeof import('naive-ui')['NModal']
NPopconfirm: typeof import('naive-ui')['NPopconfirm']
NScrollbar: typeof import('naive-ui')['NScrollbar'] NScrollbar: typeof import('naive-ui')['NScrollbar']
NSpace: typeof import('naive-ui')['NSpace']
NSwitch: typeof import('naive-ui')['NSwitch']
NTag: typeof import('naive-ui')['NTag'] NTag: typeof import('naive-ui')['NTag']
NText: typeof import('naive-ui')['NText'] NText: typeof import('naive-ui')['NText']
NTime: typeof import('naive-ui')['NTime']
PointGoodsItem: typeof import('./components/manage/PointGoodsItem.vue')['default'] PointGoodsItem: typeof import('./components/manage/PointGoodsItem.vue')['default']
PointHistoryCard: typeof import('./components/manage/PointHistoryCard.vue')['default'] PointHistoryCard: typeof import('./components/manage/PointHistoryCard.vue')['default']
PointOrderCard: typeof import('./components/manage/PointOrderCard.vue')['default'] PointOrderCard: typeof import('./components/manage/PointOrderCard.vue')['default']
@@ -55,7 +42,6 @@ declare module 'vue' {
SongList: typeof import('./components/SongList.vue')['default'] SongList: typeof import('./components/SongList.vue')['default']
SongPlayer: typeof import('./components/SongPlayer.vue')['default'] SongPlayer: typeof import('./components/SongPlayer.vue')['default']
TempComponent: typeof import('./components/TempComponent.vue')['default'] TempComponent: typeof import('./components/TempComponent.vue')['default']
ToolDynamicNineGrid: typeof import('./components/manage/tools/ToolDynamicNineGrid.vue')['default']
TurnstileVerify: typeof import('./components/TurnstileVerify.vue')['default'] TurnstileVerify: typeof import('./components/TurnstileVerify.vue')['default']
UpdateNoteContainer: typeof import('./components/UpdateNoteContainer.vue')['default'] UpdateNoteContainer: typeof import('./components/UpdateNoteContainer.vue')['default']
UserBasicInfoCard: typeof import('./components/UserBasicInfoCard.vue')['default'] UserBasicInfoCard: typeof import('./components/UserBasicInfoCard.vue')['default']

View File

@@ -1,9 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { ScheduleWeekInfo } from '@/api/api-models' import { ScheduleWeekInfo, ScheduleDayInfo } from '@/api/api-models'
import { useWindowSize } from '@vueuse/core' import { useWindowSize } from '@vueuse/core'
import { NBadge, NButton, NCard, NEllipsis, NEmpty, NGrid, NGridItem, NList, NListItem, NPopconfirm, NSpace, NText, NTime } from 'naive-ui' import { NBadge, NButton, NCard, NEllipsis, NEmpty, NGrid, NGridItem, NIcon, NList, NListItem, NPopconfirm, NSpace, NText, NTime, useThemeVars } from 'naive-ui'
import { Clock20Regular, Bed20Regular } from '@vicons/fluent'
import { h } from 'vue'
const { width } = useWindowSize() const { width } = useWindowSize()
const themeVars = useThemeVars()
const weekdays = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'] const weekdays = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
function getDateFromWeek(year: number, week: number, dayOfWeek: number): Date { function getDateFromWeek(year: number, week: number, dayOfWeek: number): Date {
@@ -24,6 +27,8 @@ const emit = defineEmits<{
(e: 'onUpdate', schedule: ScheduleWeekInfo): void (e: 'onUpdate', schedule: ScheduleWeekInfo): void
(e: 'onDelete', schedule: ScheduleWeekInfo): void (e: 'onDelete', schedule: ScheduleWeekInfo): void
(e: 'onCopy', schedule: ScheduleWeekInfo): void (e: 'onCopy', schedule: ScheduleWeekInfo): void
(e: 'onEditItem', schedule: ScheduleWeekInfo, dayIndex: number, item: ScheduleDayInfo): void
(e: 'onDeleteItem', schedule: ScheduleWeekInfo, dayIndex: number, item: ScheduleDayInfo): void
}>() }>()
</script> </script>
@@ -78,67 +83,199 @@ const emit = defineEmits<{
</template> </template>
<NGrid <NGrid
x-gap="8" x-gap="8"
y-gap="8"
cols="1 1200:7" cols="1 1200:7"
style="align-items: stretch;"
> >
<NGridItem <NGridItem
v-for="(day, index) in item.days" v-for="(daySchedules, index) in item.days"
:key="index" :key="index"
style="display: flex;"
> >
<NCard <div style="display: flex; flex-direction: column; height: 100%; width: 100%;">
size="small" <div
:style="{ height: '65px', backgroundColor: day.tagColor + '1f' }" :style="{
content-style="padding: 5px;" marginBottom: '6px',
header-style="padding: 0px 6px 0px 6px;" padding: '4px 8px',
:embedded="day?.tag != undefined" fontSize: '13px',
> fontWeight: '600',
<template #header-extra> background: `linear-gradient(135deg, ${themeVars.primaryColorSuppl}15 0%, ${themeVars.primaryColorSuppl}25 100%)`,
<template v-if="day.tag"> borderRadius: '4px',
<NSpace :size="5"> display: 'flex',
<NBadge alignItems: 'center',
v-if="day.tagColor" gap: '6px',
dot }"
:color="day.tagColor" >
/> <NTime
<NEllipsis> :time="getDateFromWeek(item.year, item.week, index)"
<NText :style="{ color: day.tagColor }"> format="MM/dd"
{{ day.tag }} :style="{ color: themeVars.primaryColor }"
</NText> />
</NEllipsis> <NText>{{ weekdays[index] }}</NText>
</NSpace> </div>
</template> <div style="flex: 1; display: flex; flex-direction: column; min-height: 65px;">
<template v-else> <NCard
<NText v-if="daySchedules.length === 0"
depth="3" size="small"
style="font-size: 11px" :style="{
italic minHeight: '40px',
> background: `linear-gradient(135deg, ${themeVars.cardColor} 0%, ${themeVars.bodyColor} 100%)`,
休息 border: `1px dashed ${themeVars.dividerColor}`,
</NText> cursor: isSelf ? 'pointer' : 'default',
</template> transition: 'all 0.2s ease',
</template> }"
<template #header> :hoverable="isSelf"
content-style="display: flex; align-items: center; justify-content: center; gap: 4px;"
@click="isSelf && $emit('onUpdate', item)"
>
<NIcon :size="14" :component="Bed20Regular" :color="themeVars.textColor3" />
<NText <NText
:depth="3" depth="3"
style="font-size: 12px" style="font-size: 11px; font-style: italic;"
strong :style="{ opacity: isSelf ? 0.5 : 0.6 }"
:italic="!day.tag"
> >
<NTime 休息
:time="getDateFromWeek(item.year, item.week, index)"
format="MM/dd"
/>
{{ weekdays[index] }}
{{ day.time }}
</NText> </NText>
</template> </NCard>
<template v-if="day?.title"> <NSpace
<NEllipsis> v-else
<NText style="font-size: 13px"> vertical
{{ day.title }} :size="4"
</NText> >
</NEllipsis> <NCard
</template> v-for="(schedule, scheduleIndex) in daySchedules"
</NCard> :key="schedule.id || `${index}-${scheduleIndex}`"
size="small"
:style="{
backgroundColor: schedule.tagColor ? schedule.tagColor + '12' : themeVars.cardColor,
borderLeft: schedule.tagColor ? `3px solid ${schedule.tagColor}` : `3px solid ${themeVars.dividerColor}`,
cursor: isSelf ? 'pointer' : 'default',
transition: 'all 0.2s ease',
padding: '0'
}"
:bordered="true"
:hoverable="isSelf"
content-style="padding: 3px; padding-left: 5px;padding-bottom: 1px;"
@click="isSelf && $emit('onEditItem', item, index, schedule)"
>
<div style="padding: 4px 6px;">
<!-- 标签和时间行 (仅当有标签或时间时显示) -->
<div
v-if="schedule.tag || schedule.time"
style="
display: flex;
align-items: center;
gap: 4px;
margin-bottom: 3px;
flex-wrap: nowrap;
"
>
<!-- 标签 -->
<div
v-if="schedule.tag"
:style="{
display: 'inline-flex',
alignItems: 'center',
gap: '3px',
padding: '1px 5px',
borderRadius: '3px',
backgroundColor: schedule.tagColor ? schedule.tagColor + '22' : themeVars.primaryColorSuppl + '22',
flexShrink: 0
}"
>
<NBadge
v-if="schedule.tagColor"
dot
:color="schedule.tagColor"
:style="{ transform: 'scale(0.85)' }"
/>
<NText
:style="{
color: schedule.tagColor || themeVars.primaryColor,
fontWeight: '600',
fontSize: '10.5px',
whiteSpace: 'nowrap',
lineHeight: '1.2'
}"
>
{{ schedule.tag }}
</NText>
</div>
<!-- 时间 -->
<div
v-if="schedule.time"
:style="{
display: 'inline-flex',
alignItems: 'center',
gap: '2px',
flexShrink: 0
}"
>
<NIcon :size="12" :component="Clock20Regular" :color="themeVars.textColor3" />
<NText
depth="2"
:style="{
fontSize: '10.5px',
fontFamily: 'monospace',
whiteSpace: 'nowrap',
fontWeight: '500'
}"
>
{{ schedule.time }}
</NText>
</div>
<!-- 删除按钮 -->
<NButton
v-if="isSelf"
size="tiny"
type="error"
quaternary
circle
style="margin-left: auto; flex-shrink: 0; width: 18px; height: 18px; padding: 0;"
@click.stop="$emit('onDeleteItem', item, index, schedule)"
>
<template #icon>
<span style="font-size: 14px; line-height: 1;">×</span>
</template>
</NButton>
</div>
<!-- 内容 -->
<div v-if="schedule?.title">
<NEllipsis :line-clamp="2">
<NText
:style="{
fontSize: '12.5px',
lineHeight: '1.4',
color: themeVars.textColor2
}"
>
{{ schedule.title }}
</NText>
</NEllipsis>
</div>
<!-- 如果既没有标签也没有时间但有删除按钮 -->
<div
v-if="!schedule.tag && !schedule.time && isSelf && !schedule?.title"
style="display: flex; justify-content: flex-end;"
>
<NButton
size="tiny"
type="error"
quaternary
circle
style="width: 18px; height: 18px; padding: 0;"
@click.stop="$emit('onDeleteItem', item, index, schedule)"
>
<template #icon>
<span style="font-size: 14px; line-height: 1;">×</span>
</template>
</NButton>
</div>
</div>
</NCard>
</NSpace>
</div>
</div>
</NGridItem> </NGridItem>
</NGrid> </NGrid>
</NCard> </NCard>

View File

@@ -23,7 +23,7 @@ import {
VideoAdd20Filled, VideoAdd20Filled,
Mail24Filled, Mail24Filled,
} from '@vicons/fluent' } from '@vicons/fluent'
import { AnalyticsSharp, BrowsersOutline, Chatbox, Moon, MusicalNote, Sunny, Eye, PlayForward, PlayBack, Play, Pause, VolumeHigh, ChevronUp, ChevronDown, TrashBin } from '@vicons/ionicons5' import { AnalyticsSharp, BrowsersOutline, Chatbox, Moon, MusicalNote, Sunny, Eye, PlayForward, PlayBack, Play, Pause, VolumeHigh, ChevronUp, ChevronDown, TrashBin, Bookmark, BookmarkOutline } from '@vicons/ionicons5'
import { useElementSize, useStorage } from '@vueuse/core' import { useElementSize, useStorage } from '@vueuse/core'
import { import {
NAlert, NAlert,
@@ -68,6 +68,50 @@ const themeType = useStorage('Settings.Theme', ThemeType.Auto)
// 收藏功能相关 // 收藏功能相关
const favoriteMenuItems = useStorage<string[]>('Settings.FavoriteMenuItems', []) const favoriteMenuItems = useStorage<string[]>('Settings.FavoriteMenuItems', [])
const isFavorite = (key: string) => favoriteMenuItems.value?.includes(key)
const toggleFavorite = (key: string) => {
const list = favoriteMenuItems.value ?? []
const idx = list.indexOf(key)
if (idx === -1) list.unshift(key)
else list.splice(idx, 1)
favoriteMenuItems.value = [...list]
}
const renderFavoriteExtra = (key: string) => () =>
h(
'span',
{ class: ['menu-fav', isFavorite(key) ? 'active' : ''] },
[
h(
NTooltip,
{ placement: 'right' },
{
trigger: () =>
h(
NButton,
{
text: true,
size: 'tiny',
circle: true,
onClick: (e: MouseEvent) => {
e.stopPropagation()
toggleFavorite(key)
},
style: 'padding: 0; height: 18px; width: 18px;'
},
{
icon: () =>
h(NIcon, {
component: isFavorite(key) ? Bookmark : BookmarkOutline,
size: 16,
color: isFavorite(key) ? '#f5c451' : undefined,
}),
},
),
default: () => (isFavorite(key) ? '取消收藏' : '收藏'),
},
),
],
)
// 侧边栏和布局相关 // 侧边栏和布局相关
const sider = ref() const sider = ref()
@@ -130,259 +174,323 @@ const isBiliVerified = computed(() => accountInfo.value?.isBiliVerified)
// 图标渲染函数 - 用于菜单项 // 图标渲染函数 - 用于菜单项
const renderIcon = (icon: any) => () => h(NIcon, null, { default: () => h(icon) }) const renderIcon = (icon: any) => () => h(NIcon, null, { default: () => h(icon) })
// 菜单配置 // 菜单配置(支持分组与收藏置顶)
const menuOptions = computed(() => { const menuOptions = computed(() => {
return [ // 通用的菜单项工厂,自动挂载收藏按钮到叶子节点
{ const withFavoriteExtra = (item: any): any => {
if (item?.children?.length) {
return {
...item,
children: item.children.map(withFavoriteExtra),
}
}
return {
...item,
extra: width.value >= 180 ? renderFavoriteExtra(item.key) : undefined,
}
}
const commonItems = [
withFavoriteExtra({
label: () => h(RouterLink, { to: { name: 'manage-history' } }, { default: () => '历史' }), label: () => h(RouterLink, { to: { name: 'manage-history' } }, { default: () => '历史' }),
key: 'manage-history', key: 'manage-history',
disabled: accountInfo.value?.isEmailVerified === false, disabled: accountInfo.value?.isEmailVerified === false,
icon: renderIcon(AnalyticsSharp), icon: renderIcon(AnalyticsSharp),
}, }),
{ withFavoriteExtra({
label: () => h(RouterLink, { to: { name: 'manage-live' } }, { default: () => '直播记录' }), label: () => h(RouterLink, { to: { name: 'manage-live' } }, { default: () => '直播记录' }),
key: 'manage-live', key: 'manage-live',
disabled: accountInfo.value?.isEmailVerified === false, disabled: accountInfo.value?.isEmailVerified === false,
icon: renderIcon(Live24Filled), icon: renderIcon(Live24Filled),
}, }),
{ withFavoriteExtra({
label: () => h(RouterLink, { to: { name: 'manage-analyze' } }, { default: () => '直播数据' }), label: () => h(RouterLink, { to: { name: 'manage-analyze' } }, { default: () => '直播数据' }),
key: 'manage-analyze', key: 'manage-analyze',
disabled: accountInfo.value?.isEmailVerified === false, disabled: accountInfo.value?.isEmailVerified === false,
icon: renderIcon(Eye), icon: renderIcon(Eye),
}, }),
{ ]
const dataItems = [
withFavoriteExtra({
label: () => h(RouterLink, { to: { name: 'manage-event' } }, { default: () => '舰长和SC' }), label: () => h(RouterLink, { to: { name: 'manage-event' } }, { default: () => '舰长和SC' }),
key: 'manage-event', key: 'manage-event',
disabled: accountInfo.value?.isEmailVerified === false, disabled: accountInfo.value?.isEmailVerified === false,
icon: renderIcon(VehicleShip24Filled), icon: renderIcon(VehicleShip24Filled),
}, }),
{ withFavoriteExtra({
label: () => h(RouterLink, { to: { name: 'manage-point' } }, { default: () => '积分和礼物' }), label: () => h(RouterLink, { to: { name: 'manage-point' } }, { default: () => '积分和礼物' }),
key: 'manage-point', key: 'manage-point',
disabled: accountInfo.value?.isEmailVerified === false, disabled: accountInfo.value?.isEmailVerified === false,
icon: renderIcon(BookCoins20Filled), icon: renderIcon(BookCoins20Filled),
}, }),
{ ]
const toolsItems = [
withFavoriteExtra({
label: () => h(RouterLink, { to: { name: 'manage-schedule' } }, { default: () => '日程' }), label: () => h(RouterLink, { to: { name: 'manage-schedule' } }, { default: () => '日程' }),
key: 'manage-schedule', key: 'manage-schedule',
icon: renderIcon(CalendarClock24Filled), icon: renderIcon(CalendarClock24Filled),
disabled: accountInfo.value?.isEmailVerified === false, disabled: accountInfo.value?.isEmailVerified === false,
}, }),
{ withFavoriteExtra({
label: () => h(RouterLink, { to: { name: 'manage-songList' } }, { default: () => '歌单' }), label: () => h(RouterLink, { to: { name: 'manage-songList' } }, { default: () => '歌单' }),
key: 'manage-songList', key: 'manage-songList',
icon: renderIcon(MusicalNote), icon: renderIcon(MusicalNote),
disabled: accountInfo.value?.isEmailVerified === false, disabled: accountInfo.value?.isEmailVerified === false,
}, }),
{ withFavoriteExtra({
label: () => h(RouterLink, { to: { name: 'manage-questionBox' } }, { default: () => '棉花糖 (提问箱' }), label: () => h(RouterLink, { to: { name: 'manage-questionBox' } }, { default: () => '棉花糖 (提问箱' }),
key: 'manage-questionBox', key: 'manage-questionBox',
icon: renderIcon(Chatbox), icon: renderIcon(Chatbox),
disabled: accountInfo.value?.isEmailVerified === false, disabled: accountInfo.value?.isEmailVerified === false,
}, }),
{ withFavoriteExtra({
label: () => h(RouterLink, { to: { name: 'manage-videoCollect' } }, { default: () => '视频征集' }), label: () => h(RouterLink, { to: { name: 'manage-videoCollect' } }, { default: () => '视频征集' }),
key: 'manage-videoCollect', key: 'manage-videoCollect',
icon: renderIcon(VideoAdd20Filled), icon: renderIcon(VideoAdd20Filled),
disabled: accountInfo.value?.isEmailVerified === false, disabled: accountInfo.value?.isEmailVerified === false,
}, }),
{ withFavoriteExtra({
label: () => h(RouterLink, { to: { name: 'manage-lottery' } }, { default: () => '动态抽奖' }), label: () => h(RouterLink, { to: { name: 'manage-lottery' } }, { default: () => '动态抽奖' }),
key: 'manage-lottery', key: 'manage-lottery',
icon: renderIcon(Lottery24Filled), icon: renderIcon(Lottery24Filled),
}, }),
{
label: () => h(
NTooltip,
{},
{
trigger: () => h(
NText,
() => [
'弹幕相关',
h(
NTooltip,
{ style: 'padding: 0;' },
{
trigger: () => h(NIcon, { component: Info24Filled }),
default: () => h(
NAlert,
{
type: 'warning',
size: 'small',
title: '可用性警告',
style: 'max-width: 600px;',
},
() => h('div', {}, [
' 当浏览器在后台运行时, 定时器和 Websocket 连接将受到严格限制, 这会导致弹幕接收功能无法正常工作 (详见',
h(
NButton,
{
text: true,
tag: 'a',
href: 'https://developer.chrome.com/blog/background_tabs/',
target: '_blank',
type: 'info',
},
() => '此文章',
),
'), 虽然本站已经针对此问题做出了处理, 一般情况下即使掉线了也会重连, 不过还是有可能会遗漏事件',
h('br'),
'为避免这种情况, 建议注册本站账后使用',
h(
NButton,
{
type: 'primary',
text: true,
size: 'small',
tag: 'a',
href: 'https://www.wolai.com/fje5wLtcrDoZcb9rk2zrFs',
target: '_blank',
},
() => 'VtsuruEventFetcher',
),
', 否则请在使用功能时尽量保持网页在前台运行, 同时关闭浏览器的 页面休眠/内存节省 功能',
h('br'),
'Chrome: ',
h(
NButton,
{
type: 'info',
text: true,
size: 'small',
tag: 'a',
href: 'https://support.google.com/chrome/answer/12929150?hl=zh-Hans#zippy=%2C%E5%BC%80%E5%90%AF%E6%88%96%E5%85%B3%E9%97%AD%E7%9C%81%E5%86%85%E5%AD%98%E6%A8%A1%E5%BC%8F%2C%E8%AE%A9%E7%89%B9%E5%AE%9A%E7%BD%91%E7%AB%99%E4%BF%9D%E6%8C%81%E6%B4%BB%E5%8A%A8%E7%8A%B6%E6%80%81',
target: '_blank',
},
() => '让特定网站保持活动状态',
),
', Edge: ',
h(
NButton,
{
type: 'info',
text: true,
size: 'small',
tag: 'a',
href: 'https://support.microsoft.com/zh-cn/topic/%E4%BA%86%E8%A7%A3-microsoft-edge-%E4%B8%AD%E7%9A%84%E6%80%A7%E8%83%BD%E5%8A%9F%E8%83%BD-7b36f363-2119-448a-8de6-375cfd88ab25',
target: '_blank',
},
() => '永远不想进入睡眠状态的网站',
),
]),
),
},
),
]
),
default: () => isBiliVerified.value
? '需要使用直播弹幕的功能'
: '你尚未进行 Bilibili 认证, 请前往面板进行绑定',
},
),
key: 'manage-danmaku',
icon: renderIcon(Chat24Filled),
disabled: accountInfo.value?.isEmailVerified === false || !isBiliVerified.value,
children: [
{
label: () => !isBiliVerified.value ? '弹幕机' : h(
NBadge,
{ value: '新', offset: [15, 12], type: 'info' },
() => h(
NTooltip,
{},
{
trigger: () => h(
RouterLink,
{ to: { name: 'manage-danmuji' } },
{ default: () => '弹幕机' },
),
default: () => '兼容 blivechat 样式 (其实就是直接用的 blivechat 组件',
}
)
),
key: 'manage-danmuji',
disabled: !isBiliVerified.value,
icon: renderIcon(Lottery24Filled),
},
{
label: () => !isBiliVerified.value ? '抽奖' : h(
RouterLink,
{ to: { name: 'manage-liveLottery' } },
{ default: () => '抽奖' },
),
key: 'manage-liveLottery',
icon: renderIcon(Lottery24Filled),
disabled: !isBiliVerified.value,
},
{
label: () => !isBiliVerified.value ? '点播' : h(
NTooltip,
{},
{
trigger: () => h(
RouterLink,
{ to: { name: 'manage-liveRequest' } },
{ default: () => '点播' },
),
default: () => '歌势之类用的, 可以用来点歌或者跳舞什么的',
},
),
key: 'manage-liveRequest',
icon: renderIcon(MusicalNote),
disabled: !isBiliVerified.value,
},
{
label: () => !isBiliVerified.value ? '点歌' : h(
NTooltip,
{},
{
trigger: () => h(
RouterLink,
{ to: { name: 'manage-musicRequest' } },
{ default: () => '点歌' },
),
default: () => '就是传统的点歌机, 发弹幕后播放指定的歌曲',
},
),
key: 'manage-musicRequest',
icon: renderIcon(MusicalNote),
disabled: !isBiliVerified.value,
},
{
label: () => !isBiliVerified.value ? '排队' : h(
RouterLink,
{ to: { name: 'manage-liveQueue' } },
{ default: () => '排队' },
),
key: 'manage-liveQueue',
icon: renderIcon(PeopleQueue24Filled),
disabled: !isBiliVerified.value,
},
{
label: () => !isBiliVerified.value ? '读弹幕' : h(
RouterLink,
{ to: { name: 'manage-speech' } },
{ default: () => '读弹幕' },
),
key: 'manage-speech',
icon: renderIcon(TabletSpeaker24Filled),
disabled: !isBiliVerified.value,
},
/*{
label: () => !isBiliVerified.value ? '弹幕投票' : h(
RouterLink,
{ to: { name: 'manage-danmakuVote' } },
{ default: () => '弹幕投票' },
),
key: 'manage-danmakuVote',
icon: renderIcon(Chat24Filled),
disabled: !isBiliVerified.value,
},*/
],
},
] ]
const danmakuItem = {
label: () => h(
NTooltip,
{},
{
trigger: () => h(
NText,
() => [
'弹幕相关',
h(
NTooltip,
{ style: 'padding: 0;' },
{
trigger: () => h(NIcon, { component: Info24Filled }),
default: () => h(
NAlert,
{
type: 'warning',
size: 'small',
title: '可用性警告',
style: 'max-width: 600px;',
},
() => h('div', {}, [
' 当浏览器在后台运行时, 定时器和 Websocket 连接将受到严格限制, 这会导致弹幕接收功能无法正常工作 (详见',
h(
NButton,
{
text: true,
tag: 'a',
href: 'https://developer.chrome.com/blog/background_tabs/',
target: '_blank',
type: 'info',
},
() => '此文章',
),
'), 虽然本站已经针对此问题做出了处理, 一般情况下即使掉线了也会重连, 不过还是有可能会遗漏事件',
h('br'),
'为避免这种情况, 建议注册本站账后使用',
h(
NButton,
{
type: 'primary',
text: true,
size: 'small',
tag: 'a',
href: 'https://www.wolai.com/fje5wLtcrDoZcb9rk2zrFs',
target: '_blank',
},
() => 'VtsuruEventFetcher',
),
', 否则请在使用功能时尽量保持网页在前台运行, 同时关闭浏览器的 页面休眠/内存节省 功能',
h('br'),
'Chrome: ',
h(
NButton,
{
type: 'info',
text: true,
size: 'small',
tag: 'a',
href: 'https://support.google.com/chrome/answer/12929150?hl=zh-Hans#zippy=%2C%E5%BC%80%E5%90%AF%E6%88%96%E5%85%B3%E9%97%AD%E7%9C%81%E5%86%85%E5%AD%98%E6%A8%A1%E5%BC%8F%2C%E8%AE%A9%E7%89%B9%E5%AE%9A%E7%BD%91%E7%AB%99%E4%BF%9D%E6%8C%81%E6%B4%BB%E5%8A%A8%E7%8A%B6%E6%80%81',
target: '_blank',
},
() => '让特定网站保持活动状态',
),
', Edge: ',
h(
NButton,
{
type: 'info',
text: true,
size: 'small',
tag: 'a',
href: 'https://support.microsoft.com/zh-cn/topic/%E4%BA%86%E8%A7%A3-microsoft-edge-%E4%B8%AD%E7%9A%84%E6%80%A7%E8%83%BD%E5%8A%9F%E8%83%BD-7b36f363-2119-448a-8de6-375cfd88ab25',
target: '_blank',
},
() => '永远不想进入睡眠状态的网站',
),
]),
),
},
),
]
),
default: () => (isBiliVerified.value
? '需要使用直播弹幕的功能'
: '你尚未进行 Bilibili 认证, 请前往面板进行绑定'),
},
),
key: 'manage-danmaku',
icon: renderIcon(Chat24Filled),
disabled: accountInfo.value?.isEmailVerified === false || !isBiliVerified.value,
children: [
withFavoriteExtra({
label: () => !isBiliVerified.value ? '弹幕机' : h(NTooltip,
{},
{
trigger: () => h(
RouterLink,
{ to: { name: 'manage-danmuji' } },
{ default: () => '弹幕机' },
),
default: () => '兼容 blivechat 样式 (其实就是直接用的 blivechat 组件',
}),
key: 'manage-danmuji',
disabled: !isBiliVerified.value,
icon: renderIcon(Lottery24Filled),
}),
withFavoriteExtra({
label: () => !isBiliVerified.value ? '抽奖' : h(
RouterLink,
{ to: { name: 'manage-liveLottery' } },
{ default: () => '抽奖' },
),
key: 'manage-liveLottery',
icon: renderIcon(Lottery24Filled),
disabled: !isBiliVerified.value,
}),
withFavoriteExtra({
label: () => !isBiliVerified.value ? '点播' : h(
NTooltip,
{},
{
trigger: () => h(
RouterLink,
{ to: { name: 'manage-liveRequest' } },
{ default: () => '点播' },
),
default: () => '歌势之类用的, 可以用来点歌或者跳舞什么的',
},
),
key: 'manage-liveRequest',
icon: renderIcon(MusicalNote),
disabled: !isBiliVerified.value,
}),
withFavoriteExtra({
label: () => !isBiliVerified.value ? '点歌' : h(
NTooltip,
{},
{
trigger: () => h(
RouterLink,
{ to: { name: 'manage-musicRequest' } },
{ default: () => '点歌' },
),
default: () => '就是传统的点歌机, 发弹幕后播放指定的歌曲',
},
),
key: 'manage-musicRequest',
icon: renderIcon(MusicalNote),
disabled: !isBiliVerified.value,
}),
withFavoriteExtra({
label: () => !isBiliVerified.value ? '排队' : h(
RouterLink,
{ to: { name: 'manage-liveQueue' } },
{ default: () => '排队' },
),
key: 'manage-liveQueue',
icon: renderIcon(PeopleQueue24Filled),
disabled: !isBiliVerified.value,
}),
withFavoriteExtra({
label: () => !isBiliVerified.value ? '读弹幕' : h(
RouterLink,
{ to: { name: 'manage-speech' } },
{ default: () => '读弹幕' },
),
key: 'manage-speech',
icon: renderIcon(TabletSpeaker24Filled),
disabled: !isBiliVerified.value,
}),
/*withFavoriteExtra({
label: () => !isBiliVerified.value ? '弹幕投票' : h(
RouterLink,
{ to: { name: 'manage-danmakuVote' } },
{ default: () => '弹幕投票' },
),
key: 'manage-danmakuVote',
icon: renderIcon(Chat24Filled),
disabled: !isBiliVerified.value,
}),*/
],
}
// 扁平化叶子项用于收藏置顶
const flattenLeaf = (items: any[]): any[] => {
const result: any[] = []
for (const it of items) {
if (it.children?.length) {
result.push(...flattenLeaf(it.children))
} else {
result.push(it)
}
}
return result
}
const allLeaf = [
...flattenLeaf(commonItems),
...flattenLeaf(dataItems),
...flattenLeaf(toolsItems),
...flattenLeaf(danmakuItem.children ?? []),
]
const leafMap = new Map(allLeaf.map(i => [i.key, i]))
const favorites = (favoriteMenuItems.value ?? [])
.map(k => leafMap.get(k))
.filter(Boolean) as any[]
const notFav = (i: any) => !isFavorite(i.key)
const danmakuChildren = (danmakuItem.children ?? []).filter(notFav)
const danmakuForGroup = danmakuChildren.length > 0 ? { ...danmakuItem, children: danmakuChildren } : null
const groups: any[] = []
if (favorites.length > 0) {
groups.push({ type: 'group', key: 'group-favorites', label: '我的收藏', children: favorites })
}
if (commonItems.filter(notFav).length > 0) {
groups.push({ type: 'group', key: 'group-common', label: '常用', children: commonItems.filter(notFav) })
}
if (dataItems.filter(notFav).length > 0) {
groups.push({ type: 'group', key: 'group-data', label: '数据', children: dataItems.filter(notFav) })
}
const toolsGroupChildren = [
...(danmakuForGroup ? [danmakuForGroup] : []),
...toolsItems.filter(notFav),
]
if (toolsGroupChildren.length > 0) {
groups.push({ type: 'group', key: 'group-tools', label: '互动与工具', children: toolsGroupChildren })
}
return groups
}) })
// 重发验证邮件 // 重发验证邮件
@@ -590,11 +698,15 @@ onMounted(() => {
<!-- 主导航菜单 --> <!-- 主导航菜单 -->
<NMenu <NMenu
class="manage-sider-menu"
style="margin-top: 12px" style="margin-top: 12px"
:disabled="accountInfo?.isEmailVerified !== true" :disabled="accountInfo?.isEmailVerified !== true"
:default-value="($route.meta.parent as string) ?? $route.name?.toString()" :default-value="($route.meta.parent as string) ?? $route.name?.toString()"
:collapsed-width="64" :collapsed-width="64"
:collapsed-icon-size="22" :collapsed-icon-size="22"
:icon-size="16"
:root-indent="10"
:indent="12"
:options="menuOptions" :options="menuOptions"
/> />
@@ -1337,4 +1449,40 @@ onMounted(() => {
.music-player-card .n-tag:hover { .music-player-card .n-tag:hover {
transform: scale(1.05); transform: scale(1.05);
} }
/* 侧边栏菜单收藏按钮与紧凑样式 */
:deep(.manage-sider-menu .menu-fav) {
opacity: 0;
width: 0;
margin-left: 0;
overflow: hidden;
transition: opacity 0.15s ease, width 0.15s ease, margin-left 0.15s ease;
pointer-events: none; /* 不阻挡文字区域点击 */
display: inline-flex;
align-items: center;
justify-content: center;
}
:deep(.manage-sider-menu .n-menu-item:hover .menu-fav),
:deep(.manage-sider-menu .menu-fav.active) {
opacity: 1;
width: 18px;
margin-left: 6px;
pointer-events: auto;
}
:deep(.manage-sider-menu .menu-fav .n-button) {
padding: 0;
height: 18px;
width: 18px;
}
/* 略微收紧图标与文本的间距,提升有效可读宽度 */
:deep(.manage-sider-menu .n-menu-item .n-menu-item-content .n-menu-item-content__icon) {
margin-right: 6px;
}
:deep(.manage-sider-menu .n-menu-item .n-menu-item-content) {
padding-right: 6px;
}
</style> </style>

View File

@@ -1,21 +1,22 @@
<script setup lang="ts"> <script setup lang="ts">
import { DisableFunction, EnableFunction, useAccount } from '@/api/account' import { DisableFunction, EnableFunction, useAccount } from '@/api/account'
import { FunctionTypes, ScheduleWeekInfo } from '@/api/api-models' import { FunctionTypes, ScheduleDayInfo, ScheduleWeekInfo } from '@/api/api-models'
import { QueryGetAPI, QueryPostAPI } from '@/api/query' import { QueryGetAPI, QueryPostAPI } from '@/api/query'
import ScheduleList from '@/components/ScheduleList.vue' import ScheduleList from '@/components/ScheduleList.vue'
import { BASE_API_URL, CN_HOST, CURRENT_HOST, SCHEDULE_API_URL } from '@/data/constants' import { CURRENT_HOST, SCHEDULE_API_URL } from '@/data/constants'
import { copyToClipboard } from '@/Utils' import { copyToClipboard } from '@/Utils'
import { TagQuestionMark16Filled } from '@vicons/fluent' import { TagQuestionMark16Filled } from '@vicons/fluent'
import { useStorage } from '@vueuse/core'
import { addWeeks, endOfWeek, endOfYear, format, isBefore, startOfWeek, startOfYear } from 'date-fns' import { addWeeks, endOfWeek, endOfYear, format, isBefore, startOfWeek, startOfYear } from 'date-fns'
import { import {
NAlert, NAlert,
NBadge, NBadge,
NButton, NButton,
NCard,
NCheckbox, NCheckbox,
NColorPicker, NColorPicker,
NDivider, NDivider,
NFlex, NFlex,
NIcon,
NInput, NInput,
NInputGroup, NInputGroup,
NInputGroupLabel, NInputGroupLabel,
@@ -24,12 +25,13 @@ import {
NSpace, NSpace,
NSpin, NSpin,
NSwitch, NSwitch,
NText,
NTimePicker, NTimePicker,
NTooltip, NTooltip,
useMessage, useMessage,
} from 'naive-ui' } from 'naive-ui'
import { SelectMixedOption, SelectOption } from 'naive-ui/es/select/src/interface' import { SelectMixedOption, SelectOption } from 'naive-ui/es/select/src/interface'
import { VNode, computed, h, onMounted, ref } from 'vue' import { VNode, computed, h, onMounted, ref, watch } from 'vue'
const rules = { const rules = {
user: { user: {
@@ -76,33 +78,219 @@ const weekOptions = computed(() => {
return weeks return weeks
}) })
const dayOptions = computed(() => { const dayOptions = computed(() => {
const days = [] as SelectMixedOption[] const days: SelectMixedOption[] = []
for (let i = 0; i < 7; i++) { for (let i = 0; i < 7; i++) {
try { const entries = updateScheduleModel.value?.days?.[i] ?? []
days.push({ const count = entries.length
label: updateScheduleModel.value?.days[i].tag ? weekdays[i] + ' (已安排)' : weekdays[i], days.push({
value: i, label: count > 0 ? `${weekdays[i]} (共${count}项)` : weekdays[i],
}) value: i,
} catch (err) { })
console.error(err)
}
} }
return days return days
}) })
const existTagOptions = computed(() => { const existTagOptions = computed(() => {
const colors = [] as SelectMixedOption[] const colors: SelectMixedOption[] = []
const exists = new Set<string>()
schedules.value?.forEach((s) => { schedules.value?.forEach((s) => {
s.days.forEach((d) => { s.days.forEach((dayList) => {
if (d.tag && !colors.find((c) => c.value == d.tagColor && c.label == d.tag)) { dayList.forEach((item) => {
colors.push({ const tag = item.tag ?? ''
label: d.tag, const color = normalizeColor(item.tagColor) ?? ''
value: d.tagColor ?? '', if (tag) {
}) const key = `${tag}__${color}`
} if (!exists.has(key)) {
exists.add(key)
colors.push({
label: tag,
value: color,
})
}
}
})
}) })
}) })
return colors return colors
}) })
function normalizeColor(color: any): string | null {
// 如果是 null 或 undefined返回 null
if (color == null) return null
// 将 HSL 转 RGB返回 #RRGGBB
const hslToHex = (h: number, s: number, l: number) => {
const hue = ((h % 360) + 360) % 360 / 360 // 归一化到 [0, 1)
const saturation = Math.min(Math.max(s / 100, 0), 1)
const lightness = Math.min(Math.max(l / 100, 0), 1)
const hue2rgb = (p: number, q: number, t: number) => {
if (t < 0) t += 1
if (t > 1) t -= 1
if (t < 1 / 6) return p + (q - p) * 6 * t
if (t < 1 / 2) return q
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6
return p
}
let r: number, g: number, b: number
if (saturation === 0) {
r = g = b = lightness
} else {
const q = lightness < 0.5 ? lightness * (1 + saturation) : lightness + saturation - lightness * saturation
const p = 2 * lightness - q
r = hue2rgb(p, q, hue + 1 / 3)
g = hue2rgb(p, q, hue)
b = hue2rgb(p, q, hue - 1 / 3)
}
const toHex = (c: number) => {
const hex = Math.round(Math.min(Math.max(c, 0), 1) * 255).toString(16)
return hex.length === 1 ? '0' + hex : hex
}
return `#${toHex(r)}${toHex(g)}${toHex(b)}`.toUpperCase()
}
// 如果是字符串,尝试解析并规范化
if (typeof color === 'string') {
const str = color.trim()
// 1) 处理 hex统一为 #RRGGBB 大写
if (str.startsWith('#')) {
const hex = str.replace('#', '')
if (hex.length === 3) {
const r = hex[0]
const g = hex[1]
const b = hex[2]
return (`#${r}${r}${g}${g}${b}${b}`).toUpperCase()
}
if (hex.length >= 6) {
return (`#${hex.substring(0, 6)}`).toUpperCase()
}
return str.toUpperCase()
}
// 2) 处理 hsla/hsl 字符串
const hslMatch = str.match(/^hsla?\(\s*([+-]?\d+(?:\.\d+)?)\s*,\s*([+-]?\d+(?:\.\d+)?)%\s*,\s*([+-]?\d+(?:\.\d+)?)%\s*(?:,\s*([+-]?\d*(?:\.\d+)?)\s*)?\)$/i)
if (hslMatch) {
const h = parseFloat(hslMatch[1])
const s = parseFloat(hslMatch[2])
const l = parseFloat(hslMatch[3])
return hslToHex(h, s, l)
}
// 3) 处理 rgba/rgb 字符串,忽略 alpha
const rgbMatch = str.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([+-]?\d*(?:\.\d+)?)\s*)?\)$/i)
if (rgbMatch) {
const r = Math.min(255, Math.max(0, parseInt(rgbMatch[1])))
const g = Math.min(255, Math.max(0, parseInt(rgbMatch[2])))
const b = Math.min(255, Math.max(0, parseInt(rgbMatch[3])))
const toHex = (n: number) => n.toString(16).padStart(2, '0')
return `#${toHex(r)}${toHex(g)}${toHex(b)}`.toUpperCase()
}
// 4) 其他字符串,原样返回(交由下游使用场景判断)
return str
}
// 如果是数组([h, s, l, (a)] HS(L)A转换为十六进制
if (Array.isArray(color) && color.length >= 3) {
const [h, s, l] = color
return hslToHex(Number(h), Number(s), Number(l))
}
return null
}
function createEmptyDay(): ScheduleDayInfo {
return {
title: null,
tag: null,
tagColor: null,
time: null,
id: null,
}
}
function createEmptyDays(): ScheduleDayInfo[][] {
return Array.from({ length: 7 }, () => [] as ScheduleDayInfo[])
}
function normalizeWeek(week?: ScheduleWeekInfo): ScheduleWeekInfo {
const normalizedDays = Array.from({ length: 7 }, (_, index) => {
const list = week?.days?.[index]
if (!Array.isArray(list)) return [] as ScheduleDayInfo[]
return list
.filter(Boolean)
.map((item) => ({
title: item?.title ?? null,
tag: item?.tag ?? null,
tagColor: normalizeColor(item?.tagColor),
time: item?.time ?? null,
id: item?.id ?? null,
}))
})
return {
year: week?.year ?? new Date().getFullYear(),
week: week?.week ?? Number(format(Date.now(), 'w')) + 1,
days: normalizedDays,
}
}
function cloneWeek(week: ScheduleWeekInfo, options: { resetIds?: boolean } = {}): ScheduleWeekInfo {
// 深度克隆以完全断开响应式引用
const deepCloned = JSON.parse(JSON.stringify(week))
const normalized = normalizeWeek(deepCloned)
return {
year: normalized.year,
week: normalized.week,
days: normalized.days.map((dayList) =>
dayList.map((item) => ({
title: item.title ?? null,
tag: item.tag ?? null,
tagColor: normalizeColor(item.tagColor),
time: item.time ?? null,
id: options.resetIds ? null : item.id ?? null,
})),
),
}
}
function createEmptyWeek(year?: number, week?: number): ScheduleWeekInfo {
return {
year: year ?? new Date().getFullYear(),
week: week ?? Number(format(Date.now(), 'w')) + 1,
days: createEmptyDays(),
}
}
function ensureDayInitialized(target: ScheduleWeekInfo, dayIndex: number) {
if (!target.days || !Array.isArray(target.days)) {
target.days = createEmptyDays()
}
if (!Array.isArray(target.days[dayIndex])) {
target.days[dayIndex] = []
}
if (target.days[dayIndex].length === 0) {
target.days[dayIndex].push(createEmptyDay())
}
}
function sanitizeDays(days?: ScheduleDayInfo[][]): ScheduleDayInfo[][] {
return Array.from({ length: 7 }, (_, index) => {
const list = days?.[index] ?? []
return list
.filter((item) => !!item && (item.title?.trim() || item.tag?.trim() || item.time?.trim()))
.map((item) => ({
title: item.title?.trim() || null,
tag: item.tag?.trim() || null,
tagColor: normalizeColor(item.tagColor),
time: item.time?.trim() || null,
id: item.id ?? null,
}))
})
}
function getAllWeeks(year: number) { function getAllWeeks(year: number) {
const startDate = startOfYear(new Date(year, 0, 1)) const startDate = startOfYear(new Date(year, 0, 1))
const endDate = endOfYear(new Date(year, 11, 31)) const endDate = endOfYear(new Date(year, 11, 31))
@@ -125,7 +313,7 @@ function getAllWeeks(year: number) {
return weeks return weeks
} }
const accountInfo = useAccount() const accountInfo = useAccount()
const schedules = ref<ScheduleWeekInfo[]>() const schedules = ref<ScheduleWeekInfo[]>([])
const message = useMessage() const message = useMessage()
const isLoading = ref(true) const isLoading = ref(true)
@@ -133,13 +321,47 @@ const isLoading = ref(true)
const showUpdateModal = ref(false) const showUpdateModal = ref(false)
const showAddModal = ref(false) const showAddModal = ref(false)
const showCopyModal = ref(false) const showCopyModal = ref(false)
const updateScheduleModel = ref<ScheduleWeekInfo>({} as ScheduleWeekInfo) const updateScheduleModel = ref<ScheduleWeekInfo>(createEmptyWeek())
const selectedExistTag = ref() const selectedExistTag = ref()
const editingItemIndex = ref<number | null>(null)
const selectedDay = ref(0) const selectedDay = ref(0)
const selectedScheduleYear = ref(new Date().getFullYear()) const selectedScheduleYear = ref(new Date().getFullYear())
const selectedScheduleWeek = ref(Number(format(Date.now(), 'w')) + 1) const selectedScheduleWeek = ref(Number(format(Date.now(), 'w')) + 1)
watch(showUpdateModal, (visible) => {
if (visible) {
ensureDayInitialized(updateScheduleModel.value, selectedDay.value)
// 清理所有可能的数组格式颜色值
updateScheduleModel.value.days.forEach(dayList => {
dayList.forEach(item => {
if (item.tagColor && Array.isArray(item.tagColor)) {
item.tagColor = normalizeColor(item.tagColor)
}
})
})
}
})
// 深度监听 updateScheduleModel 的 tagColor确保它们始终是字符串格式
watch(
() => updateScheduleModel.value.days,
(days) => {
days?.forEach(dayList => {
dayList?.forEach(item => {
if (item.tagColor && Array.isArray(item.tagColor)) {
item.tagColor = normalizeColor(item.tagColor)
}
})
})
},
{ deep: true }
)
watch(selectedDay, (value) => {
ensureDayInitialized(updateScheduleModel.value, value)
})
async function get() { async function get() {
isLoading.value = true isLoading.value = true
await QueryGetAPI<ScheduleWeekInfo[]>(SCHEDULE_API_URL + 'get', { await QueryGetAPI<ScheduleWeekInfo[]>(SCHEDULE_API_URL + 'get', {
@@ -147,12 +369,12 @@ async function get() {
}) })
.then((data) => { .then((data) => {
if (data.code == 200) { if (data.code == 200) {
schedules.value = data.data schedules.value = (data.data ?? []).map((week) => normalizeWeek(week))
} else { } else {
message.error('加载失败: ' + data.message) message.error('加载失败: ' + data.message)
} }
}) })
.catch((err) => { .catch(() => {
message.error('加载失败') message.error('加载失败')
}) })
.finally(() => (isLoading.value = false)) .finally(() => (isLoading.value = false))
@@ -160,15 +382,17 @@ async function get() {
const isFetching = ref(false) const isFetching = ref(false)
async function addSchedule() { async function addSchedule() {
isFetching.value = true isFetching.value = true
const emptyWeek = createEmptyWeek(selectedScheduleYear.value, selectedScheduleWeek.value)
await QueryPostAPI(SCHEDULE_API_URL + 'update', { await QueryPostAPI(SCHEDULE_API_URL + 'update', {
year: selectedScheduleYear.value, year: emptyWeek.year,
week: selectedScheduleWeek.value, week: emptyWeek.week,
days: emptyWeek.days,
}) })
.then((data) => { .then((data) => {
if (data.code == 200) { if (data.code == 200) {
message.success('添加成功') message.success('添加成功')
showAddModal.value = false showAddModal.value = false
get() schedules.value = [...schedules.value, emptyWeek]
} else { } else {
message.error('添加失败: ' + data.message) message.error('添加失败: ' + data.message)
} }
@@ -183,30 +407,60 @@ async function onCopySchedule() {
} else { } else {
updateScheduleModel.value.year = selectedScheduleYear.value updateScheduleModel.value.year = selectedScheduleYear.value
updateScheduleModel.value.week = selectedScheduleWeek.value updateScheduleModel.value.week = selectedScheduleWeek.value
await onUpdateSchedule() ensureDayInitialized(updateScheduleModel.value, selectedDay.value)
await saveSchedule(null)
showCopyModal.value = false showCopyModal.value = false
} }
} }
async function onUpdateSchedule() { async function saveSchedule(day: number | null) {
isFetching.value = true isFetching.value = true
await QueryPostAPI(SCHEDULE_API_URL + 'update', { const sanitizedDays = sanitizeDays(updateScheduleModel.value.days)
const payload: {
year: number
week: number
day?: number
days: ScheduleDayInfo[][]
} = {
year: updateScheduleModel.value.year, year: updateScheduleModel.value.year,
week: updateScheduleModel.value.week, week: updateScheduleModel.value.week,
day: selectedDay.value, days: sanitizedDays,
days: updateScheduleModel.value?.days, }
})
if (day !== null && day !== undefined) {
payload.day = day
}
await QueryPostAPI(SCHEDULE_API_URL + 'update', payload)
.then((data) => { .then((data) => {
if (data.code == 200) { if (data.code == 200) {
message.success('成功') message.success('成功')
const s = schedules.value?.find( const normalizedWeek = normalizeWeek({
(s) => s.year == selectedScheduleYear.value && s.week == selectedScheduleWeek.value, year: payload.year,
week: payload.week,
days: sanitizedDays,
})
const index = schedules.value.findIndex(
(s) => s.year == updateScheduleModel.value.year && s.week == updateScheduleModel.value.week,
) )
if (s) {
s.days[selectedDay.value] = updateScheduleModel.value.days[selectedDay.value] if (index >= 0) {
if (day !== null && day !== undefined) {
const current = cloneWeek(schedules.value[index])
current.days[day] = normalizedWeek.days[day]
schedules.value.splice(index, 1, current)
} else {
schedules.value.splice(index, 1, normalizedWeek)
}
} else { } else {
schedules.value?.push(updateScheduleModel.value) schedules.value.push(normalizedWeek)
} }
//updateScheduleModel.value = {} as ScheduleWeekInfo updateScheduleModel.value = normalizeWeek({
year: payload.year,
week: payload.week,
days: sanitizedDays,
})
ensureDayInitialized(updateScheduleModel.value, selectedDay.value)
} else { } else {
message.error('修改失败: ' + data.message) message.error('修改失败: ' + data.message)
} }
@@ -215,6 +469,9 @@ async function onUpdateSchedule() {
isFetching.value = false isFetching.value = false
}) })
} }
async function onUpdateSchedule() {
await saveSchedule(selectedDay.value)
}
async function onDeleteSchedule(schedule: ScheduleWeekInfo) { async function onDeleteSchedule(schedule: ScheduleWeekInfo) {
await QueryGetAPI(SCHEDULE_API_URL + 'del', { await QueryGetAPI(SCHEDULE_API_URL + 'del', {
year: schedule.year, year: schedule.year,
@@ -229,19 +486,97 @@ async function onDeleteSchedule(schedule: ScheduleWeekInfo) {
}) })
} }
function onOpenUpdateModal(schedule: ScheduleWeekInfo) { function onOpenUpdateModal(schedule: ScheduleWeekInfo) {
updateScheduleModel.value = JSON.parse(JSON.stringify(schedule)) updateScheduleModel.value = cloneWeek(schedule)
selectedDay.value = 0
ensureDayInitialized(updateScheduleModel.value, selectedDay.value)
showUpdateModal.value = true showUpdateModal.value = true
} }
function onOpenCopyModal(schedule: ScheduleWeekInfo) { function onOpenCopyModal(schedule: ScheduleWeekInfo) {
updateScheduleModel.value = JSON.parse(JSON.stringify(schedule)) updateScheduleModel.value = cloneWeek(schedule, { resetIds: true })
selectedDay.value = 0
ensureDayInitialized(updateScheduleModel.value, selectedDay.value)
showCopyModal.value = true showCopyModal.value = true
} }
function onSelectChange(value: string | null, option: SelectMixedOption) { function onEditScheduleItem(schedule: ScheduleWeekInfo, dayIndex: number, item: ScheduleDayInfo) {
updateScheduleModel.value = cloneWeek(schedule)
selectedDay.value = dayIndex
ensureDayInitialized(updateScheduleModel.value, dayIndex)
showUpdateModal.value = true
}
async function onDeleteScheduleItem(schedule: ScheduleWeekInfo, dayIndex: number, item: ScheduleDayInfo) {
const targetSchedule = schedules.value.find(s => s.year === schedule.year && s.week === schedule.week)
if (!targetSchedule) return
const itemIndex = targetSchedule.days[dayIndex].findIndex(i =>
i.id === item.id || (i.title === item.title && i.time === item.time && i.tag === item.tag)
)
if (itemIndex === -1) return
const updatedDays = targetSchedule.days.map((dayList, idx) => {
if (idx === dayIndex) {
return dayList.filter((_, i) => i !== itemIndex)
}
return dayList
})
await QueryPostAPI(SCHEDULE_API_URL + 'update', {
year: schedule.year,
week: schedule.week,
days: sanitizeDays(updatedDays),
}).then((data) => {
if (data.code == 200) {
message.success('已删除')
const index = schedules.value.findIndex(s => s.year === schedule.year && s.week === schedule.week)
if (index >= 0) {
schedules.value[index] = normalizeWeek({
year: schedule.year,
week: schedule.week,
days: updatedDays,
})
}
} else {
message.error('删除失败: ' + data.message)
}
})
}
function onSelectChange(value: string | null, option: SelectMixedOption, itemIndex: number) {
if (value) { if (value) {
updateScheduleModel.value.days[selectedDay.value].tagColor = value ensureDayInitialized(updateScheduleModel.value, selectedDay.value)
updateScheduleModel.value.days[selectedDay.value].tag = option.label as string const entry = updateScheduleModel.value.days[selectedDay.value][itemIndex]
if (entry) {
entry.tagColor = value
entry.tag = option.label as string
}
} }
} }
function addScheduleItem() {
ensureDayInitialized(updateScheduleModel.value, selectedDay.value)
updateScheduleModel.value.days[selectedDay.value].push(createEmptyDay())
}
function removeScheduleItem(index: number) {
const dayList = updateScheduleModel.value.days[selectedDay.value]
if (dayList && dayList.length > 0) {
dayList.splice(index, 1)
if (dayList.length === 0) {
dayList.push(createEmptyDay())
}
}
}
function moveScheduleItem(index: number, direction: 'up' | 'down') {
const dayList = updateScheduleModel.value.days[selectedDay.value]
if (!dayList) return
const targetIndex = direction === 'up' ? index - 1 : index + 1
if (targetIndex < 0 || targetIndex >= dayList.length) return
const temp = dayList[index]
dayList[index] = dayList[targetIndex]
dayList[targetIndex] = temp
}
const renderOption = ({ node, option }: { node: VNode; option: SelectOption }) => const renderOption = ({ node, option }: { node: VNode; option: SelectOption }) =>
h(NSpace, { align: 'center', size: 3, style: 'margin-left: 5px' }, () => [ h(NSpace, { align: 'center', size: 3, style: 'margin-left: 5px' }, () => [
option.value ? h(NBadge, { dot: true, color: option.value?.toString() }) : null, option.value ? h(NBadge, { dot: true, color: option.value?.toString() }) : null,
@@ -395,7 +730,7 @@ onMounted(() => {
</NModal> </NModal>
<NModal <NModal
v-model:show="showUpdateModal" v-model:show="showUpdateModal"
style="width: 600px; max-width: 90vw" style="width: 800px; max-width: 95vw; max-height: 90vh;"
preset="card" preset="card"
title="编辑周程" title="编辑周程"
> >
@@ -405,60 +740,142 @@ onMounted(() => {
/> />
<NDivider /> <NDivider />
<template v-if="updateScheduleModel"> <template v-if="updateScheduleModel">
<NSpace vertical> <div
<NSpace> style="
<NInputGroup> max-height: calc(90vh - 300px);
<NInputGroupLabel type="primary"> overflow-y: auto;
标签 padding-right: 8px;
</NInputGroupLabel> scrollbar-width: thin;
<NInput scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
v-model:value="updateScheduleModel.days[selectedDay].tag" "
placeholder="标签 | 留空视为无安排" >
style="max-width: 300px" <NSpace
maxlength="10" vertical
show-count :size="12"
/>
</NInputGroup>
<NSelect
v-model:value="selectedExistTag"
:options="existTagOptions"
filterable
clearable
placeholder="使用过的标签"
style="max-width: 150px"
:render-option="renderOption"
@update:value="onSelectChange"
/>
</NSpace>
<NInputGroup>
<NInputGroupLabel> 内容 </NInputGroupLabel>
<NInput
v-model:value="updateScheduleModel.days[selectedDay].title"
placeholder="内容"
style="max-width: 200px"
maxlength="30"
show-count
/>
</NInputGroup>
<NTimePicker
v-model:formatted-value="updateScheduleModel.days[selectedDay].time"
default-formatted-value="20:00"
format="HH:mm"
/>
<NColorPicker
v-model:value="updateScheduleModel.days[selectedDay].tagColor"
:swatches="['#FFFFFF', '#18A058', '#2080F0', '#F0A020', 'rgba(208, 48, 80, 1)']"
default-value="#61B589"
:show-alpha="false"
:modes="['hex']"
/>
<NButton
:loading="isFetching"
@click="onUpdateSchedule()"
> >
保存 <NButton
</NButton> type="primary"
</NSpace> secondary
@click="addScheduleItem"
>
+ 添加行程项
</NButton>
<NCard
v-for="(item, itemIndex) in updateScheduleModel.days[selectedDay]"
:key="itemIndex"
size="small"
:bordered="true"
:style="{
borderLeft: item.tagColor ? `4px solid ${item.tagColor}` : 'none',
backgroundColor: item.tagColor ? item.tagColor + '08' : 'transparent'
}"
>
<template #header>
<NSpace align="center" :size="8">
<NText strong style="font-size: 14px;">行程 {{ itemIndex + 1 }}</NText>
<NButton
v-if="itemIndex > 0"
size="tiny"
quaternary
@click="moveScheduleItem(itemIndex, 'up')"
>
</NButton>
<NButton
v-if="itemIndex < updateScheduleModel.days[selectedDay].length - 1"
size="tiny"
quaternary
@click="moveScheduleItem(itemIndex, 'down')"
>
</NButton>
</NSpace>
</template>
<template #header-extra>
<NButton
size="tiny"
type="error"
quaternary
@click="removeScheduleItem(itemIndex)"
>
删除
</NButton>
</template>
<NSpace
vertical
:size="12"
>
<NSpace align="center" :size="8" style="flex-wrap: wrap;">
<NInputGroup style="width: auto; min-width: 200px;">
<NInputGroupLabel type="primary" style="min-width: 50px;">
标签
</NInputGroupLabel>
<NInput
v-model:value="item.tag"
placeholder="标签名称"
style="width: 150px;"
maxlength="10"
show-count
/>
</NInputGroup>
<NSelect
:value="null"
:options="existTagOptions"
filterable
clearable
placeholder="选择已用标签"
style="width: 140px;"
:render-option="renderOption"
@update:value="(val, opt) => onSelectChange(val, opt, itemIndex)"
/>
</NSpace>
<NInputGroup>
<NInputGroupLabel style="min-width: 50px;"> 内容 </NInputGroupLabel>
<NInput
v-model:value="item.title"
placeholder="事件内容描述"
maxlength="50"
show-count
/>
</NInputGroup>
<NSpace align="center" :size="8">
<NInputGroup style="width: auto;">
<NInputGroupLabel style="min-width: 50px;">时间</NInputGroupLabel>
<NTimePicker
v-model:formatted-value="item.time"
default-formatted-value="20:00"
format="HH:mm"
style="width: 120px"
clearable
/>
</NInputGroup>
<NInputGroup style="width: auto;">
<NInputGroupLabel style="min-width: 50px;">颜色</NInputGroupLabel>
<NColorPicker
:key="`color-${selectedDay}-${itemIndex}-${item.id || 'new'}`"
:value="normalizeColor(item.tagColor)"
@update:value="(val) => item.tagColor = normalizeColor(val)"
:swatches="['#18A058', '#2080F0', '#F0A020', '#D03050', '#9333EA', '#14B8A6']"
default-value="#2080F0"
:show-alpha="false"
:modes="['hex']"
style="width: 120px;"
/>
</NInputGroup>
</NSpace>
</NSpace>
</NCard>
</NSpace>
</div>
<NDivider />
<NButton
type="primary"
:loading="isFetching"
block
@click="onUpdateSchedule()"
>
保存全部
</NButton>
</template> </template>
</NModal> </NModal>
<NSpin <NSpin
@@ -472,5 +889,39 @@ onMounted(() => {
@on-update="onOpenUpdateModal" @on-update="onOpenUpdateModal"
@on-delete="onDeleteSchedule" @on-delete="onDeleteSchedule"
@on-copy="onOpenCopyModal" @on-copy="onOpenCopyModal"
@on-edit-item="onEditScheduleItem"
@on-delete-item="onDeleteScheduleItem"
/> />
</template> </template>
<style scoped>
/* 自定义滚动条样式 - Webkit浏览器 */
div::-webkit-scrollbar {
width: 8px;
height: 8px;
}
div::-webkit-scrollbar-track {
background: transparent;
border-radius: 4px;
}
div::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.2);
border-radius: 4px;
transition: background 0.2s;
}
div::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.3);
}
/* 深色模式下的滚动条 */
html.dark div::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
}
html.dark div::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}
</style>

View File

@@ -99,48 +99,55 @@
year: 2023, year: 2023,
week: 30, week: 30,
days: [ days: [
{ [{
title: '唱唱歌!', title: '唱唱歌!',
tag: '歌回', tag: '歌回',
tagColor: '#61B589', tagColor: '#61B589',
time: '10:00 AM', time: '10:00 AM',
}, id: null,
{ }],
[{
title: '玩点游戏', title: '玩点游戏',
tag: '游戏', tag: '游戏',
tagColor: '#A36565', tagColor: '#A36565',
time: '20:00 PM', time: '20:00 PM',
}, id: null,
{ }],
[{
title: 'Title 3', title: 'Title 3',
tag: 'Tag 3', tag: 'Tag 3',
tagColor: '#7BCDEF', tagColor: '#7BCDEF',
time: '11:00 PM', time: '11:00 PM',
}, id: null,
{ }],
[{
title: null, title: null,
tag: null, tag: null,
tagColor: null, tagColor: null,
time: null, time: null,
}, id: null,
{ }],
[{
title: null, title: null,
tag: null, tag: null,
tagColor: null, tagColor: null,
time: null, time: null,
}, id: null,
{ }],
[{
title: null, title: null,
tag: null, tag: null,
tagColor: null, tagColor: null,
time: null, time: null,
}, id: null,
{ }],
[{
title: null, title: null,
tag: null, tag: null,
tagColor: null, tagColor: null,
time: null, time: null,
}, id: null,
}],
], ],
}, },
] as ScheduleWeekInfo[], ] as ScheduleWeekInfo[],

View File

@@ -162,16 +162,20 @@ const daysOfWeek = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
// Formatted schedule data for display // Formatted schedule data for display
const formattedSchedule = computed(() => { const formattedSchedule = computed(() => {
if (!currentWeekData.value || !Array.isArray(currentWeekData.value.days)) return []; if (!currentWeekData.value || !Array.isArray(currentWeekData.value.days)) return [];
const scheduleMap = new Map<string, ScheduleDayInfo>();
currentWeekData.value.days.forEach((day: ScheduleDayInfo, index: number) => { return daysOfWeek.map((dayKey, index) => {
const dayKey = daysOfWeek[index] || `day${index}`; const dayList = currentWeekData.value!.days[index];
scheduleMap.set(dayKey, day); // 如果当天有多个行程,取第一个展示;如果没有则显示默认
const firstItem = Array.isArray(dayList) && dayList.length > 0
? dayList[0]
: { time: '', tag: '', title: '', tagColor: '', id: null };
return {
key: dayKey,
label: dayMap[dayKey] || dayKey,
data: firstItem
};
}); });
return daysOfWeek.map(dayKey => ({
key: dayKey,
label: dayMap[dayKey] || dayKey,
data: scheduleMap.get(dayKey) || { time: '', tag: '', title: '' }
}));
}); });
// --- 方法 --- // --- 方法 ---

View File

@@ -21,6 +21,19 @@ const currentWeek = computed(() => {
return isTodayInWeek(item.year, item.week) return isTodayInWeek(item.year, item.week)
}) })
}) })
const formattedDays = computed(() => {
const weekData = currentWeek.value
if (!weekData || !Array.isArray(weekData.days)) return []
return weekData.days.map((dayList) => {
// 取每天第一个行程展示
if (Array.isArray(dayList) && dayList.length > 0) {
return dayList[0]
}
return { time: '', tag: '', title: '', tagColor: '', id: null }
})
})
const options = computed(() => { const options = computed(() => {
return props.data?.map((item) => { return props.data?.map((item) => {
return { return {
@@ -56,7 +69,7 @@ onMounted(() => {
/> />
<SaveCompoent <SaveCompoent
:compoent="table" :compoent="table"
:file-name="`周表_${selectedDate}_${userInfo?.name}`" :file-name="`周表_${selectedDate}_${props.userInfo?.name}`"
/> />
</NSpace> </NSpace>
<NDivider /> <NDivider />
@@ -66,7 +79,7 @@ onMounted(() => {
> >
<div class="schedule-template pinky day-container"> <div class="schedule-template pinky day-container">
<div <div
v-for="(item, index) in currentWeek?.days" v-for="(item, index) in formattedDays"
:id="index.toString()" :id="index.toString()"
:key="index" :key="index"
class="schedule-template pinky day-item" class="schedule-template pinky day-item"
@@ -76,15 +89,15 @@ onMounted(() => {
{{ days[index] }} {{ days[index] }}
</span> </span>
<span class="schedule-template pinky time"> <span class="schedule-template pinky time">
{{ item.time }} {{ item?.time }}
</span> </span>
<span class="schedule-template pinky tag"> <span class="schedule-template pinky tag">
{{ item.tag }} {{ item?.tag }}
</span> </span>
</div> </div>
<div class="schedule-template pinky day-content-container"> <div class="schedule-template pinky day-content-container">
<span <span
v-if="item.tag" v-if="item?.tag"
id="work" id="work"
class="schedule-template pinky day-content" class="schedule-template pinky day-content"
> >

View File

@@ -1,9 +1,9 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "esnext", "target": "ESNext",
"module": "esnext", "module": "ESNext",
"strict": true, "strict": true,
"moduleResolution": "bundler", "moduleResolution": "Bundler",
"skipLibCheck": true, "skipLibCheck": true,
"esModuleInterop": true, "esModuleInterop": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
@@ -16,7 +16,9 @@
"paths": { "paths": {
"@/*": ["src/*"] "@/*": ["src/*"]
}, },
"lib": ["esnext", "dom", "dom.iterable", "scripthost"] "lib": ["ESNext", "DOM", "DOM.Iterable", "ScriptHost"],
"resolveJsonModule": true,
"isolatedModules": true
}, },
"include": [ "include": [
"src/**/*.ts", "src/**/*.ts",
@@ -27,7 +29,9 @@
"env.d.ts", "env.d.ts",
"default.d.ts", "default.d.ts",
"src/data/chat/ChatClientDirectOpenLive.js", "src/data/chat/ChatClientDirectOpenLive.js",
"src/data/chat/models.js", "src/store/useDanmakuClient.ts", "src/data/chat/models.js",
"src/store/useDanmakuClient.ts"
], ],
"exclude": ["node_modules"] "exclude": ["node_modules"],
"references": [{ "path": "./tsconfig.node.json" }]
} }

10
tsconfig.node.json Normal file
View File

@@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["node"]
},
"include": ["vite.config.mts", "eslint.config.mjs"]
}

View File

@@ -7,7 +7,6 @@ import { NaiveUiResolver } from 'unplugin-vue-components/resolvers';
import Components from 'unplugin-vue-components/vite'; import Components from 'unplugin-vue-components/vite';
import Markdown from 'unplugin-vue-markdown/vite'; import Markdown from 'unplugin-vue-markdown/vite';
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import oxlintPlugin from 'vite-plugin-oxlint';
import svgLoader from 'vite-svg-loader'; import svgLoader from 'vite-svg-loader';
import { VineVitePlugin } from 'vue-vine/vite'; import { VineVitePlugin } from 'vue-vine/vite';
@@ -18,11 +17,11 @@ const removeSodipodiInkscape = {
fn: () => { fn: () => {
return { return {
element: { element: {
enter: (node, parentNode) => { enter: (node: any, parentNode: any) => {
// 检查元素名称是否以sodipodi:或inkscape:开头 // 检查元素名称是否以sodipodi:或inkscape:开头
if (node.name && (node.name.startsWith('sodipodi:') || node.name.startsWith('inkscape:'))) { if (node.name && (node.name.startsWith('sodipodi:') || node.name.startsWith('inkscape:'))) {
// 从父节点的children数组中过滤掉当前节点 // 从父节点的children数组中过滤掉当前节点
parentNode.children = parentNode.children.filter(child => child !== node); parentNode.children = parentNode.children.filter((child: any) => child !== node);
} }
}, },
}, },
@@ -85,10 +84,8 @@ export default defineConfig({
resolvers: [NaiveUiResolver()], resolvers: [NaiveUiResolver()],
dts: 'src/components.d.ts', dts: 'src/components.d.ts',
extensions: ['vue', 'md'], extensions: ['vue', 'md'],
include: [/\.vue$/, /\.vue\?vue/, /\.md$/, /\.vine$/], include: [/\.vue$/, /\.vue\?vue/, /\.md$/, /\.vine$/],
}), }),
oxlintPlugin(),
VineVitePlugin(), VineVitePlugin(),
], ],
server: { port: 51000 }, server: { port: 51000 },
@@ -103,5 +100,16 @@ export default defineConfig({
}, },
build: { build: {
sourcemap: true, sourcemap: true,
target: 'esnext',
minify: 'esbuild',
chunkSizeWarningLimit: 1000,
rollupOptions: {
output: {
manualChunks: {
'vue-vendor': ['vue', 'vue-router', 'pinia'],
'ui-vendor': ['naive-ui', '@vueuse/core'],
}
}
}
}, },
}); });