chore: init read-star monorepo

This commit is contained in:
2026-01-16 13:06:44 +08:00
commit 1e510efe8e
508 changed files with 44803 additions and 0 deletions

View File

@ -0,0 +1,39 @@
<script setup lang="ts">
import { useDialog, useLoadingBar, useMessage, useNotification } from 'naive-ui'
import { createTextVNode, defineComponent } from 'vue'
defineOptions({
name: 'AppProvider',
})
const ContextHolder = defineComponent({
name: 'ContextHolder',
setup() {
function register() {
window.$loadingBar = useLoadingBar()
window.$dialog = useDialog()
window.$message = useMessage()
window.$notification = useNotification()
}
register()
return () => createTextVNode()
},
})
</script>
<template>
<NLoadingBarProvider>
<NDialogProvider>
<NNotificationProvider>
<NMessageProvider>
<ContextHolder />
<slot />
</NMessageProvider>
</NNotificationProvider>
</NDialogProvider>
</NLoadingBarProvider>
</template>
<style scoped></style>

View File

@ -0,0 +1,17 @@
<script setup lang="ts">
defineOptions({ name: 'DarkModeContainer' })
defineProps<Props>()
interface Props {
inverted?: boolean
}
</script>
<template>
<div class="bg-container text-base-text transition-300" :class="{ 'bg-inverted text-#1f1f1f': inverted }">
<slot />
</div>
</template>
<style scoped></style>

View File

@ -0,0 +1,45 @@
<script lang="ts" setup>
import { computed } from 'vue'
import { useRouterPush } from '@/hooks/common/router'
import { $t } from '@/locales'
defineOptions({ name: 'ExceptionBase' })
const props = defineProps<Props>()
type ExceptionType = '403' | '404' | '500'
interface Props {
/**
* Exception type
*
* - 403: no permission
* - 404: not found
* - 500: service error
*/
type: ExceptionType
}
const { routerPushByKey } = useRouterPush()
const iconMap: Record<ExceptionType, string> = {
403: 'no-permission',
404: 'not-found',
500: 'service-error',
}
const icon = computed(() => iconMap[props.type])
</script>
<template>
<div class="size-full min-h-520px flex-col-center gap-24px overflow-hidden">
<div class="flex text-400px text-primary">
<SvgIcon :local-icon="icon" />
</div>
<NButton type="primary" @click="routerPushByKey('root')">
{{ $t('common.backToHome') }}
</NButton>
</div>
</template>
<style scoped></style>

View File

@ -0,0 +1,22 @@
<script setup lang="ts">
import { $t } from '@/locales'
defineOptions({
name: 'FullScreen',
})
defineProps<Props>()
interface Props {
full?: boolean
}
</script>
<template>
<ButtonIcon :key="String(full)" :tooltip-content="full ? $t('icon.fullscreenExit') : $t('icon.fullscreen')">
<icon-gridicons-fullscreen-exit v-if="full" />
<icon-gridicons-fullscreen v-else />
</ButtonIcon>
</template>
<style scoped></style>

View File

@ -0,0 +1,42 @@
<script lang="ts" setup>
import type { PopoverPlacement } from 'naive-ui'
import { computed, useSlots } from 'vue'
defineOptions({ name: 'IconTooltip' })
const props = withDefaults(defineProps<Props>(), {
icon: 'mdi-help-circle',
localIcon: '',
desc: '',
placement: 'top',
})
interface Props {
icon?: string
localIcon?: string
desc?: string
placement?: PopoverPlacement
}
const slots = useSlots()
const hasCustomTrigger = computed(() => Boolean(slots.trigger))
if (!hasCustomTrigger.value && !props.icon && !props.localIcon) {
throw new Error('icon or localIcon is required when no custom trigger slot is provided')
}
</script>
<template>
<NTooltip :placement="placement">
<template #trigger>
<slot name="trigger">
<div class="cursor-pointer">
<SvgIcon :icon="icon" :local-icon="localIcon" />
</div>
</slot>
</template>
<slot>
<span>{{ desc }}</span>
</slot>
</NTooltip>
</template>

View File

@ -0,0 +1,62 @@
<script setup lang="ts">
import { computed } from 'vue'
import { $t } from '@/locales'
defineOptions({
name: 'LangSwitch',
})
const props = withDefaults(defineProps<Props>(), {
showTooltip: true,
})
const emit = defineEmits<Emits>()
interface Props {
/** Current language */
lang: App.I18n.LangType
/** Language options */
langOptions: App.I18n.LangOption[]
/** Show tooltip */
showTooltip?: boolean
}
interface Emits {
(e: 'changeLang', lang: App.I18n.LangType): void
}
const tooltipContent = computed(() => {
if (!props.showTooltip)
return ''
return $t('icon.lang')
})
/** Add bottom margin to all options except the last one for proper visual separation */
const dropdownOptions = computed(() => {
const lastIndex = props.langOptions.length - 1
return props.langOptions.map((option, index) => ({
...option,
props: {
class: index < lastIndex ? 'mb-1' : undefined,
},
}))
})
function changeLang(lang: App.I18n.LangType) {
emit('changeLang', lang)
}
</script>
<template>
<NDropdown :value="lang" :options="dropdownOptions" trigger="hover" @select="changeLang">
<div>
<ButtonIcon :tooltip-content="tooltipContent" tooltip-placement="left">
<SvgIcon icon="heroicons:language" />
</ButtonIcon>
</div>
</NDropdown>
</template>
<style scoped></style>

View File

@ -0,0 +1,53 @@
<script lang="ts" setup>
import { computed } from 'vue'
import { $t } from '@/locales'
defineOptions({ name: 'MenuToggler' })
const props = withDefaults(defineProps<Props>(), {
arrowIcon: false,
zIndex: 98,
})
interface Props {
/** Show collapsed icon */
collapsed?: boolean
/** Arrow style icon */
arrowIcon?: boolean
zIndex?: number
}
type NumberBool = 0 | 1
const icon = computed(() => {
const icons: Record<NumberBool, Record<NumberBool, string>> = {
0: {
0: 'line-md:menu-fold-left',
1: 'line-md:menu-fold-right',
},
1: {
0: 'ph-caret-double-left-bold',
1: 'ph-caret-double-right-bold',
},
}
const arrowIcon = Number(props.arrowIcon || false) as NumberBool
const collapsed = Number(props.collapsed || false) as NumberBool
return icons[arrowIcon][collapsed]
})
</script>
<template>
<ButtonIcon
:key="String(collapsed)"
:tooltip-content="collapsed ? $t('icon.expand') : $t('icon.collapse')"
tooltip-placement="bottom-start"
:z-index="zIndex"
>
<SvgIcon :icon="icon" />
</ButtonIcon>
</template>
<style scoped></style>

View File

@ -0,0 +1,26 @@
<script lang="ts" setup>
import { computed } from 'vue'
import { $t } from '@/locales'
defineOptions({ name: 'PinToggler' })
const props = defineProps<Props>()
interface Props {
pin?: boolean
}
const icon = computed(() => (props.pin ? 'mdi-pin-off' : 'mdi-pin'))
</script>
<template>
<ButtonIcon
:tooltip-content="pin ? $t('icon.unpin') : $t('icon.pin')"
tooltip-placement="bottom-start"
:z-index="100"
>
<SvgIcon :icon="icon" />
</ButtonIcon>
</template>
<style scoped></style>

View File

@ -0,0 +1,21 @@
<script setup lang="ts">
import { $t } from '@/locales'
defineOptions({
name: 'ReloadButton',
})
defineProps<Props>()
interface Props {
loading?: boolean
}
</script>
<template>
<ButtonIcon :tooltip-content="$t('icon.reload')">
<icon-ant-design-reload-outlined :class="{ 'animate-spin animate-duration-750': loading }" />
</ButtonIcon>
</template>
<style scoped></style>

View File

@ -0,0 +1,160 @@
<template>
<div class="app-logo">
<svg
width="100%"
height="100%"
version="1.1"
viewBox="0 0 1000 1000"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
>
<g>
<path
d="M 200,866 C 100,866 50,779.4 100,692.8 L 200,519.6 C 220,485 240,490 265,499.6 S 360,542.68 360,542.68 C 480.5,601 498,642.5 500,720 C 498,811 462,856 420,866"
fill="url(#LinearGradient)"
fill-rule="nonzero"
opacity="1"
stroke="none"
/>
<path
d="M 420,866 C 455,861 478,846 500,827 C 614,696 615,597 500,517 C 394,444 333,374 380,207.82 L 260,415.67 C 240.22,450 254.37,465.1 275.28,481.79 S 360,542.68 360,542.68 C 480.5,601 498,642.5 500,720 C 498,811 462,856 420,866"
fill="url(#LinearGradient_2)"
fill-rule="nonzero"
opacity="1"
stroke="none"
/>
<path
d="M 500,517 C 394,444 333,374 380,207.82 L 400,173.2 C 367,295 421,350 603,428 C 572,440 524,474 500,517"
fill="url(#LinearGradient_3)"
fill-rule="nonzero"
opacity="1"
stroke="none"
/>
<path
d="M 500,827 L 660,660 C 738,589 710,482 603,428 C 572,440 524,474 500,517 C 615,597 614,696 500,827"
fill="url(#LinearGradient_4)"
fill-rule="nonzero"
opacity="1"
stroke="none"
/>
<path
d="M 400,173.2 C 367,295 421,350 603,428 C 690,389, 750,445 788,500 L 600,173.2 C 550,86.6 450,86.6 400,173.2"
fill="url(#LinearGradient_5)"
fill-rule="nonzero"
opacity="1"
stroke="none"
/>
<path
d="M 500,827 L 660,660 C 738,589 710,482 603,428 C 690,389, 750,445 788,500 C 816,554 797,606 750,640 L 500,827"
fill="url(#LinearGradient_6)"
fill-rule="nonzero"
opacity="1"
stroke="none"
/>
<path
d="M 788,500 C 816,554 797,606 750,640 L 500,827 C 497,851 513,862 540,866 L 800,866 C 900,866 950,779.4 900,692.8 L 788,500"
fill="url(#LinearGradient_7)"
fill-rule="nonzero"
opacity="1"
stroke="none"
/>
</g>
<defs>
<linearGradient
id="LinearGradient"
gradientTransform="matrix(104.391 -73.3432 73.3432 104.391 277.441 710.122)"
gradientUnits="userSpaceOnUse"
x1="0"
x2="1"
y1="0"
y2="0"
>
<stop offset="0" stop-color="var(--logo-color-700)" />
<stop offset="1" stop-color="var(--logo-color-600)" />
</linearGradient>
<linearGradient
id="LinearGradient_2"
gradientTransform="matrix(-173.747 557.324 -557.324 -173.747 508.829 258.172)"
gradientUnits="userSpaceOnUse"
x1="0"
x2="1"
y1="0"
y2="0"
>
<stop offset="0" stop-color="var(--logo-color-300)" />
<stop offset="1" stop-color="var(--logo-color-500)" />
</linearGradient>
<linearGradient
id="LinearGradient_3"
gradientTransform="matrix(157.951 295.666 -295.666 157.951 382.944 193.642)"
gradientUnits="userSpaceOnUse"
x1="0"
x2="1"
y1="0"
y2="0"
>
<stop offset="0" stop-color="var(--logo-color-600)" />
<stop offset="1" stop-color="var(--logo-color-700)" />
</linearGradient>
<linearGradient
id="LinearGradient_4"
gradientTransform="matrix(-44.3023 219.578 -219.578 -44.3023 619.69 469.652)"
gradientUnits="userSpaceOnUse"
x1="0"
x2="1"
y1="0"
y2="0"
>
<stop offset="0" stop-color="var(--logo-color-400)" />
<stop offset="1" stop-color="var(--logo-color-600)" />
</linearGradient>
<linearGradient
id="LinearGradient_5"
gradientTransform="matrix(125.52 334.256 -334.256 125.52 539.723 235.139)"
gradientUnits="userSpaceOnUse"
x1="0"
x2="1"
y1="0"
y2="0"
>
<stop offset="0" stop-color="var(--logo-color-500)" />
<stop offset="1" stop-color="var(--logo-color-300)" />
</linearGradient>
<linearGradient
id="LinearGradient_6"
gradientTransform="matrix(-241.23 357.206 -357.206 -241.23 754.054 449.312)"
gradientUnits="userSpaceOnUse"
x1="0"
x2="1"
y1="0"
y2="0"
>
<stop offset="0" stop-color="var(--logo-color-300)" />
<stop offset="1" stop-color="var(--logo-color-500)" />
</linearGradient>
<linearGradient
id="LinearGradient_7"
gradientTransform="matrix(125.978 210.065 -210.065 125.978 596.433 613.665)"
gradientUnits="userSpaceOnUse"
x1="0"
x2="1"
y1="0"
y2="0"
>
<stop offset="0" stop-color="var(--logo-color-700)" />
<stop offset="1" stop-color="var(--logo-color-600)" />
</linearGradient>
</defs>
</svg>
</div>
</template>
<style scoped>
.app-logo {
--logo-color-300: rgb(var(--primary-300-color));
--logo-color-400: rgb(var(--primary-400-color));
--logo-color-500: rgb(var(--primary-500-color));
--logo-color-600: rgb(var(--primary-600-color));
--logo-color-700: rgb(var(--primary-700-color));
}
</style>

View File

@ -0,0 +1,57 @@
<script setup lang="ts">
import type { PopoverPlacement } from 'naive-ui'
import { computed } from 'vue'
import { $t } from '@/locales'
defineOptions({ name: 'ThemeSchemaSwitch' })
const props = withDefaults(defineProps<Props>(), {
showTooltip: true,
tooltipPlacement: 'bottom',
})
const emit = defineEmits<Emits>()
interface Props {
/** Theme schema */
themeSchema: UnionKey.ThemeScheme
/** Show tooltip */
showTooltip?: boolean
/** Tooltip placement */
tooltipPlacement?: PopoverPlacement
}
interface Emits {
(e: 'switch'): void
}
function handleSwitch() {
emit('switch')
}
const icons: Record<UnionKey.ThemeScheme, string> = {
light: 'material-symbols:sunny',
dark: 'material-symbols:nightlight-rounded',
auto: 'material-symbols:hdr-auto',
}
const icon = computed(() => icons[props.themeSchema])
const tooltipContent = computed(() => {
if (!props.showTooltip)
return ''
return $t('icon.themeSchema')
})
</script>
<template>
<ButtonIcon
:icon="icon"
:tooltip-content="tooltipContent"
:tooltip-placement="tooltipPlacement"
@click="handleSwitch"
/>
</template>
<style scoped></style>