feat: 对接OSS配置管理

This commit is contained in:
AN 2025-04-27 14:53:55 +08:00
parent 74fcdc4e2c
commit 19fe1b05eb
13 changed files with 738 additions and 7 deletions

View File

@ -184,7 +184,8 @@ const local: App.I18n.Schema = {
system_client: 'Client Management',
system_notice: 'Notice Management',
'social-callback': 'Social Callback',
system_oss: 'File Management'
system_oss: 'File Management',
'system_oss-config': 'OSS Config'
},
page: {
login: {

View File

@ -184,7 +184,8 @@ const local: App.I18n.Schema = {
system_client: '客户端管理',
system_notice: '通知公告',
'social-callback': '单点登录回调',
system_oss: '文件管理'
system_oss: '文件管理',
'system_oss-config': 'OSS配置'
},
page: {
login: {

View File

@ -32,6 +32,7 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
system_dict_type: () => import("@/views/system/dict/type/index.vue"),
system_menu: () => import("@/views/system/menu/index.vue"),
system_notice: () => import("@/views/system/notice/index.vue"),
"system_oss-config": () => import("@/views/system/oss-config/index.vue"),
system_oss: () => import("@/views/system/oss/index.vue"),
system_post: () => import("@/views/system/post/index.vue"),
system_tenant: () => import("@/views/system/tenant/index.vue"),

View File

@ -211,6 +211,17 @@ export const generatedRoutes: GeneratedRoute[] = [
i18nKey: 'route.system_oss'
}
},
{
name: 'system_oss-config',
path: '/oss-config',
component: 'view.system_oss-config',
meta: {
title: 'system_oss-config',
i18nKey: 'route.system_oss-config',
constant: true,
hideInMenu: true
}
},
{
name: 'system_post',
path: '/system/post',

View File

@ -183,6 +183,7 @@ const routeMap: RouteMap = {
"system_menu": "/system/menu",
"system_notice": "/system/notice",
"system_oss": "/system/oss",
"system_oss-config": "/oss-config",
"system_post": "/system/post",
"system_tenant": "/system/tenant",
"system_user": "/system/user",

View File

@ -112,6 +112,31 @@ const dynamicConstantRoutes: ElegantRoute[] = [
hideInMenu: true,
icon: 'simple-icons:authy'
}
},
{
name: 'system',
path: '/system',
component: 'layout.base',
meta: {
title: 'system',
i18nKey: 'route.system',
localIcon: 'menu-system',
order: 1
},
children: [
{
name: 'system_oss-config',
path: '/oss-config',
component: 'view.system_oss-config',
meta: {
title: 'system_oss-config',
i18nKey: 'route.system_oss-config',
constant: true,
hideInMenu: true,
icon: 'hugeicons:configuration-01'
}
}
]
}
];

View File

@ -0,0 +1,45 @@
import { request } from '@/service/request';
/** 获取OSS配置列表 */
export function fetchGetOssConfigList(params?: Api.System.OssConfigSearchParams) {
return request<Api.System.OssConfigList>({
url: '/resource/oss/config/list',
method: 'get',
params
});
}
/** 新增OSS配置 */
export function fetchCreateOssConfig(data: Api.System.OssConfigOperateParams) {
return request<boolean>({
url: '/resource/oss/config',
method: 'post',
data
});
}
/** 修改OSS配置 */
export function fetchUpdateOssConfig(data: Api.System.OssConfigOperateParams) {
return request<boolean>({
url: '/resource/oss/config',
method: 'put',
data
});
}
/** 批量删除OSS配置 */
export function fetchBatchDeleteOssConfig(ossConfigIds: CommonType.IdType[]) {
return request<boolean>({
url: `/resource/oss/config/${ossConfigIds.join(',')}`,
method: 'delete'
});
}
/** 状态修改 */
export function fetchUpdateOssConfigStatus(data: Api.System.OssConfigOperateParams) {
return request<boolean>({
url: '/resource/oss/config/changeStatus',
method: 'put',
data
});
}

View File

@ -642,5 +642,67 @@ declare namespace Api {
/** oss list */
type OssList = Api.Common.PaginatingQueryRecord<Oss>;
/** oss config */
type OssConfig = Common.CommonRecord<{
/** 主键 */
ossConfigId: CommonType.IdType;
/** 租户编号 */
tenantId: CommonType.IdType;
/** 配置名称 */
configKey: string;
/** accessKey */
accessKey: string;
/** 秘钥secretKey */
secretKey: string;
/** 桶名称 */
bucketName: string;
/** 前缀 */
prefix: string;
/** 访问站点 */
endpoint: string;
/** 自定义域名 */
domain: string;
/** 是否httpsY=是,N=否) */
isHttps: string;
/** 域 */
region: string;
/** 桶权限类型 */
accessPolicy: string;
/** 是否默认0=是,1=否) */
status: string;
/** 扩展字段 */
ext1: string;
/** 备注 */
remark: string;
}>;
/** oss config search params */
type OssConfigSearchParams = CommonType.RecordNullable<
Pick<Api.System.OssConfig, 'configKey' | 'bucketName' | 'region' | 'status'> & Api.Common.CommonSearchParams
>;
/** oss config operate params */
type OssConfigOperateParams = CommonType.RecordNullable<
Pick<
Api.System.OssConfig,
| 'ossConfigId'
| 'configKey'
| 'accessKey'
| 'secretKey'
| 'bucketName'
| 'prefix'
| 'endpoint'
| 'domain'
| 'isHttps'
| 'region'
| 'accessPolicy'
| 'status'
| 'remark'
>
>;
/** oss config list */
type OssConfigList = Api.Common.PaginatingQueryRecord<OssConfig>;
}
}

View File

@ -37,6 +37,7 @@ declare module "@elegant-router/types" {
"system_menu": "/system/menu";
"system_notice": "/system/notice";
"system_oss": "/system/oss";
"system_oss-config": "/oss-config";
"system_post": "/system/post";
"system_tenant": "/system/tenant";
"system_user": "/system/user";
@ -117,6 +118,7 @@ declare module "@elegant-router/types" {
| "system_menu"
| "system_notice"
| "system_oss"
| "system_oss-config"
| "system_post"
| "system_tenant"
| "system_user"

View File

@ -0,0 +1,249 @@
<script setup lang="tsx">
import { NButton, NPopconfirm, NTag } from 'naive-ui';
import {
fetchBatchDeleteOssConfig,
fetchGetOssConfigList,
fetchUpdateOssConfigStatus
} from '@/service/api/system/oss-config';
import { useAppStore } from '@/store/modules/app';
import { useAuth } from '@/hooks/business/auth';
import { useTable, useTableOperate } from '@/hooks/common/table';
import { useDict } from '@/hooks/business/dict';
import StatusSwitch from '@/components/custom/status-switch.vue';
import { $t } from '@/locales';
import OssConfigOperateDrawer from './modules/oss-config-operate-drawer.vue';
import OssConfigSearch from './modules/oss-config-search.vue';
defineOptions({
name: 'OssConfigList'
});
useDict('sys_yes_no');
const appStore = useAppStore();
const { hasAuth } = useAuth();
const {
columns,
columnChecks,
data,
getData,
getDataByPage,
loading,
mobilePagination,
searchParams,
resetSearchParams
} = useTable({
apiFn: fetchGetOssConfigList,
apiParams: {
pageNum: 1,
pageSize: 10,
// if you want to use the searchParams in Form, you need to define the following properties, and the value is null
// the value can not be undefined, otherwise the property in Form will not be reactive
configKey: null,
bucketName: null,
region: null,
status: null
},
columns: () => [
{
type: 'selection',
align: 'center',
width: 48
},
{
key: 'index',
title: $t('common.index'),
align: 'center',
width: 64
},
{
key: 'configKey',
title: '配置名称',
align: 'center',
minWidth: 120
},
{
key: 'bucketName',
title: '桶名称',
align: 'center',
minWidth: 120
},
{
key: 'endpoint',
title: '访问站点',
align: 'center',
minWidth: 120
},
{
key: 'region',
title: '域',
align: 'center',
minWidth: 120
},
{
key: 'accessPolicy',
title: '桶权限类型',
align: 'center',
minWidth: 120,
render(row) {
if (row.accessPolicy === '0') {
return <NTag type="info">私有</NTag>;
}
if (row.accessPolicy === '1') {
return <NTag type="success">公有</NTag>;
}
if (row.accessPolicy === '2') {
return <NTag type="warning">自定义</NTag>;
}
return null;
}
},
{
key: 'status',
title: '是否默认',
align: 'center',
minWidth: 120,
render(row) {
return (
<StatusSwitch
v-model:value={row.status}
info={row.configKey}
onSubmitted={(value, callback) => handleStatusChange(row, value, callback)}
/>
);
}
},
{
key: 'remark',
title: '备注',
align: 'center',
minWidth: 120
},
{
key: 'operate',
title: $t('common.operate'),
align: 'center',
width: 130,
render: row => {
const editBtn = () => {
if (!hasAuth('system:ossConfig:edit')) {
return null;
}
return (
<NButton type="primary" ghost size="small" onClick={() => edit(row.ossConfigId!)}>
{$t('common.edit')}
</NButton>
);
};
const deleteBtn = () => {
if (!hasAuth('system:ossConfig:remove')) {
return null;
}
return (
<NPopconfirm onPositiveClick={() => handleDelete(row.ossConfigId!)}>
{{
default: () => $t('common.confirmDelete'),
trigger: () => (
<NButton type="error" ghost size="small">
{$t('common.delete')}
</NButton>
)
}}
</NPopconfirm>
);
};
return (
<div class="flex-center gap-8px">
{editBtn()}
{deleteBtn()}
</div>
);
}
}
]
});
const { drawerVisible, operateType, editingData, handleAdd, handleEdit, checkedRowKeys, onBatchDeleted, onDeleted } =
useTableOperate(data, getData);
async function handleBatchDelete() {
// request
const { error } = await fetchBatchDeleteOssConfig(checkedRowKeys.value);
if (error) return;
onBatchDeleted();
}
async function handleDelete(ossConfigId: CommonType.IdType) {
// request
const { error } = await fetchBatchDeleteOssConfig([ossConfigId]);
if (error) return;
onDeleted();
}
async function edit(ossConfigId: CommonType.IdType) {
handleEdit('ossConfigId', ossConfigId);
}
/** 处理状态切换 */
async function handleStatusChange(
row: Api.System.OssConfig,
value: Api.Common.EnableStatus,
callback: (flag: boolean) => void
) {
const { error } = await fetchUpdateOssConfigStatus({
ossConfigId: row.ossConfigId,
status: value
});
callback(!error);
if (!error) {
window.$message?.success('状态修改成功');
getData();
}
}
</script>
<template>
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
<OssConfigSearch v-model:model="searchParams" @reset="resetSearchParams" @search="getDataByPage" />
<NCard title="OSS配置列表" :bordered="false" size="small" class="sm:flex-1-hidden card-wrapper">
<template #header-extra>
<TableHeaderOperation
v-model:columns="columnChecks"
:disabled-delete="checkedRowKeys.length === 0"
:loading="loading"
:show-add="hasAuth('system:ossConfig:add')"
:show-delete="hasAuth('system:ossConfig:remove')"
:show-export="false"
@add="handleAdd"
@delete="handleBatchDelete"
@refresh="getData"
/>
</template>
<NDataTable
v-model:checked-row-keys="checkedRowKeys"
:columns="columns"
:data="data"
size="small"
:flex-height="!appStore.isMobile"
:scroll-x="962"
:loading="loading"
remote
:row-key="row => row.ossConfigId"
:pagination="mobilePagination"
class="sm:h-full"
/>
<OssConfigOperateDrawer
v-model:visible="drawerVisible"
:operate-type="operateType"
:row-data="editingData"
@submitted="getDataByPage"
/>
</NCard>
</div>
</template>
<style scoped></style>

View File

@ -0,0 +1,245 @@
<script setup lang="ts">
import { computed, reactive, ref, watch } from 'vue';
import { fetchCreateOssConfig, fetchUpdateOssConfig } from '@/service/api/system/oss-config';
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
import { $t } from '@/locales';
defineOptions({
name: 'OssConfigOperateDrawer'
});
interface Props {
/** the type of operation */
operateType: NaiveUI.TableOperateType;
/** the edit row data */
rowData?: Api.System.OssConfig | null;
}
const props = defineProps<Props>();
interface Emits {
(e: 'submitted'): void;
}
const emit = defineEmits<Emits>();
const visible = defineModel<boolean>('visible', {
default: false
});
const { formRef, validate, restoreValidation } = useNaiveForm();
const { createRequiredRule } = useFormRules();
const title = computed(() => {
const titles: Record<NaiveUI.TableOperateType, string> = {
add: '新增OSS配置',
edit: '编辑OSS配置'
};
return titles[props.operateType];
});
type Model = Api.System.OssConfigOperateParams;
const model: Model = reactive(createDefaultModel());
function createDefaultModel(): Model {
return {
configKey: '',
accessKey: '',
secretKey: '',
bucketName: '',
prefix: '',
endpoint: '',
domain: '',
isHttps: '0',
region: '',
accessPolicy: '0',
remark: ''
};
}
type RuleKey = Extract<
keyof Model,
'ossConfigId' | 'configKey' | 'accessKey' | 'secretKey' | 'bucketName' | 'endpoint' | 'accessPolicy'
>;
const rules: Record<RuleKey, App.Global.FormRule> = {
ossConfigId: createRequiredRule('主键不能为空'),
configKey: createRequiredRule('配置名称不能为空'),
accessKey: createRequiredRule('accessKey不能为空'),
secretKey: createRequiredRule('secretKey不能为空'),
bucketName: createRequiredRule('桶名称不能为空'),
endpoint: createRequiredRule('访问站点不能为空'),
accessPolicy: createRequiredRule('桶权限类型不能为空')
};
const accessPolicyOptions = ref<CommonType.Option[]>([
{ label: '私有', value: '0' },
{ label: '公有', value: '1' },
{ label: '自定义', value: '2' }
]);
function handleUpdateModelWhenEdit() {
if (props.operateType === 'add') {
Object.assign(model, createDefaultModel());
return;
}
if (props.operateType === 'edit' && props.rowData) {
Object.assign(model, props.rowData);
}
}
function closeDrawer() {
visible.value = false;
}
async function handleSubmit() {
await validate();
// request
if (props.operateType === 'add') {
const {
configKey,
accessKey,
secretKey,
bucketName,
prefix,
endpoint,
domain,
isHttps,
region,
accessPolicy,
remark
} = model;
const { error } = await fetchCreateOssConfig({
configKey,
accessKey,
secretKey,
bucketName,
prefix,
endpoint,
domain,
isHttps,
region,
accessPolicy,
remark
});
if (error) return;
}
if (props.operateType === 'edit') {
const {
ossConfigId,
configKey,
accessKey,
secretKey,
bucketName,
prefix,
endpoint,
domain,
isHttps,
region,
accessPolicy,
remark
} = model;
const { error } = await fetchUpdateOssConfig({
ossConfigId,
configKey,
accessKey,
secretKey,
bucketName,
prefix,
endpoint,
domain,
isHttps,
region,
accessPolicy,
remark
});
if (error) return;
}
window.$message?.success($t('common.updateSuccess'));
closeDrawer();
emit('submitted');
}
watch(visible, () => {
if (visible.value) {
handleUpdateModelWhenEdit();
restoreValidation();
}
});
</script>
<template>
<NDrawer v-model:show="visible" :title="title" display-directive="show" :width="800" class="max-w-90%">
<NDrawerContent :title="title" :native-scrollbar="false" closable>
<NForm ref="formRef" :model="model" :rules="rules">
<NDivider>基本信息</NDivider>
<NFormItem label="配置名称" path="configKey">
<NInput v-model:value="model.configKey" placeholder="请输入配置名称" />
</NFormItem>
<NFormItem label="访问站点" path="endpoint">
<NInputGroup>
<NInputGroupLabel>http://</NInputGroupLabel>
<NInput v-model:value="model.endpoint" placeholder="请输入访问站点" />
</NInputGroup>
</NFormItem>
<NFormItem label="自定义域名" path="domain">
<NInput v-model:value="model.domain" placeholder="请输入自定义域名" />
</NFormItem>
<NDivider>认证信息</NDivider>
<NFormItem label="accessKey" path="accessKey">
<NInput v-model:value="model.accessKey" placeholder="请输入accessKey" />
</NFormItem>
<NFormItem label="secretKey" path="secretKey">
<NInput v-model:value="model.secretKey" placeholder="请输入秘钥secretKey" />
</NFormItem>
<NDivider>桶信息</NDivider>
<NFormItem label="桶名称" path="bucketName">
<NInput v-model:value="model.bucketName" placeholder="请输入桶名称" />
</NFormItem>
<NFormItem label="前缀" path="prefix">
<NInput v-model:value="model.prefix" placeholder="请输入前缀" />
</NFormItem>
<NGrid :cols="2" :x-gap="24">
<NGridItem>
<NFormItem label="是否https" path="isHttps">
<DictRadio v-model:value="model.isHttps" dict-code="sys_yes_no" />
</NFormItem>
</NGridItem>
<NGridItem>
<NFormItem label="桶权限类型" path="accessPolicy">
<NRadioGroup v-model:value="model.accessPolicy">
<NSpace>
<NRadio
v-for="option in accessPolicyOptions"
:key="option.value"
:value="option.value"
:label="option.label"
/>
</NSpace>
</NRadioGroup>
</NFormItem>
</NGridItem>
</NGrid>
<NFormItem label="域" path="region">
<NInput v-model:value="model.region" placeholder="请输入域" />
</NFormItem>
<NFormItem label="备注" path="remark">
<NInput v-model:value="model.remark" :rows="3" type="textarea" placeholder="请输入备注" />
</NFormItem>
</NForm>
<template #footer>
<NSpace :size="16">
<NButton @click="closeDrawer">{{ $t('common.cancel') }}</NButton>
<NButton type="primary" @click="handleSubmit">{{ $t('common.confirm') }}</NButton>
</NSpace>
</template>
</NDrawerContent>
</NDrawer>
</template>
<style scoped></style>

View File

@ -0,0 +1,83 @@
<script setup lang="ts">
import { ref } from 'vue';
import type { SelectOption } from 'naive-ui';
import { useNaiveForm } from '@/hooks/common/form';
import { $t } from '@/locales';
defineOptions({
name: 'OssConfigSearch'
});
interface Emits {
(e: 'reset'): void;
(e: 'search'): void;
}
const emit = defineEmits<Emits>();
const { formRef, validate, restoreValidation } = useNaiveForm();
const model = defineModel<Api.System.OssConfigSearchParams>('model', { required: true });
const isDefaltOptions = ref<SelectOption[]>([
{
label: '是',
value: '0'
},
{
label: '否',
value: '1'
}
]);
async function reset() {
await restoreValidation();
emit('reset');
}
async function search() {
await validate();
emit('search');
}
</script>
<template>
<NCard :bordered="false" size="small" class="card-wrapper">
<NCollapse>
<NCollapseItem :title="$t('common.search')" name="user-search">
<NForm ref="formRef" :model="model" label-placement="left" :label-width="80">
<NGrid responsive="screen" item-responsive>
<NFormItemGi span="24 s:12 m:6" label="配置名称" path="configKey" class="pr-24px">
<NInput v-model:value="model.configKey" placeholder="请输入配置名称" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="桶名称" path="bucketName" class="pr-24px">
<NInput v-model:value="model.bucketName" placeholder="请输入桶名称" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="域" path="region" class="pr-24px">
<NInput v-model:value="model.region" placeholder="请输入域" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="是否默认" path="status" class="pr-24px">
<NSelect v-model:value="model.status" placeholder="请选择是否默认" :options="isDefaltOptions" clearable />
</NFormItemGi>
<NFormItemGi span="24" class="pr-24px">
<NSpace class="w-full" justify="end">
<NButton @click="reset">
<template #icon>
<icon-ic-round-refresh class="text-icon" />
</template>
{{ $t('common.reset') }}
</NButton>
<NButton type="primary" ghost @click="search">
<template #icon>
<icon-ic-round-search class="text-icon" />
</template>
{{ $t('common.search') }}
</NButton>
</NSpace>
</NFormItemGi>
</NGrid>
</NForm>
</NCollapseItem>
</NCollapse>
</NCard>
</template>
<style scoped></style>

View File

@ -8,24 +8,25 @@ import { useAppStore } from '@/store/modules/app';
import { useTable, useTableOperate } from '@/hooks/common/table';
import { useAuth } from '@/hooks/business/auth';
import { useDownload } from '@/hooks/business/download';
import { useRouterPush } from '@/hooks/common/router';
import { isImage } from '@/utils/common';
import { $t } from '@/locales';
import ButtonIcon from '@/components/custom/button-icon.vue';
import OssSearch from './modules/oss-search.vue';
import OssUploadModal from './modules/oss-upload-modal.vue';
defineOptions({
name: 'OssList'
});
const { routerPushByKey } = useRouterPush();
const { hasAuth } = useAuth();
const { oss } = useDownload();
const appStore = useAppStore();
const fileUploadType = ref<'file' | 'image'>('file');
const { bool: preview, setBool: setPreview } = useBoolean(true);
const { loading: previewLoading, startLoading: startPreviewLoading, endLoading: endPreviewLoading } = useLoading(false);
const { bool: uploadVisible, setTrue: showFUploadModal } = useBoolean(false);
const {
columns,
columnChecks,
@ -229,6 +230,10 @@ async function handleUpdatePreview(checked: boolean) {
}
});
}
function handleToOssConfig() {
routerPushByKey('system_oss-config');
}
</script>
<template>
@ -275,7 +280,7 @@ async function handleUpdatePreview(checked: boolean) {
</template>
上传图片
</NButton>
<NButton type="primary" size="small" ghost>
<NButton type="primary" size="small" ghost @click="handleToOssConfig">
<template #icon>
<icon-hugeicons:configuration-01 />
</template>