数据源和 oss

This commit is contained in:
2026-08-13 23:17:55 +08:00
parent f1a0b17bb0
commit c4bf7ee03b
40 changed files with 1975 additions and 922 deletions
-8
View File
@@ -76,14 +76,6 @@ export const updateApi = (data) => {
// @Param data body api.CreateApiParams true "更新api"
// @Success 200 {string} json "{"success":true,"data":{},"msg":"更新成功"}"
// @Router /api/setAuthApi [post]
export const setAuthApi = (data) => {
return service({
url: '/api/setAuthApi',
method: 'post',
data
})
}
// @Tags Api
// @Summary 获取所有的Api 不分页
// @Security ApiKeyAuth
-6
View File
@@ -102,9 +102,3 @@ export const getSysErrorList = (params) => {
// @Param data query systemReq.SysErrorSearch true "分页获取错误日志列表"
// @Success 200 {object} response.Response{data=object,msg=string} "获取成功"
// @Router /sysError/getSysErrorPublic [get]
export const getSysErrorPublic = () => {
return service({
url: '/sysError/getSysErrorPublic',
method: 'get',
})
}
-47
View File
@@ -1,47 +0,0 @@
<template>
<VCharts
v-if="renderChart"
:option="options"
:autoresize="autoResize"
:style="{ width, height }"
/>
</template>
<script setup>
import { ref, nextTick } from 'vue'
import VCharts from 'vue-echarts'
import { useWindowResize } from '@/hooks/use-windows-resize'
defineProps({
options: {
type: Object,
default() {
return {}
}
},
autoResize: {
type: Boolean,
default: true
},
width: {
type: String,
default: '100%'
},
height: {
type: String,
default: '100%'
}
})
const renderChart = ref(false)
nextTick(() => {
renderChart.value = true
})
useWindowResize(() => {
renderChart.value = false
nextTick(() => {
renderChart.value = true
})
})
</script>
<style scoped lang="less"></style>
+1 -1
View File
@@ -1,5 +1,5 @@
// 基础组件库:以无样式的 reka-ui 原语为底座,仅用项目既有的 UnoCSS 主题 token 上色,
// 跟随 themeStore 换肤 / 暗色模式。规范见 aiDoc/frontend-backend/component-library.md。
// 跟随 themeStore 换肤与暗色模式。
export * from './button'
export * from './input'
export * from './checkbox'
-1
View File
@@ -6,7 +6,6 @@ import * as ElIconModules from '@element-plus/icons-vue'
import svgIcon from '@/components/svgIcon/svgIcon.vue'
// 基础组件库(reka-ui 底座,src/core/componentLibrary):全局以 g- 前缀(kebab-case)注册,
// 全站可直接 <g-button /> / <g-select /> 使用,无需逐文件 import。
// 规范见 aiDoc/frontend-backend/component-library.md
import * as ComponentLibrary from '@/core/componentLibrary'
// 导入转换图标名称的函数
@@ -1,187 +0,0 @@
<template>
<Chart :height="height" :option="chartOption" />
</template>
<script setup>
import Chart from '@/components/charts/index.vue'
import useChartOption from '@/hooks/charts'
import { graphic } from 'echarts'
import { computed, ref } from 'vue'
import { useThemeStore } from '@/pinia'
import { storeToRefs } from 'pinia'
import { addOpacityToColor } from '@/theme/color'
const themeStore = useThemeStore()
const { settings } = storeToRefs(themeStore)
defineProps({
height: {
type: String,
default: '128px'
}
})
const axisTextColor = computed(() => {
return themeStore.isDark ? 'rgba(255,255,255,0.70)' : 'rgba(0,0,0,0.70)'
})
const dotColor = computed(() => {
return themeStore.isDark ? 'rgba(255,255,255,0.12)' : 'rgba(0,0,0,0.08)'
})
const primaryColor = (opacity) => addOpacityToColor(settings.value.themeColor, opacity)
const graphicFactory = (side) => {
return {
type: 'text',
bottom: '8',
...side,
style: {
text: '',
textAlign: 'center',
fill: axisTextColor.value,
fontSize: 12
}
}
}
const xAxis = ref([
'2024-1',
'2024-2',
'2024-3',
'2024-4',
'2024-5',
'2024-6',
'2024-7',
'2024-8'
])
const chartsData = ref([12, 22, 32, 45, 32, 78, 89, 92])
const graphicElements = ref([
graphicFactory({ left: '5%' }),
graphicFactory({ right: 0 })
])
const { chartOption } = useChartOption(() => {
return {
grid: {
left: '40',
right: '0',
top: '10',
bottom: '30'
},
xAxis: {
type: 'category',
offset: 2,
data: xAxis.value,
boundaryGap: false,
axisLabel: {
color: axisTextColor.value,
formatter(value, idx) {
if (idx === 0) return ''
if (idx === xAxis.value.length - 1) return ''
return `${value}`
}
},
axisLine: {
show: false
},
axisTick: {
show: false
},
splitLine: {
show: true,
interval: (idx) => {
if (idx === 0) return false
if (idx === xAxis.value.length - 1) return false
return true
},
lineStyle: {
color: dotColor.value
}
},
axisPointer: {
show: true,
lineStyle: {
color: primaryColor(1),
width: 2
}
}
},
yAxis: {
type: 'value',
axisLine: {
show: false
},
axisLabel: {
formatter(value, idx) {
if (idx === 0) return value
return `${value}k`
}
},
splitLine: {
show: true,
lineStyle: {
type: 'dashed',
color: dotColor.value
}
}
},
tooltip: {
trigger: 'axis',
formatter(params) {
const [firstElement] = params
return `<div>
<p class="tooltip-title">${firstElement.axisValueLabel}</p>
<div class="content-panel"><span>总内容量</span><span class="tooltip-value">${(
Number(firstElement.value) * 10000
).toLocaleString()}</span></div>
</div>`
},
className: 'echarts-tooltip-diy'
},
graphic: {
elements: graphicElements.value
},
series: [
{
data: chartsData.value,
type: 'line',
smooth: true,
// symbol: 'circle',
symbolSize: 12,
emphasis: {
focus: 'series',
itemStyle: {
borderWidth: 2
}
},
lineStyle: {
width: 3,
color: new graphic.LinearGradient(0, 0, 1, 0, [
{
offset: 0,
color: primaryColor(0.5)
},
{
offset: 0.5,
color: primaryColor(0.57)
},
{
offset: 1,
color: primaryColor(1)
}
])
},
showSymbol: false,
areaStyle: {
opacity: 0.8,
color: new graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: primaryColor(0.13)
},
{
offset: 1,
color: primaryColor(0.03)
}
])
}
}
]
}
})
</script>
<style scoped lang="scss"></style>
@@ -1,132 +0,0 @@
<template>
<Chart :height="height" :option="chartOption" />
</template>
<script setup>
import Chart from '@/components/charts/index.vue'
import useChartOption from '@/hooks/charts'
import { graphic } from 'echarts'
import { ref } from 'vue'
import { storeToRefs } from 'pinia'
import { useThemeStore } from '@/pinia'
import { addOpacityToColor } from '@/theme/color'
const themeStore = useThemeStore()
const { settings } = storeToRefs(themeStore)
const prop = defineProps({
height: {
type: String,
default: '128px'
},
data: {
type: Array,
default: () => []
}
})
const graphicFactory = (side) => {
return {
type: 'text',
bottom: '8',
...side,
style: {
text: '',
textAlign: 'center',
fill: themeStore.isDark ? '#FFFFFF' : '#000000',
fontSize: 12
}
}
}
const graphicElements = ref([
graphicFactory({ left: '5%' }),
graphicFactory({ right: 0 })
])
const primaryColor = (opacity) => addOpacityToColor(settings.value.themeColor, opacity)
const { chartOption } = useChartOption(() => {
return {
grid: {
left: '40',
right: '0',
top: '10',
bottom: '30'
},
xAxis: {
type: 'category',
offset: 2,
show: false,
boundaryGap: false,
axisLine: {
show: false
},
axisTick: {
show: false
},
splitLine: {
show: false
}
},
yAxis: {
type: 'value',
show: false,
axisLine: {
show: false
},
axisLabel: {
show: false
},
splitLine: {
show: false
}
},
graphic: {
elements: graphicElements.value
},
series: [
{
data: prop.data,
type: 'line',
smooth: true,
symbolSize: 12,
emphasis: {
focus: 'series',
itemStyle: {
borderWidth: 2
}
},
lineStyle: {
width: 3,
color: new graphic.LinearGradient(0, 0, 1, 0, [
{
offset: 0,
color: primaryColor(0.2)
},
{
offset: 0.5,
color: primaryColor(0.39)
},
{
offset: 1,
color: primaryColor(1)
}
])
},
showSymbol: false,
areaStyle: {
opacity: 0.8,
color: new graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: primaryColor(0.13)
},
{
offset: 1,
color: primaryColor(0.03)
}
])
}
}
]
}
})
</script>
<style scoped lang="scss"></style>
@@ -1,49 +0,0 @@
<template>
<div class="">
<div class="flex items-center justify-between mb-2">
<div v-if="title" class="text-sm font-semibold tracking-tight text-base-text">
{{ title }}
</div>
<slot v-else name="title" />
</div>
<div class="w-full relative">
<div v-if="type !== 4">
<div class="mt-4 text-3xl font-mono text-base-text">
<el-statistic :value="268500" />
</div>
<div class="mt-2 text-xs font-mono text-muted-foreground">
+80% <el-icon class="align-middle"><TopRight /></el-icon>
</div>
</div>
<div class="absolute top-0 right-2 w-[50%] h-20">
<charts-people-number v-if="type === 1" :data="data[0]" height="100%" />
<charts-people-number v-if="type === 2" :data="data[1]" height="100%" />
<charts-people-number v-if="type === 3" :data="data[2]" height="100%" />
</div>
<charts-content-number v-if="type === 4" height="14rem" />
</div>
</div>
</template>
<script setup>
import chartsPeopleNumber from './charts-people-numbers.vue'
import chartsContentNumber from './charts-content-numbers.vue'
defineProps({
type: {
type: Number,
default: 1
},
title: {
type: String,
default: ''
}
})
const data = [
[12, 22, 32, 45, 32, 78, 89, 92],
[1, 2, 43, 5, 67, 78, 89, 12],
[12, 22, 32, 45, 32, 78, 89, 92]
]
</script>
<style scoped lang="scss"></style>
@@ -1,11 +1,9 @@
import KraCard from './card.vue'
import KraChart from './charts.vue'
import KraNotice from './notice.vue'
import KraQuickLink from './quickLinks.vue'
export {
KraCard,
KraChart,
KraNotice,
KraQuickLink,
}
+12 -29
View File
@@ -30,37 +30,20 @@
</template>
<script setup>
const notices = [
{
typeTitle: '通知',
time: '今天',
title: 'Kra 管理后台已完成核心系统、公告和邮件模块接入。',
import { onMounted, ref } from 'vue'
import { getInfoList } from '@/modules/announcement/api/info'
const notices = ref([])
onMounted(async () => {
const response = await getInfoList({ page: 1, pageSize: 8, status: 1 })
if (response.code !== 0) return
notices.value = (response.data?.list || []).map((item) => ({
typeTitle: item.type === 2 ? '公告' : '通知',
time: item.createdAt ? new Date(item.createdAt).toLocaleDateString('zh-CN') : '',
title: item.title,
dotClass: 'bg-cyan-500',
tagClass: 'bg-cyan-100 text-cyan-700 dark:bg-cyan-900/40 dark:text-cyan-200'
},
{
typeTitle: '安全',
time: '2天前',
title: '请在部署前修改 JWT 密钥并配置数据库连接信息。',
dotClass: 'bg-emerald-500',
tagClass: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-200'
},
{
typeTitle: '运维',
time: '3天前',
title: '邮件告警默认关闭,填写 SMTP 配置后自动启用。',
dotClass: 'bg-amber-500',
tagClass: 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-200'
},
{
typeTitle: '存储',
time: '5天前',
title: '媒体文件默认存储在本地 uploads/file 目录。',
dotClass: 'bg-violet-500',
tagClass: 'bg-violet-100 text-violet-700 dark:bg-violet-900/40 dark:text-violet-200'
}
]
}))
})
</script>
<style scoped lang="scss"></style>
+25 -68
View File
@@ -1,81 +1,38 @@
<template>
<template>
<div class="h-full kra-container2 overflow-auto bg-main">
<div class="space-y-2 py-2">
<kra-card
class="relative overflow-hidden rounded-xl border border-slate-200/80 bg-white px-5 py-6 shadow-sm dark:border-slate-700 dark:bg-slate-900"
>
<div class="relative flex flex-col gap-2 lg:flex-row lg:items-end lg:justify-between">
<div>
<p class="text-xs tracking-[0.2em] text-muted-foreground">DASHBOARD</p>
<h1 class="mt-2 text-xl font-semibold text-base-text lg:text-2xl">
欢迎回来,开始今天的Coding节奏
</h1>
<p class="mt-2 text-sm text-muted-foreground">
{{ today }} · 已为你聚合核心业务数据和系统公告
</p>
</div>
</div>
<kra-card class="rounded-xl border border-slate-200/80 bg-white px-5 py-6 shadow-sm dark:border-slate-700 dark:bg-slate-900">
<p class="text-xs tracking-[0.2em] text-muted-foreground">DASHBOARD</p>
<h1 class="mt-2 text-xl font-semibold text-base-text lg:text-2xl">欢迎回来,{{ userStore.userInfo.nickName }}</h1>
<p class="mt-2 text-sm text-muted-foreground">{{ today }} · 当前展示后台实时数据</p>
</kra-card>
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2 xl:grid-cols-3">
<kra-card>
<kra-chart :type="1" title="访问人数" />
</kra-card>
<kra-card>
<kra-chart :type="2" title="新增客户" />
</kra-card>
<kra-card>
<kra-chart :type="3" title="解决数量" />
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2 xl:grid-cols-4">
<kra-card v-for="item in statistics" :key="item.title">
<div class="p-2"><div class="text-sm text-muted-foreground">{{ item.title }}</div><div class="mt-3 text-3xl font-semibold text-base-text">{{ item.value }}</div></div>
</kra-card>
</div>
<div class="grid grid-cols-1 items-stretch gap-2 xl:grid-cols-12">
<div class="grid grid-cols-1 gap-2 content-start xl:col-span-8 xl:h-full">
<kra-card title="内容数据">
<kra-chart :type="4" />
</kra-card>
</div>
<div class="flex flex-col gap-2 xl:col-span-4 xl:h-full">
<kra-card title="快捷功能" show-action custom-class="min-h-[300px]">
<kra-quick-link />
</kra-card>
<kra-card title="公告" show-action custom-class="min-h-[300px]">
<kra-notice />
</kra-card>
</div>
<div class="grid grid-cols-1 items-stretch gap-2 xl:grid-cols-2">
<kra-card title="快捷功能" show-action custom-class="min-h-[300px]"><kra-quick-link /></kra-card>
<kra-card title="公告" show-action custom-class="min-h-[300px]"><kra-notice /></kra-card>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import {
KraChart,
KraNotice,
KraQuickLink,
KraCard
} from './components'
const today = computed(() => {
try {
const d = new Date()
return d.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
})
} catch (e) {
return new Date().toISOString().slice(0, 10)
}
})
defineOptions({
name: 'Dashboard'
import { computed, onMounted, ref } from 'vue'
import { useUserStore } from '@/pinia/modules/user'
import { getUserList } from '@/api/user'
import { getSysOperationRecordList } from '@/api/sysOperationRecord'
import { getFileList } from '@/api/fileUploadAndDownload'
import { getInfoList } from '@/modules/announcement/api/info'
import { KraNotice, KraQuickLink, KraCard } from './components'
defineOptions({ name: 'Dashboard' })
const userStore = useUserStore()
const today = computed(() => new Date().toLocaleDateString('zh-CN'))
const statistics = ref([{ title: '系统用户', value: '—' }, { title: '操作记录', value: '—' }, { title: '媒体文件', value: '—' }, { title: '系统公告', value: '—' }])
onMounted(async () => {
const results = await Promise.allSettled([getUserList({ page: 1, pageSize: 1 }), getSysOperationRecordList({ page: 1, pageSize: 1 }), getFileList({ page: 1, pageSize: 1 }), getInfoList({ page: 1, pageSize: 1 })])
results.forEach((result, index) => { if (result.status === 'fulfilled' && result.value?.code === 0) statistics.value[index].value = result.value.data?.total ?? 0 })
})
</script>
<style lang="scss" scoped></style>
-69
View File
@@ -1,15 +1,6 @@
<template>
<!-- 工具按钮组:扁平图标,无独立描边/阴影,hover 时图标 morph 为文案(取代 tooltip)。 -->
<div class="flex items-center gap-1 mx-3">
<g-dropdown-menu
v-if="isDev"
trigger="hover"
:items="videoList"
@select="(item) => toDoc(item.value)"
>
<icon-button icon="lucide:clapperboard" label="教程" />
</g-dropdown-menu>
<icon-button
v-if="settings.header.search.visible"
icon="lucide:search"
@@ -50,8 +41,6 @@
import { emitter } from '@/utils/bus.js'
import CommandMenu from '@/components/commandMenu/index.vue'
import IconButton from '@/components/iconButton/index.vue'
import { toDoc } from '@/utils/doc'
import { isDev } from '@/utils/env.js'
const themeStore = useThemeStore()
const { settings } = storeToRefs(themeStore)
@@ -95,62 +84,4 @@
initPage()
const videoList = [
{
label: '1.clone项目和安装依赖',
value: 'https://www.bilibili.com/video/BV1jx4y1s7xx'
},
{
label: '2.初始化项目',
value: 'https://www.bilibili.com/video/BV1sr421K7sv'
},
{
label: '3.开启调试工具+创建初始化包',
value: 'https://www.bilibili.com/video/BV1iH4y1c7Na'
},
{
label: '4.手动使用自动化创建功能',
value: 'https://www.bilibili.com/video/BV1UZ421T7fV'
},
{
label: '5.使用已有表格创建业务',
value: 'https://www.bilibili.com/video/BV1NE4m1977s'
},
{
label: '6.使用AI创建业务和创建数据源模式的可选项',
value: 'https://www.bilibili.com/video/BV17i421a7DE'
},
{
label: '7.创建自己的后端方法',
value: 'https://www.bilibili.com/video/BV1Yw4m1k7fg'
},
{
label: '8.新增一个前端页面',
value: 'https://www.bilibili.com/video/BV12y411i7oE'
},
{
label: '9.配置一个前端二级页面',
value: 'https://www.bilibili.com/video/BV1ZM4m1y7i3'
},
{
label: '10.配置一个前端菜单参数',
value: 'https://www.bilibili.com/video/BV1WS42197DZ'
},
{
label: '11.菜单参数实战+动态菜单标题+菜单高亮配置',
value: 'https://www.bilibili.com/video/BV1NE4m1979c'
},
{
label: '12.增加菜单可控按钮',
value: 'https://www.bilibili.com/video/BV1Sw4m1k746'
},
{
label: '14.新增客户角色和其相关配置教学',
value: 'https://www.bilibili.com/video/BV1Ki421a7X2'
},
{
label: '15.发布项目上线',
value: 'https://www.bilibili.com/video/BV1Lx4y1s77D'
}
]
</script>
+50 -2
View File
@@ -32,6 +32,36 @@
</el-form>
</el-tab-pane>
<el-tab-pane label="对象存储" name="storage">
<el-form label-position="top" class="grid grid-cols-1 md:grid-cols-2 gap-x-5">
<el-form-item label="存储类型" class="md:col-span-2">
<el-select v-model="config.admin.storage.type" class="!w-full">
<el-option v-for="item in storageTypes" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<template v-if="config.admin.storage.type === 'qiniu'">
<el-form-item label="区域"><el-input v-model.trim="config.admin.storage.qiniu.zone" placeholder="ZoneHuadong" /></el-form-item>
<el-form-item label="Bucket"><el-input v-model.trim="config.admin.storage.qiniu.bucket" /></el-form-item>
<el-form-item label="访问域名"><el-input v-model.trim="config.admin.storage.qiniu.base_url" /></el-form-item>
<el-form-item label="Access Key"><el-input v-model.trim="config.admin.storage.qiniu.access_key" /></el-form-item>
<el-form-item label="Secret Key"><el-input v-model="config.admin.storage.qiniu.secret_key" show-password /></el-form-item>
<el-form-item label="连接选项"><div class="flex gap-5"><el-switch v-model="config.admin.storage.qiniu.use_https" active-text="HTTPS" /><el-switch v-model="config.admin.storage.qiniu.use_cdn_domains" active-text="CDN 域名" /></div></el-form-item>
</template>
<template v-else-if="currentObjectStorage">
<el-form-item label="Endpoint"><el-input v-model.trim="currentObjectStorage.endpoint" placeholder="可留空使用提供商默认地址" /></el-form-item>
<el-form-item label="Region"><el-input v-model.trim="currentObjectStorage.region" /></el-form-item>
<el-form-item label="Bucket"><el-input v-model.trim="currentObjectStorage.bucket" /></el-form-item>
<el-form-item label="访问域名"><el-input v-model.trim="currentObjectStorage.base_url" /></el-form-item>
<el-form-item label="Access Key"><el-input v-model.trim="currentObjectStorage.access_key" /></el-form-item>
<el-form-item label="Secret Key"><el-input v-model="currentObjectStorage.secret_key" show-password /></el-form-item>
<el-form-item label="对象前缀"><el-input v-model.trim="currentObjectStorage.path_prefix" /></el-form-item>
<el-form-item v-if="config.admin.storage.type === 'cloudflare-r2'" label="Account ID"><el-input v-model.trim="currentObjectStorage.account_id" /></el-form-item>
<el-form-item label="连接选项"><div class="flex gap-5"><el-switch v-model="currentObjectStorage.use_ssl" active-text="HTTPS" /><el-switch v-model="currentObjectStorage.force_path_style" active-text="Path Style" /></div></el-form-item>
</template>
<p class="md:col-span-2 text-sm text-gray-500">对象存储客户端在服务启动时创建,保存后需重启服务生效。</p>
</el-form>
</el-tab-pane>
<el-tab-pane label="邮件设置" name="email">
<el-form label-position="top" class="grid grid-cols-1 md:grid-cols-2 gap-x-5">
<el-form-item label="默认收件人">
@@ -68,7 +98,7 @@
</template>
<script setup>
import { ref } from 'vue'
import { computed, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getSystemConfig, reloadSystem, setSystemConfig } from '@/api/system'
import { emailTest } from '@/api/email'
@@ -82,13 +112,22 @@
admin: {
routerPrefix: '',
jwt: { signingKey: '******', issuer: 'kra' },
local: { storePath: 'uploads/file', pathPrefix: 'uploads/file' }
local: { storePath: 'uploads/file', pathPrefix: 'uploads/file' },
storage: { type: 'local', qiniu: {}, aliyun_oss: {}, huawei_obs: {}, tencent_cos: {}, aws_s3: {}, cloudflare_r2: {}, minio: {} }
},
email: {
to: '', from: '', host: '', secret: '******', nickname: '', port: 465,
'is-ssl': true, 'is-loginauth': false
}
})
const storageTypes = [
{ label: '本地存储', value: 'local' }, { label: '七牛云', value: 'qiniu' },
{ label: '阿里云 OSS', value: 'aliyun-oss' }, { label: '华为云 OBS', value: 'huawei-obs' },
{ label: '腾讯云 COS', value: 'tencent-cos' }, { label: 'AWS S3', value: 'aws-s3' },
{ label: 'Cloudflare R2', value: 'cloudflare-r2' }, { label: 'MinIO', value: 'minio' }
]
const storageKeys = { 'aliyun-oss': 'aliyun_oss', 'huawei-obs': 'huawei_obs', 'tencent-cos': 'tencent_cos', 'aws-s3': 'aws_s3', 'cloudflare-r2': 'cloudflare_r2', minio: 'minio' }
const currentObjectStorage = computed(() => config.value.admin.storage[storageKeys[config.value.admin.storage.type]])
const initForm = async () => {
const res = await getSystemConfig()
@@ -97,6 +136,15 @@
admin: { ...config.value.admin, ...res.data.config.admin,
jwt: { ...config.value.admin.jwt, ...res.data.config.admin?.jwt },
local: { ...config.value.admin.local, ...res.data.config.admin?.local }
, storage: { ...config.value.admin.storage, ...res.data.config.admin?.storage,
qiniu: { ...config.value.admin.storage.qiniu, ...res.data.config.admin?.storage?.qiniu },
aliyun_oss: { ...config.value.admin.storage.aliyun_oss, ...res.data.config.admin?.storage?.aliyun_oss },
huawei_obs: { ...config.value.admin.storage.huawei_obs, ...res.data.config.admin?.storage?.huawei_obs },
tencent_cos: { ...config.value.admin.storage.tencent_cos, ...res.data.config.admin?.storage?.tencent_cos },
aws_s3: { ...config.value.admin.storage.aws_s3, ...res.data.config.admin?.storage?.aws_s3 },
cloudflare_r2: { ...config.value.admin.storage.cloudflare_r2, ...res.data.config.admin?.storage?.cloudflare_r2 },
minio: { ...config.value.admin.storage.minio, ...res.data.config.admin?.storage?.minio }
}
},
email: { ...config.value.email, ...res.data.config.email }
}