feat: i18n

v3
veypi 4 weeks ago
parent 877b2a2021
commit 932b20fe72

@ -4,10 +4,9 @@ import (
"oa/cfg"
"oa/libs/auth"
M "oa/models"
"strings"
"github.com/google/uuid"
"github.com/veypi/OneBD/rest"
"gorm.io/gorm"
)
func useApp(r rest.Router) {
@ -52,7 +51,7 @@ func appList(x *rest.X) (any, error) {
UserStatus string `json:"user_status"`
}, 0, 10)
query := cfg.DB().Debug().Table("apps").Select("apps.*,app_users.status user_status")
query := cfg.DB().Table("apps").Select("apps.*,app_users.status user_status")
uid := x.Request.Context().Value("uid").(string)
if opts.Name != nil {
query = query.Joins("LEFT JOIN app_users ON app_users.app_id = apps.id AND app_users.user_id = ? AND apps.name LIKE ?", uid, opts.Name)
@ -108,12 +107,24 @@ func appPost(x *rest.X) (any, error) {
}
data := &M.App{}
data.ID = strings.ReplaceAll(uuid.New().String(), "-", "")
data.Name = opts.Name
data.Icon = opts.Icon
data.Des = opts.Des
data.Typ = opts.Typ
err = cfg.DB().Create(data).Error
if opts.Des != nil {
data.Des = *opts.Des
}
err = cfg.DB().Transaction(func(tx *gorm.DB) error {
err := tx.Create(data).Error
if err != nil {
return err
}
au := &M.AppUser{
AppID: data.ID,
UserID: x.Request.Context().Value("uid").(string),
Status: M.AUSTATUS_OK,
}
return tx.Create(au).Error
})
return data, err
}

@ -23,7 +23,7 @@ type AppDelete struct {
type AppPost struct {
Name string `json:"name" parse:"json"`
Icon string `json:"icon" parse:"json"`
Des string `json:"des" parse:"json"`
Des *string `json:"des" parse:"json"`
Typ string `json:"typ" gorm:"default:auto" parse:"json"`
Status string `json:"status" gorm:"default:ok" parse:"json"`
}

@ -5,11 +5,16 @@ import (
"gorm.io/gorm"
)
const AUSTATUS_OK = "ok"
const AUSTATUS_NO = "no"
const AUSTATUS_APPLYING = "applying"
const AUSTATUS_REJECT = "reject"
type App struct {
BaseModel
Name string `json:"name" methods:"post,*patch,*list" parse:"json"`
Icon string `json:"icon" methods:"post,*patch" parse:"json"`
Des string `json:"des" methods:"post,*patch" parse:"json"`
Des string `json:"des" methods:"*post,*patch" parse:"json"`
Typ string `json:"typ" gorm:"default:public" methods:"post,*patch" parse:"json"`
Status string `json:"status" gorm:"default:ok" methods:"post,*patch" parse:"json"`
InitRoleID *string `json:"init_role_id" gorm:"index;type:varchar(32);default: null" methods:"*patch" parse:"json"`
@ -49,14 +54,14 @@ func (m *AppUser) onOk(tx *gorm.DB) (err error) {
}
func (m *AppUser) AfterCreate(tx *gorm.DB) error {
if m.Status == "ok" {
if m.Status == AUSTATUS_OK {
logv.AssertError(m.onOk(tx))
}
return tx.Model(&App{}).Where("id = ?", m.AppID).Update("user_count", gorm.Expr("user_count + ?", 1)).Error
}
func (m *AppUser) AfterUpdate(tx *gorm.DB) error {
if m.Status == "ok" {
if m.Status == AUSTATUS_OK {
return m.onOk(tx)
}
return nil

@ -49,7 +49,7 @@ class davWraper {
}
}
}
putFileContents(filename: string, data: string, options?: webdav.PutFileContentsOptions) {
putFileContents(filename: string, data: string | webdav.BufferLike, options?: webdav.PutFileContentsOptions) {
return this.retry(() => this.client.putFileContents(filename, data, options))
}
getFileContents(filename: string, options?: webdav.GetFileContentsOptions) {
@ -118,8 +118,8 @@ const rename = (o: string, n?: string) => {
if (n) {
return n + ext
}
let d = new Date().getTime()
return d + o + ext
const d = performance.now().toString(36);
return d + ext
}

@ -8,6 +8,10 @@
export default defineAppConfig({
// host: window.location.protocol + '//' + window.location.host,
ui: {
// primary: '#2196f3',
// gray: '#111'
},
host: '',
id: 'tMRxWz77P9ABNZA3ZIuoNQILjVBBIUdf',
layout: {

@ -1,11 +1,9 @@
<template>
<NuxtLayout>
<NuxtPage keepalive :page-key="route => route.fullPath" />
<NuxtPage keepalive :page-key="(r: any) => r.fullPath" />
</NuxtLayout>
</template>
<script setup lang="ts">
let app = useAppConfig()
let menu = useMenuStore()
onMounted(() => {

@ -16,7 +16,8 @@
--color-error: #f44336;
--color-warning: #ff5722;
--color-info: #ffc107;
--color-success: #4caf50;
--color-success: #53de58;
--color-ignore: #d1d5db;
--input-line-default: #002f55;
--input-line-shine: #1467ff;
@ -46,8 +47,7 @@
}
.div-btn {
@apply cursor-pointer transition duration-500 ease-in-out
transform hover:scale-110 hover:opacity-50;
@apply cursor-pointer transition duration-500 ease-in-out transform hover:scale-110 hover:opacity-50;
}
.div-btn hr {
@ -88,3 +88,43 @@
opacity: 0.8;
}
.divimg {
border: 1px solid #ddd;
background-image: var(--bgurl);
background-repeat: no-repeat;
background-position: center;
background-size: cover;
}
img.error {
display: inline-block;
transform: scale(1);
}
img.error::before {
content: '';
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: #f5f5f5 url("data:image/svg+xml,%3Csvg class='icon' viewBox='0 0 1024 1024' xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cpath d='M304.128 456.192c48.64 0 88.064-39.424 88.064-88.064s-39.424-88.064-88.064-88.064-88.064 39.424-88.064 88.064 39.424 88.064 88.064 88.064zm0-116.224c15.36 0 28.16 12.288 28.16 28.16s-12.288 28.16-28.16 28.16-28.16-12.288-28.16-28.16 12.288-28.16 28.16-28.16z' fill='%23e6e6e6'/%3E%3Cpath d='M887.296 159.744H136.704C96.768 159.744 64 192 64 232.448v559.104c0 39.936 32.256 72.704 72.704 72.704h198.144L500.224 688.64l-36.352-222.72 162.304-130.56-61.44 143.872 92.672 214.016-105.472 171.008h335.36C927.232 864.256 960 832 960 791.552V232.448c0-39.936-32.256-72.704-72.704-72.704zm-138.752 71.68v.512H857.6c16.384 0 30.208 13.312 30.208 30.208v399.872L673.28 408.064l75.264-176.64zM304.64 792.064H165.888c-16.384 0-30.208-13.312-30.208-30.208v-9.728l138.752-164.352 104.96 124.416-74.752 79.872zm81.92-355.84l37.376 228.864-.512.512-142.848-169.984c-3.072-3.584-9.216-3.584-12.288 0L135.68 652.8V262.144c0-16.384 13.312-30.208 30.208-30.208h474.624L386.56 436.224zm501.248 325.632c0 16.896-13.312 30.208-29.696 30.208H680.96l57.344-93.184-87.552-202.24 7.168-7.68 229.888 272.896z' fill='%23e6e6e6'/%3E%3C/svg%3E") no-repeat center / 50% 50%;
color: transparent;
}
img.error::after {
content: attr(alt);
position: absolute;
left: 0;
bottom: 0;
width: 100%;
line-height: 2;
background-color: rgba(0, 0, 0, .5);
color: white;
font-size: 12px;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

@ -1,4 +1,4 @@
<!--
<!--
* menu.vue
* Copyright (C) 2024 veypi <i@veypi.com>
* 2024-06-06 16:18
@ -14,7 +14,7 @@
<OneIcon :name='v.ico' />
</div>
<div class="text-nowrap" v-if="show_name">
{{ v.name }}
{{ v.name.startsWith('!') ? v.name.slice(1) : $t(v.name) }}
</div>
</slot>
</div>
@ -71,4 +71,3 @@ withDefaults(defineProps<{
}
}
</style>

@ -1,12 +1,14 @@
<template>
<div @click="click">
<input enctype="multipart/form-data" ref="file" name="files" :multiple="multiple" type="file" hidden @change="upload">
<input enctype="multipart/form-data" ref="file" name="files" :multiple="multiple" type="file" hidden
@change="upload">
<slot></slot>
</div>
</template>
<script lang="ts" setup>
import { oafs } from '@veypi/oaer'
import { fs } from '@veypi/oaer';
let file = ref<HTMLInputElement>()
let emits = defineEmits<{
@ -19,6 +21,7 @@ let props = withDefaults(defineProps<{
dir?: string,
}>(), {
multiple: false,
dir: '/',
renames: ''
})
@ -28,11 +31,27 @@ function click() {
const upload = (evt: Event) => {
evt.preventDefault()
let dir = props.dir
if (!dir.endsWith('/')) {
dir = dir + '/'
}
let f = (evt.target as HTMLInputElement).files as FileList
oafs.upload(f, props.dir, props.renames?.split(/[, ]+/)).then((e: any) => {
for (let i = 0; i < f.length; i++) {
let name = dir + fs.rename(f[i].name)
f[i].arrayBuffer().then((v) => {
fs.app.putFileContents(name, v).then((e) => {
emits('success', fs.app.urlwrap(name))
}).catch((e) => {
console.log(e)
emits('success', props.multiple ? e : e[0])
emits('failed')
})
})
}
console.log(f)
// oafs.upload(f, props.dir, props.renames?.split(/[, ]+/)).then((e: any) => {
// console.log(e)
// emits('success', props.multiple ? e : e[0])
// })
}

@ -1,6 +1,7 @@
<template>
<div :vtype="type" :class="[hideBorder ? 'hide-hr' : '', flexy ? 'flex-col' : '', disabled ? 'cursor-not-allowed' : '']"
ref="all" class="vinput center flex justify-center items-center relative" :style="dy_style">
<div :vtype="type"
:class="[hideBorder ? 'hide-hr' : '', flexy ? 'flex-col' : '', disabled ? 'cursor-not-allowed' : '']" ref="all"
class="vinput center flex justify-center items-center relative" :style="dy_style">
<div v-if="props.label" class="flex-shrink" :style="{ 'width': labelWidth }">
<slot name="label">{{ props.label }}</slot>
</div>
@ -34,27 +35,27 @@
<slot name='file'>
{{ value == "" ? "no file chosen" : value }}
</slot>
<input class="absolute w-full h-full" type="file" style="color:white;font-size: large;opacity: 0%;top:0;left:0"
@change="choose_file($event)" />
<input class="absolute w-full h-full" type="file"
style="color:white;font-size: large;opacity: 0%;top:0;left:0" @change="choose_file($event)" />
</div>
<!-- @click="setParameter(v.key,vv.key, c.value)"-->
</template>
<template v-else-if="type === 'radio'">
<div class="flex justify-between gap-4">
{{ transDic }}
<!-- <template :key="ok" v-for="(ov, ok) of transDic"> -->
<!-- <div :class="[value === ok ? 'radio-btn-active' : -->
<!-- 'div-btn']" @click="setSelect(ok)" style="color:white;" -->
<!-- class="div-center font-bold grow truncate radio-btn rounded-md transition duration-500"> -->
<!-- {{ ov || ok }} -->
<!-- </div> -->
<!-- </template> -->
<template :key="ok" v-for="(ov, ok) of transDic">
<div :class="[value === ok ? 'radio-btn-active' :
'div-btn']" @click="setSelect(ok)" style="color:white;"
class="div-center font-bold grow truncate radio-btn rounded-md transition duration-500">
{{ ov || ok }}
</div>
</template>
</div>
</template>
<template v-else-if="type === 'select'">
<div class="noborder cursor-pointer w-full overflow-x-auto whitespace-nowrap" @click="showSelect" :title="title">
<div class="noborder cursor-pointer w-full overflow-x-auto whitespace-nowrap" @click="showSelect"
:title="title">
<span v-if="value === undefined || value === null"></span>
<span v-else-if="!Array.isArray(value)">{{ transDic[value] || value }}</span>
<template v-else>
@ -75,15 +76,15 @@
<template v-else-if="type === 'region'">
<div class="flex items-center justify-center">
<template v-if="value[0] !== '∞'">
<OneIcon class="div-btn" @click="updateIndex(0, '∞')">kuohao</OneIcon>
<OneIcon class="div-btn" @click="updateIndex(0, '∞')" name='kuohao' />
<input type="number" :disabled="disabled" @input="check()" v-model="value[0]" @focusout="update"
@focusin="change('input')" class="noborder w-1/3 text-center" @blur="update" @keyup.enter="update">
</template>
<template v-else>
<OneIcon class="div-btn" @click="updateIndex(0, 0)">zuokuohao</OneIcon>
<OneIcon class="div-btn" @click="updateIndex(0, 0)" name='zuokuohao' />
<div class="w-1/3 flex justify-center items-center">
<OneIcon>minus</OneIcon>
<OneIcon>infinite</OneIcon>
<OneIcon name='minus' />
<OneIcon name='infinite' />
</div>
</template>
<div>,</div>
@ -91,14 +92,14 @@
<template v-if="value[1] !== '∞'">
<input type="number" :disabled="disabled" v-model="value[1]" @focusout="update" @focusin="change('input')"
class="noborder w-1/3 text-center" @blur="update" @keyup.enter="update">
<OneIcon class="div-btn" @click="updateIndex(1, '∞')">kuohao-r</OneIcon>
<OneIcon class="div-btn" @click="updateIndex(1, '∞')" name='kuohao-r' />
</template>
<template v-else>
<div class="w-1/3 flex justify-center items-center">
<OneIcon>plus</OneIcon>
<OneIcon>infinite</OneIcon>
<OneIcon name='plus' />
<OneIcon name='infinite' />
</div>
<OneIcon class="div-btn" @click="updateIndex(1, 1)">youkuohao</OneIcon>
<OneIcon class="div-btn" @click="updateIndex(1, 1)" name='youkuohao' />
</template>
</div>
</template>
@ -186,7 +187,7 @@ const dy_style = computed(() => `text-align:${props.align}`)
let inputRef = ref<HTMLInputElement>()
let all = ref<HTMLElement>()
const transDic = ref({})
const transDic = ref({} as { [key: string]: string })
const change = (s: string) => {
if (props.disabled) {
@ -310,7 +311,7 @@ const setSelect = (e: any) => {
if (Array.isArray(value.value)) {
for (let i in value.value) {
if (value.value[i] === e) {
value.value.splice(i, 1)
value.value.splice(Number(i), 1)
update()
return
}

@ -29,7 +29,7 @@ export function Delete(app_id: string) {
export interface PostOpts {
name: string
icon: string
des: string
des?: string
typ: string
status: string
}

@ -23,6 +23,9 @@ export interface App {
status: string
user_status: string
}
export const AppTyp = ['public', 'apply', 'private']
export interface AppUser {
id: string
created_at: Date

@ -58,7 +58,7 @@ export const token = {
}
// 请求拦截
const beforeRequest = (config: any) => {
config.retryTimes = 3
config.retryTimes = 1
// NOTE 添加自定义头部
token.value && (config.headers.Authorization = `Bearer ${token.value}`)
// config.headers['auth_token'] = ''

@ -8,8 +8,9 @@
<div class="page">
<div class="header flex justify-center items-center">
<div class="ico" @click="router.push('/')"></div>
<div>统一认证系统</div>
<div>OneAuth</div>
<div class="grow"></div>
<OneIcon class="mx-2" @click="toggle_lang" :name="$i18n.locale !== 'zh-CN' ? 'in-Zh_Cn' : 'in-en'"></OneIcon>
<OneIcon class="mx-2" @click="toggle_fullscreen" :name="app.layout.fullscreen ? 'compress' : 'expend'"></OneIcon>
<OneIcon class="mx-2" @click="toggle_theme" :name="app.layout.theme === '' ? 'light' : 'dark'"></OneIcon>
<!-- <OAer class="mx-2" v-if="user.ready" @logout="user.logout" :is-dark="app.layout.theme !== ''"> -->
@ -34,9 +35,12 @@
<script lang="ts" setup>
import { OneIcon } from '@veypi/one-icon'
import oaer from '@veypi/oaer'
import { useI18n } from 'vue-i18n';
let i18n = useI18n()
let app = useAppConfig()
let router = useRouter()
const colorMode = useColorMode()
app.host = window.location.protocol + '//' + window.location.host
api.apitoken.value = oaer.Token()
@ -63,6 +67,14 @@ let toggle_menu = (m: 0 | 1 | 2) => {
}
toggle_menu(2)
const toggle_lang = () => {
if (i18n.locale.value === 'zh-CN') {
i18n.locale.value = 'en-US'
} else {
i18n.locale.value = 'zh-CN'
}
localStorage.setItem('lang', i18n.locale.value)
}
const toggle_fullscreen = () => {
app.layout.fullscreen = !app.layout.fullscreen
if (app.layout.fullscreen) {
@ -76,10 +88,12 @@ const toggle_theme = () => {
app.layout.theme =
app.layout.theme === '' ? 'dark' : ''
document.documentElement.setAttribute('theme', app.layout.theme)
colorMode.preference = app.layout.theme === '' ? 'light' : 'dark'
}
onMounted(() => {
oaer.render_ui('oaer')
useMenuStore().default()
})

@ -56,6 +56,6 @@ export default defineNuxtConfig({
]
},
},
modules: ["@pinia/nuxt"],
modules: ["@pinia/nuxt", '@nuxt/ui', '@nuxtjs/tailwindcss'],
compatibilityDate: '2024-10-16'
})

@ -10,6 +10,8 @@
"postinstall": "nuxt prepare"
},
"dependencies": {
"@nuxt/ui": "^2.18.7",
"@nuxtjs/tailwindcss": "^6.12.2",
"@pinia/nuxt": "^0.5.1",
"@veypi/msg": "^0.2.0",
"@veypi/oaer": "^0.3.0",
@ -21,6 +23,7 @@
"js-base64": "^3.7.7",
"nuxt": "^3.11.2",
"vue": "^3.4.27",
"vue-i18n": "10",
"vue-router": "^4.3.2"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",

@ -28,10 +28,10 @@ let core = ref({} as models.App)
const set_menu = () => {
let p = '/app/' + core.value.id
menu.set([
{ ico: 'home', name: core.value.name, path: p },
{ ico: 'user', name: '用户管理', path: p + '/user' },
{ ico: 'team', name: '权限设置', path: p + '/auth' },
{ ico: 'setting', name: '应用设置', path: p + '/cfg' },
{ ico: 'home', name: '!' + core.value.name, path: p },
{ ico: 'user', name: 'menu.user', path: p + '/user' },
{ ico: 'team', name: 'menu.auth', path: p + '/auth' },
{ ico: 'setting', name: 'menu.setting', path: p + '/cfg' },
])
}

@ -7,12 +7,12 @@
<template>
<div>
<div v-if="ofApps.length > 0">
<div class="flex justify-between">
<h1 class="page-h1">我的应用</h1>
<h1 class="page-h1">{{ $t('c.myapps') }}</h1>
<div class="my-5 mr-10">
<div class='vbtn bg-gray-400' @click="new_flag = true" v-if="oaer.access().Get('app', '').CanCreate()">
<div class='vbtn bg-gray-300' @click="new_flag = true" v-if="oaer.access().Get('app', '').CanCreate()">
{{ $t('p.index.create') }}
</div>
</div>
</div>
@ -23,17 +23,43 @@
</div>
</div>
<div class="mt-20" v-if="apps.length > 0">
<h1 class="page-h1">应用中心</h1>
<h1 class="page-h1">{{ $t('c.app store') }}</h1>
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 text-center">
<div v-for="(item, k) in apps" class="flex items-center justify-center" :key="k">
<Appcard :core="item" :is_part="false"></Appcard>
</div>
</div>
</div>
<UModal v-model="new_flag">
<UCard>
<template #header>
<div class="text-h6">{{ $t('p.index.create') }} </div>
</template>
<div class="p-4">
<Uploader @success="temp_app.icon = $event" dir="app_icon">
<!-- <img alt="LOGO" class="rounded-full w-16 h-16 mx-auto my-4" :src="temp_app.icon"> -->
<div alt="LOGO" class="divimg rounded-full w-16 h-16 mx-auto" :style="`--bgurl:url('${temp_app.icon}')`">
</div>
</Uploader>
<Vinput class="mt-4" v-model="temp_app.name" :validate="/^\w{2,}$/" :label="$t('c.appname')" type="text">
</Vinput>
{{ temp_app.typ }}
<Vinput class='mt-12' v-model="temp_app.typ" type="radio"
:options="{ 'public': $t('atyp.public'), 'apply': $t('atyp.apply'), 'private': $t('atyp.private') }"
:validate="/^\w{2,}$/">
</Vinput>
</div>
<template #footer>
<div class='flex justify-end gap-4'>
<div class="vbtn bg-vignore" @click="new_flag = false">{{ $t('c.cancel') }}</div>
<div class="vbtn bg-vsuccess" @click="create_new">{{ $t('c.ok') }}</div>
</div>
</template>
</UCard>
</UModal>
<!-- <q-dialog :square="false" v-model="new_flag"> -->
<!-- <q-card class="w-4/5 md:w-96 rounded-2xl"> -->
<!-- <q-card-section> -->
<!-- <div class="text-h6">创建应用</div> -->
<!-- </q-card-section> -->
<!-- <q-separator></q-separator> -->
<!-- <q-card-section> -->
@ -81,24 +107,19 @@ function getApps() {
}
const rand_icon = () => {
return "/media/icon/sign/scenery-" + util.randomNum(1, 20) + ".png"
}
let new_flag = ref(false);
let temp_app = ref({
name: "",
icon: rand_icon()
icon: '',
typ: 'public',
});
let rules = {
name: [
(v: string) => (v && v.length >= 2 && v.length <= 16) || "长度要求2~16"
],
};
function create_new() {
api.app.Post({
name: temp_app.value.name, icon: temp_app.value.icon,
des: "", typ: "public", status: "ok"
name: temp_app.value.name,
icon: temp_app.value.icon,
typ: temp_app.value.typ,
status: "ok"
}).then((e: models.App) => {
ofApps.value.push(e);
msg.Info("创建成功");

@ -0,0 +1,36 @@
/*
* en.ts
* Copyright (C) 2024 veypi <i@veypi.com>
* 2024-10-30 23:57
* Distributed under terms of the GPL license.
*/
export default {
c: {
'app': 'App',
'myapps': 'MyApps',
'appname': 'App Name',
'app store': 'App Store',
'ok': 'OK',
'cancel': 'Cancel',
},
atyp: {
'public': 'Public',
'apply': 'Apply',
'private': 'Private',
},
menu: {
'app': 'Apps',
'user': 'User',
'doc': 'Doc',
'appstat': 'AppStat',
'setting': 'Setting',
'auth': 'Auth',
},
p: {
index: {
'create': 'Create App',
}
}
}

@ -0,0 +1,35 @@
/*
* zh.ts
* Copyright (C) 2024 veypi <i@veypi.com>
* 2024-10-30 23:57
* Distributed under terms of the GPL license.
*/
export default {
c: {
'app': '应用',
'myapps': '我的应用',
'appname': '应用名称',
'app store': '应用中心',
'ok': '确定',
'cancel': '取消',
},
atyp: {
'public': '公开注册',
'apply': '申请注册',
'private': '邀请注册(应用隐藏)',
},
menu: {
'app': '应用',
'user': '用户设置',
'doc': '文档',
'appstat': '应用统计',
'setting': '设置',
'auth': '权限',
},
p: {
index: {
'create': '创建应用',
}
}
}

@ -7,9 +7,28 @@
import oneicon from '@veypi/one-icon'
import { createI18n } from 'vue-i18n'
import langEn from './i18n/en'
import langZh from './i18n/zh'
const navLang = navigator.language; //判断当前浏览器使用的语言
const localLang = navLang === 'zh-CN' ? 'zh-CN' : 'en-US';
let lang = localStorage.getItem('lang') || localLang || 'en-US';
export const i18n = createI18n({
locale: lang,
fallbackLocale: 'en',
messages: {
en: langEn,
zh: langZh,
},
modifiers: {
snakeCase: (str: any) => str.split(' ').join('_')
}
});
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.use(oneicon)
nuxtApp.vueApp.use(i18n)
})

File diff suppressed because one or more lines are too long

@ -14,11 +14,11 @@ interface item {
}
const default_menu = [
{ ico: 'home', name: '应用中心', path: '/' },
{ ico: 'user', name: '用户设置', path: '/user' },
{ ico: 'file-exception', name: '文档中心', path: '/docs' },
{ ico: 'data-view', name: '应用统计', path: '/stats' },
{ ico: 'setting', name: '系统设置', path: '/setting' },
{ ico: 'home', name: 'menu.app', path: '/' },
{ ico: 'user', name: 'menu.user', path: '/user' },
{ ico: 'file-exception', name: 'menu.doc', path: '/docs' },
{ ico: 'data-view', name: 'menu.appstat', path: '/stats' },
{ ico: 'setting', name: 'menu.setting', path: '/setting' },
]
export const useMenuStore = defineStore('menu', {

@ -9,7 +9,18 @@ export default {
"./error.vue",
],
theme: {
extend: {},
extend: {
colors: {
vprimary: '#2196f3',
vsecondary: '#ecc94b',
vaccents: '#ff9800',
verror: '#f44336',
vwaring: '#ff5722',
vinfo: '#ffc107',
vsuccess: '#53de58',
vignore: '#d1d5db',
}
},
},
plugins: [],
}

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save