feat:【infra】【system】新增文件与字典管理前端页面及相关 API 封装

This commit is contained in:
YunaiV
2025-12-21 11:26:32 +08:00
parent d3014baa2e
commit fc5e60965f
20 changed files with 2918 additions and 0 deletions

View 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}`)
}

View File

@@ -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}`)
}

View 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}`)
}

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>