first commit
This commit is contained in:
146
src/pages-infra/api-access-log/components/search-form.vue
Normal file
146
src/pages-infra/api-access-log/components/search-form.vue
Normal file
@@ -0,0 +1,146 @@
|
||||
<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.userId"
|
||||
placeholder="请输入用户编号"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
应用名
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.applicationName"
|
||||
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({
|
||||
userId: undefined as number | undefined,
|
||||
applicationName: undefined as string | undefined,
|
||||
createTime: [undefined, undefined] as [number | undefined, number | undefined],
|
||||
})
|
||||
|
||||
/** 搜索条件 placeholder 拼接 */
|
||||
const placeholder = computed(() => {
|
||||
const conditions: string[] = []
|
||||
if (formData.userId) {
|
||||
conditions.push(`用户编号:${formData.userId}`)
|
||||
}
|
||||
if (formData.applicationName) {
|
||||
conditions.push(`应用名:${formData.applicationName}`)
|
||||
}
|
||||
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
|
||||
const dateRange = formatDateRange(formData.createTime)
|
||||
emit('search', {
|
||||
userId: formData.userId,
|
||||
applicationName: formData.applicationName,
|
||||
beginTime: dateRange?.[0],
|
||||
endTime: dateRange?.[1],
|
||||
})
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
function handleReset() {
|
||||
formData.userId = undefined
|
||||
formData.applicationName = undefined
|
||||
formData.createTime = [undefined, undefined]
|
||||
visible.value = false
|
||||
emit('reset')
|
||||
}
|
||||
</script>
|
||||
133
src/pages-infra/api-access-log/detail/index.vue
Normal file
133
src/pages-infra/api-access-log/detail/index.vue
Normal file
@@ -0,0 +1,133 @@
|
||||
<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="formData?.id" />
|
||||
<wd-cell title="链路追踪" :value="formData?.traceId || '-'" />
|
||||
<wd-cell title="应用名" :value="formData?.applicationName" />
|
||||
<wd-cell title="用户编号" :value="formData?.userId ?? '-'" />
|
||||
<wd-cell title="用户类型" :value="getDictLabel(DICT_TYPE.USER_TYPE, formData?.userType) || '-'" />
|
||||
<wd-cell title="用户 IP" :value="formData?.userIp" />
|
||||
<wd-cell title="用户 UA" :value="formData?.userAgent" />
|
||||
<wd-cell title="请求信息" :value="getRequestInfo()" />
|
||||
<wd-cell title="请求参数" is-link @click="handleCopyText(formData?.requestParams, '请求参数')">
|
||||
<view class="max-w-400rpx truncate text-right">
|
||||
{{ formData?.requestParams || '-' }}
|
||||
</view>
|
||||
</wd-cell>
|
||||
<wd-cell title="请求结果" is-link @click="handleCopyText(formData?.responseBody, '请求结果')">
|
||||
<view class="max-w-400rpx truncate text-right">
|
||||
{{ formData?.responseBody || '-' }}
|
||||
</view>
|
||||
</wd-cell>
|
||||
<wd-cell title="请求时间" :value="getRequestTimeRange()" />
|
||||
<wd-cell title="请求耗时" :value="`${formData.duration} ms`" />
|
||||
<wd-cell title="操作结果">
|
||||
<template v-if="formData?.resultCode === 0">
|
||||
<wd-tag type="success" plain>
|
||||
正常
|
||||
</wd-tag>
|
||||
</template>
|
||||
<template v-else-if="formData?.resultCode">
|
||||
<text>失败 | {{ formData.resultCode }} | {{ formData.resultMsg }}</text>
|
||||
</template>
|
||||
<template v-else>
|
||||
<text>-</text>
|
||||
</template>
|
||||
</wd-cell>
|
||||
<wd-cell title="操作模块" :value="formData?.operateModule || '-'" />
|
||||
<wd-cell title="操作名" :value="formData?.operateName || '-'" />
|
||||
<wd-cell title="操作类型" :value="getDictLabel(DICT_TYPE.INFRA_OPERATE_TYPE, formData?.operateType) || '-'" />
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ApiAccessLog } from '@/api/infra/api-access-log'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { getApiAccessLog } from '@/api/infra/api-access-log'
|
||||
import { getDictLabel } from '@/hooks/useDict'
|
||||
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 formData = ref<ApiAccessLog>() // 详情数据
|
||||
const toast = useToast()
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-infra/api-access-log/index')
|
||||
}
|
||||
|
||||
/** 复制文本并提示 */
|
||||
function handleCopyText(text?: string, title?: string) {
|
||||
if (!text || text === '-') {
|
||||
return
|
||||
}
|
||||
uni.setClipboardData({
|
||||
data: text,
|
||||
success: () => {
|
||||
uni.hideToast()
|
||||
toast.success(`${title || '内容'}已复制`)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 加载详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
toast.loading('加载中...')
|
||||
try {
|
||||
formData.value = await getApiAccessLog(props.id)
|
||||
} finally {
|
||||
toast.close()
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取请求信息 */
|
||||
function getRequestInfo() {
|
||||
if (formData.value?.requestMethod && formData.value?.requestUrl) {
|
||||
return `${formData.value.requestMethod} ${formData.value.requestUrl}`
|
||||
}
|
||||
return '-'
|
||||
}
|
||||
|
||||
/** 获取请求时间范围 */
|
||||
function getRequestTimeRange() {
|
||||
if (formData.value?.beginTime && formData.value?.endTime) {
|
||||
return `${formatDateTime(formData.value.beginTime)} ~ ${formatDateTime(formData.value.endTime)}`
|
||||
}
|
||||
return '-'
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
158
src/pages-infra/api-access-log/index.vue
Normal file
158
src/pages-infra/api-access-log/index.vue
Normal file
@@ -0,0 +1,158 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="API 访问日志"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 搜索组件 -->
|
||||
<SearchForm @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 gap-16rpx">
|
||||
<view class="min-w-0 flex-1 truncate text-28rpx text-[#333] font-semibold">
|
||||
{{ item.requestMethod }} {{ item.requestUrl }}
|
||||
</view>
|
||||
<view class="flex-shrink-0">
|
||||
<wd-tag v-if="item.resultCode === 0" type="success" plain>
|
||||
成功
|
||||
</wd-tag>
|
||||
<wd-tag v-else type="danger" plain>
|
||||
失败
|
||||
</wd-tag>
|
||||
</view>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-26rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">应用名:</text>
|
||||
<text>{{ item.applicationName }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-26rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">用户编号:</text>
|
||||
<text>{{ item.userId }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-26rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">执行时长:</text>
|
||||
<text>{{ item.duration }} ms</text>
|
||||
</view>
|
||||
<view v-if="item.operateName" class="mb-12rpx flex items-center text-26rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">操作名:</text>
|
||||
<text class="line-clamp-1">{{ item.operateName }}</text>
|
||||
</view>
|
||||
<view class="flex items-center text-24rpx text-[#999]">
|
||||
<text>{{ formatDateTime(item.beginTime) }}</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>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ApiAccessLog } from '@/api/infra/api-access-log'
|
||||
import type { LoadMoreState } from '@/http/types'
|
||||
import { onReachBottom } from '@dcloudio/uni-app'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { getApiAccessLogPage } from '@/api/infra/api-access-log'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import SearchForm from './components/search-form.vue'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const total = ref(0)
|
||||
const list = ref<ApiAccessLog[]>([])
|
||||
const loadMoreState = ref<LoadMoreState>('loading')
|
||||
const queryParams = ref({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus()
|
||||
}
|
||||
|
||||
/** 查询日志列表 */
|
||||
async function getList() {
|
||||
loadMoreState.value = 'loading'
|
||||
try {
|
||||
const data = await getApiAccessLogPage(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 handleDetail(item: ApiAccessLog) {
|
||||
uni.navigateTo({
|
||||
url: `/pages-infra/api-access-log/detail/index?id=${item.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 触底加载更多 */
|
||||
onReachBottom(() => {
|
||||
loadMore()
|
||||
})
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
172
src/pages-infra/api-error-log/components/search-form.vue
Normal file
172
src/pages-infra/api-error-log/components/search-form.vue
Normal file
@@ -0,0 +1,172 @@
|
||||
<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.userId"
|
||||
placeholder="请输入用户编号"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
应用名
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.applicationName"
|
||||
placeholder="请输入应用名"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
处理状态
|
||||
</view>
|
||||
<wd-radio-group v-model="formData.processStatus" shape="button">
|
||||
<wd-radio :value="-1">
|
||||
全部
|
||||
</wd-radio>
|
||||
<wd-radio :value="0">
|
||||
未处理
|
||||
</wd-radio>
|
||||
<wd-radio :value="1">
|
||||
已处理
|
||||
</wd-radio>
|
||||
<wd-radio :value="2">
|
||||
已忽略
|
||||
</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="visibleExceptionTime[0] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.exceptionTime?.[0]) || '开始日期' }}
|
||||
</view>
|
||||
</view>
|
||||
-
|
||||
<view class="flex-1" @click="visibleExceptionTime[1] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.exceptionTime?.[1]) || '结束日期' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleExceptionTime[0]" v-model="tempExceptionTime[0]" type="date" />
|
||||
<view v-if="visibleExceptionTime[0]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleExceptionTime[0] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleExceptionTime0Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleExceptionTime[1]" v-model="tempExceptionTime[1]" type="date" />
|
||||
<view v-if="visibleExceptionTime[1]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleExceptionTime[1] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleExceptionTime1Confirm">
|
||||
确定
|
||||
</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({
|
||||
userId: undefined as number | undefined,
|
||||
applicationName: undefined as string | undefined,
|
||||
processStatus: -1, // -1 表示全部
|
||||
exceptionTime: [undefined, undefined] as [number | undefined, number | undefined],
|
||||
})
|
||||
|
||||
/** 搜索条件 placeholder 拼接 */
|
||||
const placeholder = computed(() => {
|
||||
const conditions: string[] = []
|
||||
if (formData.userId) {
|
||||
conditions.push(`用户编号:${formData.userId}`)
|
||||
}
|
||||
if (formData.applicationName) {
|
||||
conditions.push(`应用名:${formData.applicationName}`)
|
||||
}
|
||||
if (formData.processStatus !== -1) {
|
||||
const statusMap: Record<number, string> = { 0: '未处理', 1: '已处理', 2: '已忽略' }
|
||||
conditions.push(`状态:${statusMap[formData.processStatus]}`)
|
||||
}
|
||||
if (formData.exceptionTime?.[0] && formData.exceptionTime?.[1]) {
|
||||
conditions.push(`时间:${formatDate(formData.exceptionTime[0])}~${formatDate(formData.exceptionTime[1])}`)
|
||||
}
|
||||
return conditions.length > 0 ? conditions.join(' | ') : '搜索日志'
|
||||
})
|
||||
|
||||
// 时间范围选择器状态
|
||||
const visibleExceptionTime = ref<[boolean, boolean]>([false, false])
|
||||
const tempExceptionTime = ref<[number, number]>([Date.now(), Date.now()])
|
||||
|
||||
/** 异常时间[0]确认 */
|
||||
function handleExceptionTime0Confirm() {
|
||||
formData.exceptionTime = [tempExceptionTime.value[0], formData.exceptionTime?.[1]]
|
||||
visibleExceptionTime.value[0] = false
|
||||
}
|
||||
|
||||
/** 异常时间[1]确认 */
|
||||
function handleExceptionTime1Confirm() {
|
||||
formData.exceptionTime = [formData.exceptionTime?.[0], tempExceptionTime.value[1]]
|
||||
visibleExceptionTime.value[1] = false
|
||||
}
|
||||
|
||||
/** 搜索 */
|
||||
function handleSearch() {
|
||||
visible.value = false
|
||||
emit('search', {
|
||||
...formData,
|
||||
processStatus: formData.processStatus === -1 ? undefined : formData.processStatus,
|
||||
exceptionTime: formatDateRange(formData.exceptionTime),
|
||||
})
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
function handleReset() {
|
||||
formData.userId = undefined
|
||||
formData.applicationName = undefined
|
||||
formData.processStatus = -1
|
||||
formData.exceptionTime = [undefined, undefined]
|
||||
visible.value = false
|
||||
emit('reset')
|
||||
}
|
||||
</script>
|
||||
160
src/pages-infra/api-error-log/detail/index.vue
Normal file
160
src/pages-infra/api-error-log/detail/index.vue
Normal file
@@ -0,0 +1,160 @@
|
||||
<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="formData?.id" />
|
||||
<wd-cell title="链路追踪" :value="formData?.traceId || '-'" />
|
||||
<wd-cell title="应用名" :value="formData?.applicationName" />
|
||||
<wd-cell title="用户编号" :value="formData?.userId ?? '-'" />
|
||||
<wd-cell title="用户类型" :value="getDictLabel(DICT_TYPE.USER_TYPE, formData?.userType) || '-'" />
|
||||
<wd-cell title="用户 IP" :value="formData?.userIp" />
|
||||
<wd-cell title="用户 UA" :value="formData?.userAgent" />
|
||||
<wd-cell title="请求信息" :value="getRequestInfo()" />
|
||||
<wd-cell title="请求参数" is-link @click="handleCopyText(formData?.requestParams, '请求参数')">
|
||||
<view class="max-w-400rpx truncate text-right">
|
||||
{{ formData?.requestParams || '-' }}
|
||||
</view>
|
||||
</wd-cell>
|
||||
<wd-cell title="异常时间" :value="formatDateTime(formData?.exceptionTime) || '-'" />
|
||||
<wd-cell title="异常名" :value="formData?.exceptionName || '-'" />
|
||||
<wd-cell title="处理状态">
|
||||
<dict-tag :type="DICT_TYPE.INFRA_API_ERROR_LOG_PROCESS_STATUS" :value="formData?.processStatus" />
|
||||
</wd-cell>
|
||||
<wd-cell v-if="formData?.processUserId" title="处理人" :value="formData.processUserId" />
|
||||
<wd-cell v-if="formData?.processTime" title="处理时间" :value="formatDateTime(formData.processTime) || '-'" />
|
||||
</wd-cell-group>
|
||||
|
||||
<!-- 异常堆栈 -->
|
||||
<view v-if="formData?.exceptionStackTrace" class="mt-24rpx">
|
||||
<view class="mb-16rpx text-28rpx text-[#333] font-semibold">
|
||||
异常堆栈
|
||||
</view>
|
||||
<view class="rounded-12rpx bg-white p-24rpx shadow-sm">
|
||||
<scroll-view scroll-y class="max-h-600rpx">
|
||||
<text class="whitespace-pre-wrap break-all text-24rpx text-[#666] leading-relaxed">{{ formData.exceptionStackTrace }}</text>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<view v-if="formData?.processStatus === InfraApiErrorLogProcessStatusEnum.INIT" class="yd-detail-footer">
|
||||
<view class="yd-detail-footer-actions">
|
||||
<wd-button class="flex-1" type="success" :loading="processing" @click="handleProcess(InfraApiErrorLogProcessStatusEnum.DONE)">
|
||||
已处理
|
||||
</wd-button>
|
||||
<wd-button class="flex-1" type="warning" :loading="processing" @click="handleProcess(InfraApiErrorLogProcessStatusEnum.IGNORE)">
|
||||
已忽略
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ApiErrorLog } from '@/api/infra/api-error-log'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { getApiErrorLog, updateApiErrorLogStatus } from '@/api/infra/api-error-log'
|
||||
import { getDictLabel } from '@/hooks/useDict'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE, InfraApiErrorLogProcessStatusEnum } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
|
||||
const props = defineProps<{
|
||||
id: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const formData = ref<ApiErrorLog>() // 详情数据
|
||||
const processing = ref(false) // 处理中
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-infra/api-error-log/index')
|
||||
}
|
||||
|
||||
/** 复制文本并提示 */
|
||||
function handleCopyText(text?: string, title?: string) {
|
||||
if (!text || text === '-') {
|
||||
return
|
||||
}
|
||||
uni.setClipboardData({
|
||||
data: text,
|
||||
success: () => {
|
||||
uni.hideToast()
|
||||
toast.success(`${title || '内容'}已复制`)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 加载详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
toast.loading('加载中...')
|
||||
try {
|
||||
formData.value = await getApiErrorLog(props.id)
|
||||
} finally {
|
||||
toast.close()
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取请求信息 */
|
||||
function getRequestInfo() {
|
||||
if (formData.value?.requestMethod && formData.value?.requestUrl) {
|
||||
return `${formData.value.requestMethod} ${formData.value.requestUrl}`
|
||||
}
|
||||
return '-'
|
||||
}
|
||||
|
||||
/** 处理日志 */
|
||||
function handleProcess(processStatus: number) {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
const statusText = processStatus === InfraApiErrorLogProcessStatusEnum.DONE ? '已处理' : '已忽略'
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: `确定标记为${statusText}吗?`,
|
||||
success: async (res) => {
|
||||
if (!res.confirm) {
|
||||
return
|
||||
}
|
||||
processing.value = true
|
||||
try {
|
||||
await updateApiErrorLogStatus(props.id, processStatus)
|
||||
toast.success('操作成功')
|
||||
// 刷新详情
|
||||
await getDetail()
|
||||
} finally {
|
||||
processing.value = false
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
149
src/pages-infra/api-error-log/index.vue
Normal file
149
src/pages-infra/api-error-log/index.vue
Normal file
@@ -0,0 +1,149 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="API 错误日志"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 搜索组件 -->
|
||||
<SearchForm @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="line-clamp-1 mr-16rpx flex-1 text-28rpx text-[#333] font-semibold">
|
||||
{{ item.exceptionName }}
|
||||
</view>
|
||||
<!-- DONE @芽艺:字典 -->
|
||||
<dict-tag :type="DICT_TYPE.INFRA_API_ERROR_LOG_PROCESS_STATUS" :value="item.processStatus" />
|
||||
</view>
|
||||
<view class="mb-12rpx flex text-26rpx text-[#666]">
|
||||
<text class="mr-8rpx flex-shrink-0 text-[#999]">请求:</text>
|
||||
<text class="line-clamp-2 break-all">{{ item.requestMethod }} {{ item.requestUrl }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-26rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">应用名:</text>
|
||||
<text>{{ item.applicationName }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-26rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">用户编号:</text>
|
||||
<text>{{ item.userId }}</text>
|
||||
</view>
|
||||
<view class="flex items-center text-24rpx text-[#999]">
|
||||
<text>{{ formatDateTime(item.exceptionTime) }}</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>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ApiErrorLog } from '@/api/infra/api-error-log'
|
||||
import type { LoadMoreState } from '@/http/types'
|
||||
import { onReachBottom } from '@dcloudio/uni-app'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { getApiErrorLogPage } from '@/api/infra/api-error-log'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import SearchForm from './components/search-form.vue'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const total = ref(0)
|
||||
const list = ref<ApiErrorLog[]>([])
|
||||
const loadMoreState = ref<LoadMoreState>('loading')
|
||||
const queryParams = ref({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus()
|
||||
}
|
||||
|
||||
/** 查询日志列表 */
|
||||
async function getList() {
|
||||
loadMoreState.value = 'loading'
|
||||
try {
|
||||
const data = await getApiErrorLogPage(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 handleDetail(item: ApiErrorLog) {
|
||||
uni.navigateTo({
|
||||
url: `/pages-infra/api-error-log/detail/index?id=${item.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 触底加载更多 */
|
||||
onReachBottom(() => {
|
||||
loadMore()
|
||||
})
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
169
src/pages-infra/config/components/search-form.vue
Normal file
169
src/pages-infra/config/components/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.key"
|
||||
placeholder="请输入参数键名"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
系统内置
|
||||
</view>
|
||||
<wd-radio-group v-model="formData.type" shape="button">
|
||||
<wd-radio :value="-1">
|
||||
全部
|
||||
</wd-radio>
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.INFRA_CONFIG_TYPE)"
|
||||
: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,
|
||||
key: undefined as string | undefined,
|
||||
type: -1, // -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.key) {
|
||||
conditions.push(`键名:${formData.key}`)
|
||||
}
|
||||
if (formData.type !== -1) {
|
||||
conditions.push(`类型:${getDictLabel(DICT_TYPE.INFRA_CONFIG_TYPE, 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', {
|
||||
name: formData.name || undefined,
|
||||
key: formData.key || undefined,
|
||||
type: formData.type === -1 ? undefined : formData.type,
|
||||
createTime: formatDateRange(formData.createTime),
|
||||
})
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
function handleReset() {
|
||||
formData.name = undefined
|
||||
formData.key = undefined
|
||||
formData.type = -1
|
||||
formData.createTime = [undefined, undefined]
|
||||
visible.value = false
|
||||
emit('reset')
|
||||
}
|
||||
</script>
|
||||
133
src/pages-infra/config/detail/index.vue
Normal file
133
src/pages-infra/config/detail/index.vue
Normal file
@@ -0,0 +1,133 @@
|
||||
<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="formData?.id" />
|
||||
<wd-cell title="参数分类" :value="formData?.category" />
|
||||
<wd-cell title="参数名称" :value="formData?.name" />
|
||||
<wd-cell title="参数键名" :value="formData?.key" />
|
||||
<wd-cell title="参数键值" :value="formData?.value" />
|
||||
<wd-cell title="是否可见">
|
||||
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="formData?.visible" />
|
||||
</wd-cell>
|
||||
<wd-cell title="系统内置">
|
||||
<dict-tag :type="DICT_TYPE.INFRA_CONFIG_TYPE" :value="formData?.type" />
|
||||
</wd-cell>
|
||||
<wd-cell title="备注" :value="formData?.remark ?? '-'" />
|
||||
<wd-cell title="创建时间" :value="formatDateTime(formData?.createTime) || '-'" />
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<view class="yd-detail-footer-actions">
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['infra:config:update'])"
|
||||
class="flex-1" type="warning" @click="handleEdit"
|
||||
>
|
||||
编辑
|
||||
</wd-button>
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['infra:config:delete'])"
|
||||
class="flex-1" type="error" :loading="deleting" @click="handleDelete"
|
||||
>
|
||||
删除
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Config } from '@/api/infra/config'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { deleteConfig, getConfig } from '@/api/infra/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<Config>()
|
||||
const deleting = ref(false)
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-infra/config/index')
|
||||
}
|
||||
|
||||
/** 加载参数配置详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
toast.loading('加载中...')
|
||||
formData.value = await getConfig(props.id)
|
||||
} finally {
|
||||
toast.close()
|
||||
}
|
||||
}
|
||||
|
||||
/** 编辑参数配置 */
|
||||
function handleEdit() {
|
||||
uni.navigateTo({
|
||||
url: `/pages-infra/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 deleteConfig(props.id)
|
||||
toast.success('删除成功')
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
167
src/pages-infra/config/form/index.vue
Normal file
167
src/pages-infra/config/form/index.vue
Normal file
@@ -0,0 +1,167 @@
|
||||
<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.category"
|
||||
label="参数分类"
|
||||
label-width="200rpx"
|
||||
prop="category"
|
||||
clearable
|
||||
placeholder="请输入参数分类"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="formData.name"
|
||||
label="参数名称"
|
||||
label-width="200rpx"
|
||||
prop="name"
|
||||
clearable
|
||||
placeholder="请输入参数名称"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="formData.key"
|
||||
label="参数键名"
|
||||
label-width="200rpx"
|
||||
prop="key"
|
||||
clearable
|
||||
placeholder="请输入参数键名"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="formData.value"
|
||||
label="参数键值"
|
||||
label-width="200rpx"
|
||||
prop="value"
|
||||
clearable
|
||||
placeholder="请输入参数键值"
|
||||
/>
|
||||
<wd-cell title="是否可见" title-width="200rpx" prop="visible" center>
|
||||
<wd-radio-group v-model="formData.visible" shape="button">
|
||||
<wd-radio :value="true">
|
||||
是
|
||||
</wd-radio>
|
||||
<wd-radio :value="false">
|
||||
否
|
||||
</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="yd-detail-footer">
|
||||
<wd-button
|
||||
type="primary"
|
||||
block
|
||||
:loading="formLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
保存
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance } from 'wot-design-uni/components/wd-form/types'
|
||||
import type { Config } from '@/api/infra/config'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { createConfig, getConfig, updateConfig } from '@/api/infra/config'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
|
||||
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<Config>({
|
||||
id: undefined,
|
||||
category: '',
|
||||
name: '',
|
||||
key: '',
|
||||
value: '',
|
||||
type: undefined,
|
||||
visible: true,
|
||||
remark: '',
|
||||
})
|
||||
const formRules = {
|
||||
category: [{ required: true, message: '参数分类不能为空' }],
|
||||
name: [{ required: true, message: '参数名称不能为空' }],
|
||||
key: [{ required: true, message: '参数键名不能为空' }],
|
||||
value: [{ required: true, message: '参数键值不能为空' }],
|
||||
visible: [{ required: true, message: '是否可见不能为空' }],
|
||||
}
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-infra/config/index')
|
||||
}
|
||||
|
||||
/** 加载参数配置详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
formData.value = await getConfig(props.id)
|
||||
}
|
||||
|
||||
/** 提交表单 */
|
||||
async function handleSubmit() {
|
||||
const { valid } = await formRef.value.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
|
||||
formLoading.value = true
|
||||
try {
|
||||
if (props.id) {
|
||||
await updateConfig(formData.value)
|
||||
toast.success('修改成功')
|
||||
} else {
|
||||
await createConfig(formData.value)
|
||||
toast.success('新增成功')
|
||||
}
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
171
src/pages-infra/config/index.vue
Normal file
171
src/pages-infra/config/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"
|
||||
/>
|
||||
|
||||
<!-- 搜索组件 -->
|
||||
<SearchForm @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.INFRA_CONFIG_TYPE" :value="item.type" />
|
||||
</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.category }}</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.key }}</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.value }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">是否可见:</text>
|
||||
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="item.visible" />
|
||||
</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(['infra:config:create'])"
|
||||
position="right-bottom"
|
||||
type="primary"
|
||||
:expandable="false"
|
||||
@click="handleAdd"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Config } from '@/api/infra/config'
|
||||
import type { LoadMoreState } from '@/http/types'
|
||||
import { onReachBottom } from '@dcloudio/uni-app'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { getConfigPage } from '@/api/infra/config'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import SearchForm from './components/search-form.vue'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const total = ref(0)
|
||||
const list = ref<Config[]>([])
|
||||
const loadMoreState = ref<LoadMoreState>('loading')
|
||||
const queryParams = ref({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus()
|
||||
}
|
||||
|
||||
/** 查询参数配置列表 */
|
||||
async function getList() {
|
||||
loadMoreState.value = 'loading'
|
||||
try {
|
||||
const data = await getConfigPage(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/config/form/index',
|
||||
})
|
||||
}
|
||||
|
||||
/** 查看详情 */
|
||||
function handleDetail(item: Config) {
|
||||
uni.navigateTo({
|
||||
url: `/pages-infra/config/detail/index?id=${item.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 触底加载更多 */
|
||||
onReachBottom(() => {
|
||||
loadMore()
|
||||
})
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
142
src/pages-infra/data-source-config/detail/index.vue
Normal file
142
src/pages-infra/data-source-config/detail/index.vue
Normal file
@@ -0,0 +1,142 @@
|
||||
<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="formData?.id" />
|
||||
<wd-cell title="数据源名称" :value="formData?.name" />
|
||||
<wd-cell title="数据源连接" is-link @click="handleCopyText(formData?.url, '数据源连接')">
|
||||
<view class="max-w-400rpx truncate text-right">
|
||||
{{ formData?.url }}
|
||||
</view>
|
||||
</wd-cell>
|
||||
<wd-cell title="用户名" :value="formData?.username" />
|
||||
<wd-cell title="创建时间" :value="formatDateTime(formData?.createTime)" />
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作按钮(主数据源不可编辑/删除) -->
|
||||
<view v-if="formData?.id !== 0" class="yd-detail-footer">
|
||||
<view class="yd-detail-footer-actions">
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['infra:data-source-config:update'])"
|
||||
class="flex-1" type="warning" @click="handleEdit"
|
||||
>
|
||||
编辑
|
||||
</wd-button>
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['infra:data-source-config:delete'])"
|
||||
class="flex-1" type="error" :loading="deleting" @click="handleDelete"
|
||||
>
|
||||
删除
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { DataSourceConfig } from '@/api/infra/data-source-config'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { deleteDataSourceConfig, getDataSourceConfig } from '@/api/infra/data-source-config'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
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<DataSourceConfig>()
|
||||
const deleting = ref(false)
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-infra/data-source-config/index')
|
||||
}
|
||||
|
||||
/** 复制文本并提示 */
|
||||
function handleCopyText(text?: string, title?: string) {
|
||||
if (!text || text === '-') {
|
||||
return
|
||||
}
|
||||
uni.setClipboardData({
|
||||
data: text,
|
||||
success: () => {
|
||||
uni.hideToast()
|
||||
toast.success(`${title || '内容'}已复制`)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 加载数据源配置详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
toast.loading('加载中...')
|
||||
formData.value = await getDataSourceConfig(props.id)
|
||||
} finally {
|
||||
toast.close()
|
||||
}
|
||||
}
|
||||
|
||||
/** 编辑数据源配置 */
|
||||
function handleEdit() {
|
||||
uni.navigateTo({
|
||||
url: `/pages-infra/data-source-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 deleteDataSourceConfig(props.id)
|
||||
toast.success('删除成功')
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
146
src/pages-infra/data-source-config/form/index.vue
Normal file
146
src/pages-infra/data-source-config/form/index.vue
Normal file
@@ -0,0 +1,146 @@
|
||||
<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.url"
|
||||
label="数据源连接"
|
||||
label-width="200rpx"
|
||||
prop="url"
|
||||
clearable
|
||||
placeholder="请输入数据源连接"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="formData.username"
|
||||
label="用户名"
|
||||
label-width="200rpx"
|
||||
prop="username"
|
||||
clearable
|
||||
placeholder="请输入用户名"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="formData.password"
|
||||
label="密码"
|
||||
label-width="200rpx"
|
||||
prop="password"
|
||||
show-password
|
||||
clearable
|
||||
placeholder="请输入密码"
|
||||
/>
|
||||
</wd-cell-group>
|
||||
</wd-form>
|
||||
</view>
|
||||
|
||||
<!-- 底部保存按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<wd-button
|
||||
type="primary"
|
||||
block
|
||||
:loading="formLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
保存
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance } from 'wot-design-uni/components/wd-form/types'
|
||||
import type { DataSourceConfig } from '@/api/infra/data-source-config'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { createDataSourceConfig, getDataSourceConfig, updateDataSourceConfig } from '@/api/infra/data-source-config'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
|
||||
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<DataSourceConfig>({
|
||||
id: undefined,
|
||||
name: '',
|
||||
url: '',
|
||||
username: '',
|
||||
password: '',
|
||||
})
|
||||
const formRules = {
|
||||
name: [{ required: true, message: '数据源名称不能为空' }],
|
||||
url: [{ required: true, message: '数据源连接不能为空' }],
|
||||
username: [{ required: true, message: '用户名不能为空' }],
|
||||
password: [{ required: true, message: '密码不能为空' }],
|
||||
}
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-infra/data-source-config/index')
|
||||
}
|
||||
|
||||
/** 加载数据源详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
formData.value = await getDataSourceConfig(props.id)
|
||||
}
|
||||
|
||||
/** 提交表单 */
|
||||
async function handleSubmit() {
|
||||
const { valid } = await formRef.value.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
|
||||
formLoading.value = true
|
||||
try {
|
||||
if (props.id) {
|
||||
await updateDataSourceConfig(formData.value)
|
||||
toast.success('修改成功')
|
||||
} else {
|
||||
await createDataSourceConfig(formData.value)
|
||||
toast.success('新增成功')
|
||||
}
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
122
src/pages-infra/data-source-config/index.vue
Normal file
122
src/pages-infra/data-source-config/index.vue
Normal file
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="数据源配置管理"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 数据源配置列表 -->
|
||||
<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 v-if="item.id === 0" class="rounded-4rpx bg-[#e6f7ff] px-12rpx py-4rpx text-24rpx text-[#1890ff]">
|
||||
主数据源
|
||||
</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.id }}</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.url }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">用户名:</text>
|
||||
<text>{{ item.username }}</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="!loading && list.length === 0" class="py-100rpx text-center">
|
||||
<wd-status-tip image="content" tip="暂无数据源配置数据" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 新增按钮 -->
|
||||
<wd-fab
|
||||
v-if="hasAccessByCodes(['infra:data-source-config:create'])"
|
||||
position="right-bottom"
|
||||
type="primary"
|
||||
:expandable="false"
|
||||
@click="handleAdd"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { DataSourceConfig } from '@/api/infra/data-source-config'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { getDataSourceConfigList } from '@/api/infra/data-source-config'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const toast = useToast()
|
||||
const list = ref<DataSourceConfig[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus()
|
||||
}
|
||||
|
||||
/** 查询数据源配置列表 */
|
||||
async function getList() {
|
||||
loading.value = true
|
||||
try {
|
||||
toast.loading('加载中...')
|
||||
list.value = await getDataSourceConfigList()
|
||||
} finally {
|
||||
loading.value = false
|
||||
toast.close()
|
||||
}
|
||||
}
|
||||
|
||||
/** 新增数据源配置 */
|
||||
function handleAdd() {
|
||||
uni.navigateTo({
|
||||
url: '/pages-infra/data-source-config/form/index',
|
||||
})
|
||||
}
|
||||
|
||||
/** 查看详情 */
|
||||
function handleDetail(item: DataSourceConfig) {
|
||||
uni.navigateTo({
|
||||
url: `/pages-infra/data-source-config/detail/index?id=${item.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
208
src/pages-infra/file/components/config-list.vue
Normal file
208
src/pages-infra/file/components/config-list.vue
Normal file
@@ -0,0 +1,208 @@
|
||||
<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']) && !item.master"
|
||||
size="small" type="info" @click.stop="handleMaster(item)"
|
||||
>
|
||||
设为主配置
|
||||
</wd-button>
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['infra:file-config:update'])"
|
||||
size="small" type="info" @click.stop="handleTest(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) {
|
||||
toast.loading('测试上传中...')
|
||||
const url = await testFileConfig(item.id!)
|
||||
toast.close()
|
||||
uni.showModal({
|
||||
title: '测试上传成功',
|
||||
content: '是否要访问该文件?',
|
||||
confirmText: '访问',
|
||||
success: (res) => {
|
||||
if (!res.confirm || !url) {
|
||||
return
|
||||
}
|
||||
// 复制链接到剪贴板
|
||||
uni.setClipboardData({
|
||||
data: url,
|
||||
success: () => {
|
||||
uni.hideToast()
|
||||
toast.success('链接已复制,请在浏览器中打开')
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 设为主配置 */
|
||||
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.close()
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 触底加载更多 */
|
||||
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>
|
||||
225
src/pages-infra/file/components/file-list.vue
Normal file
225
src/pages-infra/file/components/file-list.vue
Normal file
@@ -0,0 +1,225 @@
|
||||
<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="line-clamp-1 text-32rpx text-[#333] font-semibold">
|
||||
{{ 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 class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">上传时间:</text>
|
||||
<text>{{ formatDateTime(item.createTime) }}</text>
|
||||
</view>
|
||||
<view v-if="item.type && item.type.includes('image')" class="mb-12rpx">
|
||||
<wd-img
|
||||
:src="item.url"
|
||||
mode="aspectFit"
|
||||
width="100%"
|
||||
height="200rpx"
|
||||
enable-preview
|
||||
@click.stop
|
||||
/>
|
||||
</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 { formatFileSize } from '@/utils/download'
|
||||
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,
|
||||
})
|
||||
|
||||
/** 查询列表 */
|
||||
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 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="formData?.id" />
|
||||
<wd-cell title="配置名" :value="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="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="formData.config.basePath ?? '-'" />
|
||||
<template v-if="formData.storage >= 11 && formData.storage <= 12">
|
||||
<wd-cell title="主机地址" :value="formData.config.host ?? '-'" />
|
||||
<wd-cell title="主机端口" :value="formData.config.port ?? '-'" />
|
||||
<wd-cell title="用户名" :value="formData.config.username ?? '-'" />
|
||||
<wd-cell title="密码" :value="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="formData.config.endpoint" />
|
||||
<wd-cell title="存储 bucket" :value="formData.config.bucket" />
|
||||
<wd-cell title="accessKey" :value="formData.config.accessKey" />
|
||||
<wd-cell title="accessSecret" :value="formData.config.accessSecret" />
|
||||
<wd-cell title="Path Style" :value="formData.config.enablePathStyleAccess ? '启用' : '禁用'" />
|
||||
<wd-cell title="公开访问" :value="formData.config.enablePublicAccess ? '公开' : '私有'" />
|
||||
<wd-cell title="区域" :value="formData.config.region ?? '-'" />
|
||||
</template>
|
||||
<!-- 通用配置 -->
|
||||
<wd-cell title="自定义域名" :value="formData.config.domain ?? '-'" />
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<view class="yd-detail-footer-actions">
|
||||
<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>
|
||||
308
src/pages-infra/file/config/form/index.vue
Normal file
308
src/pages-infra/file/config/form/index.vue
Normal file
@@ -0,0 +1,308 @@
|
||||
<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="yd-detail-footer">
|
||||
<wd-button
|
||||
type="primary"
|
||||
block
|
||||
:loading="formLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
保存
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance } from 'wot-design-uni/components/wd-form/types'
|
||||
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<FormInstance>()
|
||||
|
||||
/** 返回上一页 */
|
||||
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>
|
||||
145
src/pages-infra/file/detail/index.vue
Normal file
145
src/pages-infra/file/detail/index.vue
Normal file
@@ -0,0 +1,145 @@
|
||||
<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="formData?.id" />
|
||||
<wd-cell title="文件名" :value="formData?.name" />
|
||||
<wd-cell title="文件路径" :value="formData?.path" />
|
||||
<wd-cell title="文件 URL" :value="formData?.url" />
|
||||
<wd-cell title="文件大小" :value="formatFileSize(formData?.size)" />
|
||||
<wd-cell title="文件类型" :value="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>
|
||||
<wd-img
|
||||
:src="formData.url"
|
||||
mode="aspectFit"
|
||||
width="100%"
|
||||
height="400rpx"
|
||||
enable-preview
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<view class="yd-detail-footer-actions">
|
||||
<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 type { FileVO } from '@/api/infra/file'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { deleteFile, getFile } from '@/api/infra/file'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import { formatFileSize } from '@/utils/download'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const toast = useToast()
|
||||
const formData = ref<FileVO>()
|
||||
const deleting = ref(false)
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-infra/file/index')
|
||||
}
|
||||
|
||||
/** 加载详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
toast.loading('加载中...')
|
||||
formData.value = await getFile(props.id)
|
||||
} finally {
|
||||
toast.close()
|
||||
}
|
||||
}
|
||||
|
||||
/** 复制链接 */
|
||||
function handleCopyUrl() {
|
||||
if (!formData.value?.url) {
|
||||
toast.show('文件 URL 为空')
|
||||
return
|
||||
}
|
||||
uni.setClipboardData({
|
||||
data: formData.value.url,
|
||||
success: () => {
|
||||
uni.hideToast()
|
||||
toast.success('复制成功')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除 */
|
||||
function handleDelete() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要删除该文件吗?',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) {
|
||||
return
|
||||
}
|
||||
deleting.value = true
|
||||
try {
|
||||
await deleteFile(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>
|
||||
158
src/pages-infra/job/components/job-list.vue
Normal file
158
src/pages-infra/job/components/job-list.vue
Normal file
@@ -0,0 +1,158 @@
|
||||
<template>
|
||||
<view>
|
||||
<!-- 搜索组件 -->
|
||||
<JobSearchForm @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.INFRA_JOB_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.handlerName }}</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.handlerParam || '-' }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">CRON 表达式:</text>
|
||||
<text class="min-w-0 flex-1 truncate">{{ item.cronExpression }}</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="flex justify-end -mt-8">
|
||||
<wd-button size="small" type="info" @click.stop="handleViewLog(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:job:create'])"
|
||||
position="right-bottom"
|
||||
type="primary"
|
||||
:expandable="false"
|
||||
@click="handleAdd"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Job } from '@/api/infra/job'
|
||||
import type { LoadMoreState } from '@/http/types'
|
||||
import { ref } from 'vue'
|
||||
import { getJobPage } from '@/api/infra/job'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import JobSearchForm from './job-search-form.vue'
|
||||
|
||||
const emit = defineEmits<{
|
||||
viewLog: [jobId: number]
|
||||
}>()
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const total = ref(0)
|
||||
const list = ref<Job[]>([])
|
||||
const loadMoreState = ref<LoadMoreState>('loading')
|
||||
const queryParams = ref({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
|
||||
/** 查询列表 */
|
||||
async function getList() {
|
||||
loadMoreState.value = 'loading'
|
||||
try {
|
||||
const data = await getJobPage(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/job/job/form/index',
|
||||
})
|
||||
}
|
||||
|
||||
/** 查看详情 */
|
||||
function handleDetail(item: Job) {
|
||||
uni.navigateTo({
|
||||
url: `/pages-infra/job/job/detail/index?id=${item.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 查看调度日志 */
|
||||
function handleViewLog(item: Job) {
|
||||
emit('viewLog', item.id)
|
||||
}
|
||||
|
||||
/** 触底加载更多 */
|
||||
onReachBottom(() => {
|
||||
loadMore()
|
||||
})
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
110
src/pages-infra/job/components/job-search-form.vue
Normal file
110
src/pages-infra/job/components/job-search-form.vue
Normal file
@@ -0,0 +1,110 @@
|
||||
<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.handlerName"
|
||||
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.INFRA_JOB_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({
|
||||
name: undefined as string | undefined,
|
||||
handlerName: undefined as string | undefined,
|
||||
status: -1, // -1 表示全部
|
||||
})
|
||||
|
||||
/** 搜索条件 placeholder 拼接 */
|
||||
const placeholder = computed(() => {
|
||||
const conditions: string[] = []
|
||||
if (formData.name) {
|
||||
conditions.push(`任务名称:${formData.name}`)
|
||||
}
|
||||
if (formData.handlerName) {
|
||||
conditions.push(`处理器:${formData.handlerName}`)
|
||||
}
|
||||
if (formData.status !== -1) {
|
||||
conditions.push(`状态:${getDictLabel(DICT_TYPE.INFRA_JOB_STATUS, formData.status)}`)
|
||||
}
|
||||
return conditions.length > 0 ? conditions.join(' | ') : '搜索定时任务'
|
||||
})
|
||||
|
||||
/** 搜索 */
|
||||
function handleSearch() {
|
||||
visible.value = false
|
||||
emit('search', {
|
||||
name: formData.name || undefined,
|
||||
handlerName: formData.handlerName || undefined,
|
||||
status: formData.status === -1 ? undefined : formData.status,
|
||||
})
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
function handleReset() {
|
||||
formData.name = undefined
|
||||
formData.handlerName = undefined
|
||||
formData.status = -1
|
||||
visible.value = false
|
||||
emit('reset')
|
||||
}
|
||||
</script>
|
||||
141
src/pages-infra/job/components/log-list.vue
Normal file
141
src/pages-infra/job/components/log-list.vue
Normal file
@@ -0,0 +1,141 @@
|
||||
<template>
|
||||
<view>
|
||||
<!-- 搜索组件 -->
|
||||
<LogSearchForm :job-id="jobId" @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.handlerName }}
|
||||
</view>
|
||||
<dict-tag :type="DICT_TYPE.INFRA_JOB_LOG_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.jobId }}</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.handlerParam || '-' }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">执行时长:</text>
|
||||
<text>{{ item.duration ? `${item.duration} ms` : '-' }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">开始时间:</text>
|
||||
<text>{{ formatDateTime(item.beginTime) }}</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>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { JobLog } from '@/api/infra/job/log'
|
||||
import type { LoadMoreState } from '@/http/types'
|
||||
import { ref, watch } from 'vue'
|
||||
import { getJobLogPage } from '@/api/infra/job/log'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import LogSearchForm from './log-search-form.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
jobId?: number
|
||||
}>()
|
||||
|
||||
const total = ref(0)
|
||||
const list = ref<JobLog[]>([])
|
||||
const loadMoreState = ref<LoadMoreState>('loading')
|
||||
const queryParams = ref({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
|
||||
/** 查询列表 */
|
||||
async function getList() {
|
||||
loadMoreState.value = 'loading'
|
||||
try {
|
||||
const data = await getJobLogPage(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 handleDetail(item: JobLog) {
|
||||
uni.navigateTo({
|
||||
url: `/pages-infra/job/log/detail/index?id=${item.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 触底加载更多 */
|
||||
onReachBottom(() => {
|
||||
loadMore()
|
||||
})
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
|
||||
/** 监听 jobId 变化,重新查询 */
|
||||
watch(
|
||||
() => props.jobId,
|
||||
() => {
|
||||
if (props.jobId) {
|
||||
queryParams.value.pageNo = 1
|
||||
list.value = []
|
||||
getList()
|
||||
}
|
||||
},
|
||||
)
|
||||
</script>
|
||||
182
src/pages-infra/job/components/log-search-form.vue
Normal file
182
src/pages-infra/job/components/log-search-form.vue
Normal file
@@ -0,0 +1,182 @@
|
||||
<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.jobId"
|
||||
placeholder="请输入任务编号"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
处理器名称
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.handlerName"
|
||||
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.INFRA_JOB_LOG_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="visibleBeginTime[0] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.beginTime?.[0]) || '开始日期' }}
|
||||
</view>
|
||||
</view>
|
||||
-
|
||||
<view class="flex-1" @click="visibleBeginTime[1] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.beginTime?.[1]) || '结束日期' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleBeginTime[0]" v-model="tempBeginTime[0]" type="date" />
|
||||
<view v-if="visibleBeginTime[0]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleBeginTime[0] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleBeginTime0Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleBeginTime[1]" v-model="tempBeginTime[1]" type="date" />
|
||||
<view v-if="visibleBeginTime[1]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleBeginTime[1] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleBeginTime1Confirm">
|
||||
确定
|
||||
</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, watch } 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 props = defineProps<{
|
||||
jobId?: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
search: [data: Record<string, any>]
|
||||
reset: []
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const formData = reactive({
|
||||
jobId: undefined as number | undefined,
|
||||
handlerName: undefined as string | undefined,
|
||||
status: -1, // -1 表示全部
|
||||
beginTime: [undefined, undefined] as [number | undefined, number | undefined],
|
||||
})
|
||||
|
||||
/** 搜索条件 placeholder 拼接 */
|
||||
const placeholder = computed(() => {
|
||||
const conditions: string[] = []
|
||||
if (formData.jobId) {
|
||||
conditions.push(`任务编号:${formData.jobId}`)
|
||||
}
|
||||
if (formData.handlerName) {
|
||||
conditions.push(`处理器:${formData.handlerName}`)
|
||||
}
|
||||
if (formData.status !== -1) {
|
||||
conditions.push(`状态:${getDictLabel(DICT_TYPE.INFRA_JOB_LOG_STATUS, formData.status)}`)
|
||||
}
|
||||
if (formData.beginTime?.[0] && formData.beginTime?.[1]) {
|
||||
conditions.push(`时间:${formatDate(formData.beginTime[0])}~${formatDate(formData.beginTime[1])}`)
|
||||
}
|
||||
return conditions.length > 0 ? conditions.join(' | ') : '搜索调度日志'
|
||||
})
|
||||
|
||||
// 时间范围选择器状态
|
||||
const visibleBeginTime = ref<[boolean, boolean]>([false, false])
|
||||
const tempBeginTime = ref<[number, number]>([Date.now(), Date.now()])
|
||||
|
||||
/** 开始时间[0]确认 */
|
||||
function handleBeginTime0Confirm() {
|
||||
formData.beginTime = [tempBeginTime.value[0], formData.beginTime?.[1]]
|
||||
visibleBeginTime.value[0] = false
|
||||
}
|
||||
|
||||
/** 开始时间[1]确认 */
|
||||
function handleBeginTime1Confirm() {
|
||||
formData.beginTime = [formData.beginTime?.[0], tempBeginTime.value[1]]
|
||||
visibleBeginTime.value[1] = false
|
||||
}
|
||||
|
||||
/** 搜索 */
|
||||
function handleSearch() {
|
||||
visible.value = false
|
||||
emit('search', {
|
||||
jobId: formData.jobId || undefined,
|
||||
handlerName: formData.handlerName || undefined,
|
||||
status: formData.status === -1 ? undefined : formData.status,
|
||||
beginTime: formatDateRange(formData.beginTime),
|
||||
})
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
function handleReset() {
|
||||
formData.jobId = undefined
|
||||
formData.handlerName = undefined
|
||||
formData.status = -1
|
||||
formData.beginTime = [undefined, undefined]
|
||||
visible.value = false
|
||||
emit('reset')
|
||||
}
|
||||
|
||||
/** 监听外部 jobId 变化 */
|
||||
watch(
|
||||
() => props.jobId,
|
||||
(val) => {
|
||||
formData.jobId = val
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
</script>
|
||||
75
src/pages-infra/job/index.vue
Normal file
75
src/pages-infra/job/index.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<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>
|
||||
<!-- 列表内容 -->
|
||||
<JobList v-show="tabType === 'job'" @view-log="handleViewLog" />
|
||||
<LogList v-show="tabType === 'log'" :job-id="selectedJobId" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import JobList from './components/job-list.vue'
|
||||
import LogList from './components/log-list.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
tab?: string
|
||||
jobId?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const tabTypes: string[] = ['job', 'log']
|
||||
const tabIndex = ref(0)
|
||||
const tabType = computed<string>(() => tabTypes[tabIndex.value])
|
||||
const selectedJobId = ref<number>() // 选中的任务 ID
|
||||
|
||||
/** Tab 切换 */
|
||||
function handleTabChange({ index }: { index: number }) {
|
||||
tabIndex.value = index
|
||||
}
|
||||
|
||||
/** 查看调度日志 */
|
||||
function handleViewLog(jobId: number) {
|
||||
selectedJobId.value = jobId
|
||||
tabIndex.value = 1 // 切换到调度日志 tab
|
||||
}
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus()
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
// 支持通过 URL 参数切换到日志 tab
|
||||
if (props.tab === 'log') {
|
||||
tabIndex.value = 1
|
||||
if (props.jobId) {
|
||||
selectedJobId.value = Number(props.jobId)
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
229
src/pages-infra/job/job/detail/index.vue
Normal file
229
src/pages-infra/job/job/detail/index.vue
Normal file
@@ -0,0 +1,229 @@
|
||||
<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="formData?.id" />
|
||||
<wd-cell title="任务名称" :value="formData?.name" />
|
||||
<wd-cell title="任务状态">
|
||||
<dict-tag :type="DICT_TYPE.INFRA_JOB_STATUS" :value="formData?.status" />
|
||||
</wd-cell>
|
||||
<wd-cell title="处理器名称" :value="formData?.handlerName" />
|
||||
<wd-cell title="处理器参数" :value="formData?.handlerParam ?? '-'" />
|
||||
<wd-cell title="CRON 表达式" :value="formData?.cronExpression" />
|
||||
<wd-cell title="重试次数" :value="formData?.retryCount" />
|
||||
<wd-cell title="重试间隔" :value="formData?.retryInterval ? `${formData.retryInterval} ms` : '-'" />
|
||||
<wd-cell title="监控超时" :value="formData?.monitorTimeout ? `${formData.monitorTimeout} ms` : '-'" />
|
||||
<wd-cell title="创建时间" :value="formatDateTime(formData?.createTime) || '-'" />
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<view class="yd-detail-footer-actions">
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['infra:job:update'])"
|
||||
class="flex-1" type="warning" @click="handleEdit"
|
||||
>
|
||||
编辑
|
||||
</wd-button>
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['infra:job:delete'])"
|
||||
class="flex-1" type="error" :loading="deleting" @click="handleDelete"
|
||||
>
|
||||
删除
|
||||
</wd-button>
|
||||
<wd-button
|
||||
v-if="hasMoreActions"
|
||||
class="flex-1" type="info" @click="moreActionVisible = true"
|
||||
>
|
||||
更多
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 更多操作菜单 -->
|
||||
<wd-action-sheet v-model="moreActionVisible" :actions="moreActions" @select="handleMoreAction" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Job } from '@/api/infra/job'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { deleteJob, getJob, runJob, updateJobStatus } from '@/api/infra/job'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE, InfraJobStatusEnum } 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<Job>()
|
||||
const deleting = ref(false)
|
||||
|
||||
const moreActionVisible = ref(false) // 更多操作菜单
|
||||
const moreActions = computed(() => {
|
||||
const actions = []
|
||||
// 执行一次权限
|
||||
if (hasAccessByCodes(['infra:job:trigger'])) {
|
||||
actions.push({ name: '执行一次', value: 'run' })
|
||||
}
|
||||
// 更新状态权限
|
||||
if (hasAccessByCodes(['infra:job:update'])) {
|
||||
const isRunning = formData.value?.status === InfraJobStatusEnum.NORMAL
|
||||
actions.push({ name: isRunning ? '暂停任务' : '开启任务', value: 'update-status' })
|
||||
}
|
||||
// 查看调度日志权限
|
||||
if (hasAccessByCodes(['infra:job:query'])) {
|
||||
actions.push({ name: '调度日志', value: 'view-log' })
|
||||
}
|
||||
return actions
|
||||
})
|
||||
const hasMoreActions = computed(() => moreActions.value.length > 0)
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-infra/job/index')
|
||||
}
|
||||
|
||||
/** 加载详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
toast.loading('加载中...')
|
||||
formData.value = await getJob(props.id)
|
||||
} finally {
|
||||
toast.close()
|
||||
}
|
||||
}
|
||||
|
||||
/** 执行一次 */
|
||||
function handleRun() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要立即执行一次该任务吗?',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
toast.loading('执行中...')
|
||||
await runJob(props.id)
|
||||
toast.success('执行成功')
|
||||
} finally {
|
||||
toast.close()
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 编辑 */
|
||||
function handleEdit() {
|
||||
uni.navigateTo({
|
||||
url: `/pages-infra/job/job/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 deleteJob(props.id)
|
||||
toast.success('删除成功')
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 更多操作 */
|
||||
function handleMoreAction({ item }: { item: { value: string } }) {
|
||||
if (item.value === 'run') {
|
||||
handleRun()
|
||||
} else if (item.value === 'update-status') {
|
||||
handleUpdateStatus()
|
||||
} else if (item.value === 'view-log') {
|
||||
handleViewLog()
|
||||
}
|
||||
}
|
||||
|
||||
/** 更新任务状态 */
|
||||
function handleUpdateStatus() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
const isRunning = formData.value?.status === InfraJobStatusEnum.NORMAL
|
||||
const statusText = isRunning ? '暂停' : '开启'
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: `确定要${statusText}该任务吗?`,
|
||||
success: async (res) => {
|
||||
if (!res.confirm) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
toast.loading(`正在${statusText}中...`)
|
||||
const newStatus = isRunning ? InfraJobStatusEnum.STOP : InfraJobStatusEnum.NORMAL
|
||||
await updateJobStatus(props.id, newStatus)
|
||||
toast.success(`${statusText}成功`)
|
||||
await getDetail()
|
||||
} finally {
|
||||
toast.close()
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 查看调度日志 */
|
||||
function handleViewLog() {
|
||||
uni.navigateTo({
|
||||
url: `/pages-infra/job/index?tab=log&jobId=${props.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
178
src/pages-infra/job/job/form/index.vue
Normal file
178
src/pages-infra/job/job/form/index.vue
Normal file
@@ -0,0 +1,178 @@
|
||||
<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.handlerName"
|
||||
label="处理器名称"
|
||||
label-width="200rpx"
|
||||
prop="handlerName"
|
||||
clearable
|
||||
placeholder="请输入处理器名称"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="formData.handlerParam"
|
||||
label="处理器参数"
|
||||
label-width="200rpx"
|
||||
prop="handlerParam"
|
||||
clearable
|
||||
placeholder="请输入处理器参数"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="formData.cronExpression"
|
||||
label="CRON 表达式"
|
||||
label-width="200rpx"
|
||||
prop="cronExpression"
|
||||
clearable
|
||||
placeholder="请输入 CRON 表达式"
|
||||
/>
|
||||
<wd-input
|
||||
v-model.number="formData.retryCount"
|
||||
label="重试次数"
|
||||
label-width="200rpx"
|
||||
prop="retryCount"
|
||||
type="number"
|
||||
clearable
|
||||
placeholder="请输入重试次数"
|
||||
/>
|
||||
<wd-input
|
||||
v-model.number="formData.retryInterval"
|
||||
label="重试间隔(ms)"
|
||||
label-width="200rpx"
|
||||
prop="retryInterval"
|
||||
type="number"
|
||||
clearable
|
||||
placeholder="请输入重试间隔"
|
||||
/>
|
||||
<wd-input
|
||||
v-model.number="formData.monitorTimeout"
|
||||
label="监控超时(ms)"
|
||||
label-width="200rpx"
|
||||
prop="monitorTimeout"
|
||||
type="number"
|
||||
clearable
|
||||
placeholder="请输入监控超时时间"
|
||||
/>
|
||||
</wd-cell-group>
|
||||
</wd-form>
|
||||
</view>
|
||||
|
||||
<!-- 底部保存按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<wd-button
|
||||
type="primary"
|
||||
block
|
||||
:loading="formLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
保存
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance } from 'wot-design-uni/components/wd-form/types'
|
||||
import type { Job } from '@/api/infra/job'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { createJob, getJob, updateJob } from '@/api/infra/job'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { CommonStatusEnum } from '@/utils/constants/biz-system-enum'
|
||||
|
||||
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<Job>({
|
||||
id: undefined,
|
||||
name: '',
|
||||
status: CommonStatusEnum.ENABLE,
|
||||
handlerName: '',
|
||||
handlerParam: '',
|
||||
cronExpression: '',
|
||||
retryCount: 0,
|
||||
retryInterval: 0,
|
||||
monitorTimeout: 0,
|
||||
})
|
||||
const formRules = {
|
||||
name: [{ required: true, message: '任务名称不能为空' }],
|
||||
handlerName: [{ required: true, message: '处理器名称不能为空' }],
|
||||
cronExpression: [{ required: true, message: 'CRON 表达式不能为空' }],
|
||||
retryCount: [{ required: true, message: '重试次数不能为空' }],
|
||||
retryInterval: [{ required: true, message: '重试间隔不能为空' }],
|
||||
}
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-infra/job/index')
|
||||
}
|
||||
|
||||
/** 加载详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
formData.value = await getJob(props.id)
|
||||
}
|
||||
|
||||
/** 提交表单 */
|
||||
async function handleSubmit() {
|
||||
const { valid } = await formRef.value.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
|
||||
formLoading.value = true
|
||||
try {
|
||||
if (props.id) {
|
||||
await updateJob(formData.value)
|
||||
toast.success('修改成功')
|
||||
} else {
|
||||
await createJob(formData.value)
|
||||
toast.success('新增成功')
|
||||
}
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
80
src/pages-infra/job/log/detail/index.vue
Normal file
80
src/pages-infra/job/log/detail/index.vue
Normal file
@@ -0,0 +1,80 @@
|
||||
<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="formData?.id" />
|
||||
<wd-cell title="任务编号" :value="formData?.jobId" />
|
||||
<wd-cell title="处理器名称" :value="formData?.handlerName" />
|
||||
<wd-cell title="处理器参数" :value="formData?.handlerParam ?? '-'" />
|
||||
<wd-cell title="CRON 表达式" :value="formData?.cronExpression" />
|
||||
<wd-cell title="执行索引" :value="formData?.executeIndex" />
|
||||
<wd-cell title="执行状态">
|
||||
<dict-tag :type="DICT_TYPE.INFRA_JOB_LOG_STATUS" :value="formData?.status" />
|
||||
</wd-cell>
|
||||
<wd-cell title="开始时间" :value="formatDateTime(formData?.beginTime)" />
|
||||
<wd-cell title="结束时间" :value="formatDateTime(formData?.endTime)" />
|
||||
<wd-cell title="执行时长" :value="formData?.duration ? `${formData.duration} ms` : '-'" />
|
||||
<wd-cell title="执行结果" :value="formData?.result" />
|
||||
<wd-cell title="创建时间" :value="formatDateTime(formData?.createTime)" />
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { JobLog } from '@/api/infra/job/log'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { getJobLog } from '@/api/infra/job/log'
|
||||
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 toast = useToast()
|
||||
const formData = ref<JobLog>()
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-infra/job/index')
|
||||
}
|
||||
|
||||
/** 加载详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
toast.loading('加载中...')
|
||||
formData.value = await getJobLog(Number(props.id))
|
||||
} finally {
|
||||
toast.close()
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
491
src/pages-infra/web-socket/index.vue
Normal file
491
src/pages-infra/web-socket/index.vue
Normal file
@@ -0,0 +1,491 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="WebSocket 测试"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 连接状态卡片 -->
|
||||
<view class="mx-24rpx mt-24rpx overflow-hidden rounded-16rpx bg-white shadow-sm">
|
||||
<view class="p-32rpx">
|
||||
<!-- 状态指示器 -->
|
||||
<view class="mb-24rpx flex items-center">
|
||||
<view
|
||||
class="mr-16rpx h-20rpx w-20rpx rounded-full"
|
||||
:class="isConnected ? 'bg-[#07c160]' : 'bg-[#fa5151]'"
|
||||
/>
|
||||
<text class="text-32rpx text-[#333] font-semibold">连接管理</text>
|
||||
</view>
|
||||
<!-- 连接状态 -->
|
||||
<view class="mb-24rpx flex items-center rounded-12rpx bg-[#f7f8fa] p-24rpx">
|
||||
<text class="mr-16rpx text-28rpx text-[#666]">连接状态:</text>
|
||||
<wd-tag :type="isConnected ? 'success' : 'danger'">
|
||||
{{ statusText }}
|
||||
</wd-tag>
|
||||
</view>
|
||||
<!-- 服务地址 -->
|
||||
<view class="mb-24rpx">
|
||||
<text class="mb-12rpx block text-26rpx text-[#999]">服务地址</text>
|
||||
<view class="rounded-12rpx bg-[#f7f8fa] p-24rpx">
|
||||
<text class="break-all text-26rpx text-[#666]">{{ serverUrl }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 连接按钮 -->
|
||||
<wd-button
|
||||
block
|
||||
:type="isConnected ? 'error' : 'primary'"
|
||||
@click="toggleConnection"
|
||||
>
|
||||
{{ isConnected ? '断开连接' : '建立连接' }}
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 发送消息卡片 -->
|
||||
<view class="mx-24rpx mt-24rpx overflow-hidden rounded-16rpx bg-white shadow-sm">
|
||||
<view class="p-32rpx">
|
||||
<view class="mb-24rpx flex items-center">
|
||||
<wd-icon name="chat" size="36rpx" color="#1989fa" class="mr-12rpx" />
|
||||
<text class="text-32rpx text-[#333] font-semibold">发送消息</text>
|
||||
</view>
|
||||
<!-- 接收人选择 -->
|
||||
<view class="mb-24rpx">
|
||||
<text class="mb-12rpx block text-26rpx text-[#999]">接收人</text>
|
||||
<wd-picker
|
||||
v-model="sendUserId"
|
||||
:columns="userColumns"
|
||||
:disabled="!isConnected"
|
||||
@confirm="handleUserChange"
|
||||
>
|
||||
<view class="flex items-center justify-between rounded-12rpx bg-[#f7f8fa] p-24rpx">
|
||||
<text class="text-28rpx" :class="isConnected ? 'text-[#333]' : 'text-[#c8c9cc]'">
|
||||
{{ selectedUserLabel }}
|
||||
</text>
|
||||
<wd-icon name="arrow-down" size="32rpx" :color="isConnected ? '#666' : '#c8c9cc'" />
|
||||
</view>
|
||||
</wd-picker>
|
||||
</view>
|
||||
<!-- 消息内容 -->
|
||||
<view class="mb-24rpx">
|
||||
<text class="mb-12rpx block text-26rpx text-[#999]">消息内容</text>
|
||||
<wd-textarea
|
||||
v-model="sendText"
|
||||
placeholder="请输入要发送的消息..."
|
||||
:disabled="!isConnected"
|
||||
:maxlength="500"
|
||||
show-word-limit
|
||||
auto-height
|
||||
:min-height="120"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 发送按钮 -->
|
||||
<wd-button
|
||||
block
|
||||
type="primary"
|
||||
:disabled="!isConnected || !sendText.trim()"
|
||||
@click="handleSend"
|
||||
>
|
||||
<wd-icon name="send" size="32rpx" class="mr-8rpx" />
|
||||
发送消息
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 消息记录卡片 -->
|
||||
<view class="mx-24rpx mb-24rpx mt-24rpx overflow-hidden rounded-16rpx bg-white shadow-sm">
|
||||
<view class="p-32rpx">
|
||||
<view class="mb-24rpx flex items-center justify-between">
|
||||
<view class="flex items-center">
|
||||
<wd-icon name="list" size="36rpx" color="#1989fa" class="mr-12rpx" />
|
||||
<text class="text-32rpx text-[#333] font-semibold">消息记录</text>
|
||||
<wd-tag v-if="messageList.length > 0" type="primary" plain class="ml-16rpx">
|
||||
{{ messageList.length }} 条
|
||||
</wd-tag>
|
||||
</view>
|
||||
<wd-button
|
||||
v-if="messageList.length > 0"
|
||||
size="small"
|
||||
type="error"
|
||||
plain
|
||||
@click="clearMessages"
|
||||
>
|
||||
清空
|
||||
</wd-button>
|
||||
</view>
|
||||
|
||||
<!-- 消息列表 -->
|
||||
<scroll-view
|
||||
scroll-y
|
||||
class="message-list rounded-12rpx bg-[#f7f8fa]"
|
||||
:style="{ height: '600rpx' }"
|
||||
>
|
||||
<view v-if="messageList.length === 0" class="h-full flex flex-col items-center justify-center">
|
||||
<wd-icon name="inbox" size="80rpx" color="#c8c9cc" />
|
||||
<text class="mt-16rpx text-26rpx text-[#c8c9cc]">暂无消息记录</text>
|
||||
</view>
|
||||
<view v-else class="p-20rpx">
|
||||
<view
|
||||
v-for="(msg, index) in messageReverseList"
|
||||
:key="index"
|
||||
class="mb-20rpx rounded-12rpx bg-white p-24rpx shadow-sm"
|
||||
>
|
||||
<view class="mb-12rpx flex items-center justify-between">
|
||||
<view class="flex items-center">
|
||||
<view
|
||||
class="mr-12rpx h-16rpx w-16rpx rounded-full"
|
||||
:style="{ backgroundColor: getMessageBadgeColor(msg.type) }"
|
||||
/>
|
||||
<text class="text-26rpx text-[#666] font-medium">
|
||||
{{ getMessageTypeText(msg.type) }}
|
||||
</text>
|
||||
<text v-if="msg.userId" class="ml-16rpx text-24rpx text-[#999]">
|
||||
用户 ID: {{ msg.userId }}
|
||||
</text>
|
||||
</view>
|
||||
<text class="text-22rpx text-[#c8c9cc]">
|
||||
{{ formatDateTime(msg.time) }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="break-words text-28rpx text-[#333]">
|
||||
{{ msg.text }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部安全区域 -->
|
||||
<view class="h-env(safe-area-inset-bottom)" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { User } from '@/api/system/user'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { getSimpleUserList } from '@/api/system/user'
|
||||
import { useTokenStore } from '@/store/token'
|
||||
import { getEnvBaseUrlRoot, navigateBackPlus } from '@/utils'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
// ======================= 状态定义 =======================
|
||||
const tokenStore = useTokenStore()
|
||||
const toast = useToast()
|
||||
|
||||
// WebSocket 相关状态
|
||||
const socketTask = ref<UniApp.SocketTask | null>(null)
|
||||
const isConnected = ref(false)
|
||||
const statusText = computed(() => (isConnected.value ? '已连接' : '未连接'))
|
||||
|
||||
// 服务地址
|
||||
const serverUrl = computed(() => {
|
||||
const baseUrl = getEnvBaseUrlRoot()
|
||||
const wsUrl = baseUrl.replace('http', 'ws')
|
||||
const tokenInfo = tokenStore.tokenInfo as any
|
||||
const token = tokenInfo?.refreshToken || tokenStore.validToken
|
||||
return `${wsUrl}/infra/ws?token=${token}`
|
||||
})
|
||||
|
||||
// 消息相关状态
|
||||
interface Message {
|
||||
text: string
|
||||
time: number
|
||||
type?: 'single' | 'group' | 'system'
|
||||
userId?: string
|
||||
}
|
||||
const messageList = ref<Message[]>([])
|
||||
const messageReverseList = computed(() => [...messageList.value].reverse())
|
||||
|
||||
// 发送消息相关
|
||||
const sendText = ref('')
|
||||
const sendUserId = ref('all')
|
||||
const userList = ref<User[]>([])
|
||||
const userColumns = computed(() => {
|
||||
const list = [
|
||||
{ value: 'all', label: '所有人' },
|
||||
...userList.value.map(user => ({
|
||||
value: String(user.id),
|
||||
label: user.nickname || user.username,
|
||||
})),
|
||||
]
|
||||
return [list]
|
||||
}) // 用户选择器列表
|
||||
const selectedUserLabel = computed(() => {
|
||||
if (sendUserId.value === 'all') {
|
||||
return '所有人'
|
||||
}
|
||||
const user = userList.value.find(u => String(u.id) === sendUserId.value)
|
||||
return user?.nickname || user?.username || '所有人'
|
||||
}) // 选中的用户标签
|
||||
|
||||
// ======================= WebSocket 方法 =======================
|
||||
|
||||
/** 建立 WebSocket 连接 */
|
||||
function connect() {
|
||||
if (socketTask.value) {
|
||||
return
|
||||
}
|
||||
|
||||
// 1.1 发起连接请求
|
||||
socketTask.value = uni.connectSocket({
|
||||
url: serverUrl.value,
|
||||
success: () => {
|
||||
console.log('WebSocket 连接请求已发送')
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('WebSocket 连接失败:', err)
|
||||
toast.error('连接失败')
|
||||
},
|
||||
})
|
||||
// 1.2 监听连接打开
|
||||
socketTask.value.onOpen(() => {
|
||||
console.log('WebSocket 连接已打开')
|
||||
isConnected.value = true
|
||||
toast.success('连接成功')
|
||||
// 开始心跳
|
||||
startHeartbeat()
|
||||
})
|
||||
|
||||
// 2. 监听消息
|
||||
socketTask.value.onMessage((res) => {
|
||||
handleMessage(res.data as string)
|
||||
})
|
||||
|
||||
// 3.1 监听连接关闭
|
||||
socketTask.value.onClose(() => {
|
||||
console.log('WebSocket 连接已关闭')
|
||||
isConnected.value = false
|
||||
socketTask.value = null
|
||||
stopHeartbeat()
|
||||
})
|
||||
// 3.2 监听错误
|
||||
socketTask.value.onError((err) => {
|
||||
console.error('WebSocket 错误:', err)
|
||||
isConnected.value = false
|
||||
socketTask.value = null
|
||||
stopHeartbeat()
|
||||
toast.error('连接异常')
|
||||
})
|
||||
}
|
||||
|
||||
/** 关闭 WebSocket 连接 */
|
||||
function disconnect() {
|
||||
if (!socketTask.value) {
|
||||
return
|
||||
}
|
||||
socketTask.value.close({
|
||||
success: () => {
|
||||
console.log('WebSocket 连接已主动关闭')
|
||||
toast.success('已断开')
|
||||
},
|
||||
})
|
||||
socketTask.value = null
|
||||
isConnected.value = false
|
||||
stopHeartbeat()
|
||||
}
|
||||
|
||||
/** 切换连接状态 */
|
||||
function toggleConnection() {
|
||||
if (isConnected.value) {
|
||||
disconnect()
|
||||
} else {
|
||||
connect()
|
||||
}
|
||||
}
|
||||
|
||||
// ======================= 心跳机制 =======================
|
||||
let heartbeatTimer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
/** 启动心跳机制 */
|
||||
function startHeartbeat() {
|
||||
stopHeartbeat()
|
||||
// 30 秒发送一次心跳
|
||||
heartbeatTimer = setInterval(() => {
|
||||
if (socketTask.value && isConnected.value) {
|
||||
socketTask.value.send({
|
||||
data: 'ping',
|
||||
fail: (err) => {
|
||||
console.error('心跳发送失败:', err)
|
||||
},
|
||||
})
|
||||
}
|
||||
}, 30000)
|
||||
}
|
||||
|
||||
/** 停止心跳机制 */
|
||||
function stopHeartbeat() {
|
||||
if (heartbeatTimer) {
|
||||
clearInterval(heartbeatTimer)
|
||||
heartbeatTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
// ======================= 消息处理 =======================
|
||||
|
||||
/** 处理接收到的消息 */
|
||||
function handleMessage(data: string) {
|
||||
if (!data) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 心跳响应
|
||||
if (data === 'pong') {
|
||||
return
|
||||
}
|
||||
|
||||
// 1. 解析消息
|
||||
const jsonMessage = JSON.parse(data)
|
||||
const type = jsonMessage.type
|
||||
const content = JSON.parse(jsonMessage.content)
|
||||
if (!type) {
|
||||
console.warn('未知的消息类型:', data)
|
||||
return
|
||||
}
|
||||
|
||||
// 2.1 处理 demo-message-receive 消息
|
||||
if (type === 'demo-message-receive') {
|
||||
const single = content.single
|
||||
messageList.value.push({
|
||||
text: content.text,
|
||||
time: Date.now(),
|
||||
type: single ? 'single' : 'group',
|
||||
userId: content.fromUserId,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 2.2 处理 notice-push 消息
|
||||
if (type === 'notice-push') {
|
||||
messageList.value.push({
|
||||
text: content.title,
|
||||
time: Date.now(),
|
||||
type: 'system',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
console.warn('未处理的消息类型:', type, data)
|
||||
} catch (error) {
|
||||
console.error('消息解析失败:', error, data)
|
||||
}
|
||||
}
|
||||
|
||||
/** 发送消息 */
|
||||
function handleSend() {
|
||||
if (!sendText.value.trim()) {
|
||||
toast.show('请输入消息内容')
|
||||
return
|
||||
}
|
||||
if (!socketTask.value || !isConnected.value) {
|
||||
toast.show('请先建立连接')
|
||||
return
|
||||
}
|
||||
|
||||
// 1.1 构建消息内容
|
||||
const messageContent = JSON.stringify({
|
||||
text: sendText.value,
|
||||
toUserId: sendUserId.value === 'all' ? undefined : sendUserId.value,
|
||||
})
|
||||
// 1.2 构建完整消息
|
||||
const jsonMessage = JSON.stringify({
|
||||
type: 'demo-message-send',
|
||||
content: messageContent,
|
||||
})
|
||||
|
||||
// 2. 发送消息
|
||||
socketTask.value.send({
|
||||
data: jsonMessage,
|
||||
success: () => {
|
||||
toast.success('发送成功')
|
||||
sendText.value = ''
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('消息发送失败:', err)
|
||||
toast.error('发送失败')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 清空消息记录 */
|
||||
function clearMessages() {
|
||||
messageList.value = []
|
||||
}
|
||||
|
||||
// ======================= 工具方法 =======================
|
||||
|
||||
/** 获取消息类型的徽标颜色 */
|
||||
function getMessageBadgeColor(type?: string) {
|
||||
switch (type) {
|
||||
case 'group':
|
||||
return '#07c160'
|
||||
case 'single':
|
||||
return '#1989fa'
|
||||
case 'system':
|
||||
return '#fa5151'
|
||||
default:
|
||||
return '#c8c9cc'
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取消息类型的文本 */
|
||||
function getMessageTypeText(type?: string) {
|
||||
switch (type) {
|
||||
case 'group':
|
||||
return '群发'
|
||||
case 'single':
|
||||
return '单发'
|
||||
case 'system':
|
||||
return '系统'
|
||||
default:
|
||||
return '未知'
|
||||
}
|
||||
}
|
||||
|
||||
/** 处理用户选择变化 */
|
||||
function handleUserChange({ value }: { value: string[] }) {
|
||||
sendUserId.value = value[0] || 'all'
|
||||
}
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus()
|
||||
}
|
||||
|
||||
// ======================= 生命周期 =======================
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
// 获取用户列表
|
||||
try {
|
||||
userList.value = await getSimpleUserList()
|
||||
} catch (error) {
|
||||
console.error('获取用户列表失败:', error)
|
||||
}
|
||||
})
|
||||
|
||||
/** 页面卸载 */
|
||||
onUnmounted(() => {
|
||||
// 页面销毁时断开连接
|
||||
disconnect()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.message-list {
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user