refactor(system/storage): 重构存储管理页面,分页列表 => 无分页卡片

This commit is contained in:
2025-03-04 21:28:22 +08:00
parent 819fe616b7
commit 25da3019a3
13 changed files with 842 additions and 365 deletions

View File

@@ -6,8 +6,8 @@ export type * from './type'
const BASE_URL = '/system/storage'
/** @desc 查询存储列表 */
export function listStorage(query: T.StoragePageQuery) {
return http.get<PageRes<T.StorageResp[]>>(`${BASE_URL}`, query)
export function listStorage(query: T.StorageQuery) {
return http.get<T.StorageResp[]>(`${BASE_URL}/list`, query)
}
/** @desc 查询存储详情 */
@@ -29,3 +29,13 @@ export function updateStorage(data: any, id: string) {
export function deleteStorage(id: string) {
return http.del(`${BASE_URL}/${id}`)
}
/** @desc 修改存储状态 */
export function updateStorageStatus(data: any, id: string) {
return http.put(`${BASE_URL}/${id}/status`, data)
}
/** @desc 设置默认存储 */
export function setDefaultStorage(id: string) {
return http.put(`${BASE_URL}/${id}/default`)
}

View File

@@ -253,11 +253,9 @@ export interface StorageResp {
}
export interface StorageQuery {
description?: string
status?: number
type?: number
sort: Array<string>
}
export interface StoragePageQuery extends StorageQuery, PageQuery {
}
/** 终端类型 */
export interface ClientResp {

View File

@@ -142,9 +142,9 @@ const dicData: Record<string, any> = reactive({})
const getComponentBindProps = (item: ColumnItem) => {
// 组件默认配置映射表
const ConfigMap = new Map<ColumnItem['type'], Partial<ColumnItem['props'] & { placeholder: string }>>([
['input', { allowClear: true, placeholder: `请输入${item.label}`, maxLength: 255 }],
['input', { allowClear: true, placeholder: `请输入${item.label}`, maxLength: 255, showWordLimit: true }],
['input-number', { placeholder: `请输入${item.label}` }],
['textarea', { allowClear: false, placeholder: `请输入${item.label}`, maxLength: 200 }],
['textarea', { allowClear: false, placeholder: `请输入${item.label}`, maxLength: 200, showWordLimit: true, autoSize: { minRows: 3, maxRows: 5 } }],
['input-tag', { allowClear: true, placeholder: `请输入${item.label}` }],
['mention', { allowClear: true, placeholder: `请输入${item.label}` }],
['select', { allowClear: true, placeholder: `请选择${item.label}`, options: dicData[item.field] || [] }],
@@ -169,7 +169,8 @@ const valueChange = (value: any, field: string) => {
/** 表单项校验规则 */
const getFormItemRules = (item: ColumnItem) => {
if (item.required) {
return [{ required: true, message: `${item.label}为必填项` }, ...(Array.isArray(item.rules) ? item.rules : [])]
const defaultProps = getComponentBindProps(item)
return [{ required: true, message: defaultProps.placeholder || `请输入${item.label}` }, ...(Array.isArray(item.rules) ? item.rules : [])]
}
return item.rules
}

View File

@@ -213,16 +213,19 @@
// 卡片标题,标题左侧的伪类样式
.gi_card_title {
.arco-card-header-title::before {
content: '';
width: 4px;
height: 50%;
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
background-color: rgb(var(--warning-5));
border-radius: 0 4px 4px 0;
> .arco-card-header {
border-bottom: none;
.arco-card-header-title::before {
content: '';
width: 4px;
height: 50%;
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
background-color: rgb(var(--primary-5));
border-radius: 0 4px 4px 0;
}
}
}

View File

@@ -57,7 +57,12 @@ const change = (key: string | number) => {
<style scoped lang="scss">
.gi_table_page {
overflow-y: auto;
:deep(.arco-tabs) {
overflow: visible;
}
}
:deep(.arco-tabs .arco-tabs-nav-type-card-gutter .arco-tabs-tab-active) {
box-shadow: inset 0 2px 0 rgb(var(--primary-6)), inset -1px 0 0 var(--color-border-2),
inset 1px 0 0 var(--color-border-2);
@@ -77,10 +82,6 @@ const change = (key: string | number) => {
right: -20px;
}
:deep(.arco-tabs) {
overflow: visible;
}
:deep(.arco-tabs-nav) {
overflow: visible;
}

View File

@@ -1,198 +0,0 @@
<template>
<a-drawer
v-model:visible="visible"
:title="title"
:mask-closable="false"
:esc-to-close="false"
:width="width >= 500 ? 500 : '100%'"
@before-ok="save"
@close="reset"
>
<a-form ref="formRef" :model="form" :rules="rules" size="large" auto-label-width>
<a-form-item label="名称" field="name">
<a-input v-model.trim="form.name" placeholder="请输入名称" />
</a-form-item>
<a-form-item label="编码" field="code">
<a-input v-model.trim="form.code" placeholder="请输入编码" :disabled="isUpdate" />
</a-form-item>
<a-form-item label="类型" field="type">
<a-select v-model.trim="form.type" :options="storage_type_enum" placeholder="请选择类型" :disabled="isUpdate" />
</a-form-item>
<a-form-item v-if="form.type === 1" label="访问密钥" field="accessKey">
<a-input v-model.trim="form.accessKey" placeholder="请输入访问密钥" :max-length="255" />
</a-form-item>
<a-form-item v-if="form.type === 1" label="私有密钥" field="secretKey">
<a-input
v-model.trim="form.secretKey"
placeholder="请输入私有密钥"
:max-length="255"
/>
</a-form-item>
<a-form-item v-if="form.type === 1" label="终端节点" field="endpoint">
<a-input v-model.trim="form.endpoint" placeholder="请输入终端节点" />
</a-form-item>
<a-form-item label="桶名称" field="bucketName">
<a-input v-model.trim="form.bucketName" placeholder="请输入桶名称" />
</a-form-item>
<a-form-item v-if="form.type === 1" label="域名" field="domain">
<a-input v-model.trim="form.domain" placeholder="请输入域名" />
<template #extra>
<div v-if="defaultDomain">
<span>留空默认域名{{ defaultDomain }}</span>
</div>
</template>
</a-form-item>
<a-form-item
v-if="form.type === 2"
label="域名"
field="domain"
:rules="[
{
required: true,
message: '请输入域名',
},
]"
>
<a-input v-model.trim="form.domain" placeholder="请输入域名" />
</a-form-item>
<a-form-item label="排序" field="sort">
<a-input-number v-model="form.sort" placeholder="请输入排序" :min="1" mode="button" />
</a-form-item>
<a-form-item label="描述" field="description">
<a-textarea
v-model.trim="form.description"
placeholder="请输入描述"
show-word-limit
:max-length="200"
:auto-size="{ minRows: 3, maxRows: 5 }"
/>
</a-form-item>
<a-form-item label="默认存储" field="isDefault">
<a-switch
v-model="form.isDefault"
type="round"
:checked-value="true"
:unchecked-value="false"
checked-text=""
unchecked-text=""
/>
</a-form-item>
<a-form-item label="状态" field="status">
<a-switch
v-model="form.status"
type="round"
:checked-value="1"
:unchecked-value="2"
checked-text="启用"
unchecked-text="禁用"
/>
</a-form-item>
</a-form>
</a-drawer>
</template>
<script setup lang="ts">
import { type FormInstance, Message } from '@arco-design/web-vue'
import { useWindowSize } from '@vueuse/core'
import { addStorage, getStorage, updateStorage } from '@/apis/system/storage'
import { useResetReactive } from '@/hooks'
import { useDict } from '@/hooks/app'
import { encryptByRsa } from '@/utils/encrypt'
import { isIPv4 } from '@/utils/validate'
const emit = defineEmits<{
(e: 'save-success'): void
}>()
const { width } = useWindowSize()
const dataId = ref('')
const visible = ref(false)
const isUpdate = computed(() => !!dataId.value)
const title = computed(() => (isUpdate.value ? '修改存储' : '新增存储'))
const formRef = ref<FormInstance>()
const { storage_type_enum } = useDict('storage_type_enum')
const rules: FormInstance['rules'] = {
name: [{ required: true, message: '请输入名称' }],
code: [{ required: true, message: '请输入编码' }],
type: [{ required: true, message: '请选择类型' }],
accessKey: [{ required: true, message: '请输入访问密钥' }],
secretKey: [{ required: true, message: '请输入私有密钥' }],
endpoint: [{ required: true, message: '请输入终端节点' }],
bucketName: [{ required: true, message: '请输入桶名称' }],
}
const [form, resetForm] = useResetReactive({
type: 2,
isDefault: false,
sort: 999,
status: 1,
})
/** 获取url的protocol和endpoint */
const stripProtocol = (url: string): { endpoint: string, protocol: string } => {
if (url.startsWith('http://')) {
return { endpoint: url.substring(7), protocol: 'http://' }
} else if (url.startsWith('https://')) {
return { endpoint: url.substring(8), protocol: 'https://' }
}
return { endpoint: url, protocol: 'http://' }
}
/** 按规则拼接当前默认domain */
const defaultDomain = computed(() => {
const { endpoint: initialEndpoint, bucketName, domain, type } = form
if (domain || type !== 1 || !initialEndpoint || !bucketName) {
return
}
const { endpoint, protocol } = stripProtocol(initialEndpoint)
return isIPv4(endpoint) ? `${protocol}${endpoint}/${bucketName}/` : `${protocol}${bucketName}.${endpoint}/`
})
// 重置
const reset = () => {
formRef.value?.resetFields()
resetForm()
}
// 保存
const save = async () => {
try {
const isInvalid = await formRef.value?.validate()
if (isInvalid) return false
const data = {
...form,
secretKey: form.type === 1 && !form.secretKey.includes('*') ? encryptByRsa(form.secretKey) : null,
domain: form.domain || defaultDomain.value,
}
if (isUpdate.value) {
await updateStorage(data, dataId.value)
Message.success('修改成功')
} else {
await addStorage(data)
Message.success('新增成功')
}
emit('save-success')
return true
} catch (error) {
return false
}
}
// 新增
const onAdd = () => {
reset()
dataId.value = ''
visible.value = true
}
// 修改
const onUpdate = async (id: string) => {
reset()
dataId.value = id
const { data } = await getStorage(id)
Object.assign(form, data)
visible.value = true
}
defineExpose({ onAdd, onUpdate })
</script>

View File

@@ -0,0 +1,208 @@
<template>
<a-modal
v-model:visible="visible"
:title="title"
:mask-closable="false"
:esc-to-close="false"
:width="width >= 500 ? 500 : '100%'"
@before-ok="save"
@close="reset"
>
<GiForm ref="formRef" v-model="form" :columns="columns" />
</a-modal>
</template>
<script setup lang="ts">
import { Message } from '@arco-design/web-vue'
import { useWindowSize } from '@vueuse/core'
import { addStorage, getStorage, updateStorage } from '@/apis/system/storage'
import { type ColumnItem, GiForm } from '@/components/GiForm'
import { useResetReactive } from '@/hooks'
import { useDict } from '@/hooks/app'
import { encryptByRsa } from '@/utils/encrypt'
const emit = defineEmits<{
(e: 'save-success'): void
}>()
const { width } = useWindowSize()
const dataId = ref('')
const visible = ref(false)
const isUpdate = computed(() => !!dataId.value)
const storageType = ref('')
const title = computed(() => (isUpdate.value ? `修改${storageType.value}` : `新增${storageType.value}`))
const formRef = ref<InstanceType<typeof GiForm>>()
const { storage_type_enum } = useDict('storage_type_enum')
const [form, resetForm] = useResetReactive({
type: 2,
isDefault: false,
sort: 999,
status: 2,
})
const columns: ColumnItem[] = reactive([
{
label: '名称',
field: 'name',
type: 'input',
span: 24,
props: {
maxLength: 100,
},
required: true,
},
{
label: '编码',
field: 'code',
type: 'input',
span: 24,
props: {
maxLength: 30,
},
required: true,
disabled: () => isUpdate.value,
},
{
label: 'Access Key',
field: 'accessKey',
type: 'input',
span: 24,
required: true,
show: () => form.type === 2,
},
{
label: 'Secret Key',
field: 'secretKey',
type: 'input',
span: 24,
required: true,
show: () => form.type === 2,
},
{
label: 'Endpoint',
field: 'endpoint',
type: 'input',
span: 24,
required: true,
show: () => form.type === 2,
},
{
label: 'Bucket',
field: 'bucketName',
type: 'input',
span: 24,
required: true,
show: () => form.type === 2,
},
{
label: '域名',
field: 'domain',
type: 'input',
span: 24,
required: true,
show: () => form.type === 2,
},
{
label: '存储路径',
field: 'bucketName',
type: 'input',
span: 24,
required: true,
show: () => form.type === 1,
},
{
label: '访问路径',
field: 'domain',
type: 'input',
span: 24,
required: true,
show: () => form.type === 1,
},
{
label: '排序',
field: 'sort',
type: 'input-number',
span: 24,
props: {
min: 1,
mode: 'button',
},
},
{
label: '描述',
field: 'description',
type: 'textarea',
span: 24,
props: {
maxLength: 200,
},
},
{
label: '状态',
field: 'status',
type: 'switch',
span: 24,
props: {
type: 'round',
checkedValue: 1,
uncheckedValue: 2,
checkedText: '启用',
uncheckedText: '禁用',
},
},
])
// 重置
const reset = () => {
formRef.value?.formRef?.resetFields()
resetForm()
}
// 保存
const save = async () => {
try {
const isInvalid = await formRef.value?.formRef?.validate()
if (isInvalid) return false
if (isUpdate.value) {
await updateStorage({
...form,
secretKey: form.type === 2 && !form.secretKey.includes('*') ? encryptByRsa(form.secretKey) || '' : null,
}, dataId.value)
Message.success('修改成功')
} else {
await addStorage({
...form,
secretKey: form.type === 2 ? encryptByRsa(form.secretKey) || '' : form.secretKey,
})
Message.success('新增成功')
}
emit('save-success')
return true
} catch (error) {
return false
}
}
// 新增
const onAdd = (type: number) => {
reset()
dataId.value = ''
form.type = type
storageType.value = storage_type_enum.value.find((item) => item.value === type)?.label || '本地存储'
visible.value = true
}
// 修改
const onUpdate = async (id: string) => {
reset()
dataId.value = id
const { data } = await getStorage(id)
Object.assign(form, data)
storageType.value = storage_type_enum.value.find((item) => item.value === form.type)?.label || '本地存储'
visible.value = true
}
defineExpose({ onAdd, onUpdate })
</script>

View File

@@ -0,0 +1,78 @@
<template>
<div class="list-wrap">
<a-row class="list-row" :gutter="24">
<a-col
v-if="has.hasPermOr(['system:storage:add'])"
:xs="24"
:sm="24"
:md="12"
:lg="8"
:xl="8"
:xxl="6"
class="list-col"
style="min-height: 162px"
>
<CardAdd :type="1" @save-success="search" />
</a-col>
<a-empty v-if="!data.length && !has.hasPermOr(['system:storage:add'])" />
<a-col
v-for="item in data"
:key="item.id"
:xs="24"
:sm="24"
:md="12"
:lg="8"
:xl="8"
:xxl="6"
class="list-col"
style="min-height: 162px"
>
<CardBlock
:loading="loading"
:data="item"
@save-success="search"
>
<template #content>
<a-skeleton v-if="loading" :animation="true">
<a-skeleton-line :widths="['60%']" :rows="2" />
</a-skeleton>
<a-descriptions v-else :column="1">
<a-descriptions-item label="存储路径">{{ item.bucketName }}</a-descriptions-item>
<a-descriptions-item label="访问路径">{{ item.domain }}</a-descriptions-item>
</a-descriptions>
</template>
</CardBlock>
</a-col>
</a-row>
</div>
</template>
<script setup lang="ts">
import CardAdd from './components/CardAdd.vue'
import CardBlock from './components/CardBlock.vue'
import type { StorageResp } from '@/apis'
import has from '@/utils/has'
defineProps({
loading: {
type: Boolean,
default: false,
},
data: {
type: Array<StorageResp>,
default: () => [],
},
})
const emit = defineEmits<{
(e: 'save-success'): void
}>()
const search = () => {
emit('save-success')
}
</script>
<style scoped lang="scss">
</style>

View File

@@ -0,0 +1,80 @@
<template>
<div class="list-wrap">
<a-row class="list-row" :gutter="24">
<a-col
v-if="has.hasPermOr(['system:storage:add'])"
:xs="24"
:sm="24"
:md="12"
:lg="8"
:xl="8"
:xxl="6"
class="list-col"
style="min-height: 162px"
>
<CardAdd :type="2" @save-success="search" />
</a-col>
<a-empty v-if="!data.length && !has.hasPermOr(['system:storage:add'])" />
<a-col
v-for="item in data"
:key="item.id"
:xs="24"
:sm="24"
:md="12"
:lg="8"
:xl="8"
:xxl="6"
class="list-col"
style="min-height: 162px"
>
<CardBlock
:loading="loading"
:data="item"
@save-success="search"
>
<template #content>
<a-skeleton v-if="loading" :animation="true">
<a-skeleton-line :widths="['60%']" :rows="3" />
</a-skeleton>
<a-descriptions v-else :column="1">
<a-descriptions-item label="Access Key"><CellCopy :content="item.accessKey" /></a-descriptions-item>
<a-descriptions-item label="Endpoint">{{ item.endpoint }}</a-descriptions-item>
<a-descriptions-item label="Bucket">{{ item.bucketName }}</a-descriptions-item>
<a-descriptions-item label="自定义域名">{{ item.domain }}</a-descriptions-item>
</a-descriptions>
</template>
</CardBlock>
</a-col>
</a-row>
</div>
</template>
<script setup lang="ts">
import CardAdd from './components/CardAdd.vue'
import CardBlock from './components/CardBlock.vue'
import type { StorageResp } from '@/apis'
import has from '@/utils/has'
defineProps({
loading: {
type: Boolean,
default: false,
},
data: {
type: Array<StorageResp>,
default: () => [],
},
})
const emit = defineEmits<{
(e: 'save-success'): void
}>()
const search = () => {
emit('save-success')
}
</script>
<style scoped lang="scss">
</style>

View File

@@ -0,0 +1,92 @@
<template>
<a-card
:bordered="true"
size="small"
class="card-block add-card"
:class="{ 'card-large': type === 2 }"
@click="onAdd"
>
<div class="content">
<div class="add-icon">
<icon-plus />
</div>
<div class="description">点击创建{{ type === 1 ? '本地存储' : '对象存储' }}</div>
</div>
</a-card>
<StorageAddModal ref="StorageAddModalRef" @save-success="search" />
</template>
<script lang="ts" setup>
import StorageAddModal from '../StorageAddModal.vue'
const props = defineProps({
type: {
type: Number,
default: 1,
},
})
const emit = defineEmits<{
(e: 'save-success'): void
}>()
const search = () => {
emit('save-success')
}
const StorageAddModalRef = ref<InstanceType<typeof StorageAddModal>>()
// 新增
const onAdd = () => {
StorageAddModalRef.value?.onAdd(props.type)
}
</script>
<style scoped lang="less">
.card-block {
margin-bottom: 16px;
:deep(.arco-card-header) {
border-bottom: none;
height: auto;
padding: 16px;
padding-bottom: 0;
}
:deep(.arco-descriptions-item-value) {
color: var(--color-text-2);
padding-left: 6px;
}
.content {
height: 48px;
}
}
.add-card {
text-align: center;
cursor: pointer;
.add-icon {
font-size: 22px;
}
.description {
margin-top: 16px;
color: var(--color-text-3);
font-weight: 400;
}
:deep(.arco-card-body) {
padding-top: 45px;
padding-bottom: 63px;
}
}
.card-large {
:deep(.arco-card-body) {
padding-top: 65px;
padding-bottom: 73px;
}
}
</style>

View File

@@ -0,0 +1,283 @@
<template>
<a-card
:bordered="true"
size="small"
class="card-block"
>
<template v-if="loading" #title>
<a-skeleton :animation="true" class="card-block-skeleton">
<a-skeleton-line :widths="['40%']" :rows="1" />
</a-skeleton>
</template>
<template v-else #title>
<div class="title">
{{ data.name }} ({{ data.code }})
<div v-if="data.isDefault" class="status">
<a-tag size="small" color="arcoblue">
<template #icon>
<icon-check-circle-fill />
</template>
<span>默认存储</span>
</a-tag>
</div>
<div v-if="has.hasPermOr(['system:storage:setDefault', 'system:storage:update', 'system:storage:delete'])" class="more">
<a-dropdown>
<icon-more />
<template #content>
<a-doption
v-permission="['system:storage:setDefault']"
:disabled="data.isDefault"
:title="data.isDefault ? '该存储已设为默认存储' : ''"
@click="onSetDefault(data)"
>
<icon-check-circle />
设为默认
</a-doption>
<a-doption v-permission="['system:storage:update']" @click="onUpdate(data)">
<icon-edit />
修改
</a-doption>
<a-doption
v-permission="['system:storage:delete']"
class="danger"
:disabled="data.isDefault"
:title="data.isDefault ? '不允许删除默认存储' : ''"
@click="onDelete(data)"
>
<icon-delete />
删除
</a-doption>
</template>
</a-dropdown>
</div>
</div>
<div class="time">{{ data.createTime }}</div>
</template>
<div :class="data.type === 1 ? 'content' : 'content-large'">
<slot name="content"></slot>
</div>
<div class="extra">
<a-skeleton v-if="loading" :animation="true">
<a-skeleton-line :rows="2" />
</a-skeleton>
<a-switch
v-else
v-model="status"
:disabled="!has.hasPermOr(['system:storage:updateStatus']) || data.isDefault"
:title="data.isDefault ? '不允许禁用默认存储' : ''"
:loading="switchLoading"
:checked-value="1"
:unchecked-value="2"
:before-change="onUpdateStatus"
/>
</div>
</a-card>
<StorageAddModal ref="StorageAddModalRef" @save-success="search" />
</template>
<script lang="ts" setup>
import { Message, Modal } from '@arco-design/web-vue'
import StorageAddModal from '../StorageAddModal.vue'
import has from '@/utils/has'
import { type StorageResp, deleteStorage, setDefaultStorage, updateStorageStatus } from '@/apis/system'
import { useDict } from '@/hooks/app'
interface Props {
loading: boolean
data: StorageResp
}
const props = withDefaults(defineProps<Props>(), {})
const emit = defineEmits<{
(e: 'save-success'): void
}>()
const search = () => {
emit('save-success')
}
const { storage_type_enum } = useDict('storage_type_enum')
const storageType = computed(() => {
return storage_type_enum.value.find((item) => item.value === props.data.type)?.label || '本地存储'
})
const status = ref(props.data.status)
const switchLoading = ref(false)
// 更新状态
const onUpdateStatus = async (newValue: number) => {
const tip = newValue === 1 ? '启用' : '禁用'
switchLoading.value = true
Modal.warning({
title: '提示',
content: `是否确定${tip}${storageType.value}${props.data.name}(${props.data.code})]`,
hideCancel: false,
maskClosable: false,
onCancel: async () => {
switchLoading.value = false
status.value = newValue === 1 ? 2 : 1
},
onBeforeOk: async () => {
try {
const res = await updateStorageStatus({
status: newValue,
}, props.data.id)
if (res.success) {
Message.success(`${tip}成功`)
search()
}
return res.success
} catch (error) {
status.value = newValue === 1 ? 2 : 1
return false
} finally {
switchLoading.value = false
}
},
})
}
// 设为默认
const onSetDefault = (record: StorageResp) => {
Modal.warning({
title: '提示',
content: `是否确定将${storageType.value}${record.name}(${record.code})」设为默认存储?`,
hideCancel: false,
maskClosable: false,
onBeforeOk: async () => {
try {
const res = await setDefaultStorage(record.id)
if (res.success) {
Message.success('设置成功')
search()
}
return res.success
} catch (error) {
return false
}
},
})
}
// 删除
const onDelete = (record: StorageResp) => {
Modal.warning({
title: '提示',
content: `是否确定删除存储「${record.name}(${record.code})」?`,
okButtonProps: { status: 'danger' },
hideCancel: false,
maskClosable: false,
onBeforeOk: async () => {
try {
const res = await deleteStorage(record.id)
if (res.success) {
Message.success('删除成功')
search()
}
return res.success
} catch (error) {
return false
}
},
})
}
const StorageAddModalRef = ref<InstanceType<typeof StorageAddModal>>()
// 修改
const onUpdate = (record: StorageResp) => {
StorageAddModalRef.value?.onUpdate(record.id)
}
</script>
<style scoped lang="less">
.card-block {
margin-bottom: 16px;
:deep(.arco-card-header) {
border-bottom: none;
height: auto;
padding: 16px;
padding-bottom: 0;
}
.title {
display: flex;
line-height: 24px;
align-items: center;
font-size: 14px;
font-weight: 500;
.icon {
height: 24px;
width: 24px;
color: var(--color-white);
background: #626aea;
text-align: center;
line-height: 24px;
border-radius: 50%;
margin-right: 8px;
}
.status {
margin-left: 12px;
}
.more {
color: var(--color-text-4);
font-size: 16px;
position: absolute;
right: 16px;
cursor: pointer;
opacity: 0;
}
}
.time,
.content > :deep(.arco-typography),
:deep(.arco-descriptions-item-label),
:deep(.arco-descriptions-item-value) {
font-size: 12px;
font-weight: 400;
color: var(--color-text-3);
padding: 0;
line-height: 20px;
}
:deep(.arco-descriptions-item-value) {
color: var(--color-text-2);
padding-left: 6px;
}
.content {
height: 48px;
}
.content-large {
height: 78px;
}
.extra {
display: flex;
flex-direction: row-reverse;
}
&-skeleton {
:deep(.arco-skeleton-content .arco-skeleton-text-row:not(:last-child)) {
height: 14px;
margin-bottom: 8px;
}
}
}
.card-block:hover {
box-shadow: 4px 4px 10px rgba(0, 0, 0, 10%);
.title {
.more {
opacity: 1;
}
}
}
</style>

View File

@@ -1,153 +1,88 @@
<template>
<div class="gi_table_page">
<GiTable
title=""
row-key="id"
:data="dataList"
:columns="columns"
:loading="loading"
:scroll="{ x: '100%', y: '100%', minWidth: 1300 }"
:pagination="pagination"
:disabled-tools="['size']"
:disabled-column-keys="['name']"
@refresh="search"
>
<template #toolbar-left>
<a-input-search v-model="queryForm.description" placeholder="搜索名称/编码/描述" allow-clear @search="search" />
<a-select
v-model="queryForm.status"
:options="DisEnableStatusList"
placeholder="请选择状态"
<a-tabs v-model:active-key="activeKey" type="rounded" @change="change">
<a-tab-pane key="all">
<template #title>全部</template>
<a-card title="本地存储" :bordered="false" class="gi_card_title">
<StorageLocal :data="dataMap['1']" :loading="loading" @save-success="getDataList" />
</a-card>
<a-card title="对象存储" :bordered="false" class="gi_card_title">
<StorageOss :data="dataMap['2']" :loading="loading" @save-success="getDataList" />
</a-card>
</a-tab-pane>
<a-tab-pane key="1">
<template #title>本地存储</template>
<StorageLocal :data="dataMap['1']" :loading="loading" @save-success="getDataList" />
</a-tab-pane>
<a-tab-pane key="2">
<template #title>对象存储</template>
<StorageOss :data="dataMap['2']" :loading="loading" @save-success="getDataList" />
</a-tab-pane>
<template #extra>
<a-input-search
v-model="queryForm.description"
placeholder="搜索名称/编码"
style="width: 240px;"
allow-clear
style="width: 150px"
@change="search"
@search="getDataList"
/>
<a-button @click="reset">
<template #icon><icon-refresh /></template>
<template #default>重置</template>
</a-button>
</template>
<template #toolbar-right>
<a-button v-permission="['system:storage:add']" type="primary" @click="onAdd">
<template #icon><icon-plus /></template>
<template #default>新增</template>
</a-button>
</template>
<template #name="{ record }">
<a-space fill>
<span>{{ record.name }}</span>
<a-tag v-if="record.isDefault" color="arcoblue" size="small" class="gi_round">
<template #default>默认</template>
</a-tag>
</a-space>
</template>
<template #type="{ record }">
<GiCellTag :value="record.type" :dict="storage_type_enum" />
</template>
<template #status="{ record }">
<GiCellStatus :status="record.status" />
</template>
<template #action="{ record }">
<a-space>
<a-link v-permission="['system:storage:update']" title="修改" @click="onUpdate(record)">修改</a-link>
<a-link
v-permission="['system:storage:delete']"
status="danger"
:disabled="record.isDefault"
:title="record.isDefault ? '默认存储不能删除' : '删除'"
@click="onDelete(record)"
>
删除
</a-link>
</a-space>
</template>
</GiTable>
<StorageAddDrawer ref="StorageAddDrawerRef" @save-success="search" />
</a-tabs>
</div>
</template>
<script setup lang="ts">
import StorageAddDrawer from './StorageAddDrawer.vue'
import { type StorageQuery, type StorageResp, deleteStorage, listStorage } from '@/apis/system/storage'
import type { TableInstanceColumns } from '@/components/GiTable/type'
import { DisEnableStatusList } from '@/constant/common'
import { useTable } from '@/hooks'
import { useDict } from '@/hooks/app'
import { isMobile } from '@/utils'
import has from '@/utils/has'
import { groupBy } from 'xe-utils'
import StorageLocal from './StorageLocal.vue'
import StorageOss from './StorageOss.vue'
import { type StorageQuery, type StorageResp, listStorage } from '@/apis'
defineOptions({ name: 'SystemStorage' })
const { storage_type_enum } = useDict('storage_type_enum')
const queryForm = reactive<StorageQuery>({
sort: ['createTime,desc'],
})
const {
tableData: dataList,
loading,
pagination,
search,
handleDelete,
} = useTable((page) => listStorage({ ...queryForm, ...page }), { immediate: true })
const columns: TableInstanceColumns[] = [
{
title: '序号',
width: 66,
align: 'center',
render: ({ rowIndex }) => h('span', {}, rowIndex + 1 + (pagination.current - 1) * pagination.pageSize),
},
{ title: '名称', dataIndex: 'name', slotName: 'name', width: 140, ellipsis: true, tooltip: true },
{ title: '编码', dataIndex: 'code', ellipsis: true, tooltip: true },
{ title: '状态', dataIndex: 'status', slotName: 'status', align: 'center' },
{ title: '类型', dataIndex: 'type', slotName: 'type', align: 'center', ellipsis: true, tooltip: true },
{ title: '访问密钥', dataIndex: 'accessKey', ellipsis: true, tooltip: true },
{ title: '终端节点', dataIndex: 'endpoint', ellipsis: true, tooltip: true },
{ title: '桶名称', dataIndex: 'bucketName', ellipsis: true, tooltip: true },
{ title: '域名', dataIndex: 'domain', ellipsis: true, tooltip: true },
{ title: '描述', dataIndex: 'description', ellipsis: true, tooltip: true },
{ title: '创建人', dataIndex: 'createUserString', ellipsis: true, tooltip: true, show: false },
{ title: '创建时间', dataIndex: 'createTime', width: 180 },
{ title: '修改人', dataIndex: 'updateUserString', ellipsis: true, tooltip: true, show: false },
{ title: '修改时间', dataIndex: 'updateTime', width: 180, show: false },
{
title: '操作',
dataIndex: 'action',
slotName: 'action',
width: 130,
align: 'center',
fixed: !isMobile() ? 'right' : undefined,
show: has.hasPermOr(['system:storage:update', 'system:storage:delete']),
},
]
// 重置
const reset = () => {
queryForm.description = undefined
queryForm.status = undefined
search()
const loading = ref(false)
const dataMap = ref<Record<string, StorageResp[]>>({})
// 查询列表
const getDataList = async () => {
try {
loading.value = true
const { data } = await listStorage(queryForm)
dataMap.value = groupBy(data, 'type')
} finally {
loading.value = false
}
}
// 删除
const onDelete = (record: StorageResp) => {
return handleDelete(() => deleteStorage(record.id), {
content: `是否确定删除存储「${record.name}」?`,
showModal: true,
})
const activeKey = ref('all')
const change = (key: string | number) => {
activeKey.value = key as string
queryForm.type = key === 'all' ? undefined : key
getDataList()
}
const StorageAddDrawerRef = ref<InstanceType<typeof StorageAddDrawer>>()
// 新增
const onAdd = () => {
StorageAddDrawerRef.value?.onAdd()
}
// 修改
const onUpdate = (record: StorageResp) => {
StorageAddDrawerRef.value?.onUpdate(record.id)
}
onMounted(() => {
getDataList()
})
</script>
<style scoped lang="scss"></style>
<style scoped lang="scss">
.gi_table_page {
overflow-y: auto;
:deep(.arco-tabs) {
overflow: visible;
}
}
.block-title {
font-size: 14px;
margin-bottom: 12px;
}
:deep(.gi_card_title > .arco-card-body) {
padding: 0;
}
</style>

View File

@@ -1,14 +0,0 @@
export interface StorageReq {
name: string
code: string
type: number
accessKey: string
secretKey: string
endpoint: string
bucketName: string
domain: string
sort: number
description: string
isDefault: boolean
status: 1 | 2
}