feat: 静态导出 & 增加 config.json 支持

This commit is contained in:
overtrue
2025-02-23 15:25:50 +08:00
parent 99155bb662
commit 5b00db5bb2
18 changed files with 244 additions and 126 deletions
+2
View File
@@ -27,3 +27,5 @@ logs
!.env.example
pages/test
public/config.json
+1 -1
View File
@@ -24,7 +24,7 @@ import {
import { ref } from 'vue'
import { themeOverrides } from '~/config/theme'
const theme = ref<GlobalTheme | null>(darkTheme)
const theme = ref<GlobalTheme>(darkTheme)
const locale = ref<NLocale | null>(zhCN)
const dateLocale = ref<NDateLocale | null>(dateZhCN)
</script>
+21 -44
View File
@@ -1,7 +1,10 @@
<script lang="ts" setup>
import { RouterLink } from 'vue-router'
import { RouterLink, useRouter } from 'vue-router'
import { useSidebarStore } from '~/store/sidebar'
import type { SiteConfig } from '~/types/config'
const appConfig = useAppConfig()
const siteConfig = useNuxtApp().$siteConfig as unknown as SiteConfig
const router = useRouter()
const sidebarStore = useSidebarStore()
const isCollapsed = computed(() => sidebarStore.isCollapsed)
@@ -33,13 +36,13 @@ const options = computed(() => {
nav.to
? nav.target
? h(
'a',
{
href: nav.to,
target: '_blank'
},
{ default: () => nav.label }
)
'a',
{
href: nav.to,
target: '_blank'
},
{ default: () => nav.label }
)
: h(RouterLink, { to: nav.to }, { default: () => nav.label })
: nav.label,
icon: nav.icon ? iconRender(nav.icon) : undefined
@@ -63,22 +66,11 @@ const options = computed(() => {
})
</script>
<template>
<n-layout-sider
bordered
class="min-h-full"
collapse-mode="width"
:collapsed-width="64"
:width="240"
:native-scrollbar="false"
:collapsed="isCollapsed">
<n-layout-sider bordered class="min-h-full" collapse-mode="width" :collapsed-width="64" :width="240" :native-scrollbar="false" :collapsed="isCollapsed">
<div class="flex flex-col h-screen overflow-hidden gap-4 relative">
<div
class="border-b dark:border-neutral-800 flex flex-wrap h-16 items-center p-4"
:class="isCollapsed ? 'justify-center' : 'justify-between'">
<div class="border-b dark:border-neutral-800 flex flex-wrap h-16 items-center p-4" :class="isCollapsed ? 'justify-center' : 'justify-between'">
<div>
<n-avatar
v-if="isCollapsed"
class="text-center text-2xl leading-none">
<n-avatar v-if="isCollapsed" class="text-center text-2xl leading-none">
{{ appConfig.name.substring(0, 1) }}
</n-avatar>
<h2 v-else class="text-center text-2xl">
@@ -87,45 +79,30 @@ const options = computed(() => {
</h2>
</div>
<div v-if="!isCollapsed" class="px-4 flex items-center -mr-4">
<Icon
name="ri:menu-fold-fill"
class="cursor-pointer text-xl"
@click="toggleSidebar" />
<Icon name="ri:menu-fold-fill" class="cursor-pointer text-xl" @click="toggleSidebar" />
</div>
</div>
<div class="overflow-y-auto flex-1">
<n-menu
:indent="26"
:root-indent="12"
:collapsed-width="64"
:collapsed-icon-size="22"
:options="options"
default-expand-all
class="flex-1" />
<n-menu :indent="26" :root-indent="12" :collapsed-width="64" :collapsed-icon-size="22" :options="options" default-expand-all class="flex-1" />
</div>
<div
v-if="isCollapsed"
class="w-full flex items-center justify-center py-4">
<Icon
name="ri:menu-unfold-fill"
class="cursor-pointer text-xl"
@click="toggleSidebar" />
<div v-if="isCollapsed" class="w-full flex items-center justify-center py-4">
<Icon name="ri:menu-unfold-fill" class="cursor-pointer text-xl" @click="toggleSidebar" />
</div>
<div v-if="!isCollapsed" class="flex flex-col p-4 text-gray-500">
<div class="flex items-center gap-2">
<Icon name="ri-server-line" />
<span>当前版本: v0.0.1</span>
<span>当前版本: {{ siteConfig.release.version }}</span>
</div>
<div class="flex items-center gap-2">
<Icon name="ri-calendar-line" />
<span>构建日期2024-12-01</span>
<span>构建日期{{ siteConfig.release.date }}</span>
</div>
<div class="flex items-center gap-2">
<Icon name="ri-verified-badge-line" />
<span>授权协议商业初级版</span>
<span>授权协议{{ siteConfig.license.name }}</span>
</div>
</div>
+18 -3
View File
@@ -1,7 +1,7 @@
<template>
<n-dropdown :options="options" placement="right-end">
<n-dropdown :options="options" placement="right-end" @select="handleDropdownClick">
<div class="flex items-center border-t dark:border-neutral-800 py-4">
<n-avatar round size="small" src="https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg" />
<n-avatar round size="small" src="/img/rustfs.png" />
<template v-if="!isCollapsed">
<span class="px-2">RustFS</span>
<Icon name="ri:more-2-line" class="ml-auto text-xl" />
@@ -14,6 +14,21 @@
import { Icon } from '#components'
import { defineProps, withDefaults } from 'vue'
const { logout } = useAuth()
const router = useRouter()
const handleLogout = async () => {
await logout()
router.push('/auth/login')
}
const handleDropdownClick = (key: string) => {
if (key === 'logout') {
handleLogout()
}
}
const props = withDefaults(defineProps<{
isCollapsed?: boolean
}>(), {
@@ -29,7 +44,7 @@ const options = [
{
label: '退出登录',
key: 'logout',
icon: () => icon('ri:logout-box-r-line')
icon: () => icon('ri:logout-box-r-line'),
},
]
</script>
+7 -5
View File
@@ -1,5 +1,6 @@
import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts";
import type { AwsCredentialIdentity, AwsCredentialIdentityProvider } from "@aws-sdk/types";
import type { SiteConfig } from "~/types/config";
/**
* 获取 STS 临时凭证并返回
@@ -9,13 +10,14 @@ import type { AwsCredentialIdentity, AwsCredentialIdentityProvider } from "@aws-
* @returns {Promise<Credentials>}
*/
export async function getStsToken(credentials: AwsCredentialIdentity | AwsCredentialIdentityProvider, roleArn: string) {
const runtimeConfig = useRuntimeConfig().public;
console.log('S3 runtimeConfig', runtimeConfig);
const siteConfig = useNuxtApp().$siteConfig as SiteConfig;
console.log('S3 siteConfig', siteConfig);
// 1. 创建 STS 客户端
const stsClient = new STSClient({
endpoint: runtimeConfig.s3.endpoint,
region: runtimeConfig.s3.region || 'us-east-1',
endpoint: siteConfig.s3.endpoint,
region: siteConfig.s3.region || 'us-east-1',
credentials: credentials
});
@@ -24,7 +26,7 @@ export async function getStsToken(credentials: AwsCredentialIdentity | AwsCreden
const command = new AssumeRoleCommand({
RoleArn: roleArn,
RoleSessionName: "console", // 自定义角色会话名称
DurationSeconds: runtimeConfig.session.durationSeconds || 3600 * 12, // 临时凭证有效期
DurationSeconds: siteConfig.session?.durationSeconds || 3600 * 12, // 临时凭证有效期
});
const response = await stsClient.send(command);
+17 -6
View File
@@ -17,10 +17,8 @@ export default defineNuxtConfig({
'nuxtjs-naive-ui',
'@vueuse/nuxt'
],
plugins: [
'~/plugins/api.ts',
'~/plugins/s3.ts',
],
// Nuxt automatically reads the files in the plugins/ directory
plugins: [],
runtimeConfig: {
public: {
session: {
@@ -28,15 +26,28 @@ export default defineNuxtConfig({
durationSeconds: Number(process.env.SESSION_DURATION_SECONDS) || 3600 * 12
},
// API 请求基础 URL
// 下面部分修改的时候记得同步修改 public/config.json
// admin API 请求基础 URL
api: {
baseURL: process.env.API_BASE_URL || ''
},
// 临时配置,后续登录后从本地存储中获取
// 对象存储配置
s3: {
region: process.env.S3_REGION || 'us-east-1',
endpoint: process.env.S3_ENDPOINT || process.env.API_BASE_URL || ''
},
// 版本信息
release: {
version: process.env.VERSION || 'dev-0.0.0',
date: process.env.RELEASE_DATE || new Date().toISOString().slice(0, 10)
},
// 授权信息
license: {
"name": "Apache-2.0",
"url": "https://www.apache.org/licenses/LICENSE-2.0"
}
}
},
+2 -2
View File
@@ -1,6 +1,6 @@
<template>
<div>
<content>
<div>
<page-header>
<template #title>
<div class="flex items-center gap-4">
@@ -13,7 +13,7 @@
<object-list v-if="isObjectList" :bucket="bucketName" :path="key" />
<object-view v-else :bucket="bucketName" :object-key="key" />
</page-content>
</content>
</div>
<footer />
</div>
</template>
+9 -1
View File
@@ -1,3 +1,11 @@
<template>
<h1>hello</h1>
<page-content class="flex flex-col gap-4 items-center justify-center min-h-screen">
<h1 class="text-2xl font-bold">欢迎使用 Rustfs.</h1>
<div class="flex items-center justify-center">
<n-button type="primary" @click="$router.push('/browser')">进入文件浏览器</n-button>
</div>
<div class="mt-4">
<p>轻松浏览您的文件和文件夹!</p>
</div>
</page-content>
</template>
+7 -1
View File
@@ -29,7 +29,7 @@
</div>
<n-data-table ref="tableRef" class="border dark:border-neutral-700 rounded overflow-hidden" :columns="columns" :data="pilicies" :pagination="false" :bordered="false" />
</page-content>
<policies-form-item v-model:show="current" :policy="current" @saved="fetchPolicies" />
<policies-form-item v-model:show="showPolicyForm" :policy="current" @saved="fetchPolicies" />
</div>
</template>
<script lang="ts" setup>
@@ -42,6 +42,12 @@ const message = useMessage();
const pilicies = ref<any[]>([]);
const tableRef = ref<DataTableInst>();
const current = ref();
const showPolicyForm = computed({
get: () => !!current.value,
set: (val) => {
if (!val) current.value = null;
}
});
const handleEdit = (item: any) => {
current.value = item;
+28
View File
@@ -0,0 +1,28 @@
export default defineNuxtPlugin({
name: 'load-site-config',
async setup(nuxtApp) {
console.log('load-site-config setup');
let finalConfig: any
// 客户端:尝试从 public/config.json 动态加载配置
try {
const response = await fetch('/config.json')
const remoteConfig = await response.json()
// 合并 public/runtimeConfig 与 remote 配置,remote 的优先
finalConfig = { ...useRuntimeConfig().public, ...remoteConfig }
console.log('加载 public/config.json 成功:', remoteConfig)
} catch (error) {
console.error('加载 public/config.json 失败,使用 runtimeConfig:', error)
finalConfig = useRuntimeConfig().public
}
console.log('siteConfig:', finalConfig);
return {
provide: {
siteConfig: finalConfig
}
}
}
})
+42
View File
@@ -0,0 +1,42 @@
import { AwsClient } from 'aws4fetch';
import ApiClient from '~/lib/api-client';
import type { SiteConfig } from '~/types/config'; // Updated import
export default defineNuxtPlugin({
name: 'admin-api-client',
setup(nuxtApp) {
console.log('admin-api-client setup');
const siteConfig = nuxtApp.$siteConfig as SiteConfig
const { isAuthenticated, credentials } = useAuth()
if (!isAuthenticated.value) {
return
}
const accessKeyId = credentials.value?.AccessKeyId || ''
const secretAccessKey = credentials.value?.SecretAccessKey || ''
const sessionToken = credentials.value?.SessionToken || ''
const region: string = siteConfig.s3.region || 'us-east-1'
const service = 's3'
const adminApiClient: AwsClient = new AwsClient({
accessKeyId,
secretAccessKey,
sessionToken,
region,
service
})
return {
provide: {
api: new ApiClient(adminApiClient, {
baseUrl: siteConfig.api.baseURL,
headers: {
'Content-Type': 'application/json'
}
})
}
}
}
})
+33
View File
@@ -0,0 +1,33 @@
import { S3Client } from "@aws-sdk/client-s3";
import type { SiteConfig } from "~/types/config";
export default defineNuxtPlugin({
name: 's3-client',
setup(nuxtApp) {
console.log('s3-client setup');
const { credentials, isAuthenticated } = useAuth();
const siteConfig = nuxtApp.$siteConfig as SiteConfig;
if (!isAuthenticated || !credentials.value) {
return;
}
console.log('s3-client setup', siteConfig);
const client = new S3Client({
endpoint: siteConfig.s3.endpoint,
region: siteConfig.s3.region || 'us-east-1',
credentials: {
accessKeyId: credentials.value?.AccessKeyId || '',
secretAccessKey: credentials.value?.SecretAccessKey || '',
sessionToken: credentials.value?.SessionToken || '',
}
});
return {
provide: {
s3Client: client
}
}
}
})
-36
View File
@@ -1,36 +0,0 @@
import { AwsClient } from 'aws4fetch'
import ApiClient from '~/lib/api-client'
export default defineNuxtPlugin((nuxtApp) => {
const runtimeConfig = useRuntimeConfig().public
const { isAuthenticated, credentials } = useAuth()
if (!isAuthenticated.value) {
return
}
const accessKeyId = credentials.value?.AccessKeyId || ''
const secretAccessKey = credentials.value?.SecretAccessKey || ''
const sessionToken = credentials.value?.SessionToken || ''
const region = runtimeConfig.s3.region || 'us-east-1'
const service = 's3'
const adminApiClient = new AwsClient({
accessKeyId,
secretAccessKey,
sessionToken,
region,
service
})
return {
provide: {
api: new ApiClient(adminApiClient, {
baseUrl: runtimeConfig.api.baseURL,
headers: {
'Content-Type': 'application/json'
}
})
}
}
})
-27
View File
@@ -1,27 +0,0 @@
import { S3Client } from "@aws-sdk/client-s3";
export default defineNuxtPlugin((nuxtApp) => {
const { credentials, isAuthenticated } = useAuth();
const runtimeConfig = useRuntimeConfig().public;
if (!isAuthenticated || !credentials.value) {
return
}
const client = new S3Client({
endpoint: runtimeConfig.s3.endpoint,
region: runtimeConfig.s3.region || 'us-east-1',
credentials: {
accessKeyId: credentials.value?.AccessKeyId || '',
secretAccessKey: credentials.value?.SecretAccessKey || '',
sessionToken: credentials.value?.SessionToken || '',
}
});
// Expose to useNuxtApp().$api, useNuxtApp().$apiFetch
return {
provide: {
s3Client: client
}
}
})
+17
View File
@@ -0,0 +1,17 @@
{
"api": {
"baseURL": "http://localhost:9000/rustfs/admin/v3"
},
"s3": {
"endpoint": "http://localhost:9000",
"region": "us-east-1"
},
"release": {
"version": "v0.0.1",
"date": "2025-02-25"
},
"license": {
"name": "Apache-2.0",
"url": "https://www.apache.org/licenses/LICENSE-2.0"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

+8
View File
@@ -0,0 +1,8 @@
<svg width="1558" height="260" viewBox="0 0 1558 260" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1288.5 112.905H1159.75V58.4404H1262L1270 0L1074 0V260H1159.75V162.997H1296.95L1288.5 112.905Z" fill="#0196D0"/>
<path d="M1058.62 58.4404V0H789V58.4404H881.133V260H966.885V58.4404H1058.62Z" fill="#0196D0"/>
<path d="M521 179.102V0L454.973 15V161C454.973 181.124 452.084 193.146 443.5 202C434.916 211.257 419.318 214.5 400.5 214.5C381.022 214.5 366.744 210.854 357.5 202C348.916 193.548 346.357 175.721 346.357 156V0L280 15V175.48C280 208.08 290.234 229.412 309.712 241.486C329.19 253.56 358.903 260 400.5 260C440.447 260 470.159 253.56 490.297 241.486C510.766 229.412 521 208.483 521 179.102Z" fill="#0196D0"/>
<path d="M172.84 84.2813C172.84 97.7982 168.249 107.737 158.41 113.303C149.883 118.471 137.092 121.254 120.693 122.049V162.997C129.876 163.792 138.076 166.177 144.307 176.514L184.647 260H265L225.316 180.489C213.181 155.046 201.374 149.48 178.744 143.517C212.197 138.349 241.386 118.471 241.386 73.1499C241.386 53.2722 233.843 30.2141 218.756 17.8899C203.998 5.56575 183.991 0 159.394 0H120.693V48.5015H127.58C142.23 48.5015 153.6 51.4169 161.689 57.2477C169.233 62.8135 172.84 71.5596 172.84 84.2813ZM120.693 122.049C119.163 122.049 117.741 122.049 116.43 122.049H68.5457V48.5015H120.693V0H0V260H70.5137V162.997H110.526C113.806 162.997 117.741 162.997 120.693 162.997V122.049Z" fill="#0196D0"/>
<path d="M774 179.297C774 160.829 766.671 144.669 752.013 131.972C738.127 119.66 712.025 110.169 673.708 103.5C662.136 101.191 651.722 99.6523 643.235 97.3437C586.532 84.6467 594.632 52.7118 650.564 52.7118C680.651 52.7118 709.582 61.946 738.127 66.9478C742.37 67.7174 743.913 68.1021 744.298 68.1021L750.47 12.697C720.383 3.46282 684.895 0 654.036 0C616.619 0 587.689 6.54088 567.245 19.2379C546.801 31.9349 536 57.7137 536 82.3382C536 103.5 543.715 119.66 559.916 131.972C575.731 143.515 604.276 152.749 645.55 160.059C658.279 162.368 668.694 163.907 676.794 166.215C685.023 168.524 691.066 170.704 694.924 172.756C702.253 176.604 706.11 182.375 706.11 188.531C706.11 196.611 701.481 202.767 692.224 207C664.836 220.081 587.689 212.001 556.83 198.15L543.715 247.784C547.186 248.169 552.972 249.323 559.916 250.477C616.619 259.327 690.681 270.869 741.212 238.935C762.814 225.468 774 206.23 774 179.297Z" fill="#0196D0"/>
<path d="M1558 179.568C1558 160.383 1550.42 144.268 1535.67 131.99C1521.32 119.968 1494.34 110.631 1454.74 103.981C1442.38 101.679 1432.01 99.3764 1422.84 97.8416C1422.44 97.8416 1422.04 97.8416 1422.04 97.4579V112.422L1361.04 75.2038L1422.04 38.3692V52.9496C1424.7 52.9496 1427.49 52.9496 1430.41 52.9496C1461.51 52.9496 1491.42 62.5419 1521.32 67.5299C1525.31 67.9136 1526.9 67.9136 1527.3 67.9136L1533.68 12.6619C1502.98 3.83692 1465.9 0 1434 0C1395.33 0 1365.43 6.52277 1345.09 19.5683C1323.16 32.6139 1312 57.9376 1312 82.8776C1312 103.981 1320.37 120.096 1336.72 131.607C1353.46 143.885 1382.97 153.093 1425.23 160.383C1434 161.535 1441.18 162.686 1447.56 164.22L1448.36 150.791L1507.36 190.312L1445.57 224.844L1445.96 212.949C1409.68 215.635 1357.45 209.112 1333.53 197.985L1320.37 247.482C1323.56 248.249 1329.54 248.633 1336.72 250.551C1395.33 259.376 1471.88 270.887 1524.11 238.657C1546.84 225.611 1558 205.659 1558 179.568Z" fill="#0196D0"/>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

+32
View File
@@ -0,0 +1,32 @@
export type S3Config = {
region: string;
endpoint: string;
accessKeyId: string;
secretAccessKey: string;
}
export type ApiConfig = {
baseURL: string;
}
export type ReleaseConfig = {
version: string;
date: string;
}
export type LicenseConfig = {
name: string;
url: string;
}
export type SessionConfig = {
durationSeconds: number;
}
export interface SiteConfig {
api: ApiConfig;
s3: S3Config;
release: ReleaseConfig;
license: LicenseConfig;
session?: SessionConfig;
}