feat(projects): 新增文件管理页面

This commit is contained in:
xlsea
2025-11-19 17:40:48 +08:00
parent 3f6cf32bfe
commit 59959bafed
13 changed files with 1125 additions and 0 deletions
+2
View File
@@ -277,6 +277,8 @@ const local: App.I18n.Schema = {
system_dict: 'Dict Management',
system_config: 'Config Management',
system_notice: 'Notice Management',
system_oss: 'File Management',
'system_oss-config': 'OSS Config',
monitor: 'Monitor',
'social-callback': 'Social Callback',
monitor_cache: 'Cache Monitor',
+2
View File
@@ -274,6 +274,8 @@ const local: App.I18n.Schema = {
system_dict: '字典管理',
system_config: '参数设置',
system_notice: '通知公告',
system_oss: '文件管理',
'system_oss-config': 'OSS 配置',
monitor: '系统监控',
'social-callback': '单点登录回调',
monitor_cache: '缓存监控',
+2
View File
@@ -31,6 +31,8 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
system_dict: () => import("@/views/system/dict/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_role: () => import("@/views/system/role/index.vue"),
system_user: () => import("@/views/system/user/index.vue"),
+21
View File
@@ -193,6 +193,27 @@ export const generatedRoutes: GeneratedRoute[] = [
i18nKey: 'route.system_notice'
}
},
{
name: 'system_oss',
path: '/system/oss',
component: 'view.system_oss',
meta: {
title: 'system_oss',
i18nKey: 'route.system_oss'
}
},
{
name: 'system_oss-config',
path: '/system/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'
}
},
{
name: 'system_post',
path: '/system/post',
+2
View File
@@ -185,6 +185,8 @@ const routeMap: RouteMap = {
"system_dict": "/system/dict",
"system_menu": "/system/menu",
"system_notice": "/system/notice",
"system_oss": "/system/oss",
"system_oss-config": "/system/oss-config",
"system_post": "/system/post",
"system_role": "/system/role",
"system_user": "/system/user",
+2
View File
@@ -30,10 +30,12 @@ declare module 'vue' {
IconEpCopyDocument: typeof import('~icons/ep/copy-document')['default']
IconGridiconsFullscreen: typeof import('~icons/gridicons/fullscreen')['default']
IconGridiconsFullscreenExit: typeof import('~icons/gridicons/fullscreen-exit')['default']
'IconHugeicons:configuration01': typeof import('~icons/hugeicons/configuration01')['default']
IconIcRoundRefresh: typeof import('~icons/ic/round-refresh')['default']
IconIcRoundSearch: typeof import('~icons/ic/round-search')['default']
IconLocalBanner: typeof import('~icons/local/banner')['default']
IconLocalLogo: typeof import('~icons/local/logo')['default']
'IconMaterialSymbols:imageOutline': typeof import('~icons/material-symbols/image-outline')['default']
'IconMaterialSymbols:refreshRounded': typeof import('~icons/material-symbols/refresh-rounded')['default']
'IconMaterialSymbols:syncOutline': typeof import('~icons/material-symbols/sync-outline')['default']
'IconMaterialSymbols:uploadRounded': typeof import('~icons/material-symbols/upload-rounded')['default']
+4
View File
@@ -39,6 +39,8 @@ declare module "@elegant-router/types" {
"system_dict": "/system/dict";
"system_menu": "/system/menu";
"system_notice": "/system/notice";
"system_oss": "/system/oss";
"system_oss-config": "/system/oss-config";
"system_post": "/system/post";
"system_role": "/system/role";
"system_user": "/system/user";
@@ -125,6 +127,8 @@ declare module "@elegant-router/types" {
| "system_dict"
| "system_menu"
| "system_notice"
| "system_oss-config"
| "system_oss"
| "system_post"
| "system_role"
| "system_user"
+259
View File
@@ -0,0 +1,259 @@
<script setup lang="tsx">
import { ref } from 'vue';
import { NDivider, 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 { defaultTransform, useNaivePaginatedTable, useTableOperate } from '@/hooks/common/table';
import { useDict } from '@/hooks/business/dict';
import StatusSwitch from '@/components/custom/status-switch.vue';
import { $t } from '@/locales';
import ButtonIcon from '@/components/custom/button-icon.vue';
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 searchParams = ref<Api.System.OssConfigSearchParams>({
pageNum: 1,
pageSize: 10,
configKey: null,
bucketName: null,
region: null,
status: null,
params: {}
});
const { columns, columnChecks, data, getData, getDataByPage, loading, mobilePagination, scrollX } =
useNaivePaginatedTable({
api: () => fetchGetOssConfigList(searchParams.value),
transform: response => defaultTransform(response),
onPaginationParamsChange: params => {
searchParams.value.pageNum = params.page;
searchParams.value.pageSize = params.pageSize;
},
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 divider = () => {
if (!hasAuth('system:ossConfig:edit') || !hasAuth('system:ossConfig:remove')) {
return null;
}
return <NDivider vertical />;
};
const editBtn = () => {
if (!hasAuth('system:ossConfig:edit')) {
return null;
}
return (
<ButtonIcon
text
type="primary"
icon="material-symbols:drive-file-rename-outline-outline"
tooltipContent={$t('common.edit')}
onClick={() => edit(row.ossConfigId!)}
/>
);
};
const deleteBtn = () => {
if (!hasAuth('system:ossConfig:remove')) {
return null;
}
return (
<ButtonIcon
text
type="error"
icon="material-symbols:delete-outline"
tooltipContent={$t('common.delete')}
popconfirmContent={$t('common.confirmDelete')}
onPositiveClick={() => handleDelete(row.ossConfigId!)}
/>
);
};
return (
<div class="flex-center gap-8px">
{editBtn()}
{divider()}
{deleteBtn()}
</div>
);
}
}
]
});
const { drawerVisible, operateType, editingData, handleAdd, handleEdit, checkedRowKeys, onBatchDeleted, onDeleted } =
useTableOperate(data, 'ossConfigId', 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);
}
/** 处理状态切换 */
async function handleStatusChange(
row: Api.System.OssConfig,
value: Api.Common.EnableStatus,
callback: (flag: boolean) => void
) {
const { error } = await fetchUpdateOssConfigStatus({
configKey: row.configKey,
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" @search="getDataByPage" />
<NCard title="OSS配置列表" :bordered="false" size="small" class="card-wrapper sm:flex-1-hidden">
<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="scrollX"
: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="getData"
/>
</NCard>
</div>
</template>
<style scoped></style>
@@ -0,0 +1,226 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { jsonClone } from '@sa/utils';
import { ossAccessPolicyOptions, ossConfigIsHttpsOptions } from '@/constants/business';
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 = ref<Model>(createDefaultModel());
function createDefaultModel(): Model {
return {
configKey: '',
accessKey: '',
secretKey: '',
bucketName: '',
prefix: '',
endpoint: '',
domain: '',
isHttps: 'N',
region: '',
accessPolicy: '1',
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('桶权限类型不能为空')
};
function handleUpdateModelWhenEdit() {
model.value = createDefaultModel();
if (props.operateType === 'edit' && props.rowData) {
Object.assign(model.value, jsonClone(props.rowData));
}
}
function closeDrawer() {
visible.value = false;
}
async function handleSubmit() {
await validate();
const {
ossConfigId,
configKey,
accessKey,
secretKey,
bucketName,
prefix,
endpoint,
domain,
isHttps,
region,
accessPolicy,
remark
} = model.value;
// request
if (props.operateType === 'add') {
const { error } = await fetchCreateOssConfig({
configKey,
accessKey,
secretKey,
bucketName,
prefix,
endpoint,
domain,
isHttps,
region,
accessPolicy,
remark
});
if (error) return;
}
if (props.operateType === 'edit') {
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>
<NSelect
v-model:value="model.isHttps"
class="w-110px"
:options="ossConfigIsHttpsOptions"
placeholder="请选择访问协议"
/>
<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="桶权限类型" path="accessPolicy">
<NRadioGroup v-model:value="model.accessPolicy">
<NSpace>
<NRadio
v-for="option in ossAccessPolicyOptions"
: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>
@@ -0,0 +1,92 @@
<script setup lang="ts">
import { ref, toRaw } from 'vue';
import type { SelectOption } from 'naive-ui';
import { jsonClone } from '@sa/utils';
import { useNaiveForm } from '@/hooks/common/form';
import { $t } from '@/locales';
defineOptions({
name: 'OssConfigSearch'
});
interface Emits {
(e: 'search'): void;
}
const emit = defineEmits<Emits>();
const { formRef, validate, restoreValidation } = useNaiveForm();
const isDefaltOptions = ref<SelectOption[]>([
{
label: '是',
value: '0'
},
{
label: '否',
value: '1'
}
]);
const model = defineModel<Api.System.OssConfigSearchParams>('model', { required: true });
const defaultModel = jsonClone(toRaw(model.value));
function resetModel() {
Object.assign(model.value, defaultModel);
}
async function reset() {
await restoreValidation();
resetModel();
emit('search');
}
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>
+344
View File
@@ -0,0 +1,344 @@
<script setup lang="tsx">
import { onMounted, ref } from 'vue';
import { NButton, NDivider, NEllipsis, NImage, NTag, NTooltip } from 'naive-ui';
import { useBoolean, useLoading } from '@sa/hooks';
import { fetchBatchDeleteOss, fetchGetOssList } from '@/service/api/system/oss';
import { fetchGetConfigByKey, fetchUpdateConfigByKey } from '@/service/api/system/config';
import { useAppStore } from '@/store/modules/app';
import { defaultTransform, useNaivePaginatedTable, 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 { handleCopy } from '@/utils/copy';
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 searchParams = ref<Api.System.OssSearchParams>({
pageNum: 1,
pageSize: 10,
fileName: null,
originalName: null,
fileSuffix: null,
service: null,
isAsc: 'desc',
orderByColumn: 'createTime',
params: {}
});
const { columns, columnChecks, data, getData, getDataByPage, loading, mobilePagination, scrollX } =
useNaivePaginatedTable({
api: () => fetchGetOssList(searchParams.value),
transform: response => defaultTransform(response),
onPaginationParamsChange: params => {
searchParams.value.pageNum = params.page;
searchParams.value.pageSize = params.pageSize;
},
columns: () => [
{
type: 'selection',
align: 'center',
width: 48
},
{
key: 'index',
title: $t('common.index'),
align: 'center',
width: 64
},
{
key: 'ossId',
title: '对象存储主键',
align: 'center',
minWidth: 120
},
{
key: 'fileName',
title: '文件名',
align: 'center',
ellipsis: {
tooltip: true,
lineClamp: 3
},
minWidth: 120
},
{
key: 'originalName',
title: '原名',
align: 'center',
ellipsis: {
tooltip: true,
lineClamp: 3
},
minWidth: 120
},
{
key: 'fileSuffix',
title: '文件后缀名',
align: 'center',
minWidth: 100
},
{
key: 'url',
title: 'URL地址',
align: 'center',
minWidth: 120,
render: row => {
if (preview.value && isImage(row.fileSuffix)) {
return <NImage class="h-40px w-40px object-contain" src={row.url} />;
}
return (
<NTooltip>
{{
default: () => <span>点击复制</span>,
trigger: () => (
<div class="cursor-pointer" onClick={async () => await handleCopy(row.url)}>
<NEllipsis line-clamp={3} tooltip={false}>
{row.url}
</NEllipsis>
</div>
)
}}
</NTooltip>
);
}
},
{
key: 'createTime',
title: '创建时间',
align: 'center',
minWidth: 120
},
{
key: 'createByName',
title: '上传人',
align: 'center',
minWidth: 120
},
{
key: 'service',
title: '服务商',
align: 'center',
minWidth: 100,
render: row => {
return <NTag type="primary">{row.service}</NTag>;
}
},
{
key: 'operate',
title: $t('common.operate'),
align: 'center',
width: 130,
render: row => {
const divider = () => {
if (!hasAuth('system:oss:download') || !hasAuth('system:oss:delete')) {
return null;
}
return <NDivider vertical />;
};
const downloadBtn = () => {
if (!hasAuth('system:oss:download')) {
return null;
}
return (
<ButtonIcon
text
type="primary"
icon="material-symbols:download-rounded"
class="text-20px"
tooltipContent={$t('common.download')}
onClick={() => download(row.ossId!)}
/>
);
};
const deleteBtn = () => {
if (!hasAuth('system:oss:delete')) {
return null;
}
return (
<ButtonIcon
text
type="error"
icon="material-symbols:delete-outline"
class="text-20px"
tooltipContent={$t('common.delete')}
popconfirmContent={$t('common.confirmDelete')}
onPositiveClick={() => handleDelete(row.ossId!)}
/>
);
};
return (
<div class="flex-center gap-8px">
{downloadBtn()}
{divider()}
{deleteBtn()}
</div>
);
}
}
]
});
const { handleAdd, checkedRowKeys, onBatchDeleted, onDeleted } = useTableOperate(data, 'ossId', getData);
async function handleBatchDelete() {
// request
const { error } = await fetchBatchDeleteOss(checkedRowKeys.value);
if (error) return;
onBatchDeleted();
}
async function handleDelete(ossId: CommonType.IdType) {
// request
const { error } = await fetchBatchDeleteOss([ossId]);
if (error) return;
onDeleted();
}
function download(ossId: CommonType.IdType) {
oss(ossId);
}
function handleUpload(type: 'file' | 'image') {
fileUploadType.value = type;
showFUploadModal();
}
async function getConfigKey() {
const { data: previewStr, error } = await fetchGetConfigByKey('sys.oss.previewListResource');
if (error) return;
setPreview(previewStr === 'true');
}
onMounted(() => {
getConfigKey();
});
async function handleUpdatePreview(checked: boolean) {
setPreview(!checked);
window.$dialog?.warning({
title: '提示',
content: `是否确认${checked ? '开启' : '关闭'}预览?`,
positiveText: '确认',
negativeText: '取消',
onPositiveClick: async () => {
startPreviewLoading();
const { error } = await fetchUpdateConfigByKey({
configKey: 'sys.oss.previewListResource',
configValue: String(checked)
});
if (error) {
setPreview(!checked);
endPreviewLoading();
return;
}
setPreview(checked);
window.$message?.success('更新成功');
endPreviewLoading();
},
onNegativeClick: () => {
setPreview(!checked);
}
});
}
function handleToOssConfig() {
routerPushByKey('system_oss-config');
}
</script>
<template>
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
<OssSearch v-model:model="searchParams" @search="getDataByPage" />
<NCard title="OSS 对象存储列表" :bordered="false" size="small" class="card-wrapper sm:flex-1-hidden">
<template #header-extra>
<TableHeaderOperation
v-model:columns="columnChecks"
:disabled-delete="checkedRowKeys.length === 0"
:loading="loading"
:show-add="false"
:show-delete="hasAuth('system:oss:delete')"
@add="handleAdd"
@delete="handleBatchDelete"
@refresh="getData"
>
<template #prefix>
<NSwitch
v-model:value="preview"
class="mt-1px"
:loading="previewLoading"
size="large"
:round="false"
@update:value="handleUpdatePreview"
>
<template #checked>
<span class="text-14px">禁用预览</span>
</template>
<template #unchecked>
<span class="text-14px">开启预览</span>
</template>
</NSwitch>
<NButton size="small" ghost @click="handleUpload('file')">
<template #icon>
<icon-material-symbols:upload-rounded />
</template>
上传文件
</NButton>
<NButton size="small" ghost @click="handleUpload('image')">
<template #icon>
<icon-material-symbols:image-outline />
</template>
上传图片
</NButton>
<NButton type="primary" size="small" ghost @click="handleToOssConfig">
<template #icon>
<icon-hugeicons:configuration-01 />
</template>
配置管理
</NButton>
</template>
</TableHeaderOperation>
</template>
<NDataTable
v-model:checked-row-keys="checkedRowKeys"
:columns="columns"
:data="data"
size="small"
:flex-height="!appStore.isMobile"
:scroll-x="scrollX"
:loading="loading"
remote
:row-key="row => row.ossId"
:pagination="mobilePagination"
class="sm:h-full"
/>
<OssUploadModal v-model:visible="uploadVisible" :upload-type="fileUploadType" @close="getDataByPage" />
</NCard>
</div>
</template>
<style scoped>
.n-switch {
--n-rail-height: 27px !important;
}
</style>
+102
View File
@@ -0,0 +1,102 @@
<script setup lang="ts">
import { ref, toRaw } from 'vue';
import { jsonClone } from '@sa/utils';
import { useNaiveForm } from '@/hooks/common/form';
import { $t } from '@/locales';
defineOptions({
name: 'OssSearch'
});
interface Emits {
(e: 'search'): void;
}
const emit = defineEmits<Emits>();
const { formRef, validate, restoreValidation } = useNaiveForm();
const dateRangeCreateTime = ref<[string, string] | null>(null);
const model = defineModel<Api.System.OssSearchParams>('model', { required: true });
const defaultModel = jsonClone(toRaw(model.value));
function onDateRangeCreateTimeUpdate(value: [string, string] | null) {
const params = model.value.params!;
if (value && value.length === 2) {
[params.beginTime, params.endTime] = value;
} else {
params.beginTime = undefined;
params.endTime = undefined;
}
}
function resetModel() {
dateRangeCreateTime.value = null;
Object.assign(model.value, defaultModel);
}
async function reset() {
await restoreValidation();
resetModel();
emit('search');
}
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="100">
<NGrid responsive="screen" item-responsive>
<NFormItemGi span="24 s:12 m:6" label="文件名" path="fileName" class="pr-24px">
<NInput v-model:value="model.fileName" placeholder="请输入文件名" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="原名" path="originalName" class="pr-24px">
<NInput v-model:value="model.originalName" placeholder="请输入原名" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="文件后缀名" path="fileSuffix" class="pr-24px">
<NInput v-model:value="model.fileSuffix" placeholder="请输入文件后缀名" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:6" label="服务商" path="service" class="pr-24px">
<NInput v-model:value="model.service" placeholder="请输入服务商" />
</NFormItemGi>
<NFormItemGi span="24 s:12 m:12" label="创建时间" path="createTime" class="pr-24px">
<NDatePicker
v-model:formatted-value="dateRangeCreateTime"
type="datetimerange"
value-format="yyyy-MM-dd HH:mm:ss"
clearable
@update:formatted-value="onDateRangeCreateTimeUpdate"
/>
</NFormItemGi>
<NFormItemGi span="24 s:12 m:12" 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>
@@ -0,0 +1,67 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import type { UploadFileInfo } from 'naive-ui';
import FileUpload from '@/components/custom/file-upload.vue';
import { AcceptType } from '@/enum/business';
defineOptions({
name: 'OssUploadModal'
});
interface Props {
uploadType: 'file' | 'image';
}
const props = defineProps<Props>();
interface Emits {
(e: 'close'): void;
}
const emit = defineEmits<Emits>();
const visible = defineModel<boolean>('visible', {
default: false
});
const accept = computed(() => (props.uploadType === 'file' ? AcceptType.File : AcceptType.Image));
const fileList = ref<UploadFileInfo[]>([]);
function handleUpdateModelWhenUpload() {
fileList.value = [];
}
function closeDrawer() {
visible.value = false;
}
function handleClose() {
closeDrawer();
if (fileList.value?.length > 0) {
emit('close');
}
}
watch(visible, () => {
if (visible.value) {
handleUpdateModelWhenUpload();
}
});
</script>
<template>
<NModal
v-model:show="visible"
class="max-h-520px max-w-90% w-600px"
preset="card"
:title="`上传${uploadType === 'file' ? '文件' : '图片'}`"
size="huge"
:bordered="false"
@after-leave="handleClose"
>
<FileUpload v-model:file-list="fileList" :upload-type="uploadType" :accept="accept" />
</NModal>
</template>
<style scoped></style>