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,7 @@
<script setup lang="ts"></script>
<template>
<ExceptionBase type="403" />
</template>
<style scoped></style>

View File

@ -0,0 +1,7 @@
<script setup lang="ts"></script>
<template>
<ExceptionBase type="404" />
</template>
<style scoped></style>

View File

@ -0,0 +1,7 @@
<script setup lang="ts"></script>
<template>
<ExceptionBase type="500" />
</template>
<style scoped></style>

View File

@ -0,0 +1,15 @@
<script setup lang="ts">
interface Props {
url: string
}
defineProps<Props>()
</script>
<template>
<div class="h-full">
<iframe id="iframePage" class="size-full" :src="url" />
</div>
</template>
<style scoped></style>

View File

@ -0,0 +1,94 @@
<script setup lang="ts">
import type { Component } from 'vue'
import { getPaletteColorByNumber, mixColor } from '@sa/color'
import { computed } from 'vue'
import { loginModuleRecord } from '@/constants/app'
import { $t } from '@/locales'
import { useAppStore } from '@/store/modules/app'
import { useThemeStore } from '@/store/modules/theme'
import BindWechat from './modules/bind-wechat.vue'
import CodeLogin from './modules/code-login.vue'
import PwdLogin from './modules/pwd-login.vue'
import Register from './modules/register.vue'
import ResetPwd from './modules/reset-pwd.vue'
interface Props {
/** The login module */
module?: UnionKey.LoginModule
}
const props = defineProps<Props>()
const appStore = useAppStore()
const themeStore = useThemeStore()
interface LoginModule {
label: App.I18n.I18nKey
component: Component
}
const moduleMap: Record<UnionKey.LoginModule, LoginModule> = {
'pwd-login': { label: loginModuleRecord['pwd-login'], component: PwdLogin },
'code-login': { label: loginModuleRecord['code-login'], component: CodeLogin },
'register': { label: loginModuleRecord.register, component: Register },
'reset-pwd': { label: loginModuleRecord['reset-pwd'], component: ResetPwd },
'bind-wechat': { label: loginModuleRecord['bind-wechat'], component: BindWechat },
}
const activeModule = computed(() => moduleMap[props.module || 'pwd-login'])
const bgThemeColor = computed(() =>
themeStore.darkMode ? getPaletteColorByNumber(themeStore.themeColor, 600) : themeStore.themeColor,
)
const bgColor = computed(() => {
const COLOR_WHITE = '#ffffff'
const ratio = themeStore.darkMode ? 0.5 : 0.2
return mixColor(COLOR_WHITE, themeStore.themeColor, ratio)
})
</script>
<template>
<div class="relative size-full flex-center overflow-hidden" :style="{ backgroundColor: bgColor }">
<WaveBg :theme-color="bgThemeColor" />
<NCard :bordered="false" class="relative z-4 w-auto rd-12px">
<div class="w-400px lt-sm:w-300px">
<header class="flex-y-center justify-between">
<SystemLogo class="size-64px lt-sm:size-48px" />
<h3 class="text-28px text-primary font-500 lt-sm:text-22px">
{{ $t('system.title') }}
</h3>
<div class="i-flex-col">
<ThemeSchemaSwitch
:theme-schema="themeStore.themeScheme"
:show-tooltip="false"
class="text-20px lt-sm:text-18px"
@switch="themeStore.toggleThemeScheme"
/>
<LangSwitch
v-if="themeStore.header.multilingual.visible"
:lang="appStore.locale"
:lang-options="appStore.localeOptions"
:show-tooltip="false"
@change-lang="appStore.changeLocale"
/>
</div>
</header>
<main class="pt-24px">
<h3 class="text-18px text-primary font-medium">
{{ $t(activeModule.label) }}
</h3>
<div class="pt-24px">
<Transition :name="themeStore.page.animateMode" mode="out-in" appear>
<component :is="activeModule.component" />
</Transition>
</div>
</main>
</div>
</NCard>
</div>
</template>
<style scoped></style>

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
defineOptions({
name: 'BindWechat',
})
</script>
<template>
<div />
</template>
<style scoped></style>

View File

@ -0,0 +1,66 @@
<script setup lang="ts">
import { computed, reactive } from 'vue'
import { useCaptcha } from '@/hooks/business/captcha'
import { useFormRules, useNaiveForm } from '@/hooks/common/form'
import { useRouterPush } from '@/hooks/common/router'
import { $t } from '@/locales'
defineOptions({
name: 'CodeLogin',
})
const { toggleLoginModule } = useRouterPush()
const { formRef, validate } = useNaiveForm()
const { label, isCounting, loading, getCaptcha } = useCaptcha()
interface FormModel {
phone: string
code: string
}
const model: FormModel = reactive({
phone: '',
code: '',
})
const rules = computed<Record<keyof FormModel, App.Global.FormRule[]>>(() => {
const { formRules } = useFormRules()
return {
phone: formRules.phone,
code: formRules.code,
}
})
async function handleSubmit() {
await validate()
// request
window.$message?.success($t('page.login.common.validateSuccess'))
}
</script>
<template>
<NForm ref="formRef" :model="model" :rules="rules" size="large" :show-label="false" @keyup.enter="handleSubmit">
<NFormItem path="phone">
<NInput v-model:value="model.phone" :placeholder="$t('page.login.common.phonePlaceholder')" />
</NFormItem>
<NFormItem path="code">
<div class="w-full flex-y-center gap-16px">
<NInput v-model:value="model.code" :placeholder="$t('page.login.common.codePlaceholder')" />
<NButton size="large" :disabled="isCounting" :loading="loading" @click="getCaptcha(model.phone)">
{{ label }}
</NButton>
</div>
</NFormItem>
<NSpace vertical :size="18" class="w-full">
<NButton type="primary" size="large" round block @click="handleSubmit">
{{ $t('common.confirm') }}
</NButton>
<NButton size="large" round block @click="toggleLoginModule('pwd-login')">
{{ $t('page.login.common.back') }}
</NButton>
</NSpace>
</NForm>
</template>
<style scoped></style>

View File

@ -0,0 +1,120 @@
<script setup lang="ts">
import { computed, reactive } from 'vue'
import { loginModuleRecord } from '@/constants/app'
import { useFormRules, useNaiveForm } from '@/hooks/common/form'
import { useRouterPush } from '@/hooks/common/router'
import { $t } from '@/locales'
import { useAuthStore } from '@/store/modules/auth'
defineOptions({
name: 'PwdLogin',
})
const authStore = useAuthStore()
const { toggleLoginModule } = useRouterPush()
const { formRef, validate } = useNaiveForm()
interface FormModel {
userName: string
password: string
}
const model: FormModel = reactive({
userName: 'Admin',
password: '123456',
})
const rules = computed<Record<keyof FormModel, App.Global.FormRule[]>>(() => {
// inside computed to make locale reactive, if not apply i18n, you can define it without computed
const { formRules } = useFormRules()
return {
userName: formRules.userName,
password: formRules.pwd,
}
})
async function handleSubmit() {
await validate()
await authStore.login(model.userName, model.password)
}
type AccountKey = 'super' | 'admin' | 'user'
interface Account {
key: AccountKey
label: string
userName: string
password: string
}
const accounts = computed<Account[]>(() => [
{
key: 'super',
label: $t('page.login.pwdLogin.superAdmin'),
userName: 'Super',
password: '123456',
},
{
key: 'admin',
label: $t('page.login.pwdLogin.admin'),
userName: 'Admin',
password: '123456',
},
{
key: 'user',
label: $t('page.login.pwdLogin.user'),
userName: 'User',
password: '123456',
},
])
async function handleAccountLogin(account: Account) {
await authStore.login(account.userName, account.password)
}
</script>
<template>
<NForm ref="formRef" :model="model" :rules="rules" size="large" :show-label="false" @keyup.enter="handleSubmit">
<NFormItem path="userName">
<NInput v-model:value="model.userName" :placeholder="$t('page.login.common.userNamePlaceholder')" />
</NFormItem>
<NFormItem path="password">
<NInput
v-model:value="model.password"
type="password"
show-password-on="click"
:placeholder="$t('page.login.common.passwordPlaceholder')"
/>
</NFormItem>
<NSpace vertical :size="24">
<div class="flex-y-center justify-between">
<NCheckbox>{{ $t('page.login.pwdLogin.rememberMe') }}</NCheckbox>
<NButton quaternary @click="toggleLoginModule('reset-pwd')">
{{ $t('page.login.pwdLogin.forgetPassword') }}
</NButton>
</div>
<NButton type="primary" size="large" round block :loading="authStore.loginLoading" @click="handleSubmit">
{{ $t('common.confirm') }}
</NButton>
<div class="flex-y-center justify-between gap-12px">
<NButton class="flex-1" block @click="toggleLoginModule('code-login')">
{{ $t(loginModuleRecord['code-login']) }}
</NButton>
<NButton class="flex-1" block @click="toggleLoginModule('register')">
{{ $t(loginModuleRecord.register) }}
</NButton>
</div>
<NDivider class="text-14px text-#666 !m-0">
{{ $t('page.login.pwdLogin.otherAccountLogin') }}
</NDivider>
<div class="flex-center gap-12px">
<NButton v-for="item in accounts" :key="item.key" type="primary" @click="handleAccountLogin(item)">
{{ item.label }}
</NButton>
</div>
</NSpace>
</NForm>
</template>
<style scoped></style>

View File

@ -0,0 +1,88 @@
<script setup lang="ts">
import { computed, reactive } from 'vue'
import { useCaptcha } from '@/hooks/business/captcha'
import { useFormRules, useNaiveForm } from '@/hooks/common/form'
import { useRouterPush } from '@/hooks/common/router'
import { $t } from '@/locales'
defineOptions({
name: 'Register',
})
const { toggleLoginModule } = useRouterPush()
const { formRef, validate } = useNaiveForm()
const { label, isCounting, loading, getCaptcha } = useCaptcha()
interface FormModel {
phone: string
code: string
password: string
confirmPassword: string
}
const model: FormModel = reactive({
phone: '',
code: '',
password: '',
confirmPassword: '',
})
const rules = computed<Record<keyof FormModel, App.Global.FormRule[]>>(() => {
const { formRules, createConfirmPwdRule } = useFormRules()
return {
phone: formRules.phone,
code: formRules.code,
password: formRules.pwd,
confirmPassword: createConfirmPwdRule(model.password),
}
})
async function handleSubmit() {
await validate()
// request to register
window.$message?.success($t('page.login.common.validateSuccess'))
}
</script>
<template>
<NForm ref="formRef" :model="model" :rules="rules" size="large" :show-label="false" @keyup.enter="handleSubmit">
<NFormItem path="phone">
<NInput v-model:value="model.phone" :placeholder="$t('page.login.common.phonePlaceholder')" />
</NFormItem>
<NFormItem path="code">
<div class="w-full flex-y-center gap-16px">
<NInput v-model:value="model.code" :placeholder="$t('page.login.common.codePlaceholder')" />
<NButton size="large" :disabled="isCounting" :loading="loading" @click="getCaptcha(model.phone)">
{{ label }}
</NButton>
</div>
</NFormItem>
<NFormItem path="password">
<NInput
v-model:value="model.password"
type="password"
show-password-on="click"
:placeholder="$t('page.login.common.passwordPlaceholder')"
/>
</NFormItem>
<NFormItem path="confirmPassword">
<NInput
v-model:value="model.confirmPassword"
type="password"
show-password-on="click"
:placeholder="$t('page.login.common.confirmPasswordPlaceholder')"
/>
</NFormItem>
<NSpace vertical :size="18" class="w-full">
<NButton type="primary" size="large" round block @click="handleSubmit">
{{ $t('common.confirm') }}
</NButton>
<NButton size="large" round block @click="toggleLoginModule('pwd-login')">
{{ $t('page.login.common.back') }}
</NButton>
</NSpace>
</NForm>
</template>
<style scoped></style>

View File

@ -0,0 +1,82 @@
<script setup lang="ts">
import { computed, reactive } from 'vue'
import { useFormRules, useNaiveForm } from '@/hooks/common/form'
import { useRouterPush } from '@/hooks/common/router'
import { $t } from '@/locales'
defineOptions({
name: 'ResetPwd',
})
const { toggleLoginModule } = useRouterPush()
const { formRef, validate } = useNaiveForm()
interface FormModel {
phone: string
code: string
password: string
confirmPassword: string
}
const model: FormModel = reactive({
phone: '',
code: '',
password: '',
confirmPassword: '',
})
type RuleRecord = Partial<Record<keyof FormModel, App.Global.FormRule[]>>
const rules = computed<RuleRecord>(() => {
const { formRules, createConfirmPwdRule } = useFormRules()
return {
phone: formRules.phone,
password: formRules.pwd,
confirmPassword: createConfirmPwdRule(model.password),
}
})
async function handleSubmit() {
await validate()
// request to reset password
window.$message?.success($t('page.login.common.validateSuccess'))
}
</script>
<template>
<NForm ref="formRef" :model="model" :rules="rules" size="large" :show-label="false" @keyup.enter="handleSubmit">
<NFormItem path="phone">
<NInput v-model:value="model.phone" :placeholder="$t('page.login.common.phonePlaceholder')" />
</NFormItem>
<NFormItem path="code">
<NInput v-model:value="model.code" :placeholder="$t('page.login.common.codePlaceholder')" />
</NFormItem>
<NFormItem path="password">
<NInput
v-model:value="model.password"
type="password"
show-password-on="click"
:placeholder="$t('page.login.common.passwordPlaceholder')"
/>
</NFormItem>
<NFormItem path="confirmPassword">
<NInput
v-model:value="model.confirmPassword"
type="password"
show-password-on="click"
:placeholder="$t('page.login.common.confirmPasswordPlaceholder')"
/>
</NFormItem>
<NSpace vertical :size="18" class="w-full">
<NButton type="primary" size="large" round block @click="handleSubmit">
{{ $t('common.confirm') }}
</NButton>
<NButton size="large" round block @click="toggleLoginModule('pwd-login')">
{{ $t('page.login.common.back') }}
</NButton>
</NSpace>
</NForm>
</template>
<style scoped></style>

View File

@ -0,0 +1,46 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useAppStore } from '@/store/modules/app'
import CardData from './modules/card-data.vue'
import CreativityBanner from './modules/creativity-banner.vue'
import HeaderBanner from './modules/header-banner.vue'
import LineChart from './modules/line-chart.vue'
import PieChart from './modules/pie-chart.vue'
import ProjectNews from './modules/project-news.vue'
const appStore = useAppStore()
const gap = computed(() => (appStore.isMobile ? 0 : 16))
</script>
<template>
<NSpace vertical :size="16">
<NAlert :title="$t('common.tip')" type="warning">
{{ $t('page.home.branchDesc') }}
</NAlert>
<HeaderBanner />
<CardData />
<NGrid :x-gap="gap" :y-gap="16" responsive="screen" item-responsive>
<NGi span="24 s:24 m:14">
<NCard :bordered="false" class="card-wrapper">
<LineChart />
</NCard>
</NGi>
<NGi span="24 s:24 m:10">
<NCard :bordered="false" class="card-wrapper">
<PieChart />
</NCard>
</NGi>
</NGrid>
<NGrid :x-gap="gap" :y-gap="16" responsive="screen" item-responsive>
<NGi span="24 s:24 m:14">
<ProjectNews />
</NGi>
<NGi span="24 s:24 m:10">
<CreativityBanner />
</NGi>
</NGrid>
</NSpace>
</template>
<style scoped></style>

View File

@ -0,0 +1,117 @@
<script setup lang="ts">
import { createReusableTemplate } from '@vueuse/core'
import { computed } from 'vue'
import { $t } from '@/locales'
import { useThemeStore } from '@/store/modules/theme'
defineOptions({
name: 'CardData',
})
interface CardData {
key: string
title: string
value: number
unit: string
color: {
start: string
end: string
}
icon: string
}
const cardData = computed<CardData[]>(() => [
{
key: 'visitCount',
title: $t('page.home.visitCount'),
value: 9725,
unit: '',
color: {
start: '#ec4786',
end: '#b955a4',
},
icon: 'ant-design:bar-chart-outlined',
},
{
key: 'turnover',
title: $t('page.home.turnover'),
value: 1026,
unit: '$',
color: {
start: '#865ec0',
end: '#5144b4',
},
icon: 'ant-design:money-collect-outlined',
},
{
key: 'downloadCount',
title: $t('page.home.downloadCount'),
value: 970925,
unit: '',
color: {
start: '#56cdf3',
end: '#719de3',
},
icon: 'carbon:document-download',
},
{
key: 'dealCount',
title: $t('page.home.dealCount'),
value: 9527,
unit: '',
color: {
start: '#fcbc25',
end: '#f68057',
},
icon: 'ant-design:trademark-circle-outlined',
},
])
interface GradientBgProps {
gradientColor: string
}
const [DefineGradientBg, GradientBg] = createReusableTemplate<GradientBgProps>()
const themeStore = useThemeStore()
function getGradientColor(color: CardData['color']) {
return `linear-gradient(to bottom right, ${color.start}, ${color.end})`
}
</script>
<template>
<NCard :bordered="false" size="small" class="card-wrapper">
<!-- define component start: GradientBg -->
<DefineGradientBg v-slot="{ $slots, gradientColor }">
<div
class="px-16px pb-4px pt-8px text-white"
:style="{ backgroundImage: gradientColor, borderRadius: `${themeStore.themeRadius}px` }"
>
<component :is="$slots.default" />
</div>
</DefineGradientBg>
<!-- define component end: GradientBg -->
<NGrid cols="s:1 m:2 l:4" responsive="screen" :x-gap="16" :y-gap="16">
<NGi v-for="item in cardData" :key="item.key">
<GradientBg :gradient-color="getGradientColor(item.color)" class="flex-1">
<h3 class="text-16px">
{{ item.title }}
</h3>
<div class="flex justify-between pt-12px">
<SvgIcon :icon="item.icon" class="text-32px" />
<CountTo
:prefix="item.unit"
:start-value="1"
:end-value="item.value"
class="text-30px text-white dark:text-dark"
/>
</div>
</GradientBg>
</NGi>
</NGrid>
</NCard>
</template>
<style scoped></style>

View File

@ -0,0 +1,17 @@
<script setup lang="ts">
import { $t } from '@/locales'
defineOptions({
name: 'CreativityBanner',
})
</script>
<template>
<NCard :title="$t('page.home.creativity')" :bordered="false" size="small" class="h-full card-wrapper">
<div class="h-full flex-center">
<icon-local-banner class="text-400px text-primary sm:text-320px" />
</div>
</NCard>
</template>
<style scoped></style>

View File

@ -0,0 +1,68 @@
<script setup lang="ts">
import { computed } from 'vue'
import { $t } from '@/locales'
import { useAppStore } from '@/store/modules/app'
import { useAuthStore } from '@/store/modules/auth'
defineOptions({
name: 'HeaderBanner',
})
const appStore = useAppStore()
const authStore = useAuthStore()
const gap = computed(() => (appStore.isMobile ? 0 : 16))
interface StatisticData {
id: number
label: string
value: string
}
const statisticData = computed<StatisticData[]>(() => [
{
id: 0,
label: $t('page.home.projectCount'),
value: '25',
},
{
id: 1,
label: $t('page.home.todo'),
value: '4/16',
},
{
id: 2,
label: $t('page.home.message'),
value: '12',
},
])
</script>
<template>
<NCard :bordered="false" class="card-wrapper">
<NGrid :x-gap="gap" :y-gap="16" responsive="screen" item-responsive>
<NGi span="24 s:24 m:18">
<div class="flex-y-center">
<div class="size-72px shrink-0 overflow-hidden rd-1/2">
<img src="@/assets/imgs/reader-star.jpg" class="size-full">
</div>
<div class="pl-12px">
<h3 class="text-18px font-semibold">
{{ $t('page.home.greeting', { userName: authStore.userInfo.userName }) }}
</h3>
<p class="text-#999 leading-30px">
{{ $t('page.home.weatherDesc') }}
</p>
</div>
</div>
</NGi>
<NGi span="24 s:24 m:6">
<NSpace :size="24" justify="end">
<NStatistic v-for="item in statisticData" :key="item.id" class="whitespace-nowrap" v-bind="item" />
</NSpace>
</NGi>
</NGrid>
</NCard>
</template>
<style scoped></style>

View File

@ -0,0 +1,152 @@
<script setup lang="ts">
import { watch } from 'vue'
import { useEcharts } from '@/hooks/common/echarts'
import { $t } from '@/locales'
import { useAppStore } from '@/store/modules/app'
defineOptions({
name: 'LineChart',
})
const appStore = useAppStore()
const { domRef, updateOptions } = useEcharts(() => ({
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985',
},
},
},
legend: {
data: [$t('page.home.downloadCount'), $t('page.home.registerCount')],
top: '0',
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
top: '15%',
},
xAxis: {
type: 'category',
boundaryGap: false,
data: [] as string[],
},
yAxis: {
type: 'value',
},
series: [
{
color: '#8e9dff',
name: $t('page.home.downloadCount'),
type: 'line',
smooth: true,
stack: 'Total',
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0.25,
color: '#8e9dff',
},
{
offset: 1,
color: '#fff',
},
],
},
},
emphasis: {
focus: 'series',
},
data: [] as number[],
},
{
color: '#26deca',
name: $t('page.home.registerCount'),
type: 'line',
smooth: true,
stack: 'Total',
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0.25,
color: '#26deca',
},
{
offset: 1,
color: '#fff',
},
],
},
},
emphasis: {
focus: 'series',
},
data: [],
},
],
}))
async function mockData() {
await new Promise((resolve) => {
setTimeout(resolve, 1000)
})
updateOptions((opts) => {
opts.xAxis.data = ['06:00', '08:00', '10:00', '12:00', '14:00', '16:00', '18:00', '20:00', '22:00', '24:00']
opts.series[0].data = [4623, 6145, 6268, 6411, 1890, 4251, 2978, 3880, 3606, 4311]
opts.series[1].data = [2208, 2016, 2916, 4512, 8281, 2008, 1963, 2367, 2956, 678]
return opts
})
}
function updateLocale() {
updateOptions((opts, factory) => {
const originOpts = factory()
opts.legend.data = originOpts.legend.data
opts.series[0].name = originOpts.series[0].name
opts.series[1].name = originOpts.series[1].name
return opts
})
}
async function init() {
mockData()
}
watch(
() => appStore.locale,
() => {
updateLocale()
},
)
// init
init()
</script>
<template>
<NCard :bordered="false" class="card-wrapper">
<div ref="domRef" class="h-360px overflow-hidden" />
</NCard>
</template>
<style scoped></style>

View File

@ -0,0 +1,109 @@
<script setup lang="ts">
import { watch } from 'vue'
import { useEcharts } from '@/hooks/common/echarts'
import { $t } from '@/locales'
import { useAppStore } from '@/store/modules/app'
defineOptions({
name: 'PieChart',
})
const appStore = useAppStore()
const { domRef, updateOptions } = useEcharts(() => ({
tooltip: {
trigger: 'item',
},
legend: {
bottom: '1%',
left: 'center',
itemStyle: {
borderWidth: 0,
},
},
series: [
{
color: ['#5da8ff', '#8e9dff', '#fedc69', '#26deca'],
name: $t('page.home.schedule'),
type: 'pie',
radius: ['45%', '75%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 1,
},
label: {
show: false,
position: 'center',
},
emphasis: {
label: {
show: true,
fontSize: '12',
},
},
labelLine: {
show: false,
},
data: [] as { name: string, value: number }[],
},
],
}))
async function mockData() {
await new Promise((resolve) => {
setTimeout(resolve, 1000)
})
updateOptions((opts) => {
opts.series[0].data = [
{ name: $t('page.home.study'), value: 20 },
{ name: $t('page.home.entertainment'), value: 10 },
{ name: $t('page.home.work'), value: 40 },
{ name: $t('page.home.rest'), value: 30 },
]
return opts
})
}
function updateLocale() {
updateOptions((opts, factory) => {
const originOpts = factory()
opts.series[0].name = originOpts.series[0].name
opts.series[0].data = [
{ name: $t('page.home.study'), value: 20 },
{ name: $t('page.home.entertainment'), value: 10 },
{ name: $t('page.home.work'), value: 40 },
{ name: $t('page.home.rest'), value: 30 },
]
return opts
})
}
async function init() {
mockData()
}
watch(
() => appStore.locale,
() => {
updateLocale()
},
)
// init
init()
</script>
<template>
<NCard :bordered="false" class="card-wrapper">
<div ref="domRef" class="h-360px overflow-hidden" />
</NCard>
</template>
<style scoped></style>

View File

@ -0,0 +1,40 @@
<script setup lang="ts">
import { computed } from 'vue'
import { $t } from '@/locales'
defineOptions({
name: 'ProjectNews',
})
interface NewsItem {
id: number
content: string
time: string
}
const newses = computed<NewsItem[]>(() => [
{ id: 1, content: $t('page.home.projectNews.desc1'), time: '2021-05-28 22:22:22' },
{ id: 2, content: $t('page.home.projectNews.desc2'), time: '2021-10-27 10:24:54' },
{ id: 3, content: $t('page.home.projectNews.desc3'), time: '2021-10-31 22:43:12' },
{ id: 4, content: $t('page.home.projectNews.desc4'), time: '2021-11-03 20:33:31' },
{ id: 5, content: $t('page.home.projectNews.desc5'), time: '2021-11-07 22:45:32' },
])
</script>
<template>
<NCard :title="$t('page.home.projectNews.title')" :bordered="false" size="small" segmented class="card-wrapper">
<template #header-extra>
<a class="text-primary" href="javascript:;">{{ $t('page.home.projectNews.moreNews') }}</a>
</template>
<NList>
<NListItem v-for="item in newses" :key="item.id">
<template #prefix>
<ReaderStarAvatar class="size-48px!" />
</template>
<NThing :title="item.content" :description="item.time" />
</NListItem>
</NList>
</NCard>
</template>
<style scoped></style>

View File

@ -0,0 +1,9 @@
<script lang="ts" setup></script>
<template>
<div>
<h1>Mine</h1>
</div>
</template>
<style scoped></style>