feat:【infra】【system】新增文件与字典管理前端页面及相关 API 封装
This commit is contained in:
67
src/api/infra/file-config/index.ts
Normal file
67
src/api/infra/file-config/index.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import type { PageParam, PageResult } from '@/http/types'
|
||||
import { http } from '@/http/http'
|
||||
|
||||
/** 文件客户端配置 */
|
||||
export interface FileClientConfig {
|
||||
basePath?: string
|
||||
host?: string
|
||||
port?: number
|
||||
username?: string
|
||||
password?: string
|
||||
mode?: string
|
||||
endpoint?: string
|
||||
bucket?: string
|
||||
accessKey?: string
|
||||
accessSecret?: string
|
||||
enablePathStyleAccess?: boolean
|
||||
enablePublicAccess?: boolean
|
||||
region?: string
|
||||
domain?: string
|
||||
}
|
||||
|
||||
/** 文件配置信息 */
|
||||
export interface FileConfig {
|
||||
id?: number
|
||||
name: string
|
||||
storage?: number
|
||||
master?: boolean
|
||||
visible?: boolean
|
||||
config?: FileClientConfig
|
||||
remark?: string
|
||||
createTime?: Date
|
||||
}
|
||||
|
||||
/** 查询文件配置分页列表 */
|
||||
export function getFileConfigPage(params: PageParam) {
|
||||
return http.get<PageResult<FileConfig>>('/infra/file-config/page', params)
|
||||
}
|
||||
|
||||
/** 查询文件配置详情 */
|
||||
export function getFileConfig(id: number) {
|
||||
return http.get<FileConfig>(`/infra/file-config/get?id=${id}`)
|
||||
}
|
||||
|
||||
/** 新增文件配置 */
|
||||
export function createFileConfig(data: FileConfig) {
|
||||
return http.post<number>('/infra/file-config/create', data)
|
||||
}
|
||||
|
||||
/** 修改文件配置 */
|
||||
export function updateFileConfig(data: FileConfig) {
|
||||
return http.put<boolean>('/infra/file-config/update', data)
|
||||
}
|
||||
|
||||
/** 删除文件配置 */
|
||||
export function deleteFileConfig(id: number) {
|
||||
return http.delete<boolean>(`/infra/file-config/delete?id=${id}`)
|
||||
}
|
||||
|
||||
/** 更新文件配置为主配置 */
|
||||
export function updateFileConfigMaster(id: number) {
|
||||
return http.put<boolean>(`/infra/file-config/update-master?id=${id}`)
|
||||
}
|
||||
|
||||
/** 测试文件配置 */
|
||||
export function testFileConfig(id: number) {
|
||||
return http.get<string>(`/infra/file-config/test?id=${id}`)
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { PageParam, PageResult } from '@/http/types'
|
||||
import { http } from '@/http/http'
|
||||
|
||||
/** 字典数据 */
|
||||
@@ -18,3 +19,28 @@ export interface DictData {
|
||||
export function getSimpleDictDataList() {
|
||||
return http.get<DictData[]>('/system/dict-data/simple-list')
|
||||
}
|
||||
|
||||
/** 查询字典数据分页列表 */
|
||||
export function getDictDataPage(params: PageParam) {
|
||||
return http.get<PageResult<DictData>>('/system/dict-data/page', params)
|
||||
}
|
||||
|
||||
/** 查询字典数据详情 */
|
||||
export function getDictData(id: number) {
|
||||
return http.get<DictData>(`/system/dict-data/get?id=${id}`)
|
||||
}
|
||||
|
||||
/** 新增字典数据 */
|
||||
export function createDictData(data: DictData) {
|
||||
return http.post<number>('/system/dict-data/create', data)
|
||||
}
|
||||
|
||||
/** 修改字典数据 */
|
||||
export function updateDictData(data: DictData) {
|
||||
return http.put<boolean>('/system/dict-data/update', data)
|
||||
}
|
||||
|
||||
/** 删除字典数据 */
|
||||
export function deleteDictData(id: number) {
|
||||
return http.delete<boolean>(`/system/dict-data/delete?id=${id}`)
|
||||
}
|
||||
|
||||
42
src/api/system/dict/type/index.ts
Normal file
42
src/api/system/dict/type/index.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { PageParam, PageResult } from '@/http/types'
|
||||
import { http } from '@/http/http'
|
||||
|
||||
/** 字典类型 */
|
||||
export interface DictType {
|
||||
id?: number
|
||||
name: string
|
||||
type: string
|
||||
status: number
|
||||
remark?: string
|
||||
createTime?: Date
|
||||
}
|
||||
|
||||
/** 查询字典类型(精简)列表 */
|
||||
export function getSimpleDictTypeList() {
|
||||
return http.get<DictType[]>('/system/dict-type/list-all-simple')
|
||||
}
|
||||
|
||||
/** 查询字典类型分页列表 */
|
||||
export function getDictTypePage(params: PageParam) {
|
||||
return http.get<PageResult<DictType>>('/system/dict-type/page', params)
|
||||
}
|
||||
|
||||
/** 查询字典类型详情 */
|
||||
export function getDictType(id: number) {
|
||||
return http.get<DictType>(`/system/dict-type/get?id=${id}`)
|
||||
}
|
||||
|
||||
/** 新增字典类型 */
|
||||
export function createDictType(data: DictType) {
|
||||
return http.post<number>('/system/dict-type/create', data)
|
||||
}
|
||||
|
||||
/** 修改字典类型 */
|
||||
export function updateDictType(data: DictType) {
|
||||
return http.put<boolean>('/system/dict-type/update', data)
|
||||
}
|
||||
|
||||
/** 删除字典类型 */
|
||||
export function deleteDictType(id: number) {
|
||||
return http.delete<boolean>(`/system/dict-type/delete?id=${id}`)
|
||||
}
|
||||
211
src/pages-infra/file/components/config-list.vue
Normal file
211
src/pages-infra/file/components/config-list.vue
Normal file
@@ -0,0 +1,211 @@
|
||||
<template>
|
||||
<view>
|
||||
<!-- 搜索组件 -->
|
||||
<ConfigSearchForm @search="handleQuery" @reset="handleReset" />
|
||||
|
||||
<!-- 文件配置列表 -->
|
||||
<view class="p-24rpx">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
|
||||
@click="handleDetail(item)"
|
||||
>
|
||||
<view class="p-24rpx">
|
||||
<view class="mb-16rpx flex items-center justify-between">
|
||||
<view class="text-32rpx text-[#333] font-semibold">
|
||||
{{ item.name }}
|
||||
</view>
|
||||
<view class="flex items-center gap-8rpx">
|
||||
<view v-if="item.master" class="rounded-4rpx bg-green-500 px-8rpx py-2rpx text-24rpx text-white">
|
||||
主配置
|
||||
</view>
|
||||
<dict-tag :type="DICT_TYPE.INFRA_FILE_STORAGE" :value="item.storage" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">配置编号:</text>
|
||||
<text>{{ item.id }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">备注:</text>
|
||||
<text class="min-w-0 flex-1 truncate">{{ item.remark || '-' }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">创建时间:</text>
|
||||
<text>{{ formatDateTime(item.createTime) || '-' }}</text>
|
||||
</view>
|
||||
<!-- 操作按钮 -->
|
||||
<view class="mt-16rpx flex justify-end gap-16rpx">
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['infra:file-config:update'])"
|
||||
size="small" type="info" @click.stop="handleTest(item)"
|
||||
>
|
||||
测试
|
||||
</wd-button>
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['infra:file-config:update']) && !item.master"
|
||||
size="small" type="warning" @click.stop="handleMaster(item)"
|
||||
>
|
||||
设为主配置
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="loadMoreState !== 'loading' && list.length === 0" class="py-100rpx text-center">
|
||||
<wd-status-tip image="content" tip="暂无文件配置数据" />
|
||||
</view>
|
||||
<wd-loadmore
|
||||
v-if="list.length > 0"
|
||||
:state="loadMoreState"
|
||||
@reload="loadMore"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 新增按钮 -->
|
||||
<wd-fab
|
||||
v-if="hasAccessByCodes(['infra:file-config:create'])"
|
||||
position="right-bottom"
|
||||
type="primary"
|
||||
:expandable="false"
|
||||
@click="handleAdd"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FileConfig } from '@/api/infra/file-config'
|
||||
import type { LoadMoreState } from '@/http/types'
|
||||
import { ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { getFileConfigPage, testFileConfig, updateFileConfigMaster } from '@/api/infra/file-config'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import ConfigSearchForm from './config-search-form.vue'
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const toast = useToast()
|
||||
const total = ref(0)
|
||||
const list = ref<FileConfig[]>([])
|
||||
const loadMoreState = ref<LoadMoreState>('loading')
|
||||
const queryParams = ref({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
|
||||
/** 查询列表 */
|
||||
async function getList() {
|
||||
loadMoreState.value = 'loading'
|
||||
try {
|
||||
const data = await getFileConfigPage(queryParams.value)
|
||||
list.value = [...list.value, ...data.list]
|
||||
total.value = data.total
|
||||
loadMoreState.value = list.value.length >= total.value ? 'finished' : 'loading'
|
||||
} catch {
|
||||
queryParams.value.pageNo = queryParams.value.pageNo > 1 ? queryParams.value.pageNo - 1 : 1
|
||||
loadMoreState.value = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery(data?: Record<string, any>) {
|
||||
queryParams.value = {
|
||||
...data,
|
||||
pageNo: 1,
|
||||
pageSize: queryParams.value.pageSize,
|
||||
}
|
||||
list.value = []
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function handleReset() {
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 加载更多 */
|
||||
function loadMore() {
|
||||
if (loadMoreState.value === 'finished') {
|
||||
return
|
||||
}
|
||||
queryParams.value.pageNo++
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 新增 */
|
||||
function handleAdd() {
|
||||
uni.navigateTo({
|
||||
url: '/pages-infra/file/config/form/index',
|
||||
})
|
||||
}
|
||||
|
||||
/** 查看详情 */
|
||||
function handleDetail(item: FileConfig) {
|
||||
uni.navigateTo({
|
||||
url: `/pages-infra/file/config/detail/index?id=${item.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 测试文件配置 */
|
||||
async function handleTest(item: FileConfig) {
|
||||
try {
|
||||
toast.loading('测试上传中...')
|
||||
const url = await testFileConfig(item.id!)
|
||||
toast.close()
|
||||
uni.showModal({
|
||||
title: '测试上传成功',
|
||||
content: '是否要访问该文件?',
|
||||
confirmText: '访问',
|
||||
cancelText: '取消',
|
||||
success: (res) => {
|
||||
if (res.confirm && url) {
|
||||
// 复制链接到剪贴板
|
||||
uni.setClipboardData({
|
||||
data: url,
|
||||
success: () => {
|
||||
toast.success('链接已复制,请在浏览器中打开')
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
} catch {
|
||||
toast.show('测试失败')
|
||||
}
|
||||
}
|
||||
|
||||
/** 设为主配置 */
|
||||
function handleMaster(item: FileConfig) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: `是否要将"${item.name}"设为主配置?`,
|
||||
success: async (res) => {
|
||||
if (!res.confirm) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
toast.loading('设置中...')
|
||||
await updateFileConfigMaster(item.id!)
|
||||
toast.success('设置成功')
|
||||
// 刷新列表
|
||||
handleQuery()
|
||||
} catch {
|
||||
toast.show('设置失败')
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 触底加载更多 */
|
||||
onReachBottom(() => {
|
||||
loadMore()
|
||||
})
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
153
src/pages-infra/file/components/config-search-form.vue
Normal file
153
src/pages-infra/file/components/config-search-form.vue
Normal file
@@ -0,0 +1,153 @@
|
||||
<template>
|
||||
<!-- 搜索框入口 -->
|
||||
<view @click="visible = true">
|
||||
<wd-search :placeholder="placeholder" hide-cancel disabled />
|
||||
</view>
|
||||
|
||||
<!-- 搜索弹窗 -->
|
||||
<wd-popup v-model="visible" position="top" @close="visible = false">
|
||||
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
配置名
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.name"
|
||||
placeholder="请输入配置名"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
存储器
|
||||
</view>
|
||||
<wd-radio-group v-model="formData.storage" shape="button">
|
||||
<wd-radio :value="-1">
|
||||
全部
|
||||
</wd-radio>
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.INFRA_FILE_STORAGE)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
创建时间
|
||||
</view>
|
||||
<view class="yd-search-form-date-range-container">
|
||||
<view class="flex-1" @click="visibleCreateTime[0] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.createTime?.[0]) || '开始日期' }}
|
||||
</view>
|
||||
</view>
|
||||
-
|
||||
<view class="flex-1" @click="visibleCreateTime[1] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.createTime?.[1]) || '结束日期' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleCreateTime[0]" v-model="tempCreateTime[0]" type="date" />
|
||||
<view v-if="visibleCreateTime[0]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleCreateTime[0] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleCreateTime0Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleCreateTime[1]" v-model="tempCreateTime[1]" type="date" />
|
||||
<view v-if="visibleCreateTime[1]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleCreateTime[1] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleCreateTime1Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="yd-search-form-actions">
|
||||
<wd-button class="flex-1" plain @click="handleReset">
|
||||
重置
|
||||
</wd-button>
|
||||
<wd-button class="flex-1" type="primary" @click="handleSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { getDictLabel, getIntDictOptions } from '@/hooks/useDict'
|
||||
import { getNavbarHeight } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDate, formatDateRange } from '@/utils/date'
|
||||
|
||||
const emit = defineEmits<{
|
||||
search: [data: Record<string, any>]
|
||||
reset: []
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const formData = reactive({
|
||||
name: undefined as string | undefined,
|
||||
storage: -1,
|
||||
createTime: [undefined, undefined] as [number | undefined, number | undefined],
|
||||
})
|
||||
|
||||
/** 搜索条件 placeholder 拼接 */
|
||||
const placeholder = computed(() => {
|
||||
const conditions: string[] = []
|
||||
if (formData.name) {
|
||||
conditions.push(`配置名:${formData.name}`)
|
||||
}
|
||||
if (formData.storage !== -1) {
|
||||
conditions.push(`存储器:${getDictLabel(DICT_TYPE.INFRA_FILE_STORAGE, formData.storage)}`)
|
||||
}
|
||||
if (formData.createTime?.[0] && formData.createTime?.[1]) {
|
||||
conditions.push(`时间:${formatDate(formData.createTime[0])}~${formatDate(formData.createTime[1])}`)
|
||||
}
|
||||
return conditions.length > 0 ? conditions.join(' | ') : '搜索文件配置'
|
||||
})
|
||||
|
||||
// 时间范围选择器状态
|
||||
const visibleCreateTime = ref<[boolean, boolean]>([false, false])
|
||||
const tempCreateTime = ref<[number, number]>([Date.now(), Date.now()])
|
||||
|
||||
/** 创建时间[0]确认 */
|
||||
function handleCreateTime0Confirm() {
|
||||
formData.createTime = [tempCreateTime.value[0], formData.createTime?.[1]]
|
||||
visibleCreateTime.value[0] = false
|
||||
}
|
||||
|
||||
/** 创建时间[1]确认 */
|
||||
function handleCreateTime1Confirm() {
|
||||
formData.createTime = [formData.createTime?.[0], tempCreateTime.value[1]]
|
||||
visibleCreateTime.value[1] = false
|
||||
}
|
||||
|
||||
/** 搜索 */
|
||||
function handleSearch() {
|
||||
visible.value = false
|
||||
emit('search', {
|
||||
name: formData.name || undefined,
|
||||
storage: formData.storage === -1 ? undefined : formData.storage,
|
||||
createTime: formatDateRange(formData.createTime),
|
||||
})
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
function handleReset() {
|
||||
formData.name = undefined
|
||||
formData.storage = -1
|
||||
formData.createTime = [undefined, undefined]
|
||||
visible.value = false
|
||||
emit('reset')
|
||||
}
|
||||
</script>
|
||||
240
src/pages-infra/file/components/file-list.vue
Normal file
240
src/pages-infra/file/components/file-list.vue
Normal file
@@ -0,0 +1,240 @@
|
||||
<template>
|
||||
<view>
|
||||
<!-- 搜索组件 -->
|
||||
<FileSearchForm @search="handleQuery" @reset="handleReset" />
|
||||
|
||||
<!-- 文件列表 -->
|
||||
<view class="p-24rpx">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
|
||||
@click="handleDetail(item)"
|
||||
>
|
||||
<view class="p-24rpx">
|
||||
<view class="mb-16rpx flex items-center justify-between">
|
||||
<view class="text-32rpx text-[#333] font-semibold line-clamp-1">
|
||||
{{ item.name || item.path }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">文件路径:</text>
|
||||
<text class="min-w-0 flex-1 truncate">{{ item.path }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">文件类型:</text>
|
||||
<text class="min-w-0 flex-1 truncate">{{ item.type || '-' }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">文件大小:</text>
|
||||
<text>{{ formatFileSize(item.size) }}</text>
|
||||
</view>
|
||||
<!-- 文件预览 -->
|
||||
<view v-if="item.type && item.type.includes('image')" class="mb-12rpx">
|
||||
<image
|
||||
:src="item.url"
|
||||
mode="aspectFit"
|
||||
class="h-200rpx w-full rounded-8rpx"
|
||||
@click.stop="handlePreviewImage(item.url)"
|
||||
/>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">上传时间:</text>
|
||||
<text>{{ formatDateTime(item.createTime) || '-' }}</text>
|
||||
</view>
|
||||
<!-- 操作按钮 -->
|
||||
<view class="mt-16rpx flex justify-end gap-16rpx">
|
||||
<wd-button size="small" type="info" @click.stop="handleCopyUrl(item)">
|
||||
复制链接
|
||||
</wd-button>
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['infra:file:delete'])"
|
||||
size="small" type="error" @click.stop="handleDelete(item)"
|
||||
>
|
||||
删除
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="loadMoreState !== 'loading' && list.length === 0" class="py-100rpx text-center">
|
||||
<wd-status-tip image="content" tip="暂无文件数据" />
|
||||
</view>
|
||||
<wd-loadmore
|
||||
v-if="list.length > 0"
|
||||
:state="loadMoreState"
|
||||
@reload="loadMore"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 上传按钮 -->
|
||||
<wd-fab
|
||||
position="right-bottom"
|
||||
type="primary"
|
||||
:expandable="false"
|
||||
@click="handleUpload"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { LoadMoreState } from '@/http/types'
|
||||
import { ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { uploadFile } from '@/api/infra/file'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { http } from '@/http/http'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import FileSearchForm from './file-search-form.vue'
|
||||
|
||||
/** 文件信息 */
|
||||
interface FileInfo {
|
||||
id?: number
|
||||
configId?: number
|
||||
path: string
|
||||
name?: string
|
||||
url?: string
|
||||
size?: number
|
||||
type?: string
|
||||
createTime?: Date
|
||||
}
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const toast = useToast()
|
||||
const total = ref(0)
|
||||
const list = ref<FileInfo[]>([])
|
||||
const loadMoreState = ref<LoadMoreState>('loading')
|
||||
const queryParams = ref({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
|
||||
/** 格式化文件大小 */
|
||||
function formatFileSize(size?: number) {
|
||||
if (!size) return '-'
|
||||
if (size < 1024) return `${size} B`
|
||||
if (size < 1024 * 1024) return `${(size / 1024).toFixed(2)} KB`
|
||||
if (size < 1024 * 1024 * 1024) return `${(size / 1024 / 1024).toFixed(2)} MB`
|
||||
return `${(size / 1024 / 1024 / 1024).toFixed(2)} GB`
|
||||
}
|
||||
|
||||
/** 查询列表 */
|
||||
async function getList() {
|
||||
loadMoreState.value = 'loading'
|
||||
try {
|
||||
const data = await http.get<{ list: FileInfo[], total: number }>('/infra/file/page', queryParams.value)
|
||||
list.value = [...list.value, ...data.list]
|
||||
total.value = data.total
|
||||
loadMoreState.value = list.value.length >= total.value ? 'finished' : 'loading'
|
||||
} catch {
|
||||
queryParams.value.pageNo = queryParams.value.pageNo > 1 ? queryParams.value.pageNo - 1 : 1
|
||||
loadMoreState.value = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery(data?: Record<string, any>) {
|
||||
queryParams.value = {
|
||||
...data,
|
||||
pageNo: 1,
|
||||
pageSize: queryParams.value.pageSize,
|
||||
}
|
||||
list.value = []
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function handleReset() {
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 加载更多 */
|
||||
function loadMore() {
|
||||
if (loadMoreState.value === 'finished') {
|
||||
return
|
||||
}
|
||||
queryParams.value.pageNo++
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 上传文件 */
|
||||
function handleUpload() {
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
success: async (res) => {
|
||||
const filePath = res.tempFilePaths[0]
|
||||
try {
|
||||
toast.loading('上传中...')
|
||||
await uploadFile(filePath)
|
||||
toast.success('上传成功')
|
||||
// 刷新列表
|
||||
handleQuery()
|
||||
} catch {
|
||||
toast.show('上传失败')
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 复制链接 */
|
||||
function handleCopyUrl(item: FileInfo) {
|
||||
if (!item.url) {
|
||||
toast.show('文件 URL 为空')
|
||||
return
|
||||
}
|
||||
uni.setClipboardData({
|
||||
data: item.url,
|
||||
success: () => {
|
||||
toast.success('复制成功')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 预览图片 */
|
||||
function handlePreviewImage(url?: string) {
|
||||
if (!url) return
|
||||
uni.previewImage({
|
||||
urls: [url],
|
||||
})
|
||||
}
|
||||
|
||||
/** 查看详情 */
|
||||
function handleDetail(item: FileInfo) {
|
||||
uni.navigateTo({
|
||||
url: `/pages-infra/file/detail/index?id=${item.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除文件 */
|
||||
function handleDelete(item: FileInfo) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: `确定要删除文件"${item.name || item.path}"吗?`,
|
||||
success: async (res) => {
|
||||
if (!res.confirm) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
toast.loading('删除中...')
|
||||
await http.delete(`/infra/file/delete?id=${item.id}`)
|
||||
toast.success('删除成功')
|
||||
// 刷新列表
|
||||
handleQuery()
|
||||
} catch {
|
||||
toast.show('删除失败')
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 触底加载更多 */
|
||||
onReachBottom(() => {
|
||||
loadMore()
|
||||
})
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
144
src/pages-infra/file/components/file-search-form.vue
Normal file
144
src/pages-infra/file/components/file-search-form.vue
Normal file
@@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<!-- 搜索框入口 -->
|
||||
<view @click="visible = true">
|
||||
<wd-search :placeholder="placeholder" hide-cancel disabled />
|
||||
</view>
|
||||
|
||||
<!-- 搜索弹窗 -->
|
||||
<wd-popup v-model="visible" position="top" @close="visible = false">
|
||||
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
文件路径
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.path"
|
||||
placeholder="请输入文件路径"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
文件类型
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.type"
|
||||
placeholder="请输入文件类型"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
创建时间
|
||||
</view>
|
||||
<view class="yd-search-form-date-range-container">
|
||||
<view class="flex-1" @click="visibleCreateTime[0] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.createTime?.[0]) || '开始日期' }}
|
||||
</view>
|
||||
</view>
|
||||
-
|
||||
<view class="flex-1" @click="visibleCreateTime[1] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.createTime?.[1]) || '结束日期' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleCreateTime[0]" v-model="tempCreateTime[0]" type="date" />
|
||||
<view v-if="visibleCreateTime[0]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleCreateTime[0] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleCreateTime0Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleCreateTime[1]" v-model="tempCreateTime[1]" type="date" />
|
||||
<view v-if="visibleCreateTime[1]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleCreateTime[1] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleCreateTime1Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="yd-search-form-actions">
|
||||
<wd-button class="flex-1" plain @click="handleReset">
|
||||
重置
|
||||
</wd-button>
|
||||
<wd-button class="flex-1" type="primary" @click="handleSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { getNavbarHeight } from '@/utils'
|
||||
import { formatDate, formatDateRange } from '@/utils/date'
|
||||
|
||||
const emit = defineEmits<{
|
||||
search: [data: Record<string, any>]
|
||||
reset: []
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const formData = reactive({
|
||||
path: undefined as string | undefined,
|
||||
type: undefined as string | undefined,
|
||||
createTime: [undefined, undefined] as [number | undefined, number | undefined],
|
||||
})
|
||||
|
||||
/** 搜索条件 placeholder 拼接 */
|
||||
const placeholder = computed(() => {
|
||||
const conditions: string[] = []
|
||||
if (formData.path) {
|
||||
conditions.push(`路径:${formData.path}`)
|
||||
}
|
||||
if (formData.type) {
|
||||
conditions.push(`类型:${formData.type}`)
|
||||
}
|
||||
if (formData.createTime?.[0] && formData.createTime?.[1]) {
|
||||
conditions.push(`时间:${formatDate(formData.createTime[0])}~${formatDate(formData.createTime[1])}`)
|
||||
}
|
||||
return conditions.length > 0 ? conditions.join(' | ') : '搜索文件'
|
||||
})
|
||||
|
||||
// 时间范围选择器状态
|
||||
const visibleCreateTime = ref<[boolean, boolean]>([false, false])
|
||||
const tempCreateTime = ref<[number, number]>([Date.now(), Date.now()])
|
||||
|
||||
/** 创建时间[0]确认 */
|
||||
function handleCreateTime0Confirm() {
|
||||
formData.createTime = [tempCreateTime.value[0], formData.createTime?.[1]]
|
||||
visibleCreateTime.value[0] = false
|
||||
}
|
||||
|
||||
/** 创建时间[1]确认 */
|
||||
function handleCreateTime1Confirm() {
|
||||
formData.createTime = [formData.createTime?.[0], tempCreateTime.value[1]]
|
||||
visibleCreateTime.value[1] = false
|
||||
}
|
||||
|
||||
/** 搜索 */
|
||||
function handleSearch() {
|
||||
visible.value = false
|
||||
emit('search', {
|
||||
path: formData.path || undefined,
|
||||
type: formData.type || undefined,
|
||||
createTime: formatDateRange(formData.createTime),
|
||||
})
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
function handleReset() {
|
||||
formData.path = undefined
|
||||
formData.type = undefined
|
||||
formData.createTime = [undefined, undefined]
|
||||
visible.value = false
|
||||
emit('reset')
|
||||
}
|
||||
</script>
|
||||
157
src/pages-infra/file/config/detail/index.vue
Normal file
157
src/pages-infra/file/config/detail/index.vue
Normal file
@@ -0,0 +1,157 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="文件配置详情"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 详情内容 -->
|
||||
<view>
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="配置编号" :value="String(formData?.id ?? '-')" />
|
||||
<wd-cell title="配置名" :value="String(formData?.name ?? '-')" />
|
||||
<wd-cell title="存储器">
|
||||
<dict-tag :type="DICT_TYPE.INFRA_FILE_STORAGE" :value="formData?.storage" />
|
||||
</wd-cell>
|
||||
<wd-cell title="主配置">
|
||||
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="formData?.master" />
|
||||
</wd-cell>
|
||||
<wd-cell title="备注" :value="String(formData?.remark ?? '-')" />
|
||||
<wd-cell title="创建时间" :value="formatDateTime(formData?.createTime) || '-'" />
|
||||
</wd-cell-group>
|
||||
|
||||
<!-- 存储配置详情 -->
|
||||
<wd-cell-group v-if="formData?.config" border title="存储配置">
|
||||
<!-- DB / Local / FTP / SFTP 配置 -->
|
||||
<template v-if="formData.storage && formData.storage >= 10 && formData.storage <= 12">
|
||||
<wd-cell title="基础路径" :value="String(formData.config.basePath ?? '-')" />
|
||||
<template v-if="formData.storage >= 11 && formData.storage <= 12">
|
||||
<wd-cell title="主机地址" :value="String(formData.config.host ?? '-')" />
|
||||
<wd-cell title="主机端口" :value="String(formData.config.port ?? '-')" />
|
||||
<wd-cell title="用户名" :value="String(formData.config.username ?? '-')" />
|
||||
<wd-cell title="密码" :value="String(formData.config.password ?? '-')" />
|
||||
</template>
|
||||
<wd-cell v-if="formData.storage === 11" title="连接模式" :value="formData.config.mode === 'Active' ? '主动模式' : '被动模式'" />
|
||||
</template>
|
||||
<!-- S3 配置 -->
|
||||
<template v-if="formData.storage === 20">
|
||||
<wd-cell title="节点地址" :value="String(formData.config.endpoint ?? '-')" />
|
||||
<wd-cell title="存储 bucket" :value="String(formData.config.bucket ?? '-')" />
|
||||
<wd-cell title="accessKey" :value="String(formData.config.accessKey ?? '-')" />
|
||||
<wd-cell title="accessSecret" :value="String(formData.config.accessSecret ?? '-')" />
|
||||
<wd-cell title="Path Style" :value="formData.config.enablePathStyleAccess ? '启用' : '禁用'" />
|
||||
<wd-cell title="公开访问" :value="formData.config.enablePublicAccess ? '公开' : '私有'" />
|
||||
<wd-cell title="区域" :value="String(formData.config.region ?? '-')" />
|
||||
</template>
|
||||
<!-- 通用配置 -->
|
||||
<wd-cell title="自定义域名" :value="String(formData.config.domain ?? '-')" />
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<view class="fixed bottom-0 left-0 right-0 bg-white p-24rpx">
|
||||
<view class="w-full flex gap-24rpx">
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['infra:file-config:update'])"
|
||||
class="flex-1" type="warning" @click="handleEdit"
|
||||
>
|
||||
编辑
|
||||
</wd-button>
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['infra:file-config:delete'])"
|
||||
class="flex-1" type="error" :loading="deleting" @click="handleDelete"
|
||||
>
|
||||
删除
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FileConfig } from '@/api/infra/file-config'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { deleteFileConfig, getFileConfig } from '@/api/infra/file-config'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const toast = useToast()
|
||||
const formData = ref<FileConfig>()
|
||||
const deleting = ref(false)
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-infra/file/index')
|
||||
}
|
||||
|
||||
/** 加载详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
toast.loading('加载中...')
|
||||
formData.value = await getFileConfig(props.id)
|
||||
} finally {
|
||||
toast.close()
|
||||
}
|
||||
}
|
||||
|
||||
/** 编辑 */
|
||||
function handleEdit() {
|
||||
uni.navigateTo({
|
||||
url: `/pages-infra/file/config/form/index?id=${props.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除 */
|
||||
function handleDelete() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要删除该文件配置吗?',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) {
|
||||
return
|
||||
}
|
||||
deleting.value = true
|
||||
try {
|
||||
await deleteFileConfig(props.id)
|
||||
toast.success('删除成功')
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
307
src/pages-infra/file/config/form/index.vue
Normal file
307
src/pages-infra/file/config/form/index.vue
Normal file
@@ -0,0 +1,307 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
:title="getTitle"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 表单区域 -->
|
||||
<view>
|
||||
<wd-form ref="formRef" :model="formData" :rules="formRules">
|
||||
<wd-cell-group border>
|
||||
<wd-input
|
||||
v-model="formData.name"
|
||||
label="配置名"
|
||||
label-width="200rpx"
|
||||
prop="name"
|
||||
clearable
|
||||
placeholder="请输入配置名"
|
||||
/>
|
||||
<wd-cell title="存储器" title-width="200rpx" prop="storage" center>
|
||||
<wd-picker
|
||||
v-model="formData.storage"
|
||||
:columns="getIntDictOptions(DICT_TYPE.INFRA_FILE_STORAGE)"
|
||||
label-key="label"
|
||||
value-key="value"
|
||||
:disabled="!!formData.id"
|
||||
placeholder="请选择存储器"
|
||||
/>
|
||||
</wd-cell>
|
||||
<wd-textarea
|
||||
v-model="formData.remark"
|
||||
label="备注"
|
||||
label-width="200rpx"
|
||||
prop="remark"
|
||||
clearable
|
||||
placeholder="请输入备注"
|
||||
/>
|
||||
</wd-cell-group>
|
||||
|
||||
<!-- DB / Local / FTP / SFTP 配置 -->
|
||||
<wd-cell-group v-if="formData.storage && formData.storage >= 10 && formData.storage <= 12" border title="存储配置">
|
||||
<wd-input
|
||||
v-model="formData.config!.basePath"
|
||||
label="基础路径"
|
||||
label-width="200rpx"
|
||||
prop="config.basePath"
|
||||
clearable
|
||||
placeholder="请输入基础路径"
|
||||
/>
|
||||
<!-- FTP / SFTP 配置 -->
|
||||
<template v-if="formData.storage >= 11 && formData.storage <= 12">
|
||||
<wd-input
|
||||
v-model="formData.config!.host"
|
||||
label="主机地址"
|
||||
label-width="200rpx"
|
||||
prop="config.host"
|
||||
clearable
|
||||
placeholder="请输入主机地址"
|
||||
/>
|
||||
<wd-input
|
||||
v-model.number="formData.config!.port"
|
||||
label="主机端口"
|
||||
label-width="200rpx"
|
||||
prop="config.port"
|
||||
type="number"
|
||||
clearable
|
||||
placeholder="请输入主机端口"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="formData.config!.username"
|
||||
label="用户名"
|
||||
label-width="200rpx"
|
||||
prop="config.username"
|
||||
clearable
|
||||
placeholder="请输入用户名"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="formData.config!.password"
|
||||
label="密码"
|
||||
label-width="200rpx"
|
||||
prop="config.password"
|
||||
clearable
|
||||
placeholder="请输入密码"
|
||||
/>
|
||||
</template>
|
||||
<!-- FTP 连接模式 -->
|
||||
<wd-cell v-if="formData.storage === 11" title="连接模式" title-width="200rpx" prop="config.mode" center>
|
||||
<wd-radio-group v-model="formData.config!.mode" shape="button">
|
||||
<wd-radio value="Active">
|
||||
主动模式
|
||||
</wd-radio>
|
||||
<wd-radio value="Passive">
|
||||
被动模式
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-cell>
|
||||
</wd-cell-group>
|
||||
|
||||
<!-- S3 配置 -->
|
||||
<wd-cell-group v-if="formData.storage === 20" border title="S3 配置">
|
||||
<wd-input
|
||||
v-model="formData.config!.endpoint"
|
||||
label="节点地址"
|
||||
label-width="200rpx"
|
||||
prop="config.endpoint"
|
||||
clearable
|
||||
placeholder="请输入节点地址"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="formData.config!.bucket"
|
||||
label="存储 bucket"
|
||||
label-width="200rpx"
|
||||
prop="config.bucket"
|
||||
clearable
|
||||
placeholder="请输入 bucket"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="formData.config!.accessKey"
|
||||
label="accessKey"
|
||||
label-width="200rpx"
|
||||
prop="config.accessKey"
|
||||
clearable
|
||||
placeholder="请输入 accessKey"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="formData.config!.accessSecret"
|
||||
label="accessSecret"
|
||||
label-width="200rpx"
|
||||
prop="config.accessSecret"
|
||||
clearable
|
||||
placeholder="请输入 accessSecret"
|
||||
/>
|
||||
<wd-cell title="Path Style" title-width="200rpx" prop="config.enablePathStyleAccess" center>
|
||||
<wd-radio-group v-model="formData.config!.enablePathStyleAccess" shape="button">
|
||||
<wd-radio :value="true">
|
||||
启用
|
||||
</wd-radio>
|
||||
<wd-radio :value="false">
|
||||
禁用
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-cell>
|
||||
<wd-cell title="公开访问" title-width="200rpx" prop="config.enablePublicAccess" center>
|
||||
<wd-radio-group v-model="formData.config!.enablePublicAccess" shape="button">
|
||||
<wd-radio :value="true">
|
||||
公开
|
||||
</wd-radio>
|
||||
<wd-radio :value="false">
|
||||
私有
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-cell>
|
||||
<wd-input
|
||||
v-model="formData.config!.region"
|
||||
label="区域"
|
||||
label-width="200rpx"
|
||||
prop="config.region"
|
||||
clearable
|
||||
placeholder="请填写区域,一般仅 AWS 需要填写"
|
||||
/>
|
||||
</wd-cell-group>
|
||||
|
||||
<!-- 通用配置 -->
|
||||
<wd-cell-group v-if="formData.storage" border title="通用配置">
|
||||
<wd-input
|
||||
v-model="formData.config!.domain"
|
||||
label="自定义域名"
|
||||
label-width="200rpx"
|
||||
prop="config.domain"
|
||||
clearable
|
||||
placeholder="请输入自定义域名"
|
||||
/>
|
||||
</wd-cell-group>
|
||||
</wd-form>
|
||||
</view>
|
||||
|
||||
<!-- 底部保存按钮 -->
|
||||
<view class="fixed bottom-0 left-0 right-0 bg-white p-24rpx">
|
||||
<wd-button
|
||||
type="primary"
|
||||
block
|
||||
:loading="formLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
保存
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FileConfig } from '@/api/infra/file-config'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { createFileConfig, getFileConfig, updateFileConfig } from '@/api/infra/file-config'
|
||||
import { getIntDictOptions } from '@/hooks/useDict'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const getTitle = computed(() => props.id ? '编辑文件配置' : '新增文件配置')
|
||||
const formLoading = ref(false)
|
||||
const formData = ref<FileConfig>({
|
||||
id: undefined,
|
||||
name: '',
|
||||
storage: undefined,
|
||||
remark: '',
|
||||
config: {
|
||||
basePath: '',
|
||||
host: '',
|
||||
port: undefined,
|
||||
username: '',
|
||||
password: '',
|
||||
mode: 'Passive',
|
||||
endpoint: '',
|
||||
bucket: '',
|
||||
accessKey: '',
|
||||
accessSecret: '',
|
||||
enablePathStyleAccess: false,
|
||||
enablePublicAccess: false,
|
||||
region: '',
|
||||
domain: '',
|
||||
},
|
||||
})
|
||||
const formRules = {
|
||||
name: [{ required: true, message: '配置名不能为空' }],
|
||||
storage: [{ required: true, message: '存储器不能为空' }],
|
||||
}
|
||||
const formRef = ref()
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-infra/file/index')
|
||||
}
|
||||
|
||||
/** 加载详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
const data = await getFileConfig(props.id)
|
||||
formData.value = {
|
||||
...data,
|
||||
config: data.config || {
|
||||
basePath: '',
|
||||
host: '',
|
||||
port: undefined,
|
||||
username: '',
|
||||
password: '',
|
||||
mode: 'Passive',
|
||||
endpoint: '',
|
||||
bucket: '',
|
||||
accessKey: '',
|
||||
accessSecret: '',
|
||||
enablePathStyleAccess: false,
|
||||
enablePublicAccess: false,
|
||||
region: '',
|
||||
domain: '',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/** 提交表单 */
|
||||
async function handleSubmit() {
|
||||
const { valid } = await formRef.value.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
|
||||
formLoading.value = true
|
||||
try {
|
||||
if (props.id) {
|
||||
await updateFileConfig(formData.value)
|
||||
toast.success('修改成功')
|
||||
} else {
|
||||
await createFileConfig(formData.value)
|
||||
toast.success('新增成功')
|
||||
}
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
171
src/pages-infra/file/detail/index.vue
Normal file
171
src/pages-infra/file/detail/index.vue
Normal file
@@ -0,0 +1,171 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="文件详情"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 详情内容 -->
|
||||
<view>
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="文件编号" :value="String(formData?.id ?? '-')" />
|
||||
<wd-cell title="文件名" :value="String(formData?.name ?? '-')" />
|
||||
<wd-cell title="文件路径" :value="String(formData?.path ?? '-')" />
|
||||
<wd-cell title="文件 URL" :value="String(formData?.url ?? '-')" />
|
||||
<wd-cell title="文件大小" :value="formatFileSize(formData?.size)" />
|
||||
<wd-cell title="文件类型" :value="String(formData?.type ?? '-')" />
|
||||
<wd-cell title="上传时间" :value="formatDateTime(formData?.createTime) || '-'" />
|
||||
</wd-cell-group>
|
||||
|
||||
<!-- 文件预览 -->
|
||||
<view v-if="formData?.type && formData.type.includes('image')" class="m-24rpx">
|
||||
<view class="mb-16rpx text-28rpx text-[#999]">
|
||||
文件预览
|
||||
</view>
|
||||
<image
|
||||
:src="formData.url"
|
||||
mode="aspectFit"
|
||||
class="w-full rounded-8rpx"
|
||||
@click="handlePreviewImage"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<view class="fixed bottom-0 left-0 right-0 bg-white p-24rpx">
|
||||
<view class="w-full flex gap-24rpx">
|
||||
<wd-button class="flex-1" type="info" @click="handleCopyUrl">
|
||||
复制链接
|
||||
</wd-button>
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['infra:file:delete'])"
|
||||
class="flex-1" type="error" :loading="deleting" @click="handleDelete"
|
||||
>
|
||||
删除
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { http } from '@/http/http'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
|
||||
/** 文件信息 */
|
||||
interface FileInfo {
|
||||
id?: number
|
||||
configId?: number
|
||||
path: string
|
||||
name?: string
|
||||
url?: string
|
||||
size?: number
|
||||
type?: string
|
||||
createTime?: Date
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const toast = useToast()
|
||||
const formData = ref<FileInfo>()
|
||||
const deleting = ref(false)
|
||||
|
||||
/** 格式化文件大小 */
|
||||
function formatFileSize(size?: number) {
|
||||
if (!size) return '-'
|
||||
if (size < 1024) return `${size} B`
|
||||
if (size < 1024 * 1024) return `${(size / 1024).toFixed(2)} KB`
|
||||
if (size < 1024 * 1024 * 1024) return `${(size / 1024 / 1024).toFixed(2)} MB`
|
||||
return `${(size / 1024 / 1024 / 1024).toFixed(2)} GB`
|
||||
}
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-infra/file/index')
|
||||
}
|
||||
|
||||
/** 加载详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
toast.loading('加载中...')
|
||||
formData.value = await http.get<FileInfo>(`/infra/file/get?id=${props.id}`)
|
||||
} finally {
|
||||
toast.close()
|
||||
}
|
||||
}
|
||||
|
||||
/** 复制链接 */
|
||||
function handleCopyUrl() {
|
||||
if (!formData.value?.url) {
|
||||
toast.show('文件 URL 为空')
|
||||
return
|
||||
}
|
||||
uni.setClipboardData({
|
||||
data: formData.value.url,
|
||||
success: () => {
|
||||
toast.success('复制成功')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 预览图片 */
|
||||
function handlePreviewImage() {
|
||||
if (!formData.value?.url) return
|
||||
uni.previewImage({
|
||||
urls: [formData.value.url],
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除 */
|
||||
function handleDelete() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要删除该文件吗?',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) {
|
||||
return
|
||||
}
|
||||
deleting.value = true
|
||||
try {
|
||||
await http.delete(`/infra/file/delete?id=${props.id}`)
|
||||
toast.success('删除成功')
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
52
src/pages-infra/file/index.vue
Normal file
52
src/pages-infra/file/index.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="文件管理"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- Tab 切换 -->
|
||||
<view class="bg-white">
|
||||
<wd-tabs v-model="tabIndex" shrink @change="handleTabChange">
|
||||
<wd-tab title="文件列表" />
|
||||
<wd-tab title="文件配置" />
|
||||
</wd-tabs>
|
||||
</view>
|
||||
<!-- 列表内容 -->
|
||||
<FileList v-show="tabType === 'file'" />
|
||||
<ConfigList v-show="tabType === 'config'" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import ConfigList from './components/config-list.vue'
|
||||
import FileList from './components/file-list.vue'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const tabTypes: string[] = ['file', 'config']
|
||||
const tabIndex = ref(0)
|
||||
const tabType = computed<string>(() => tabTypes[tabIndex.value])
|
||||
|
||||
/** Tab 切换 */
|
||||
function handleTabChange({ index }: { index: number }) {
|
||||
tabIndex.value = index
|
||||
}
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
210
src/pages-system/dict/components/data-list.vue
Normal file
210
src/pages-system/dict/components/data-list.vue
Normal file
@@ -0,0 +1,210 @@
|
||||
<template>
|
||||
<view>
|
||||
<!-- 搜索组件 -->
|
||||
<DataSearchForm @search="handleQuery" @reset="handleReset" />
|
||||
|
||||
<!-- 字典数据列表 -->
|
||||
<view class="p-24rpx">
|
||||
<!-- 当前字典类型提示 -->
|
||||
<view v-if="dictType" class="mb-24rpx rounded-12rpx bg-blue-50 p-16rpx text-28rpx text-blue-600">
|
||||
当前字典类型:{{ dictType }}
|
||||
</view>
|
||||
<view v-else class="mb-24rpx rounded-12rpx bg-orange-50 p-16rpx text-28rpx text-orange-600">
|
||||
请先在"字典类型"中选择一个字典类型
|
||||
</view>
|
||||
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
|
||||
@click="handleDetail(item)"
|
||||
>
|
||||
<view class="p-24rpx">
|
||||
<view class="mb-16rpx flex items-center justify-between">
|
||||
<view class="text-32rpx text-[#333] font-semibold">
|
||||
{{ item.label }}
|
||||
</view>
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="item.status" />
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">字典键值:</text>
|
||||
<text class="min-w-0 flex-1 truncate">{{ item.value }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">字典排序:</text>
|
||||
<text>{{ item.sort }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">颜色类型:</text>
|
||||
<view v-if="item.colorType" class="rounded-4rpx px-8rpx py-2rpx text-24rpx text-white" :style="{ backgroundColor: getColorStyle(item.colorType) }">
|
||||
{{ item.colorType }}
|
||||
</view>
|
||||
<text v-else>-</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">CSS Class:</text>
|
||||
<view v-if="item.cssClass" class="rounded-4rpx px-8rpx py-2rpx text-24rpx text-white" :style="{ backgroundColor: item.cssClass }">
|
||||
{{ item.cssClass }}
|
||||
</view>
|
||||
<text v-else>-</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">创建时间:</text>
|
||||
<text>{{ formatDateTime(item.createTime) || '-' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="loadMoreState !== 'loading' && list.length === 0" class="py-100rpx text-center">
|
||||
<wd-status-tip image="content" tip="暂无字典数据" />
|
||||
</view>
|
||||
<wd-loadmore
|
||||
v-if="list.length > 0"
|
||||
:state="loadMoreState"
|
||||
@reload="loadMore"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 新增按钮 -->
|
||||
<wd-fab
|
||||
v-if="hasAccessByCodes(['system:dict:create']) && dictType"
|
||||
position="right-bottom"
|
||||
type="primary"
|
||||
:expandable="false"
|
||||
@click="handleAdd"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { DictData } from '@/api/system/dict/data'
|
||||
import type { LoadMoreState } from '@/http/types'
|
||||
import { ref, watch } from 'vue'
|
||||
import { getDictDataPage } from '@/api/system/dict/data'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import DataSearchForm from './data-search-form.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
dictType?: string
|
||||
}>()
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const total = ref(0)
|
||||
const list = ref<DictData[]>([])
|
||||
const loadMoreState = ref<LoadMoreState>('loading')
|
||||
const queryParams = ref({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
dictType: undefined as string | undefined,
|
||||
})
|
||||
|
||||
/** 颜色类型映射 */
|
||||
const colorMap: Record<string, string> = {
|
||||
processing: '#1890ff',
|
||||
success: '#52c41a',
|
||||
default: '#d9d9d9',
|
||||
warning: '#faad14',
|
||||
error: '#ff4d4f',
|
||||
pink: '#eb2f96',
|
||||
red: '#f5222d',
|
||||
orange: '#fa8c16',
|
||||
green: '#52c41a',
|
||||
cyan: '#13c2c2',
|
||||
blue: '#1890ff',
|
||||
purple: '#722ed1',
|
||||
}
|
||||
|
||||
/** 获取颜色样式 */
|
||||
function getColorStyle(colorType: string) {
|
||||
return colorMap[colorType] || colorType
|
||||
}
|
||||
|
||||
/** 查询列表 */
|
||||
async function getList() {
|
||||
if (!props.dictType) {
|
||||
list.value = []
|
||||
loadMoreState.value = 'finished'
|
||||
return
|
||||
}
|
||||
loadMoreState.value = 'loading'
|
||||
try {
|
||||
const data = await getDictDataPage({
|
||||
...queryParams.value,
|
||||
dictType: props.dictType,
|
||||
})
|
||||
list.value = [...list.value, ...data.list]
|
||||
total.value = data.total
|
||||
loadMoreState.value = list.value.length >= total.value ? 'finished' : 'loading'
|
||||
} catch {
|
||||
queryParams.value.pageNo = queryParams.value.pageNo > 1 ? queryParams.value.pageNo - 1 : 1
|
||||
loadMoreState.value = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery(data?: Record<string, any>) {
|
||||
queryParams.value = {
|
||||
...data,
|
||||
pageNo: 1,
|
||||
pageSize: queryParams.value.pageSize,
|
||||
dictType: props.dictType,
|
||||
}
|
||||
list.value = []
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function handleReset() {
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 加载更多 */
|
||||
function loadMore() {
|
||||
if (loadMoreState.value === 'finished') {
|
||||
return
|
||||
}
|
||||
queryParams.value.pageNo++
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 新增 */
|
||||
function handleAdd() {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/dict/data/form/index?dictType=${props.dictType}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 查看详情 */
|
||||
function handleDetail(item: DictData) {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/dict/data/detail/index?id=${item.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 监听 dictType 变化,重新查询 */
|
||||
watch(
|
||||
() => props.dictType,
|
||||
() => {
|
||||
if (props.dictType) {
|
||||
queryParams.value.pageNo = 1
|
||||
list.value = []
|
||||
getList()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
/** 触底加载更多 */
|
||||
onReachBottom(() => {
|
||||
loadMore()
|
||||
})
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
if (props.dictType) {
|
||||
getList()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
94
src/pages-system/dict/components/data-search-form.vue
Normal file
94
src/pages-system/dict/components/data-search-form.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<!-- 搜索框入口 -->
|
||||
<view @click="visible = true">
|
||||
<wd-search :placeholder="placeholder" hide-cancel disabled />
|
||||
</view>
|
||||
|
||||
<!-- 搜索弹窗 -->
|
||||
<wd-popup v-model="visible" position="top" @close="visible = false">
|
||||
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
字典标签
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.label"
|
||||
placeholder="请输入字典标签"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
状态
|
||||
</view>
|
||||
<wd-radio-group v-model="formData.status" shape="button">
|
||||
<wd-radio :value="-1">
|
||||
全部
|
||||
</wd-radio>
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
<view class="yd-search-form-actions">
|
||||
<wd-button class="flex-1" plain @click="handleReset">
|
||||
重置
|
||||
</wd-button>
|
||||
<wd-button class="flex-1" type="primary" @click="handleSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { getDictLabel, getIntDictOptions } from '@/hooks/useDict'
|
||||
import { getNavbarHeight } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
|
||||
const emit = defineEmits<{
|
||||
search: [data: Record<string, any>]
|
||||
reset: []
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const formData = reactive({
|
||||
label: undefined as string | undefined,
|
||||
status: -1,
|
||||
})
|
||||
|
||||
/** 搜索条件 placeholder 拼接 */
|
||||
const placeholder = computed(() => {
|
||||
const conditions: string[] = []
|
||||
if (formData.label) {
|
||||
conditions.push(`标签:${formData.label}`)
|
||||
}
|
||||
if (formData.status !== -1) {
|
||||
conditions.push(`状态:${getDictLabel(DICT_TYPE.COMMON_STATUS, formData.status)}`)
|
||||
}
|
||||
return conditions.length > 0 ? conditions.join(' | ') : '搜索字典数据'
|
||||
})
|
||||
|
||||
/** 搜索 */
|
||||
function handleSearch() {
|
||||
visible.value = false
|
||||
emit('search', {
|
||||
label: formData.label || undefined,
|
||||
status: formData.status === -1 ? undefined : formData.status,
|
||||
})
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
function handleReset() {
|
||||
formData.label = undefined
|
||||
formData.status = -1
|
||||
visible.value = false
|
||||
emit('reset')
|
||||
}
|
||||
</script>
|
||||
154
src/pages-system/dict/components/type-list.vue
Normal file
154
src/pages-system/dict/components/type-list.vue
Normal file
@@ -0,0 +1,154 @@
|
||||
<template>
|
||||
<view>
|
||||
<!-- 搜索组件 -->
|
||||
<TypeSearchForm @search="handleQuery" @reset="handleReset" />
|
||||
|
||||
<!-- 字典类型列表 -->
|
||||
<view class="p-24rpx">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
|
||||
@click="handleDetail(item)"
|
||||
>
|
||||
<view class="p-24rpx">
|
||||
<view class="mb-16rpx flex items-center justify-between">
|
||||
<view class="text-32rpx text-[#333] font-semibold">
|
||||
{{ item.name }}
|
||||
</view>
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="item.status" />
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">字典类型:</text>
|
||||
<text class="min-w-0 flex-1 truncate">{{ item.type }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">备注:</text>
|
||||
<text class="min-w-0 flex-1 truncate">{{ item.remark || '-' }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">创建时间:</text>
|
||||
<text>{{ formatDateTime(item.createTime) || '-' }}</text>
|
||||
</view>
|
||||
<!-- 查看数据按钮 -->
|
||||
<view class="mt-16rpx flex justify-end">
|
||||
<wd-button size="small" type="info" @click.stop="handleSelectType(item)">
|
||||
查看数据
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="loadMoreState !== 'loading' && list.length === 0" class="py-100rpx text-center">
|
||||
<wd-status-tip image="content" tip="暂无字典类型数据" />
|
||||
</view>
|
||||
<wd-loadmore
|
||||
v-if="list.length > 0"
|
||||
:state="loadMoreState"
|
||||
@reload="loadMore"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 新增按钮 -->
|
||||
<wd-fab
|
||||
v-if="hasAccessByCodes(['system:dict:create'])"
|
||||
position="right-bottom"
|
||||
type="primary"
|
||||
:expandable="false"
|
||||
@click="handleAdd"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { DictType } from '@/api/system/dict/type'
|
||||
import type { LoadMoreState } from '@/http/types'
|
||||
import { ref } from 'vue'
|
||||
import { getDictTypePage } from '@/api/system/dict/type'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import TypeSearchForm from './type-search-form.vue'
|
||||
|
||||
const emit = defineEmits<{
|
||||
select: [dictType: string]
|
||||
}>()
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const total = ref(0)
|
||||
const list = ref<DictType[]>([])
|
||||
const loadMoreState = ref<LoadMoreState>('loading')
|
||||
const queryParams = ref({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
|
||||
/** 查询列表 */
|
||||
async function getList() {
|
||||
loadMoreState.value = 'loading'
|
||||
try {
|
||||
const data = await getDictTypePage(queryParams.value)
|
||||
list.value = [...list.value, ...data.list]
|
||||
total.value = data.total
|
||||
loadMoreState.value = list.value.length >= total.value ? 'finished' : 'loading'
|
||||
} catch {
|
||||
queryParams.value.pageNo = queryParams.value.pageNo > 1 ? queryParams.value.pageNo - 1 : 1
|
||||
loadMoreState.value = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery(data?: Record<string, any>) {
|
||||
queryParams.value = {
|
||||
...data,
|
||||
pageNo: 1,
|
||||
pageSize: queryParams.value.pageSize,
|
||||
}
|
||||
list.value = []
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function handleReset() {
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 加载更多 */
|
||||
function loadMore() {
|
||||
if (loadMoreState.value === 'finished') {
|
||||
return
|
||||
}
|
||||
queryParams.value.pageNo++
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 新增 */
|
||||
function handleAdd() {
|
||||
uni.navigateTo({
|
||||
url: '/pages-system/dict/type/form/index',
|
||||
})
|
||||
}
|
||||
|
||||
/** 查看详情 */
|
||||
function handleDetail(item: DictType) {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/dict/type/detail/index?id=${item.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 选择字典类型,查看数据 */
|
||||
function handleSelectType(item: DictType) {
|
||||
emit('select', item.type)
|
||||
}
|
||||
|
||||
/** 触底加载更多 */
|
||||
onReachBottom(() => {
|
||||
loadMore()
|
||||
})
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
169
src/pages-system/dict/components/type-search-form.vue
Normal file
169
src/pages-system/dict/components/type-search-form.vue
Normal file
@@ -0,0 +1,169 @@
|
||||
<template>
|
||||
<!-- 搜索框入口 -->
|
||||
<view @click="visible = true">
|
||||
<wd-search :placeholder="placeholder" hide-cancel disabled />
|
||||
</view>
|
||||
|
||||
<!-- 搜索弹窗 -->
|
||||
<wd-popup v-model="visible" position="top" @close="visible = false">
|
||||
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
字典名称
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.name"
|
||||
placeholder="请输入字典名称"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
字典类型
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.type"
|
||||
placeholder="请输入字典类型"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
状态
|
||||
</view>
|
||||
<wd-radio-group v-model="formData.status" shape="button">
|
||||
<wd-radio :value="-1">
|
||||
全部
|
||||
</wd-radio>
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
创建时间
|
||||
</view>
|
||||
<view class="yd-search-form-date-range-container">
|
||||
<view class="flex-1" @click="visibleCreateTime[0] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.createTime?.[0]) || '开始日期' }}
|
||||
</view>
|
||||
</view>
|
||||
-
|
||||
<view class="flex-1" @click="visibleCreateTime[1] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.createTime?.[1]) || '结束日期' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleCreateTime[0]" v-model="tempCreateTime[0]" type="date" />
|
||||
<view v-if="visibleCreateTime[0]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleCreateTime[0] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleCreateTime0Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleCreateTime[1]" v-model="tempCreateTime[1]" type="date" />
|
||||
<view v-if="visibleCreateTime[1]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleCreateTime[1] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleCreateTime1Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="yd-search-form-actions">
|
||||
<wd-button class="flex-1" plain @click="handleReset">
|
||||
重置
|
||||
</wd-button>
|
||||
<wd-button class="flex-1" type="primary" @click="handleSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { getDictLabel, getIntDictOptions } from '@/hooks/useDict'
|
||||
import { getNavbarHeight } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDate, formatDateRange } from '@/utils/date'
|
||||
|
||||
const emit = defineEmits<{
|
||||
search: [data: Record<string, any>]
|
||||
reset: []
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const formData = reactive({
|
||||
name: undefined as string | undefined,
|
||||
type: undefined as string | undefined,
|
||||
status: -1,
|
||||
createTime: [undefined, undefined] as [number | undefined, number | undefined],
|
||||
})
|
||||
|
||||
/** 搜索条件 placeholder 拼接 */
|
||||
const placeholder = computed(() => {
|
||||
const conditions: string[] = []
|
||||
if (formData.name) {
|
||||
conditions.push(`名称:${formData.name}`)
|
||||
}
|
||||
if (formData.type) {
|
||||
conditions.push(`类型:${formData.type}`)
|
||||
}
|
||||
if (formData.status !== -1) {
|
||||
conditions.push(`状态:${getDictLabel(DICT_TYPE.COMMON_STATUS, formData.status)}`)
|
||||
}
|
||||
if (formData.createTime?.[0] && formData.createTime?.[1]) {
|
||||
conditions.push(`时间:${formatDate(formData.createTime[0])}~${formatDate(formData.createTime[1])}`)
|
||||
}
|
||||
return conditions.length > 0 ? conditions.join(' | ') : '搜索字典类型'
|
||||
})
|
||||
|
||||
// 时间范围选择器状态
|
||||
const visibleCreateTime = ref<[boolean, boolean]>([false, false])
|
||||
const tempCreateTime = ref<[number, number]>([Date.now(), Date.now()])
|
||||
|
||||
/** 创建时间[0]确认 */
|
||||
function handleCreateTime0Confirm() {
|
||||
formData.createTime = [tempCreateTime.value[0], formData.createTime?.[1]]
|
||||
visibleCreateTime.value[0] = false
|
||||
}
|
||||
|
||||
/** 创建时间[1]确认 */
|
||||
function handleCreateTime1Confirm() {
|
||||
formData.createTime = [formData.createTime?.[0], tempCreateTime.value[1]]
|
||||
visibleCreateTime.value[1] = false
|
||||
}
|
||||
|
||||
/** 搜索 */
|
||||
function handleSearch() {
|
||||
visible.value = false
|
||||
emit('search', {
|
||||
name: formData.name || undefined,
|
||||
type: formData.type || undefined,
|
||||
status: formData.status === -1 ? undefined : formData.status,
|
||||
createTime: formatDateRange(formData.createTime),
|
||||
})
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
function handleReset() {
|
||||
formData.name = undefined
|
||||
formData.type = undefined
|
||||
formData.status = -1
|
||||
formData.createTime = [undefined, undefined]
|
||||
visible.value = false
|
||||
emit('reset')
|
||||
}
|
||||
</script>
|
||||
163
src/pages-system/dict/data/detail/index.vue
Normal file
163
src/pages-system/dict/data/detail/index.vue
Normal file
@@ -0,0 +1,163 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="字典数据详情"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 详情内容 -->
|
||||
<view>
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="字典编码" :value="String(formData?.id ?? '-')" />
|
||||
<wd-cell title="字典类型" :value="String(formData?.dictType ?? '-')" />
|
||||
<wd-cell title="字典标签" :value="String(formData?.label ?? '-')" />
|
||||
<wd-cell title="字典键值" :value="String(formData?.value ?? '-')" />
|
||||
<wd-cell title="字典排序" :value="String(formData?.sort ?? '-')" />
|
||||
<wd-cell title="状态">
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="formData?.status" />
|
||||
</wd-cell>
|
||||
<wd-cell title="颜色类型">
|
||||
<view v-if="formData?.colorType" class="rounded-4rpx px-8rpx py-2rpx text-24rpx text-white" :style="{ backgroundColor: getColorStyle(formData.colorType) }">
|
||||
{{ formData.colorType }}
|
||||
</view>
|
||||
<text v-else>-</text>
|
||||
</wd-cell>
|
||||
<wd-cell title="CSS Class">
|
||||
<view v-if="formData?.cssClass" class="rounded-4rpx px-8rpx py-2rpx text-24rpx text-white" :style="{ backgroundColor: formData.cssClass }">
|
||||
{{ formData.cssClass }}
|
||||
</view>
|
||||
<text v-else>-</text>
|
||||
</wd-cell>
|
||||
<wd-cell title="备注" :value="String(formData?.remark ?? '-')" />
|
||||
<wd-cell title="创建时间" :value="formatDateTime(formData?.createTime) || '-'" />
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<view class="fixed bottom-0 left-0 right-0 bg-white p-24rpx">
|
||||
<view class="w-full flex gap-24rpx">
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['system:dict:update'])"
|
||||
class="flex-1" type="warning" @click="handleEdit"
|
||||
>
|
||||
编辑
|
||||
</wd-button>
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['system:dict:delete'])"
|
||||
class="flex-1" type="error" :loading="deleting" @click="handleDelete"
|
||||
>
|
||||
删除
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { DictData } from '@/api/system/dict/data'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { deleteDictData, getDictData } from '@/api/system/dict/data'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const toast = useToast()
|
||||
const formData = ref<DictData>()
|
||||
const deleting = ref(false)
|
||||
|
||||
/** 颜色类型映射 */
|
||||
const colorMap: Record<string, string> = {
|
||||
processing: '#1890ff',
|
||||
success: '#52c41a',
|
||||
default: '#d9d9d9',
|
||||
warning: '#faad14',
|
||||
error: '#ff4d4f',
|
||||
pink: '#eb2f96',
|
||||
red: '#f5222d',
|
||||
orange: '#fa8c16',
|
||||
green: '#52c41a',
|
||||
cyan: '#13c2c2',
|
||||
blue: '#1890ff',
|
||||
purple: '#722ed1',
|
||||
}
|
||||
|
||||
/** 获取颜色样式 */
|
||||
function getColorStyle(colorType: string) {
|
||||
return colorMap[colorType] || colorType
|
||||
}
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-system/dict/index')
|
||||
}
|
||||
|
||||
/** 加载详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
toast.loading('加载中...')
|
||||
formData.value = await getDictData(props.id)
|
||||
} finally {
|
||||
toast.close()
|
||||
}
|
||||
}
|
||||
|
||||
/** 编辑 */
|
||||
function handleEdit() {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/dict/data/form/index?id=${props.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除 */
|
||||
function handleDelete() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要删除该字典数据吗?',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) {
|
||||
return
|
||||
}
|
||||
deleting.value = true
|
||||
try {
|
||||
await deleteDictData(props.id)
|
||||
toast.success('删除成功')
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
222
src/pages-system/dict/data/form/index.vue
Normal file
222
src/pages-system/dict/data/form/index.vue
Normal file
@@ -0,0 +1,222 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
:title="getTitle"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 表单区域 -->
|
||||
<view>
|
||||
<wd-form ref="formRef" :model="formData" :rules="formRules">
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="字典类型" title-width="200rpx" prop="dictType" center>
|
||||
<wd-picker
|
||||
v-model="formData.dictType"
|
||||
:columns="dictTypeOptions"
|
||||
label-key="label"
|
||||
value-key="value"
|
||||
:disabled="!!formData.id"
|
||||
placeholder="请选择字典类型"
|
||||
/>
|
||||
</wd-cell>
|
||||
<wd-input
|
||||
v-model="formData.label"
|
||||
label="数据标签"
|
||||
label-width="200rpx"
|
||||
prop="label"
|
||||
clearable
|
||||
placeholder="请输入数据标签"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="formData.value"
|
||||
label="数据键值"
|
||||
label-width="200rpx"
|
||||
prop="value"
|
||||
clearable
|
||||
placeholder="请输入数据键值"
|
||||
/>
|
||||
<wd-input
|
||||
v-model.number="formData.sort"
|
||||
label="显示排序"
|
||||
label-width="200rpx"
|
||||
prop="sort"
|
||||
type="number"
|
||||
clearable
|
||||
placeholder="请输入显示排序"
|
||||
/>
|
||||
<wd-cell title="状态" title-width="200rpx" prop="status" center>
|
||||
<wd-radio-group v-model="formData.status" shape="button">
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-cell>
|
||||
<wd-cell title="颜色类型" title-width="200rpx" prop="colorType" center>
|
||||
<wd-picker
|
||||
v-model="formData.colorType"
|
||||
:columns="colorOptions"
|
||||
label-key="label"
|
||||
value-key="value"
|
||||
placeholder="请选择颜色类型"
|
||||
/>
|
||||
</wd-cell>
|
||||
<wd-input
|
||||
v-model="formData.cssClass"
|
||||
label="CSS Class"
|
||||
label-width="200rpx"
|
||||
prop="cssClass"
|
||||
clearable
|
||||
placeholder="请输入 CSS Class,如 #108ee9"
|
||||
/>
|
||||
<wd-textarea
|
||||
v-model="formData.remark"
|
||||
label="备注"
|
||||
label-width="200rpx"
|
||||
prop="remark"
|
||||
clearable
|
||||
placeholder="请输入备注"
|
||||
/>
|
||||
</wd-cell-group>
|
||||
</wd-form>
|
||||
</view>
|
||||
|
||||
<!-- 底部保存按钮 -->
|
||||
<view class="fixed bottom-0 left-0 right-0 bg-white p-24rpx">
|
||||
<wd-button
|
||||
type="primary"
|
||||
block
|
||||
:loading="formLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
保存
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { DictData } from '@/api/system/dict/data'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { createDictData, getDictData, updateDictData } from '@/api/system/dict/data'
|
||||
import { getSimpleDictTypeList } from '@/api/system/dict/type'
|
||||
import { getIntDictOptions } from '@/hooks/useDict'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
dictType?: string | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const getTitle = computed(() => props.id ? '编辑字典数据' : '新增字典数据')
|
||||
const formLoading = ref(false)
|
||||
const formData = ref<DictData>({
|
||||
id: undefined,
|
||||
dictType: props.dictType || '',
|
||||
label: '',
|
||||
value: '',
|
||||
sort: 0,
|
||||
status: 0,
|
||||
colorType: '',
|
||||
cssClass: '',
|
||||
remark: '',
|
||||
})
|
||||
const formRules = {
|
||||
dictType: [{ required: true, message: '字典类型不能为空' }],
|
||||
label: [{ required: true, message: '数据标签不能为空' }],
|
||||
value: [{ required: true, message: '数据键值不能为空' }],
|
||||
sort: [{ required: true, message: '显示排序不能为空' }],
|
||||
status: [{ required: true, message: '状态不能为空' }],
|
||||
}
|
||||
const formRef = ref()
|
||||
|
||||
/** 字典类型选项 */
|
||||
const dictTypeOptions = ref<{ label: string, value: string }[]>([])
|
||||
|
||||
/** 颜色类型选项 */
|
||||
const colorOptions = [
|
||||
{ value: '', label: '无' },
|
||||
{ value: 'processing', label: '主要' },
|
||||
{ value: 'success', label: '成功' },
|
||||
{ value: 'default', label: '默认' },
|
||||
{ value: 'warning', label: '警告' },
|
||||
{ value: 'error', label: '危险' },
|
||||
{ value: 'pink', label: 'pink' },
|
||||
{ value: 'red', label: 'red' },
|
||||
{ value: 'orange', label: 'orange' },
|
||||
{ value: 'green', label: 'green' },
|
||||
{ value: 'cyan', label: 'cyan' },
|
||||
{ value: 'blue', label: 'blue' },
|
||||
{ value: 'purple', label: 'purple' },
|
||||
]
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-system/dict/index')
|
||||
}
|
||||
|
||||
/** 加载字典类型列表 */
|
||||
async function loadDictTypeList() {
|
||||
const list = await getSimpleDictTypeList()
|
||||
dictTypeOptions.value = list.map(item => ({
|
||||
label: item.name,
|
||||
value: item.type,
|
||||
}))
|
||||
}
|
||||
|
||||
/** 加载详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
formData.value = await getDictData(props.id)
|
||||
}
|
||||
|
||||
/** 提交表单 */
|
||||
async function handleSubmit() {
|
||||
const { valid } = await formRef.value.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
|
||||
formLoading.value = true
|
||||
try {
|
||||
if (props.id) {
|
||||
await updateDictData(formData.value)
|
||||
toast.success('修改成功')
|
||||
} else {
|
||||
await createDictData(formData.value)
|
||||
toast.success('新增成功')
|
||||
}
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
await loadDictTypeList()
|
||||
await getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
59
src/pages-system/dict/index.vue
Normal file
59
src/pages-system/dict/index.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="字典管理"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- Tab 切换 -->
|
||||
<view class="bg-white">
|
||||
<wd-tabs v-model="tabIndex" shrink @change="handleTabChange">
|
||||
<wd-tab title="字典类型" />
|
||||
<wd-tab title="字典数据" />
|
||||
</wd-tabs>
|
||||
</view>
|
||||
<!-- 列表内容 -->
|
||||
<TypeList v-show="tabType === 'type'" @select="handleTypeSelect" />
|
||||
<DataList v-show="tabType === 'data'" :dict-type="selectedDictType" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import DataList from './components/data-list.vue'
|
||||
import TypeList from './components/type-list.vue'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const tabTypes: string[] = ['type', 'data']
|
||||
const tabIndex = ref(0)
|
||||
const tabType = computed<string>(() => tabTypes[tabIndex.value])
|
||||
const selectedDictType = ref<string>() // 选中的字典类型
|
||||
|
||||
/** Tab 切换 */
|
||||
function handleTabChange({ index }: { index: number }) {
|
||||
tabIndex.value = index
|
||||
}
|
||||
|
||||
/** 选择字典类型 */
|
||||
function handleTypeSelect(dictType: string) {
|
||||
selectedDictType.value = dictType
|
||||
tabIndex.value = 1 // 切换到字典数据 tab
|
||||
}
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
128
src/pages-system/dict/type/detail/index.vue
Normal file
128
src/pages-system/dict/type/detail/index.vue
Normal file
@@ -0,0 +1,128 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="字典类型详情"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 详情内容 -->
|
||||
<view>
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="字典编号" :value="String(formData?.id ?? '-')" />
|
||||
<wd-cell title="字典名称" :value="String(formData?.name ?? '-')" />
|
||||
<wd-cell title="字典类型" :value="String(formData?.type ?? '-')" />
|
||||
<wd-cell title="状态">
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="formData?.status" />
|
||||
</wd-cell>
|
||||
<wd-cell title="备注" :value="String(formData?.remark ?? '-')" />
|
||||
<wd-cell title="创建时间" :value="formatDateTime(formData?.createTime) || '-'" />
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<view class="fixed bottom-0 left-0 right-0 bg-white p-24rpx">
|
||||
<view class="w-full flex gap-24rpx">
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['system:dict:update'])"
|
||||
class="flex-1" type="warning" @click="handleEdit"
|
||||
>
|
||||
编辑
|
||||
</wd-button>
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['system:dict:delete'])"
|
||||
class="flex-1" type="error" :loading="deleting" @click="handleDelete"
|
||||
>
|
||||
删除
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { DictType } from '@/api/system/dict/type'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { deleteDictType, getDictType } from '@/api/system/dict/type'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const toast = useToast()
|
||||
const formData = ref<DictType>()
|
||||
const deleting = ref(false)
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-system/dict/index')
|
||||
}
|
||||
|
||||
/** 加载详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
toast.loading('加载中...')
|
||||
formData.value = await getDictType(props.id)
|
||||
} finally {
|
||||
toast.close()
|
||||
}
|
||||
}
|
||||
|
||||
/** 编辑 */
|
||||
function handleEdit() {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/dict/type/form/index?id=${props.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除 */
|
||||
function handleDelete() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要删除该字典类型吗?',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) {
|
||||
return
|
||||
}
|
||||
deleting.value = true
|
||||
try {
|
||||
await deleteDictType(props.id)
|
||||
toast.success('删除成功')
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
149
src/pages-system/dict/type/form/index.vue
Normal file
149
src/pages-system/dict/type/form/index.vue
Normal file
@@ -0,0 +1,149 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
:title="getTitle"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 表单区域 -->
|
||||
<view>
|
||||
<wd-form ref="formRef" :model="formData" :rules="formRules">
|
||||
<wd-cell-group border>
|
||||
<wd-input
|
||||
v-model="formData.name"
|
||||
label="字典名称"
|
||||
label-width="200rpx"
|
||||
prop="name"
|
||||
clearable
|
||||
placeholder="请输入字典名称"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="formData.type"
|
||||
label="字典类型"
|
||||
label-width="200rpx"
|
||||
prop="type"
|
||||
clearable
|
||||
:disabled="!!formData.id"
|
||||
placeholder="请输入字典类型"
|
||||
/>
|
||||
<wd-cell title="状态" title-width="200rpx" prop="status" center>
|
||||
<wd-radio-group v-model="formData.status" shape="button">
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-cell>
|
||||
<wd-textarea
|
||||
v-model="formData.remark"
|
||||
label="备注"
|
||||
label-width="200rpx"
|
||||
prop="remark"
|
||||
clearable
|
||||
placeholder="请输入备注"
|
||||
/>
|
||||
</wd-cell-group>
|
||||
</wd-form>
|
||||
</view>
|
||||
|
||||
<!-- 底部保存按钮 -->
|
||||
<view class="fixed bottom-0 left-0 right-0 bg-white p-24rpx">
|
||||
<wd-button
|
||||
type="primary"
|
||||
block
|
||||
:loading="formLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
保存
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { DictType } from '@/api/system/dict/type'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { createDictType, getDictType, updateDictType } from '@/api/system/dict/type'
|
||||
import { getIntDictOptions } from '@/hooks/useDict'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const getTitle = computed(() => props.id ? '编辑字典类型' : '新增字典类型')
|
||||
const formLoading = ref(false)
|
||||
const formData = ref<DictType>({
|
||||
id: undefined,
|
||||
name: '',
|
||||
type: '',
|
||||
status: 0,
|
||||
remark: '',
|
||||
})
|
||||
const formRules = {
|
||||
name: [{ required: true, message: '字典名称不能为空' }],
|
||||
type: [{ required: true, message: '字典类型不能为空' }],
|
||||
status: [{ required: true, message: '状态不能为空' }],
|
||||
}
|
||||
const formRef = ref()
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-system/dict/index')
|
||||
}
|
||||
|
||||
/** 加载详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
formData.value = await getDictType(props.id)
|
||||
}
|
||||
|
||||
/** 提交表单 */
|
||||
async function handleSubmit() {
|
||||
const { valid } = await formRef.value.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
|
||||
formLoading.value = true
|
||||
try {
|
||||
if (props.id) {
|
||||
await updateDictType(formData.value)
|
||||
toast.success('修改成功')
|
||||
} else {
|
||||
await createDictType(formData.value)
|
||||
toast.success('新增成功')
|
||||
}
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
Reference in New Issue
Block a user