mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-10-08 18:49:09 +00:00
refactor(frontend): 清理废弃资源与代码,优化项目结构
1. 删除大量旧版静态图标、图片资源,替换为新的svg图标 2. 移除废弃的API模块、页面组件与配置文件 3. 新增全局消息store与通用列表分页hook 4. 优化代码逻辑:简化错误处理、调整样式类顺序、更新路由鉴权 5. 替换旧的按钮样式属性,统一代码规范
This commit is contained in:
@@ -1,10 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import type { DashboardStats } from '@/api/module_monitor/dashboard'
|
||||
import type { ServerInfo } from '@/api/module_monitor/server'
|
||||
import { onLoad, onPullDownRefresh, onShow } from '@dcloudio/uni-app'
|
||||
import { computed, ref } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
import { DashboardAPI } from '@/api/module_monitor/dashboard'
|
||||
import { ServerAPI } from '@/api/module_monitor/server'
|
||||
import SkeletonPage from '@/components/SkeletonPage.vue'
|
||||
|
||||
definePage({
|
||||
@@ -15,7 +13,6 @@ definePage({
|
||||
const toast = useToast()
|
||||
const loading = ref(false)
|
||||
const stats = ref<DashboardStats | null>(null)
|
||||
const serverInfo = ref<ServerInfo | null>(null)
|
||||
|
||||
// 7-day mock login trend data
|
||||
const loginTrend = ref([
|
||||
@@ -34,31 +31,13 @@ const alerts = ref([
|
||||
{ id: 3, level: 'info', text: '新用户注册量异常增长', time: '1小时前' },
|
||||
])
|
||||
|
||||
const cpuPercent = computed(() => serverInfo.value?.cpu?.used_percent ?? 0)
|
||||
const memPercent = computed(() => serverInfo.value?.memory?.used_percent ?? 0)
|
||||
|
||||
function healthColor(percent: number) {
|
||||
if (percent < 50)
|
||||
return 'var(--success-color, #10B981)'
|
||||
if (percent < 80)
|
||||
return 'var(--warning-color, #F59E0B)'
|
||||
return 'var(--danger-color, #EF4444)'
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const [dashRes, serverRes] = await Promise.all([
|
||||
DashboardAPI.getStats().catch(() => null),
|
||||
ServerAPI.getInfo().catch(() => null),
|
||||
])
|
||||
if (dashRes)
|
||||
stats.value = dashRes
|
||||
if (serverRes)
|
||||
serverInfo.value = serverRes
|
||||
stats.value = await DashboardAPI.getStats()
|
||||
}
|
||||
catch (e) {
|
||||
toast.error(getErrorMessage(e, '加载失败'))
|
||||
catch {
|
||||
toast.error('加载失败')
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
@@ -111,14 +90,6 @@ onPullDownRefresh(() => loadData())
|
||||
今日活跃
|
||||
</text>
|
||||
</view>
|
||||
<view class="stat-card card-pressable">
|
||||
<text class="stat-card__value" style="color: var(--warning-color);">
|
||||
{{ cpuPercent }}%
|
||||
</text>
|
||||
<text class="stat-card__label">
|
||||
系统负载
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Chart section -->
|
||||
@@ -145,32 +116,6 @@ onPullDownRefresh(() => loadData())
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- System health -->
|
||||
<view class="health-row">
|
||||
<view class="health-card card-pressable">
|
||||
<view class="health-card__info">
|
||||
<text class="health-card__label">
|
||||
CPU
|
||||
</text>
|
||||
<text class="health-card__value" :style="{ color: healthColor(cpuPercent) }">
|
||||
{{ cpuPercent }}%
|
||||
</text>
|
||||
</view>
|
||||
<view class="health-card__dot" :style="{ background: healthColor(cpuPercent) }" />
|
||||
</view>
|
||||
<view class="health-card card-pressable">
|
||||
<view class="health-card__info">
|
||||
<text class="health-card__label">
|
||||
内存
|
||||
</text>
|
||||
<text class="health-card__value" :style="{ color: healthColor(memPercent) }">
|
||||
{{ memPercent }}%
|
||||
</text>
|
||||
</view>
|
||||
<view class="health-card__dot" :style="{ background: healthColor(memPercent) }" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Alerts -->
|
||||
<view class="alert-card">
|
||||
<view class="alert-card__header">
|
||||
@@ -332,46 +277,6 @@ onPullDownRefresh(() => loadData())
|
||||
}
|
||||
}
|
||||
|
||||
.health-row {
|
||||
display: flex;
|
||||
gap: var(--spacing-md, 16rpx);
|
||||
margin-bottom: var(--spacing-lg, 20rpx);
|
||||
}
|
||||
|
||||
.health-card {
|
||||
flex: 1;
|
||||
background: var(--card-bg-color, #FFFFFF);
|
||||
border-radius: var(--radius-md, 16rpx);
|
||||
padding: var(--spacing-md, 16rpx);
|
||||
box-shadow: var(--shadow-sm, 0 1rpx 2rpx rgba(1, 77, 178, 0.06));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
&__info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4rpx;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: var(--font-xs, 20rpx);
|
||||
color: var(--text-color-3, #6B7280);
|
||||
}
|
||||
|
||||
&__value {
|
||||
font-size: var(--font-xl, 36rpx);
|
||||
font-weight: 700;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
&__dot {
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.alert-card {
|
||||
background: var(--card-bg-color, #FFFFFF);
|
||||
border-radius: var(--radius-md, 16rpx);
|
||||
|
||||
@@ -18,7 +18,7 @@ const STATUS_OPTIONS = [{ value: 0, label: '正常' }, { value: 1, label: '禁
|
||||
/* ===== Type list ===== */
|
||||
const { list, total, loading, pageParams, loadData, toFirst, loadPrev, loadNext } = useListPage<DictTypeItem>({
|
||||
fetcher: p => DictAPI.getTypePage({ ...p, dict_name: searchName.value || undefined }),
|
||||
onError: e => toast.error(getErrorMessage(e, '加载失败')),
|
||||
onError: () => toast.error('加载失败'),
|
||||
})
|
||||
function onSearch() {
|
||||
toFirst()
|
||||
@@ -52,7 +52,7 @@ async function handleSubmit() {
|
||||
showForm.value = false
|
||||
loadData()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '操作失败')) }
|
||||
catch { toast.error('操作失败') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
function handleDelete(id: number) {
|
||||
@@ -63,7 +63,7 @@ function handleDelete(id: number) {
|
||||
toast.success('删除成功')
|
||||
loadData()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '删除失败')) }
|
||||
catch { toast.error('删除失败') }
|
||||
}
|
||||
} })
|
||||
}
|
||||
@@ -88,7 +88,7 @@ const {
|
||||
loadNext: dataLoadNext,
|
||||
} = useListPage<DictDataItem>({
|
||||
fetcher: p => DictAPI.getDataPage({ ...p, dict_type_id: currentTypeId.value }),
|
||||
onError: e => toast.error(getErrorMessage(e, '加载失败')),
|
||||
onError: () => toast.error('加载失败'),
|
||||
})
|
||||
|
||||
function loadDictData(item: DictTypeItem) {
|
||||
@@ -126,7 +126,7 @@ async function openEditData(id: number) {
|
||||
})
|
||||
showDataForm.value = true
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '获取详情失败')) }
|
||||
catch { toast.error('获取详情失败') }
|
||||
}
|
||||
async function handleSubmitData() {
|
||||
dataLoading.value = true
|
||||
@@ -142,7 +142,7 @@ async function handleSubmitData() {
|
||||
showDataForm.value = false
|
||||
loadDataList()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '操作失败')) }
|
||||
catch { toast.error('操作失败') }
|
||||
finally { dataLoading.value = false }
|
||||
}
|
||||
function handleDeleteData(id: number) {
|
||||
@@ -153,7 +153,7 @@ function handleDeleteData(id: number) {
|
||||
toast.success('删除成功')
|
||||
loadDataList()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '删除失败')) }
|
||||
catch { toast.error('删除失败') }
|
||||
}
|
||||
} })
|
||||
}
|
||||
@@ -177,10 +177,10 @@ onLoad(() => loadData())
|
||||
<view class="search-bar">
|
||||
<view class="flex items-center gap-sm">
|
||||
<wd-input v-model="searchName" placeholder="搜索字典名称" clearable class="flex-1" />
|
||||
<wd-button size="small" type="primary" plain @click="onSearch">
|
||||
<wd-button size="small" type="primary" variant="plain" @click="onSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
<wd-button size="small" plain @click="onReset">
|
||||
<wd-button size="small" variant="plain" @click="onReset">
|
||||
重置
|
||||
</wd-button>
|
||||
</view>
|
||||
@@ -277,7 +277,7 @@ onLoad(() => loadData())
|
||||
<wd-textarea v-model="dataFormData.description" placeholder="请输入" />
|
||||
</wd-form-item>
|
||||
</wd-form><view class="gap-md mt-xl flex">
|
||||
<wd-button plain block @click="showDataForm = false">
|
||||
<wd-button variant="plain" block @click="showDataForm = false">
|
||||
取消
|
||||
</wd-button><wd-button block type="primary" :loading="dataLoading" @click="handleSubmitData">
|
||||
保存
|
||||
@@ -305,7 +305,7 @@ onLoad(() => loadData())
|
||||
<wd-textarea v-model="formData.description" placeholder="请输入" />
|
||||
</wd-form-item>
|
||||
</wd-form><view class="gap-md mt-xl flex">
|
||||
<wd-button plain block @click="showForm = false">
|
||||
<wd-button variant="plain" block @click="showForm = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button block type="primary" :loading="loading" @click="handleSubmit">
|
||||
|
||||
@@ -27,7 +27,7 @@ function statusLabel(s: number | string | undefined) {
|
||||
|
||||
const { list, total, loading, pageParams, loadData, toFirst, loadPrev, loadNext } = useListPage<NoticeItem>({
|
||||
fetcher: p => NoticeAPI.getPage({ ...p, notice_title: searchTitle.value || undefined }),
|
||||
onError: e => toast.error(getErrorMessage(e, '加载失败')),
|
||||
onError: () => toast.error('加载失败'),
|
||||
})
|
||||
|
||||
function onSearch() {
|
||||
@@ -62,7 +62,7 @@ async function openEdit(id: number) {
|
||||
})
|
||||
showForm.value = true
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '获取详情失败')) }
|
||||
catch { toast.error('获取详情失败') }
|
||||
}
|
||||
async function handleSubmit() {
|
||||
loading.value = true
|
||||
@@ -78,7 +78,7 @@ async function handleSubmit() {
|
||||
showForm.value = false
|
||||
loadData()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '操作失败')) }
|
||||
catch { toast.error('操作失败') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
function handleDelete(id: number) {
|
||||
@@ -92,7 +92,7 @@ function handleDelete(id: number) {
|
||||
toast.success('删除成功')
|
||||
loadData()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '删除失败')) }
|
||||
catch { toast.error('删除失败') }
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -113,15 +113,21 @@ onLoad(() => loadData())
|
||||
<view class="search-bar">
|
||||
<view class="flex items-center gap-sm">
|
||||
<wd-input v-model="searchTitle" placeholder="搜索公告标题" clearable class="flex-1" />
|
||||
<wd-button size="small" type="primary" plain @click="onSearch">搜索</wd-button>
|
||||
<wd-button size="small" plain @click="onReset">重置</wd-button>
|
||||
<wd-button size="small" type="primary" variant="plain" @click="onSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
<wd-button size="small" variant="plain" @click="onReset">
|
||||
重置
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="action-bar">
|
||||
<text class="text-md font-bold text-muted">
|
||||
<text class="text-md text-muted font-bold">
|
||||
共 {{ total }} 条
|
||||
</text>
|
||||
<wd-button size="small" type="primary" @click="openCreate">+ 新增</wd-button>
|
||||
<wd-button size="small" type="primary" @click="openCreate">
|
||||
+ 新增
|
||||
</wd-button>
|
||||
</view>
|
||||
<SkeletonPage v-if="loading" :rows="5" search />
|
||||
<template v-else>
|
||||
@@ -132,9 +138,9 @@ onLoad(() => loadData())
|
||||
<wd-cell v-for="item in list" :key="item.id" is-link @click="openEdit(item.id!)">
|
||||
<template #title>
|
||||
<view>
|
||||
<text class="font-medium text-md">
|
||||
<text class="text-md font-medium">
|
||||
{{ item.notice_title }}
|
||||
</text><text class="text-xs text-muted block mt-xs">
|
||||
</text><text class="text-muted mt-xs block text-xs">
|
||||
{{ item.description || '' }}
|
||||
</text>
|
||||
</view>
|
||||
@@ -176,8 +182,12 @@ onLoad(() => loadData())
|
||||
<wd-textarea v-model="formData.description" placeholder="请输入" />
|
||||
</wd-form-item>
|
||||
</wd-form>
|
||||
<view class="flex gap-md mt-xl">
|
||||
<wd-button block plain @click="showForm = false">取消</wd-button><wd-button block type="primary" :loading="loading" @click="handleSubmit">保存</wd-button>
|
||||
<view class="gap-md mt-xl flex">
|
||||
<wd-button block variant="plain" @click="showForm = false">
|
||||
取消
|
||||
</wd-button><wd-button block type="primary" :loading="loading" @click="handleSubmit">
|
||||
保存
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
|
||||
@@ -42,7 +42,7 @@ async function loadData() {
|
||||
const res = await ParamsAPI.getInfo()
|
||||
list.value = res || []
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '加载失败')) }
|
||||
catch { toast.error('加载失败') }
|
||||
finally {
|
||||
loading.value = false
|
||||
uni.stopPullDownRefresh()
|
||||
@@ -72,7 +72,7 @@ async function handleSubmit() {
|
||||
showForm.value = false
|
||||
loadData()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '操作失败')) }
|
||||
catch { toast.error('操作失败') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
@@ -87,12 +87,16 @@ onLoad(() => loadData())
|
||||
<view class="search-bar">
|
||||
<view class="flex items-center gap-sm">
|
||||
<wd-input v-model="searchName" placeholder="搜索参数名称/键名" clearable class="flex-1" />
|
||||
<wd-button size="small" type="primary" plain @click="onSearch">搜索</wd-button>
|
||||
<wd-button size="small" plain @click="onReset">重置</wd-button>
|
||||
<wd-button size="small" type="primary" variant="plain" @click="onSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
<wd-button size="small" variant="plain" @click="onReset">
|
||||
重置
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="action-bar">
|
||||
<text class="text-md font-bold text-muted">
|
||||
<text class="text-md text-muted font-bold">
|
||||
共 {{ filteredList.length }} 条
|
||||
</text>
|
||||
</view>
|
||||
@@ -105,15 +109,15 @@ onLoad(() => loadData())
|
||||
<wd-cell v-for="item in filteredList" :key="item.id" is-link @click="openEdit(item)">
|
||||
<template #title>
|
||||
<view>
|
||||
<text class="font-medium text-md">
|
||||
<text class="text-md font-medium">
|
||||
{{ displayName(item) }}
|
||||
</text><text class="text-xs text-muted block mt-xs">
|
||||
</text><text class="text-muted mt-xs block text-xs">
|
||||
{{ `键: ${displayKey(item)}` }}
|
||||
</text>
|
||||
</view>
|
||||
</template>
|
||||
<template #label>
|
||||
<text class="text-xs text-muted">
|
||||
<text class="text-muted text-xs">
|
||||
{{ displayValue(item) || '-' }}
|
||||
</text>
|
||||
</template>
|
||||
@@ -146,9 +150,13 @@ onLoad(() => loadData())
|
||||
<wd-textarea v-model="formData.description" placeholder="请输入" />
|
||||
</wd-form-item>
|
||||
</wd-form>
|
||||
<view class="flex gap-md mt-xl">
|
||||
<wd-button block plain @click="showForm = false">取消</wd-button>
|
||||
<wd-button block type="primary" :loading="loading" @click="handleSubmit">保存</wd-button>
|
||||
<view class="gap-md mt-xl flex">
|
||||
<wd-button block variant="plain" @click="showForm = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button block type="primary" :loading="loading" @click="handleSubmit">
|
||||
保存
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
|
||||
@@ -4,7 +4,6 @@ import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app'
|
||||
import { reactive, ref } from 'vue'
|
||||
import { TicketAPI } from '@/api/module_system/ticket'
|
||||
import SkeletonPage from '@/components/SkeletonPage.vue'
|
||||
import { logger } from '@/utils/logger'
|
||||
|
||||
definePage({
|
||||
name: 'work-ticket-detail',
|
||||
@@ -28,8 +27,8 @@ async function loadTicket() {
|
||||
try {
|
||||
ticket.value = await TicketAPI.getDetail(ticketId.value)
|
||||
}
|
||||
catch (e) {
|
||||
toast.error(getErrorMessage(e, '加载工单详情失败'))
|
||||
catch {
|
||||
toast.error('加载工单详情失败')
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
@@ -45,7 +44,7 @@ async function loadComments() {
|
||||
commentTotal.value = res.total || 0
|
||||
}
|
||||
catch (e) {
|
||||
logger.error('加载评论失败', e)
|
||||
console.error('加载评论失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,8 +58,8 @@ async function submitComment() {
|
||||
toast.success('评论成功')
|
||||
loadComments()
|
||||
}
|
||||
catch (e) {
|
||||
toast.error(getErrorMessage(e, '评论失败'))
|
||||
catch {
|
||||
toast.error('评论失败')
|
||||
}
|
||||
finally {
|
||||
submitting.value = false
|
||||
|
||||
@@ -49,7 +49,7 @@ function displayType(item: TicketItem) {
|
||||
|
||||
const { list, total, loading, pageParams, loadData, toFirst, loadPrev, loadNext } = useListPage<TicketItem>({
|
||||
fetcher: p => TicketAPI.getPage({ ...p, title: searchTitle.value || undefined }),
|
||||
onError: e => toast.error(getErrorMessage(e, '加载失败')),
|
||||
onError: () => toast.error('加载失败'),
|
||||
})
|
||||
|
||||
function onSearch() {
|
||||
@@ -71,7 +71,7 @@ function openCreate() {
|
||||
showForm.value = true
|
||||
}
|
||||
function navigateToDetail(id: number) {
|
||||
router.push({ name: 'work-ticket-detail', query: { id } })
|
||||
router.push({ name: 'work-ticket-detail', query: { id: String(id) } })
|
||||
}
|
||||
async function handleSubmit() {
|
||||
loading.value = true
|
||||
@@ -87,7 +87,7 @@ async function handleSubmit() {
|
||||
showForm.value = false
|
||||
loadData()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '操作失败')) }
|
||||
catch { toast.error('操作失败') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
function handleDelete(id: number) {
|
||||
@@ -101,7 +101,7 @@ function handleDelete(id: number) {
|
||||
toast.success('删除成功')
|
||||
loadData()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '删除失败')) }
|
||||
catch { toast.error('删除失败') }
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -122,15 +122,21 @@ onLoad(() => loadData())
|
||||
<view class="search-bar">
|
||||
<view class="flex items-center gap-sm">
|
||||
<wd-input v-model="searchTitle" placeholder="搜索标题" clearable class="flex-1" />
|
||||
<wd-button size="small" type="primary" plain @click="onSearch">搜索</wd-button>
|
||||
<wd-button size="small" plain @click="onReset">重置</wd-button>
|
||||
<wd-button size="small" type="primary" variant="plain" @click="onSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
<wd-button size="small" variant="plain" @click="onReset">
|
||||
重置
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="action-bar">
|
||||
<text class="text-md font-bold text-muted">
|
||||
<text class="text-md text-muted font-bold">
|
||||
共 {{ total }} 条
|
||||
</text>
|
||||
<wd-button size="small" type="primary" @click="openCreate">+ 新增</wd-button>
|
||||
<wd-button size="small" type="primary" @click="openCreate">
|
||||
+ 新增
|
||||
</wd-button>
|
||||
</view>
|
||||
<SkeletonPage v-if="loading" :rows="5" search />
|
||||
<template v-else>
|
||||
@@ -141,13 +147,13 @@ onLoad(() => loadData())
|
||||
<wd-cell v-for="item in list" :key="item.id" is-link @click="navigateToDetail(item.id!)">
|
||||
<template #title>
|
||||
<view>
|
||||
<text class="font-medium text-md">
|
||||
<text class="text-md font-medium">
|
||||
{{ item.title }}
|
||||
</text>
|
||||
</view>
|
||||
</template>
|
||||
<template #label>
|
||||
<text class="text-xs text-muted">
|
||||
<text class="text-muted text-xs">
|
||||
{{ displayType(item) ? `${displayType(item)} · ` : '' }}{{ displayContent(item) }}
|
||||
</text>
|
||||
</template>
|
||||
@@ -187,8 +193,12 @@ onLoad(() => loadData())
|
||||
<wd-textarea v-model="formData.description" placeholder="请输入" />
|
||||
</wd-form-item>
|
||||
</wd-form>
|
||||
<view class="flex gap-md mt-xl">
|
||||
<wd-button block plain @click="showForm = false">取消</wd-button><wd-button block type="primary" :loading="loading" @click="handleSubmit">保存</wd-button>
|
||||
<view class="gap-md mt-xl flex">
|
||||
<wd-button block variant="plain" @click="showForm = false">
|
||||
取消
|
||||
</wd-button><wd-button block type="primary" :loading="loading" @click="handleSubmit">
|
||||
保存
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
|
||||
@@ -89,8 +89,8 @@ async function loadUser() {
|
||||
const res = await UserAPI.getUserDetail(userId.value)
|
||||
user.value = res
|
||||
}
|
||||
catch (e) {
|
||||
toast.error(getErrorMessage(e, '加载用户详情失败'))
|
||||
catch {
|
||||
toast.error('加载用户详情失败')
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
@@ -106,8 +106,8 @@ async function handleSave() {
|
||||
toast.success('保存成功')
|
||||
isEdit.value = false
|
||||
}
|
||||
catch (e) {
|
||||
toast.error(getErrorMessage(e, '保存失败'))
|
||||
catch {
|
||||
toast.error('保存失败')
|
||||
}
|
||||
finally {
|
||||
saving.value = false
|
||||
@@ -127,8 +127,8 @@ function handleToggleStatus() {
|
||||
await UserAPI.updateUser(user.value)
|
||||
toast.success(`${action}成功`)
|
||||
}
|
||||
catch (e) {
|
||||
toast.error(getErrorMessage(e, '操作失败'))
|
||||
catch {
|
||||
toast.error('操作失败')
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -27,7 +27,7 @@ function getStatusLabel(status: number | string | undefined) {
|
||||
|
||||
const { list, total, loading, pageParams, loadData, toFirst, loadPrev, loadNext } = useListPage<VersionItem>({
|
||||
fetcher: p => VersionAPI.getPage({ ...p, title: searchName.value || undefined }),
|
||||
onError: e => toast.error(getErrorMessage(e, '加载失败')),
|
||||
onError: () => toast.error('加载失败'),
|
||||
})
|
||||
|
||||
function onSearch() {
|
||||
@@ -64,7 +64,7 @@ async function openEdit(id: number) {
|
||||
})
|
||||
showForm.value = true
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '获取详情失败')) }
|
||||
catch { toast.error('获取详情失败') }
|
||||
}
|
||||
async function handleSubmit() {
|
||||
loading.value = true
|
||||
@@ -80,7 +80,7 @@ async function handleSubmit() {
|
||||
showForm.value = false
|
||||
loadData()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '操作失败')) }
|
||||
catch { toast.error('操作失败') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
function handleDelete(id: number) {
|
||||
@@ -94,7 +94,7 @@ function handleDelete(id: number) {
|
||||
toast.success('删除成功')
|
||||
loadData()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '删除失败')) }
|
||||
catch { toast.error('删除失败') }
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -115,15 +115,21 @@ onLoad(() => loadData())
|
||||
<view class="search-bar">
|
||||
<view class="flex items-center gap-sm">
|
||||
<wd-input v-model="searchName" placeholder="搜索版本标题" clearable class="flex-1" />
|
||||
<wd-button size="small" type="primary" plain @click="onSearch">搜索</wd-button>
|
||||
<wd-button size="small" plain @click="onReset">重置</wd-button>
|
||||
<wd-button size="small" type="primary" variant="plain" @click="onSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
<wd-button size="small" variant="plain" @click="onReset">
|
||||
重置
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="action-bar">
|
||||
<text class="text-md font-bold text-muted">
|
||||
<text class="text-md text-muted font-bold">
|
||||
共 {{ total }} 条
|
||||
</text>
|
||||
<wd-button size="small" type="primary" @click="openCreate">+ 新增</wd-button>
|
||||
<wd-button size="small" type="primary" @click="openCreate">
|
||||
+ 新增
|
||||
</wd-button>
|
||||
</view>
|
||||
<SkeletonPage v-if="loading" :rows="5" search />
|
||||
<template v-else>
|
||||
@@ -134,15 +140,15 @@ onLoad(() => loadData())
|
||||
<wd-cell v-for="item in list" :key="item.id" is-link @click="openEdit(item.id!)">
|
||||
<template #title>
|
||||
<view>
|
||||
<text class="font-medium text-md">
|
||||
<text class="text-md font-medium">
|
||||
{{ item.title || '未命名' }}
|
||||
</text><text class="text-xs text-muted block mt-xs">
|
||||
</text><text class="text-muted mt-xs block text-xs">
|
||||
v{{ item.version || '-' }}{{ item.date ? ` · ${item.date}` : '' }}
|
||||
</text>
|
||||
</view>
|
||||
</template>
|
||||
<template #label>
|
||||
<text class="text-xs text-muted">
|
||||
<text class="text-muted text-xs">
|
||||
{{ getStatusLabel(item.status) }}
|
||||
</text>
|
||||
</template>
|
||||
@@ -185,9 +191,13 @@ onLoad(() => loadData())
|
||||
<wd-textarea v-model="formData.description" placeholder="请输入" />
|
||||
</wd-form-item>
|
||||
</wd-form>
|
||||
<view class="flex gap-md mt-xl">
|
||||
<wd-button block plain @click="showForm = false">取消</wd-button>
|
||||
<wd-button block type="primary" :loading="loading" @click="handleSubmit">保存</wd-button>
|
||||
<view class="gap-md mt-xl flex">
|
||||
<wd-button block variant="plain" @click="showForm = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button block type="primary" :loading="loading" @click="handleSubmit">
|
||||
保存
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
|
||||
@@ -32,9 +32,7 @@ async function loadModels() {
|
||||
const res = await ChatAPI.getModels()
|
||||
models.value = res || []
|
||||
}
|
||||
catch (e) {
|
||||
toast.error(getErrorMessage(e, '加载模型列表失败'))
|
||||
}
|
||||
catch { toast.error('加载模型列表失败') }
|
||||
finally {
|
||||
loading.value = false
|
||||
uni.stopPullDownRefresh()
|
||||
@@ -81,9 +79,7 @@ async function submitForm() {
|
||||
showForm.value = false
|
||||
loadModels()
|
||||
}
|
||||
catch (e) {
|
||||
toast.error(getErrorMessage(e, '保存失败'))
|
||||
}
|
||||
catch { toast.error('保存失败') }
|
||||
finally {
|
||||
submitting.value = false
|
||||
}
|
||||
@@ -101,7 +97,7 @@ async function handleDelete(configId: number) {
|
||||
toast.success('删除成功')
|
||||
loadModels()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '删除失败')) }
|
||||
catch { toast.error('删除失败') }
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -118,7 +114,7 @@ async function handleActivate(configId: number) {
|
||||
toast.success('已切换激活模型')
|
||||
loadModels()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '切换失败')) }
|
||||
catch { toast.error('切换失败') }
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -202,9 +198,9 @@ onLoad(() => {
|
||||
<view style="height: 120rpx;" />
|
||||
|
||||
<!-- Form Popup -->
|
||||
<u-popup
|
||||
:show="showForm"
|
||||
mode="bottom"
|
||||
<wd-popup
|
||||
v-model="showForm"
|
||||
position="bottom"
|
||||
custom-style="border-radius: 32rpx 32rpx 0 0; padding-bottom: 40rpx;"
|
||||
@close="showForm = false"
|
||||
>
|
||||
@@ -222,42 +218,44 @@ onLoad(() => {
|
||||
<text class="form-group__label">
|
||||
配置名称 *
|
||||
</text>
|
||||
<u-input v-model="form.name" placeholder="如:生产环境 GPT-4" clearable border="surround" />
|
||||
<wd-input v-model="form.name" placeholder="如:生产环境 GPT-4" clearable />
|
||||
</view>
|
||||
|
||||
<view class="form-group">
|
||||
<text class="form-group__label">
|
||||
模型 ID *
|
||||
</text>
|
||||
<u-input v-model="form.model_id" placeholder="如:gpt-4o / deepseek-chat" clearable border="surround" />
|
||||
<wd-input v-model="form.model_id" placeholder="如:gpt-4o / deepseek-chat" clearable />
|
||||
</view>
|
||||
|
||||
<view class="form-group">
|
||||
<text class="form-group__label">
|
||||
API 地址 *
|
||||
</text>
|
||||
<u-input v-model="form.base_url" placeholder="https://api.openai.com/v1" clearable border="surround" />
|
||||
<wd-input v-model="form.base_url" placeholder="https://api.openai.com/v1" clearable />
|
||||
</view>
|
||||
|
||||
<view class="form-group">
|
||||
<text class="form-group__label">
|
||||
API Key *
|
||||
</text>
|
||||
<u-input v-model="form.api_key" placeholder="sk-..." password clearable border="surround" />
|
||||
<wd-input v-model="form.api_key" placeholder="sk-..." show-password clearable />
|
||||
</view>
|
||||
|
||||
<view class="form-group">
|
||||
<text class="form-group__label">
|
||||
温度 (0-2)
|
||||
</text>
|
||||
<u-input v-model="form.temperature" type="number" placeholder="0.7" border="surround" />
|
||||
<wd-input v-model="form.temperature" type="number" placeholder="0.7" />
|
||||
</view>
|
||||
|
||||
<view class="form-actions">
|
||||
<u-button :block="true" type="primary" :loading="submitting" :text="editingId ? '更新' : '创建'" @click="submitForm" />
|
||||
<wd-button block type="primary" :loading="submitting" @click="submitForm">
|
||||
{{ editingId ? '更新' : '创建' }}
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</u-popup>
|
||||
</wd-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -45,9 +45,9 @@ async function loadSessions() {
|
||||
const res = await ChatAPI.getSessions()
|
||||
sessions.value = res.list || []
|
||||
}
|
||||
catch (e) {
|
||||
catch {
|
||||
sessions.value = []
|
||||
toast.error(getErrorMessage(e, '加载会话列表失败'))
|
||||
toast.error('加载会话列表失败')
|
||||
}
|
||||
finally { loading.value = false }
|
||||
}
|
||||
@@ -63,9 +63,7 @@ async function createSession() {
|
||||
}
|
||||
toast.success('新会话已创建')
|
||||
}
|
||||
catch (e) {
|
||||
toast.error(getErrorMessage(e, '创建会话失败'))
|
||||
}
|
||||
catch { toast.error('创建会话失败') }
|
||||
}
|
||||
|
||||
async function selectSession(id: number, title?: string) {
|
||||
@@ -81,9 +79,9 @@ async function selectSession(id: number, title?: string) {
|
||||
resetWindow()
|
||||
messages.value = msgs.map(m => ({ role: m.role || 'ai', content: m.content || '', time: String(m.created_at || m.time || '') }))
|
||||
}
|
||||
catch (e) {
|
||||
catch {
|
||||
messages.value = []
|
||||
toast.error(getErrorMessage(e, '加载会话消息失败'))
|
||||
toast.error('加载会话消息失败')
|
||||
}
|
||||
finally { loading.value = false }
|
||||
}
|
||||
@@ -104,7 +102,7 @@ async function deleteSession(id: number) {
|
||||
}
|
||||
toast.success('已删除')
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '删除失败')) }
|
||||
catch { toast.error('删除失败') }
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -135,8 +133,9 @@ async function sendMessage() {
|
||||
const reply = replyRes?.content || replyRes?.reply || replyRes?.data?.content || '已收到您的消息。'
|
||||
messages.value.push({ role: 'ai', content: reply, time: new Date().toLocaleTimeString() })
|
||||
}
|
||||
catch (e) {
|
||||
messages.value.push({ role: 'ai', content: getErrorMessage(e, '请求失败,请稍后重试。'), time: new Date().toLocaleTimeString() })
|
||||
catch {
|
||||
// 错误内联展示为 AI 消息
|
||||
messages.value.push({ role: 'ai', content: '请求失败,请稍后重试。', time: new Date().toLocaleTimeString() })
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
@@ -153,33 +152,35 @@ onLoad(() => {
|
||||
<view class="chat-page" style="display:flex;flex-direction:column;height:100vh;background:var(--bg-color);">
|
||||
<!-- Header -->
|
||||
<view style="display:flex;align-items:center;gap:var(--spacing-sm,12rpx);padding:var(--spacing-md,16rpx);border-bottom:1px solid var(--border-color);background:var(--card-bg-color);">
|
||||
<u-button size="mini" :plain="true" @click="showSessions = !showSessions">
|
||||
<u-icon name="list-dot" size="16" />
|
||||
</u-button>
|
||||
<text class="font-bold text-md flex-1 text-center truncate">
|
||||
<wd-button size="small" variant="plain" @click="showSessions = !showSessions">
|
||||
<wd-icon name="menu" size="16px" />
|
||||
</wd-button>
|
||||
<text class="text-md flex-1 truncate text-center font-bold">
|
||||
{{ currentTitle }}
|
||||
</text>
|
||||
<u-button size="mini" type="primary" :plain="true" @click="createSession">
|
||||
<u-icon name="plus" size="16" />
|
||||
</u-button>
|
||||
<wd-button size="small" type="primary" variant="plain" @click="createSession">
|
||||
<wd-icon name="plus" size="16px" />
|
||||
</wd-button>
|
||||
</view>
|
||||
|
||||
<!-- Session panel overlay -->
|
||||
<view v-if="showSessions" class="session-overlay" style="position:absolute;top:88rpx;left:0;right:0;bottom:0;background:var(--bg-color);z-index:100;overflow-y:auto;">
|
||||
<view class="p-sm">
|
||||
<view class="flex items-center justify-between mb-md px-xs">
|
||||
<text class="font-bold text-lg">
|
||||
<view class="mb-md flex items-center justify-between px-xs">
|
||||
<text class="text-lg font-bold">
|
||||
会话列表
|
||||
</text>
|
||||
<u-button size="mini" type="primary" text="新建" @click="createSession" />
|
||||
<wd-button size="small" type="primary" @click="createSession">
|
||||
新建
|
||||
</wd-button>
|
||||
</view>
|
||||
<SkeletonPage v-if="loading && sessions.length === 0" />
|
||||
<view v-for="s in sessions" v-else :key="s.id" class="admin-card">
|
||||
<u-cell :title="s.title" :label="s.created_at ? String(s.created_at) : ''" :is-link="true" @click="selectSession(s.id, s.title)">
|
||||
<template #right>
|
||||
<u-icon name="trash" size="18" color="var(--danger-color)" @click.stop="deleteSession(s.id)" />
|
||||
<wd-cell :title="s.title" :label="s.created_at ? String(s.created_at) : ''" is-link @click="selectSession(s.id, s.title)">
|
||||
<template #default>
|
||||
<wd-icon name="delete" size="18px" color="var(--danger-color)" @click.stop="deleteSession(s.id)" />
|
||||
</template>
|
||||
</u-cell>
|
||||
</wd-cell>
|
||||
</view>
|
||||
<ListEmpty v-if="!loading && sessions.length === 0" text="暂无会话,点击右上角新建" />
|
||||
</view>
|
||||
@@ -195,11 +196,16 @@ onLoad(() => {
|
||||
>
|
||||
<!-- 窗口化渲染:仅渲染最近 N 条消息,支持加载更早消息 -->
|
||||
<view v-if="hasOlderMessages" style="padding:16rpx 0;text-align:center;">
|
||||
<u-button size="mini" :plain="true" :text="`加载更早消息(${messages.length - visibleMessages.length} 条)`" @click="loadOlderMessages" />
|
||||
<wd-button size="small" variant="plain" @click="loadOlderMessages">
|
||||
加载更早消息({{ messages.length - visibleMessages.length }} 条)
|
||||
</wd-button>
|
||||
</view>
|
||||
<view v-for="(msg, i) in visibleMessages" :id="`msg-${i}`" :key="i" style="margin-bottom:24rpx;">
|
||||
<view class="flex items-start gap-sm" :class="msg.role === 'user' ? 'flex-row-reverse' : ''">
|
||||
<u-avatar :size="32" shape="round" :src="msg.role === 'user' ? '/static/images/default-avatar.png' : '/static/logo.png'" />
|
||||
<view v-if="msg.role === 'user'" class="user-avatar">
|
||||
<wd-icon name="user" size="18px" color="#FFFFFF" />
|
||||
</view>
|
||||
<wd-avatar v-else size="32px" shape="round" src="/static/logo.png" />
|
||||
<view
|
||||
class="chat-bubble"
|
||||
:style="{
|
||||
@@ -221,9 +227,9 @@ onLoad(() => {
|
||||
<!-- Loading indicator -->
|
||||
<view v-if="loading" style="margin-bottom:24rpx;">
|
||||
<view class="flex items-start gap-sm">
|
||||
<u-avatar :size="32" shape="round" src="/static/logo.png" />
|
||||
<wd-avatar size="32px" shape="round" src="/static/logo.png" />
|
||||
<view class="chat-bubble" style="background:var(--card-bg-color);border:1px solid var(--border-color);border-radius:16rpx 16rpx 16rpx 4rpx;">
|
||||
<u-loading-icon />
|
||||
<wd-loading />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -232,14 +238,27 @@ onLoad(() => {
|
||||
<!-- Input -->
|
||||
<view style="border-top:1px solid var(--border-color);padding:var(--spacing-md,16rpx);background:var(--card-bg-color);padding-bottom:calc(var(--spacing-md) + env(safe-area-inset-bottom));">
|
||||
<view class="flex items-center gap-sm">
|
||||
<u-input v-model="inputText" placeholder="输入消息..." :disabled="loading" class="flex-1" confirm-type="send" border="surround" @confirm="sendMessage" />
|
||||
<u-button :loading="loading" :disabled="!inputText.trim() || !currentSession" type="primary" text="发送" @click="sendMessage" />
|
||||
<wd-input v-model="inputText" placeholder="输入消息..." :disabled="loading" class="flex-1" confirm-type="send" @confirm="sendMessage" />
|
||||
<wd-button :loading="loading" :disabled="!inputText.trim() || !currentSession" type="primary" @click="sendMessage">
|
||||
发送
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.user-avatar {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, var(--primary-color, #4F8CFF), var(--primary-color-dark, #2970FF));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.chat-bubble {
|
||||
padding: 16rpx 24rpx;
|
||||
max-width: 72%;
|
||||
|
||||
@@ -1,250 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { DbTableItem, GenTableItem } from '@/api/module_generator/gencode'
|
||||
import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app'
|
||||
import { ref } from 'vue'
|
||||
import { GencodeAPI } from '@/api/module_generator/gencode'
|
||||
|
||||
definePage({ name: 'work-gencode', style: { navigationBarTitleText: '代码生成器' } })
|
||||
|
||||
const toast = useToast()
|
||||
const activeTab = ref('biztables')
|
||||
const showDbPicker = ref(false)
|
||||
const showPreview = ref(false)
|
||||
const previewId = ref<number>(0)
|
||||
const previewContent = ref('')
|
||||
|
||||
const {
|
||||
list: bizTables,
|
||||
total: bizTotal,
|
||||
loading: bizLoading,
|
||||
pageParams: bizPageParams,
|
||||
loadData: loadBizTables,
|
||||
loadPrev: bizLoadPrev,
|
||||
loadNext: bizLoadNext,
|
||||
} = useListPage<GenTableItem>({
|
||||
fetcher: p => GencodeAPI.getPage({ page: p.page_no, page_size: p.page_size }),
|
||||
onError: e => toast.error(getErrorMessage(e, '加载失败')),
|
||||
})
|
||||
|
||||
const {
|
||||
list: dbTables,
|
||||
total: dbTotal,
|
||||
loading: dbLoading,
|
||||
pageParams: dbPageParams,
|
||||
loadData: loadDbTables,
|
||||
loadPrev: dbLoadPrev,
|
||||
loadNext: dbLoadNext,
|
||||
} = useListPage<DbTableItem>({
|
||||
fetcher: p => GencodeAPI.getDbTables({ page: p.page_no, page_size: p.page_size }),
|
||||
onError: e => toast.error(getErrorMessage(e, '加载数据库表失败')),
|
||||
})
|
||||
|
||||
async function importTable(tableName: string) {
|
||||
bizLoading.value = true
|
||||
try {
|
||||
await GencodeAPI.importTable(tableName)
|
||||
toast.success(`${tableName} 导入成功`)
|
||||
showDbPicker.value = false
|
||||
loadBizTables()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '导入失败')) }
|
||||
finally { bizLoading.value = false }
|
||||
}
|
||||
|
||||
async function previewCode(id: number) {
|
||||
bizLoading.value = true
|
||||
try {
|
||||
const res = await GencodeAPI.preview(id)
|
||||
previewContent.value = typeof res === 'string' ? res : JSON.stringify(res, null, 2)
|
||||
previewId.value = id
|
||||
showPreview.value = true
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '预览失败')) }
|
||||
finally { bizLoading.value = false }
|
||||
}
|
||||
|
||||
async function batchGenerate(ids: number[]) {
|
||||
bizLoading.value = true
|
||||
try {
|
||||
await GencodeAPI.batchGenerate(ids)
|
||||
toast.success('代码生成成功,请下载 ZIP 文件')
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '生成失败')) }
|
||||
finally { bizLoading.value = false }
|
||||
}
|
||||
|
||||
async function syncDb(tableName: string) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: `确认同步表 ${tableName} 到数据库?`,
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
bizLoading.value = true
|
||||
try {
|
||||
await GencodeAPI.syncDb(tableName)
|
||||
toast.success('同步成功')
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '同步失败')) }
|
||||
finally { bizLoading.value = false }
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
onPullDownRefresh(() => {
|
||||
if (activeTab.value === 'biztables')
|
||||
loadBizTables()
|
||||
else loadDbTables()
|
||||
})
|
||||
|
||||
onLoad(() => loadBizTables())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="page-wraper">
|
||||
<!-- Tabs -->
|
||||
<view class="p-md flex items-center gap-sm" style="background:var(--card-bg-color);border-bottom:1px solid var(--border-color);">
|
||||
<wd-button :type="activeTab === 'biztables' ? 'primary' : 'info'" size="small" @click="activeTab = 'biztables'; loadBizTables()">
|
||||
业务表
|
||||
</wd-button>
|
||||
<wd-button :type="activeTab === 'dbtables' ? 'primary' : 'info'" size="small" @click="activeTab = 'dbtables'; loadDbTables()">
|
||||
数据库表
|
||||
</wd-button>
|
||||
</view>
|
||||
|
||||
<!-- Business Tables -->
|
||||
<template v-if="activeTab === 'biztables'">
|
||||
<view class="action-bar">
|
||||
<text class="text-md text-muted font-bold">
|
||||
共 {{ bizTotal }} 张表
|
||||
</text>
|
||||
<wd-button size="small" type="primary" @click="showDbPicker = true">
|
||||
导入表
|
||||
</wd-button>
|
||||
</view>
|
||||
<SkeletonPage v-if="bizLoading" :rows="5" />
|
||||
<template v-else>
|
||||
<view class="px-sm">
|
||||
<view class="admin-card">
|
||||
<ListEmpty v-if="bizTables.length === 0" text="暂无业务表,请从数据库导入" />
|
||||
<wd-cell-group v-else>
|
||||
<wd-cell v-for="item in bizTables" :key="item.id || item.table_name">
|
||||
<template #title>
|
||||
<view class="flex items-center gap-sm">
|
||||
<wd-icon name="list" size="18px" color="var(--primary-color)" />
|
||||
<view>
|
||||
<text class="text-md font-medium">
|
||||
{{ item.table_name || '-' }}
|
||||
</text>
|
||||
<text class="text-muted block text-xs">
|
||||
{{ item.table_comment || '' }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<template #suffix>
|
||||
<wd-button size="small" type="primary" variant="plain" @click="batchGenerate([item.id])">
|
||||
生成
|
||||
</wd-button>
|
||||
<wd-button size="small" variant="plain" @click="previewCode(item.id)">
|
||||
预览
|
||||
</wd-button>
|
||||
</template>
|
||||
</wd-cell>
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
</view>
|
||||
<PaginationBar :current="bizPageParams.page_no" :page-size="bizPageParams.page_size" :total="bizTotal" @prev="bizLoadPrev" @next="bizLoadNext" />
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- Database Tables -->
|
||||
<template v-else>
|
||||
<view class="action-bar">
|
||||
<text class="text-md text-muted font-bold">
|
||||
数据库表列表
|
||||
</text>
|
||||
<wd-button size="small" type="primary" :loading="dbLoading" @click="loadDbTables">
|
||||
刷新
|
||||
</wd-button>
|
||||
</view>
|
||||
<SkeletonPage v-if="dbLoading" :rows="5" />
|
||||
<template v-else>
|
||||
<view class="px-sm">
|
||||
<view class="admin-card">
|
||||
<ListEmpty v-if="dbTables.length === 0" text="暂无数据库表" />
|
||||
<wd-cell-group v-else>
|
||||
<wd-cell v-for="item in dbTables" :key="item.table_name">
|
||||
<template #title>
|
||||
<view class="flex items-center gap-sm">
|
||||
<wd-icon name="list" size="18px" color="var(--success-color)" />
|
||||
<view>
|
||||
<text class="text-md font-medium font-mono">
|
||||
{{ item.table_name || item.name || '-' }}
|
||||
</text>
|
||||
<text class="text-muted block text-xs">
|
||||
{{ item.table_comment || item.comment || '' }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<template #suffix>
|
||||
<wd-button size="small" type="success" variant="plain" @click="importTable(item.table_name || '')">
|
||||
导入
|
||||
</wd-button>
|
||||
</template>
|
||||
</wd-cell>
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
</view>
|
||||
<PaginationBar :current="dbPageParams.page_no" :page-size="dbPageParams.page_size" :total="dbTotal" @prev="dbLoadPrev" @next="dbLoadNext" />
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- Preview Popup -->
|
||||
<wd-popup v-model="showPreview" position="bottom" custom-style="max-height:80vh;overflow-y:auto;" round @close="showPreview = false">
|
||||
<view class="p-xl">
|
||||
<wd-navbar title="代码预览" left-arrow @click-left="showPreview = false" />
|
||||
<view class="mb-md mt-lg flex items-center justify-between">
|
||||
<text class="text-md font-bold">
|
||||
预览 ID: {{ previewId }}
|
||||
</text>
|
||||
<view class="flex gap-sm">
|
||||
<wd-button size="small" type="primary" @click="batchGenerate([previewId])">
|
||||
生成下载
|
||||
</wd-button>
|
||||
<wd-button size="small" type="success" @click="syncDb(bizTables.find(t => t.id === previewId)?.table_name || '')">
|
||||
同步数据库
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="code-block" style="background:var(--bg-color-3);border-radius:12rpx;padding:24rpx;font-family:monospace;font-size:22rpx;white-space:pre-wrap;overflow-x:auto;max-height:50vh;">
|
||||
<text>{{ previewContent || '暂无预览内容' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
|
||||
<!-- Import DB Table Picker -->
|
||||
<wd-popup v-model="showDbPicker" position="bottom" custom-style="max-height:70vh;overflow-y:auto;" round @close="showDbPicker = false">
|
||||
<view class="p-xl">
|
||||
<wd-navbar title="选择数据库表" left-arrow @click-left="showDbPicker = false" />
|
||||
<view class="mt-lg">
|
||||
<wd-cell-group border>
|
||||
<wd-cell v-for="item in dbTables" :key="item.table_name" :title="item.table_name || item.name" :label="item.table_comment || item.comment || ''" center>
|
||||
<template #suffix>
|
||||
<wd-button size="small" type="success" variant="plain" @click="importTable(item.table_name || '')">
|
||||
导入
|
||||
</wd-button>
|
||||
</template>
|
||||
</wd-cell>
|
||||
</wd-cell-group>
|
||||
<view v-if="dbTables.length === 0" class="empty-state mt-xl">
|
||||
<text class="empty-state__text">
|
||||
点击"数据库表"Tab 刷新表列表
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</view>
|
||||
</template>
|
||||
@@ -1,524 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { WorkflowItem } from '@/api/module_task/workflow/flow'
|
||||
import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app'
|
||||
import { ref } from 'vue'
|
||||
import { WorkflowAPI } from '@/api/module_task/workflow/flow'
|
||||
|
||||
definePage({
|
||||
name: 'work-workflow',
|
||||
style: { navigationBarTitleText: '工作流管理' },
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const activeTab = ref<'flows' | 'nodes'>('flows')
|
||||
|
||||
// Flow form
|
||||
const showFlowForm = ref(false)
|
||||
const flowFormTitle = ref('新建工作流')
|
||||
const flowForm = reactive({ name: '', description: '' })
|
||||
const editingFlowId = ref<number | null>(null)
|
||||
|
||||
const { list: flowList, total: flowTotal, loading, pageParams, loadData, loadPrev, loadNext } = useListPage<WorkflowItem>({
|
||||
fetcher: p => WorkflowAPI.getPage(p),
|
||||
onError: e => toast.error(getErrorMessage(e, '加载工作流列表失败')),
|
||||
})
|
||||
|
||||
function openCreateFlow() {
|
||||
flowFormTitle.value = '新建工作流'
|
||||
editingFlowId.value = null
|
||||
flowForm.name = ''
|
||||
flowForm.description = ''
|
||||
showFlowForm.value = true
|
||||
}
|
||||
|
||||
async function openEditFlow(id: number) {
|
||||
flowFormTitle.value = '编辑工作流'
|
||||
editingFlowId.value = id
|
||||
try {
|
||||
const detail = await WorkflowAPI.getDetail(id)
|
||||
flowForm.name = detail.name || ''
|
||||
flowForm.description = detail.description || ''
|
||||
showFlowForm.value = true
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '获取工作流详情失败')) }
|
||||
}
|
||||
|
||||
async function submitFlow() {
|
||||
if (!flowForm.name.trim())
|
||||
return toast.warning('请输入工作流名称')
|
||||
try {
|
||||
if (editingFlowId.value) {
|
||||
await WorkflowAPI.update(editingFlowId.value, { ...flowForm })
|
||||
toast.success('更新成功')
|
||||
}
|
||||
else {
|
||||
await WorkflowAPI.create({ ...flowForm })
|
||||
toast.success('创建成功')
|
||||
}
|
||||
showFlowForm.value = false
|
||||
loadData()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '保存失败')) }
|
||||
}
|
||||
|
||||
async function handlePublish(id: number, name: string) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: `确认发布工作流「${name}」?发布后将生成最新版本。`,
|
||||
success: async (res) => {
|
||||
if (!res.confirm)
|
||||
return
|
||||
try {
|
||||
await WorkflowAPI.publish(id)
|
||||
toast.success('发布成功')
|
||||
loadData()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '发布失败')) }
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function handleExecute(id: number, name: string) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: `确认立即执行工作流「${name}」?`,
|
||||
success: async (res) => {
|
||||
if (!res.confirm)
|
||||
return
|
||||
try {
|
||||
const result = await WorkflowAPI.execute({ workflow_id: id })
|
||||
toast.success(`执行完成:${JSON.stringify(result)}`)
|
||||
loadData()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '执行失败')) }
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function handleDeleteFlow(ids: number[]) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确认删除选中的工作流?此操作不可恢复。',
|
||||
success: async (res) => {
|
||||
if (!res.confirm)
|
||||
return
|
||||
try {
|
||||
await WorkflowAPI.remove(ids)
|
||||
toast.success('删除成功')
|
||||
loadData()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '删除失败')) }
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Status helpers
|
||||
function getFlowStatusColor(status?: string) {
|
||||
const map: Record<string, string> = { published: '#10B981', draft: '#6B7280', running: '#4F8CFF', completed: '#8B5CF6', failed: '#EF4444' }
|
||||
return map[status || ''] || '#6B7280'
|
||||
}
|
||||
function getFlowStatusLabel(status?: string) {
|
||||
const map: Record<string, string> = { published: '已发布', draft: '草稿', running: '运行中', completed: '已完成', failed: '失败' }
|
||||
return map[status || ''] || status || '未知'
|
||||
}
|
||||
|
||||
onReachBottom(() => {
|
||||
if (activeTab.value === 'flows' && !loading.value)
|
||||
loadNext()
|
||||
})
|
||||
onPullDownRefresh(() => {
|
||||
if (activeTab.value === 'flows')
|
||||
loadData()
|
||||
})
|
||||
|
||||
onLoad(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="list-page">
|
||||
<!-- Tab switcher -->
|
||||
<view class="tab-bar fade-in-up">
|
||||
<view
|
||||
class="tab-item" :class="[activeTab === 'flows' && 'tab-item--active']"
|
||||
@click="activeTab = 'flows'"
|
||||
>
|
||||
<text class="tab-item__text">
|
||||
工作流列表
|
||||
</text>
|
||||
</view>
|
||||
<view
|
||||
class="tab-item" :class="[activeTab === 'nodes' && 'tab-item--active']"
|
||||
@click="activeTab = 'nodes'"
|
||||
>
|
||||
<text class="tab-item__text">
|
||||
节点类型
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Flow List Tab -->
|
||||
<template v-if="activeTab === 'flows'">
|
||||
<!-- Create button -->
|
||||
<view class="create-btn fade-in-up-1" @click="openCreateFlow">
|
||||
<text class="create-btn__icon">
|
||||
+
|
||||
</text>
|
||||
<text class="create-btn__text">
|
||||
新建工作流
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<SkeletonPage v-if="loading && flowList.length === 0" :rows="4" />
|
||||
<ListEmpty v-else-if="!loading && flowList.length === 0" text="暂无工作流" />
|
||||
<view v-else class="flows-list">
|
||||
<view v-for="(flow, idx) in flowList" :key="flow.id" class="flow-item" :class="[`fade-in-up-${Math.min(idx % 5 + 1, 5)}`]">
|
||||
<view class="flow-item__header" @click="openEditFlow(flow.id)">
|
||||
<view class="flow-item__info">
|
||||
<text class="flow-item__name">
|
||||
{{ flow.name || '未命名' }}
|
||||
</text>
|
||||
<text v-if="flow.version" class="flow-item__version">
|
||||
v{{ flow.version }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="flow-item__badge" :style="{ background: `${getFlowStatusColor(flow.status)}18`, color: getFlowStatusColor(flow.status) }">
|
||||
{{ getFlowStatusLabel(flow.status) }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<text v-if="flow.description" class="flow-item__desc">
|
||||
{{ flow.description }}
|
||||
</text>
|
||||
|
||||
<view class="flow-item__meta">
|
||||
<text v-if="flow.nodes" class="flow-item__count">
|
||||
{{ flow.nodes.length }} 个节点
|
||||
</text>
|
||||
<text v-if="flow.updated_time" class="flow-item__time">
|
||||
{{ flow.updated_time }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="flow-item__actions">
|
||||
<view class="flow-action flow-action--primary btn-press" @click="handlePublish(flow.id, flow.name)">
|
||||
<text class="flow-action__text">
|
||||
⬆ 发布
|
||||
</text>
|
||||
</view>
|
||||
<view class="flow-action flow-action--success btn-press" @click="handleExecute(flow.id, flow.name)">
|
||||
<text class="flow-action__text">
|
||||
▶ 执行
|
||||
</text>
|
||||
</view>
|
||||
<view class="flow-action btn-press" @click="openEditFlow(flow.id)">
|
||||
<text class="flow-action__text">
|
||||
✎ 编辑
|
||||
</text>
|
||||
</view>
|
||||
<view class="flow-action flow-action--danger btn-press" @click="handleDeleteFlow([flow.id])">
|
||||
<text class="flow-action__text">
|
||||
✕ 删除
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<PaginationBar :current="pageParams.page_no" :page-size="pageParams.page_size" :total="flowTotal" @prev="loadPrev" @next="loadNext" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- Node Types Tab (placeholder for future) -->
|
||||
<template v-else>
|
||||
<ListEmpty text="节点类型管理即将上线" />
|
||||
</template>
|
||||
|
||||
<view style="height: 120rpx;" />
|
||||
|
||||
<!-- Flow Form Popup -->
|
||||
<u-popup
|
||||
:show="showFlowForm"
|
||||
mode="bottom"
|
||||
custom-style="border-radius: 32rpx 32rpx 0 0; padding-bottom: 40rpx;"
|
||||
@close="showFlowForm = false"
|
||||
>
|
||||
<view class="form-panel">
|
||||
<view class="form-panel__header">
|
||||
<text class="form-panel__title">
|
||||
{{ flowFormTitle }}
|
||||
</text>
|
||||
<text class="form-panel__close" @click="showFlowForm = false">
|
||||
✕
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="form-group">
|
||||
<text class="form-group__label">
|
||||
工作流名称 *
|
||||
</text>
|
||||
<u-input v-model="flowForm.name" placeholder="请输入工作流名称" clearable border="surround" />
|
||||
</view>
|
||||
|
||||
<view class="form-group">
|
||||
<text class="form-group__label">
|
||||
描述
|
||||
</text>
|
||||
<u-textarea
|
||||
v-model="flowForm.description"
|
||||
placeholder="请输入工作流描述"
|
||||
:maxlength="500"
|
||||
:autosize="{ minRows: 3, maxRows: 6 }"
|
||||
border="surround"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="form-actions">
|
||||
<u-button :block="true" type="primary" :text="editingFlowId ? '更新' : '创建'" @click="submitFlow" />
|
||||
</view>
|
||||
</view>
|
||||
</u-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.list-page {
|
||||
padding: 0 32rpx;
|
||||
padding-bottom: 40rpx;
|
||||
background: var(--page-bg-color, #F9F9F9);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* ===== Tab bar ===== */
|
||||
.tab-bar {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
background: var(--card-bg-color, #FFFFFF);
|
||||
border-radius: 24rpx;
|
||||
padding: 8rpx;
|
||||
margin: 24rpx 0;
|
||||
box-shadow: var(--shadow-sm, 0 1rpx 2rpx rgba(1, 77, 178,0.06));
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 72rpx;
|
||||
border-radius: 18rpx;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&--active {
|
||||
background: var(--primary-color, #4F8CFF);
|
||||
|
||||
.tab-item__text { color: #FFFFFF; font-weight: 600; }
|
||||
}
|
||||
|
||||
&__text {
|
||||
font-size: 28rpx;
|
||||
color: var(--text-color-3, #6B7280);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Create button ===== */
|
||||
.create-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
height: 80rpx;
|
||||
background: var(--primary-color, #4F8CFF);
|
||||
border-radius: 24rpx;
|
||||
margin-bottom: 24rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(1, 77, 178, 0.2);
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:active { opacity: 0.85; transform: scale(0.98); }
|
||||
|
||||
&__icon {
|
||||
font-size: 36rpx;
|
||||
color: #FFFFFF;
|
||||
font-weight: 300;
|
||||
}
|
||||
&__text {
|
||||
font-size: 28rpx;
|
||||
color: #FFFFFF;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Flows list ===== */
|
||||
.flows-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.flow-item {
|
||||
background: var(--card-bg-color, #FFFFFF);
|
||||
border-radius: 24rpx;
|
||||
padding: 24rpx;
|
||||
box-shadow: var(--shadow-sm, 0 1rpx 2rpx rgba(1, 77, 178,0.06));
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12rpx;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text-color, #0A1628);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__version {
|
||||
font-size: 22rpx;
|
||||
color: var(--primary-color, #4F8CFF);
|
||||
background: var(--primary-color-light, rgba(1,77,178,0.08));
|
||||
padding: 2rpx 10rpx;
|
||||
border-radius: 8rpx;
|
||||
font-family: 'SF Mono', 'Consolas', monospace;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__badge {
|
||||
font-size: 20rpx;
|
||||
padding: 2rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__desc {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: var(--text-color-2, #4B5563);
|
||||
margin-bottom: 10rpx;
|
||||
line-height: 1.5;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
&__count {
|
||||
font-size: 22rpx;
|
||||
color: var(--text-color-3, #6B7280);
|
||||
background: var(--bg-color-2, #F5F6F8);
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
&__time {
|
||||
font-size: 22rpx;
|
||||
color: var(--text-color-4, #B0B0B0);
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
padding-top: 12rpx;
|
||||
border-top: 1rpx solid var(--border-color, #F0F0F0);
|
||||
}
|
||||
}
|
||||
|
||||
.flow-action {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 56rpx;
|
||||
border-radius: 12rpx;
|
||||
background: var(--bg-color-2, #F5F6F8);
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&--primary { background: var(--primary-color-light, rgba(1,77,178,0.08)); }
|
||||
&--success { background: rgba(16, 185, 129, 0.08); }
|
||||
&--danger { background: var(--danger-color-light, #FEF2F2); }
|
||||
|
||||
&--primary .flow-action__text { color: var(--primary-color, #4F8CFF); }
|
||||
&--success .flow-action__text { color: #10B981; }
|
||||
&--danger .flow-action__text { color: var(--danger-color, #EF4444); }
|
||||
|
||||
&__text {
|
||||
font-size: 22rpx;
|
||||
font-weight: 500;
|
||||
color: var(--text-color-2, #4B5563);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&:active { opacity: 0.7; transform: scale(0.95); }
|
||||
}
|
||||
|
||||
/* ===== Form Panel ===== */
|
||||
.form-panel {
|
||||
padding: 32rpx;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
color: var(--text-color, #0A1628);
|
||||
}
|
||||
|
||||
&__close {
|
||||
font-size: 32rpx;
|
||||
color: var(--text-color-3, #6B7280);
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: var(--bg-color-2, #F5F6F8);
|
||||
|
||||
&:active { opacity: 0.6; }
|
||||
}
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
&__label {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
color: var(--text-color-2, #4B5563);
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
margin-top: 32rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -1,94 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { CacheInfo } from '@/api/module_monitor/cache'
|
||||
import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app'
|
||||
import { ref } from 'vue'
|
||||
import { CacheAPI } from '@/api/module_monitor/cache'
|
||||
|
||||
definePage({ name: 'work-cache', style: { navigationBarTitleText: '缓存监控' } })
|
||||
|
||||
const toast = useToast()
|
||||
const loading = ref(false)
|
||||
const info = ref<CacheInfo>()
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
info.value = await CacheAPI.getInfo()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '加载失败')) }
|
||||
finally {
|
||||
loading.value = false
|
||||
uni.stopPullDownRefresh()
|
||||
}
|
||||
}
|
||||
function clearCache() {
|
||||
uni.showModal({ title: '提示', content: '确认清除所有缓存?', success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await CacheAPI.clear()
|
||||
toast.success('缓存已清除')
|
||||
loadData()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '操作失败')) }
|
||||
}
|
||||
} })
|
||||
}
|
||||
|
||||
onPullDownRefresh(() => {
|
||||
loadData()
|
||||
})
|
||||
|
||||
onLoad(() => loadData())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="page-wraper">
|
||||
<view v-if="loading" class="flex justify-center items-center py-4xl">
|
||||
<wd-loading />
|
||||
</view>
|
||||
<view v-else-if="info" class="p-md">
|
||||
<!-- Stats row -->
|
||||
<view class="stats-grid cache-stats">
|
||||
<view class="stat-card">
|
||||
<text class="stat-card__label">
|
||||
键数量
|
||||
</text><text class="stat-card__value" style="color:var(--primary-color);">
|
||||
{{ info.keys_count }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="stat-card">
|
||||
<text class="stat-card__label">
|
||||
命中率
|
||||
</text><text class="stat-card__value" style="color:var(--success-color);">
|
||||
{{ info.hit_rate || '-' }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="stat-card">
|
||||
<text class="stat-card__label">
|
||||
内存使用
|
||||
</text><text class="stat-card__value" style="color:var(--warning-color);font-size:var(--font-xl);">
|
||||
{{ info.used_memory || '-' }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="stat-card">
|
||||
<text class="stat-card__label">
|
||||
数据库大小
|
||||
</text><text class="stat-card__value" style="color:var(--info-color);font-size:var(--font-xl);">
|
||||
{{ info.db_size ?? '-' }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="action-bar justify-center">
|
||||
<wd-button size="small" type="danger" plain @click="clearCache">清除缓存</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="empty-state">
|
||||
<text class="empty-state__icon">
|
||||
⚠️
|
||||
</text>
|
||||
<text class="empty-state__text">
|
||||
加载失败,请下拉刷新重试
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -1,986 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { JobLogItem, SchedulerJob } from '@/api/module_task/cronjob/job'
|
||||
import type { NodeForm, NodeItem } from '@/api/module_task/cronjob/node'
|
||||
import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app'
|
||||
import { ref } from 'vue'
|
||||
import { JobAPI, JobLogAPI, SchedulerAPI } from '@/api/module_task/cronjob/job'
|
||||
import { NodeAPI } from '@/api/module_task/cronjob/node'
|
||||
|
||||
definePage({
|
||||
name: 'work-cronjob',
|
||||
style: { navigationBarTitleText: '定时任务' },
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const jobsLoading = ref(false)
|
||||
const schedulerRunning = ref(false)
|
||||
const schedulerJobs = ref<SchedulerJob[]>([])
|
||||
const activeTab = ref<'jobs' | 'logs' | 'nodes'>('jobs')
|
||||
|
||||
// ---- Node management ----
|
||||
const showNodeForm = ref(false)
|
||||
const nodeFormTitle = ref('新建节点')
|
||||
const editingNodeId = ref<number | null>(null)
|
||||
const nodeForm = reactive<NodeForm & { id?: number }>({
|
||||
name: '',
|
||||
host: '',
|
||||
port: 0,
|
||||
type: 'default',
|
||||
description: '',
|
||||
})
|
||||
|
||||
const {
|
||||
list: nodeList,
|
||||
total: nodeTotal,
|
||||
loading: nodeLoading,
|
||||
pageParams: nodePageParams,
|
||||
loadData: loadNodes,
|
||||
loadPrev: nodeLoadPrev,
|
||||
loadNext: nodeLoadNext,
|
||||
} = useListPage<NodeItem>({
|
||||
fetcher: p => NodeAPI.getPage(p),
|
||||
onError: e => toast.error(getErrorMessage(e, '加载节点列表失败')),
|
||||
})
|
||||
|
||||
function openCreateNode() {
|
||||
nodeFormTitle.value = '新建节点'
|
||||
editingNodeId.value = null
|
||||
Object.assign(nodeForm, { name: '', host: '', port: 0, type: 'default', description: '' })
|
||||
showNodeForm.value = true
|
||||
}
|
||||
|
||||
async function openEditNode(id: number) {
|
||||
nodeFormTitle.value = '编辑节点'
|
||||
editingNodeId.value = id
|
||||
try {
|
||||
const detail = await NodeAPI.getDetail(id)
|
||||
Object.assign(nodeForm, { name: detail.name, host: detail.host, port: detail.port, type: detail.type, description: detail.description || '' })
|
||||
showNodeForm.value = true
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '获取节点详情失败')) }
|
||||
}
|
||||
|
||||
async function submitNode() {
|
||||
if (!nodeForm.name.trim())
|
||||
return toast.warning('请输入节点名称')
|
||||
if (!nodeForm.host.trim())
|
||||
return toast.warning('请输入节点地址')
|
||||
try {
|
||||
if (editingNodeId.value) {
|
||||
await NodeAPI.update(editingNodeId.value, { ...nodeForm })
|
||||
toast.success('更新成功')
|
||||
}
|
||||
else {
|
||||
await NodeAPI.create({ ...nodeForm })
|
||||
toast.success('创建成功')
|
||||
}
|
||||
showNodeForm.value = false
|
||||
loadNodes()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '保存失败')) }
|
||||
}
|
||||
|
||||
async function handleDeleteNode(ids: number[]) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确认删除选中的节点?',
|
||||
success: async (res) => {
|
||||
if (!res.confirm)
|
||||
return
|
||||
try {
|
||||
await NodeAPI.remove(ids)
|
||||
toast.success('删除成功')
|
||||
loadNodes()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '删除失败')) }
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function handleClearNode(id: number) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确认清空该节点上的所有任务?',
|
||||
success: async (res) => {
|
||||
if (!res.confirm)
|
||||
return
|
||||
try {
|
||||
await NodeAPI.clear(id)
|
||||
toast.success('清空成功')
|
||||
loadNodes()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '清空失败')) }
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function handleExecuteNode(id: number) {
|
||||
try {
|
||||
await NodeAPI.execute(id)
|
||||
toast.success('节点调试任务已触发')
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '执行失败')) }
|
||||
}
|
||||
|
||||
async function refreshScheduler() {
|
||||
jobsLoading.value = true
|
||||
try {
|
||||
const [status, jobs] = await Promise.all([
|
||||
SchedulerAPI.getStatus(),
|
||||
SchedulerAPI.getJobs(),
|
||||
])
|
||||
schedulerRunning.value = status.running
|
||||
schedulerJobs.value = jobs || []
|
||||
}
|
||||
catch (e) {
|
||||
toast.error(getErrorMessage(e, '获取调度器状态失败'))
|
||||
}
|
||||
finally {
|
||||
jobsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleScheduler(action: 'start' | 'pause' | 'resume' | 'shutdown') {
|
||||
const labels: Record<string, string> = { start: '启动', pause: '暂停', resume: '恢复', shutdown: '关闭' }
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: `确认${labels[action]}调度器?`,
|
||||
success: async (res) => {
|
||||
if (!res.confirm)
|
||||
return
|
||||
try {
|
||||
if (action === 'start')
|
||||
await SchedulerAPI.start()
|
||||
else if (action === 'pause')
|
||||
await SchedulerAPI.pause()
|
||||
else if (action === 'resume')
|
||||
await SchedulerAPI.resume()
|
||||
else await SchedulerAPI.shutdown()
|
||||
toast.success(`${labels[action]}成功`)
|
||||
refreshScheduler()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '操作失败')) }
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function handleTask(action: 'pause' | 'resume' | 'run' | 'remove', jobId: string, jobName: string) {
|
||||
const labels: Record<string, string> = { pause: '暂停', resume: '恢复', run: '执行', remove: '移除' }
|
||||
const content = action === 'remove' ? `确认移除任务「${jobName}」?` : `确认${labels[action]}任务「${jobName}」?`
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content,
|
||||
success: async (res) => {
|
||||
if (!res.confirm)
|
||||
return
|
||||
try {
|
||||
if (action === 'pause') {
|
||||
await JobAPI.pause(jobId)
|
||||
}
|
||||
else if (action === 'resume') {
|
||||
await JobAPI.resume(jobId)
|
||||
}
|
||||
else if (action === 'run') {
|
||||
await JobAPI.run(jobId)
|
||||
toast.success('任务已触发执行')
|
||||
}
|
||||
else {
|
||||
await JobAPI.remove(jobId)
|
||||
}
|
||||
if (action !== 'run')
|
||||
toast.success(`${labels[action]}成功`)
|
||||
refreshScheduler()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '操作失败')) }
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const {
|
||||
list: logList,
|
||||
total: logTotal,
|
||||
loading: logLoading,
|
||||
pageParams: logPageParams,
|
||||
loadData: loadLogs,
|
||||
loadPrev: logLoadPrev,
|
||||
loadNext: logLoadNext,
|
||||
} = useListPage<JobLogItem>({
|
||||
fetcher: p => JobLogAPI.getPage(p),
|
||||
onError: e => toast.error(getErrorMessage(e, '加载日志失败')),
|
||||
})
|
||||
|
||||
async function handleClearLogs(ids: number[]) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确认删除这些执行日志?',
|
||||
success: async (res) => {
|
||||
if (!res.confirm)
|
||||
return
|
||||
try {
|
||||
await JobLogAPI.remove(ids)
|
||||
toast.success('删除成功')
|
||||
loadLogs()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '删除失败')) }
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
onPullDownRefresh(() => {
|
||||
if (activeTab.value === 'jobs')
|
||||
refreshScheduler()
|
||||
else if (activeTab.value === 'logs')
|
||||
loadLogs()
|
||||
else loadNodes()
|
||||
})
|
||||
|
||||
onLoad(() => {
|
||||
refreshScheduler()
|
||||
loadLogs()
|
||||
loadNodes()
|
||||
})
|
||||
|
||||
// Status helpers
|
||||
function getJobStatusColor(status?: string) {
|
||||
const map: Record<string, string> = { running: '#10B981', paused: '#F59E0B', error: '#EF4444' }
|
||||
return map[status || ''] || '#6B7280'
|
||||
}
|
||||
function getJobStatusLabel(status?: string) {
|
||||
const map: Record<string, string> = { running: '运行中', paused: '已暂停', error: '异常' }
|
||||
return map[status || ''] || status || '未知'
|
||||
}
|
||||
function getLogColor(status?: string) {
|
||||
const map: Record<string, string> = { success: '#10B981', failed: '#EF4444', running: '#4F8CFF' }
|
||||
return map[status || ''] || '#6B7280'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="list-page">
|
||||
<!-- Scheduler status bar -->
|
||||
<view class="scheduler-bar">
|
||||
<view class="scheduler-bar__status">
|
||||
<view class="scheduler-dot" :class="schedulerRunning ? 'scheduler-dot--on' : 'scheduler-dot--off'" />
|
||||
<text class="scheduler-bar__label">
|
||||
{{ schedulerRunning ? '调度器运行中' : '调度器已停止' }}
|
||||
</text>
|
||||
<text class="scheduler-bar__count">
|
||||
{{ schedulerJobs.length }} 个任务
|
||||
</text>
|
||||
</view>
|
||||
<view class="scheduler-bar__actions">
|
||||
<template v-if="schedulerRunning">
|
||||
<view class="scheduler-btn btn-press" @click="handleScheduler('pause')">
|
||||
<text class="scheduler-btn__text">
|
||||
⏸ 暂停
|
||||
</text>
|
||||
</view>
|
||||
<view class="scheduler-btn scheduler-btn--danger btn-press" @click="handleScheduler('shutdown')">
|
||||
<text class="scheduler-btn__text">
|
||||
⏹ 关闭
|
||||
</text>
|
||||
</view>
|
||||
</template>
|
||||
<template v-else>
|
||||
<view class="scheduler-btn scheduler-btn--primary btn-press" @click="handleScheduler('start')">
|
||||
<text class="scheduler-btn__text">
|
||||
▶ 启动
|
||||
</text>
|
||||
</view>
|
||||
<view class="scheduler-btn btn-press" @click="handleScheduler('resume')">
|
||||
<text class="scheduler-btn__text">
|
||||
↻ 恢复
|
||||
</text>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Tab switcher -->
|
||||
<view class="tab-bar">
|
||||
<view
|
||||
class="tab-item" :class="[activeTab === 'jobs' && 'tab-item--active']"
|
||||
@click="activeTab = 'jobs'"
|
||||
>
|
||||
<text class="tab-item__text">
|
||||
任务列表
|
||||
</text>
|
||||
</view>
|
||||
<view
|
||||
class="tab-item" :class="[activeTab === 'logs' && 'tab-item--active']"
|
||||
@click="activeTab = 'logs'"
|
||||
>
|
||||
<text class="tab-item__text">
|
||||
执行日志
|
||||
</text>
|
||||
</view>
|
||||
<view
|
||||
class="tab-item" :class="[activeTab === 'nodes' && 'tab-item--active']"
|
||||
@click="activeTab = 'nodes'"
|
||||
>
|
||||
<text class="tab-item__text">
|
||||
节点管理
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Jobs tab -->
|
||||
<template v-if="activeTab === 'jobs'">
|
||||
<SkeletonPage v-if="jobsLoading && schedulerJobs.length === 0" :rows="3" />
|
||||
<ListEmpty v-else-if="!jobsLoading && schedulerJobs.length === 0" text="暂无任务" />
|
||||
<view v-else class="jobs-list">
|
||||
<view v-for="job in schedulerJobs" :key="job.id" class="job-item">
|
||||
<view class="job-item__header">
|
||||
<text class="job-item__name">
|
||||
{{ job.name }}
|
||||
</text>
|
||||
<view class="job-item__actions">
|
||||
<text class="job-action" @click="handleTask('pause', job.id, job.name)">
|
||||
⏸
|
||||
</text>
|
||||
<text class="job-action" @click="handleTask('run', job.id, job.name)">
|
||||
▶
|
||||
</text>
|
||||
<text class="job-action job-action--danger" @click="handleTask('remove', job.id, job.name)">
|
||||
✕
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="job-item__meta">
|
||||
<text class="job-item__func">
|
||||
{{ job.func || '—' }}
|
||||
</text>
|
||||
<view class="job-item__badge" :style="{ background: `${getJobStatusColor(job.status)}18`, color: getJobStatusColor(job.status) }">
|
||||
{{ getJobStatusLabel(job.status) }}
|
||||
</view>
|
||||
</view>
|
||||
<text v-if="job.next_run_time" class="job-item__next">
|
||||
下次执行: {{ job.next_run_time }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- Logs tab -->
|
||||
<template v-else-if="activeTab === 'logs'">
|
||||
<SkeletonPage v-if="logLoading && logList.length === 0" :rows="5" />
|
||||
<ListEmpty v-else-if="!logLoading && logList.length === 0" text="暂无执行日志" />
|
||||
<view v-else class="logs-list">
|
||||
<view v-for="log in logList" :key="log.id" class="log-item">
|
||||
<view class="log-item__header">
|
||||
<text class="log-item__name">
|
||||
{{ log.job_name || '—' }}
|
||||
</text>
|
||||
<view class="log-item__badge" :style="{ background: `${getLogColor(log.status)}18`, color: getLogColor(log.status) }">
|
||||
{{ log.status || '—' }}
|
||||
</view>
|
||||
</view>
|
||||
<text class="log-item__time">
|
||||
{{ log.start_time || '—' }}
|
||||
</text>
|
||||
<text v-if="log.error_msg" class="log-item__error">
|
||||
{{ log.error_msg }}
|
||||
</text>
|
||||
<view class="log-item__delete" @click="handleClearLogs([log.id])">
|
||||
<text class="log-item__delete-icon">
|
||||
🗑
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<PaginationBar :current="logPageParams.page_no" :page-size="logPageParams.page_size" :total="logTotal" @prev="logLoadPrev" @next="logLoadNext" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- Nodes tab -->
|
||||
<template v-else-if="activeTab === 'nodes'">
|
||||
<view class="create-btn" @click="openCreateNode">
|
||||
<text class="create-btn__icon">
|
||||
+
|
||||
</text>
|
||||
<text class="create-btn__text">
|
||||
新建节点
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view v-if="nodeLoading && nodeList.length === 0">
|
||||
<SkeletonPage :rows="3" />
|
||||
</view>
|
||||
<ListEmpty v-else-if="!nodeLoading && nodeList.length === 0" text="暂无节点" />
|
||||
<view v-else class="nodes-list">
|
||||
<view v-for="node in nodeList" :key="node.id" class="node-item">
|
||||
<view class="node-item__header">
|
||||
<view class="node-item__info">
|
||||
<text class="node-item__name">
|
||||
{{ node.name || '未命名' }}
|
||||
</text>
|
||||
<text class="node-item__host">
|
||||
{{ node.host }}{{ node.port ? `:${node.port}` : '' }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="node-item__badge" :style="{ background: node.type === 'master' ? '#8B5CF618' : '#6B728018', color: node.type === 'master' ? '#8B5CF6' : '#6B7280' }">
|
||||
{{ node.type === 'master' ? '主节点' : node.type || '默认' }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<text v-if="node.description" class="node-item__desc">
|
||||
{{ node.description }}
|
||||
</text>
|
||||
|
||||
<view v-if="node.status" class="node-item__meta">
|
||||
<text class="node-item__meta-text">
|
||||
状态: {{ node.status }}
|
||||
</text>
|
||||
<text v-if="node.task_count !== undefined" class="node-item__meta-text">
|
||||
{{ node.task_count }} 个任务
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="node-item__actions">
|
||||
<view class="node-action node-action--primary btn-press" @click="handleExecuteNode(node.id)">
|
||||
<text class="node-action__text">
|
||||
▶ 调试
|
||||
</text>
|
||||
</view>
|
||||
<view class="node-action btn-press" @click="openEditNode(node.id)">
|
||||
<text class="node-action__text">
|
||||
✎ 编辑
|
||||
</text>
|
||||
</view>
|
||||
<view class="node-action btn-press" @click="handleClearNode(node.id)">
|
||||
<text class="node-action__text">
|
||||
↻ 清空
|
||||
</text>
|
||||
</view>
|
||||
<view class="node-action node-action--danger btn-press" @click="handleDeleteNode([node.id])">
|
||||
<text class="node-action__text">
|
||||
✕ 删除
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<PaginationBar :current="nodePageParams.page_no" :page-size="nodePageParams.page_size" :total="nodeTotal" @prev="nodeLoadPrev" @next="nodeLoadNext" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<!-- ========== Node Form Popup ========== -->
|
||||
<wd-popup
|
||||
v-model="showNodeForm"
|
||||
position="bottom"
|
||||
custom-style="border-radius: 32rpx 32rpx 0 0; padding-bottom: 40rpx;"
|
||||
@close="showNodeForm = false"
|
||||
>
|
||||
<view class="form-panel">
|
||||
<view class="form-panel__header">
|
||||
<text class="form-panel__title">
|
||||
{{ nodeFormTitle }}
|
||||
</text>
|
||||
<text class="form-panel__close" @click="showNodeForm = false">
|
||||
✕
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="form-group">
|
||||
<text class="form-group__label">
|
||||
节点名称 *
|
||||
</text>
|
||||
<wd-input v-model="nodeForm.name" placeholder="请输入节点名称" clearable />
|
||||
</view>
|
||||
|
||||
<view class="form-group">
|
||||
<text class="form-group__label">
|
||||
主机地址 *
|
||||
</text>
|
||||
<wd-input v-model="nodeForm.host" placeholder="请输入主机地址 (IP/域名)" clearable />
|
||||
</view>
|
||||
|
||||
<view class="form-row">
|
||||
<view class="form-group form-group--half">
|
||||
<text class="form-group__label">
|
||||
端口
|
||||
</text>
|
||||
<wd-input v-model="nodeForm.port" type="number" placeholder="端口号" />
|
||||
</view>
|
||||
<view class="form-group form-group--half">
|
||||
<text class="form-group__label">
|
||||
类型
|
||||
</text>
|
||||
<wd-input v-model="nodeForm.type" placeholder="default / master" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-group">
|
||||
<text class="form-group__label">
|
||||
描述
|
||||
</text>
|
||||
<wd-textarea v-model="nodeForm.description" placeholder="节点描述" :maxlength="200" auto-height />
|
||||
</view>
|
||||
|
||||
<view class="form-actions">
|
||||
<wd-button block type="primary" @click="submitNode">{{ editingNodeId ? '更新' : '创建' }}</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
|
||||
<view style="height: 100rpx;" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.list-page {
|
||||
padding: 0 32rpx;
|
||||
padding-bottom: 40rpx;
|
||||
background: var(--page-bg-color, #F9F9F9);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* ===== Scheduler bar ===== */
|
||||
.scheduler-bar {
|
||||
background: var(--card-bg-color, #FFFFFF);
|
||||
border-radius: 24rpx;
|
||||
padding: 24rpx;
|
||||
margin: 24rpx 0;
|
||||
box-shadow: var(--shadow-sm, 0 1rpx 2rpx rgba(1, 77, 178,0.06));
|
||||
|
||||
&__status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text-color, #0A1628);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__count {
|
||||
font-size: 24rpx;
|
||||
color: var(--text-color-3, #6B7280);
|
||||
background: var(--bg-color-2, #F5F6F8);
|
||||
padding: 4rpx 16rpx;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.scheduler-dot {
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
border-radius: 50%;
|
||||
|
||||
&--on { background: #10B981; box-shadow: 0 0 8rpx rgba(16, 185, 129, 0.4); }
|
||||
&--off { background: #9CA3AF; }
|
||||
}
|
||||
|
||||
.scheduler-btn {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 64rpx;
|
||||
border-radius: 16rpx;
|
||||
background: var(--bg-color-2, #F5F6F8);
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&--primary {
|
||||
background: var(--primary-color, #4F8CFF);
|
||||
.scheduler-btn__text { color: #FFFFFF; }
|
||||
}
|
||||
|
||||
&--danger {
|
||||
background: var(--danger-color-light, #FEF2F2);
|
||||
.scheduler-btn__text { color: var(--danger-color, #EF4444); }
|
||||
}
|
||||
|
||||
&__text {
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
color: var(--text-color-2, #4B5563);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Tab bar ===== */
|
||||
.tab-bar {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
background: var(--card-bg-color, #FFFFFF);
|
||||
border-radius: 24rpx;
|
||||
padding: 8rpx;
|
||||
margin-bottom: 24rpx;
|
||||
box-shadow: var(--shadow-sm, 0 1rpx 2rpx rgba(1, 77, 178,0.06));
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 72rpx;
|
||||
border-radius: 18rpx;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&--active {
|
||||
background: var(--primary-color, #4F8CFF);
|
||||
|
||||
.tab-item__text { color: #FFFFFF; font-weight: 600; }
|
||||
}
|
||||
|
||||
&__text {
|
||||
font-size: 28rpx;
|
||||
color: var(--text-color-3, #6B7280);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Jobs list ===== */
|
||||
.jobs-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.job-item {
|
||||
background: var(--card-bg-color, #FFFFFF);
|
||||
border-radius: 24rpx;
|
||||
padding: 24rpx;
|
||||
box-shadow: var(--shadow-sm, 0 1rpx 2rpx rgba(1, 77, 178,0.06));
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text-color, #0A1628);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
&__func {
|
||||
font-size: 24rpx;
|
||||
color: var(--text-color-3, #6B7280);
|
||||
font-family: 'SF Mono', 'Consolas', monospace;
|
||||
}
|
||||
|
||||
&__badge {
|
||||
font-size: 20rpx;
|
||||
padding: 2rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&__next {
|
||||
font-size: 22rpx;
|
||||
color: var(--text-color-4, #B0B0B0);
|
||||
}
|
||||
}
|
||||
|
||||
.job-action {
|
||||
font-size: 24rpx;
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: var(--bg-color-2, #F5F6F8);
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&--danger {
|
||||
color: var(--danger-color, #EF4444);
|
||||
}
|
||||
|
||||
&:active {
|
||||
opacity: 0.7;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Logs list ===== */
|
||||
.logs-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.log-item {
|
||||
position: relative;
|
||||
background: var(--card-bg-color, #FFFFFF);
|
||||
border-radius: 24rpx;
|
||||
padding: 24rpx 72rpx 24rpx 24rpx;
|
||||
box-shadow: var(--shadow-sm, 0 1rpx 2rpx rgba(1, 77, 178,0.06));
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text-color, #0A1628);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__badge {
|
||||
font-size: 22rpx;
|
||||
padding: 2rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&__time {
|
||||
font-size: 24rpx;
|
||||
color: var(--text-color-4, #B0B0B0);
|
||||
}
|
||||
|
||||
&__error {
|
||||
display: block;
|
||||
font-size: 22rpx;
|
||||
color: var(--danger-color, #EF4444);
|
||||
background: var(--danger-color-light, #FEF2F2);
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 8rpx;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
&__delete {
|
||||
position: absolute;
|
||||
top: 24rpx;
|
||||
right: 24rpx;
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
border-radius: 50%;
|
||||
background: var(--bg-color-2, #F5F6F8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&-icon { font-size: 24rpx; opacity: 0.4; }
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Load more ===== */
|
||||
.load-more {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 80rpx;
|
||||
background: var(--bg-color-2, #F5F6F8);
|
||||
border-radius: 16rpx;
|
||||
margin-top: 8rpx;
|
||||
|
||||
&__text {
|
||||
font-size: 26rpx;
|
||||
color: var(--text-color-3, #6B7280);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Create button ===== */
|
||||
.create-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
height: 80rpx;
|
||||
background: var(--primary-color, #4F8CFF);
|
||||
border-radius: 24rpx;
|
||||
margin-bottom: 24rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(1, 77, 178, 0.2);
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:active { opacity: 0.85; transform: scale(0.98); }
|
||||
|
||||
&__icon {
|
||||
font-size: 36rpx;
|
||||
color: #FFFFFF;
|
||||
font-weight: 300;
|
||||
}
|
||||
&__text {
|
||||
font-size: 28rpx;
|
||||
color: #FFFFFF;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Nodes list ===== */
|
||||
.nodes-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.node-item {
|
||||
background: var(--card-bg-color, #FFFFFF);
|
||||
border-radius: 24rpx;
|
||||
padding: 24rpx;
|
||||
box-shadow: var(--shadow-sm, 0 1rpx 2rpx rgba(1, 77, 178,0.06));
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
&__info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__name {
|
||||
display: block;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text-color, #0A1628);
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
&__host {
|
||||
font-size: 24rpx;
|
||||
color: var(--text-color-3, #6B7280);
|
||||
font-family: 'SF Mono', 'Consolas', monospace;
|
||||
}
|
||||
|
||||
&__badge {
|
||||
font-size: 20rpx;
|
||||
padding: 2rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
font-weight: 500;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__desc {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: var(--text-color-2, #4B5563);
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
&__meta-text {
|
||||
font-size: 22rpx;
|
||||
color: var(--text-color-4, #B0B0B0);
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
padding-top: 12rpx;
|
||||
border-top: 1rpx solid var(--border-color, #F0F0F0);
|
||||
}
|
||||
}
|
||||
|
||||
.node-action {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 56rpx;
|
||||
border-radius: 12rpx;
|
||||
background: var(--bg-color-2, #F5F6F8);
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&--primary { background: var(--primary-color-light, rgba(1,77,178,0.08)); }
|
||||
&--danger { background: var(--danger-color-light, #FEF2F2); }
|
||||
|
||||
&--primary .node-action__text { color: var(--primary-color, #4F8CFF); }
|
||||
&--danger .node-action__text { color: var(--danger-color, #EF4444); }
|
||||
|
||||
&__text {
|
||||
font-size: 22rpx;
|
||||
font-weight: 500;
|
||||
color: var(--text-color-2, #4B5563);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&:active { opacity: 0.7; transform: scale(0.95); }
|
||||
}
|
||||
|
||||
/* ===== Form Panel ===== */
|
||||
.form-panel {
|
||||
padding: 32rpx;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
color: var(--text-color, #0A1628);
|
||||
}
|
||||
|
||||
&__close {
|
||||
font-size: 32rpx;
|
||||
color: var(--text-color-3, #6B7280);
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: var(--bg-color-2, #F5F6F8);
|
||||
|
||||
&:active { opacity: 0.6; }
|
||||
}
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
&__label {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
color: var(--text-color-2, #4B5563);
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
&--half {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
margin-top: 32rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -1,373 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { OnlineUserItem } from '@/api/module_monitor/online'
|
||||
import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app'
|
||||
import { ref } from 'vue'
|
||||
import { OnlineAPI } from '@/api/module_monitor/online'
|
||||
import SkeletonPage from '@/components/SkeletonPage.vue'
|
||||
|
||||
definePage({
|
||||
name: 'work-devices',
|
||||
style: { navigationBarTitleText: '设备管理' },
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const loading = ref(false)
|
||||
const sessions = ref<OnlineUserItem[]>([])
|
||||
|
||||
async function loadSessions() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await OnlineAPI.getCurrent()
|
||||
sessions.value = res || []
|
||||
}
|
||||
catch (e) {
|
||||
toast.error(getErrorMessage(e, '加载设备列表失败'))
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
uni.stopPullDownRefresh()
|
||||
}
|
||||
}
|
||||
|
||||
async function handleForceLogout(sessionId: string) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确认强制下线此设备?该设备的登录状态将被立即清除。',
|
||||
success: async (res) => {
|
||||
if (!res.confirm)
|
||||
return
|
||||
try {
|
||||
await OnlineAPI.forceLogout(sessionId)
|
||||
toast.success('已强制下线')
|
||||
loadSessions()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '操作失败')) }
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function getDeviceIcon(os?: string) {
|
||||
if (!os)
|
||||
return '📱'
|
||||
const lower = os.toLowerCase()
|
||||
if (lower.includes('ios') || lower.includes('iphone') || lower.includes('ipad'))
|
||||
return '📱'
|
||||
if (lower.includes('android'))
|
||||
return '🤖'
|
||||
if (lower.includes('windows'))
|
||||
return '💻'
|
||||
if (lower.includes('mac'))
|
||||
return '🖥'
|
||||
if (lower.includes('linux'))
|
||||
return '🐧'
|
||||
return '📱'
|
||||
}
|
||||
|
||||
function getBrowserLabel(browser?: string) {
|
||||
if (!browser)
|
||||
return '未知浏览器'
|
||||
const lower = browser.toLowerCase()
|
||||
if (lower.includes('wechat') || lower.includes('weixin'))
|
||||
return '微信内置'
|
||||
if (lower.includes('chrome'))
|
||||
return 'Chrome'
|
||||
if (lower.includes('safari'))
|
||||
return 'Safari'
|
||||
if (lower.includes('firefox'))
|
||||
return 'Firefox'
|
||||
if (lower.includes('edge'))
|
||||
return 'Edge'
|
||||
return browser
|
||||
}
|
||||
|
||||
onPullDownRefresh(() => {
|
||||
loadSessions()
|
||||
})
|
||||
onLoad(() => {
|
||||
loadSessions()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="list-page">
|
||||
<!-- Summary Card -->
|
||||
<view class="summary-card fade-in-up">
|
||||
<view class="summary-card__icon">
|
||||
<text class="summary-card__icon-text">
|
||||
🔐
|
||||
</text>
|
||||
</view>
|
||||
<view class="summary-card__body">
|
||||
<text class="summary-card__title">
|
||||
登录设备管理
|
||||
</text>
|
||||
<text class="summary-card__desc">
|
||||
当前共有 {{ sessions.length }} 个活跃会话
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Sessions List -->
|
||||
<SkeletonPage v-if="loading && sessions.length === 0" :rows="3" />
|
||||
<ListEmpty v-else-if="!loading && sessions.length === 0" text="暂无活跃会话" />
|
||||
<view v-else class="sessions-list">
|
||||
<view
|
||||
v-for="(session, idx) in sessions"
|
||||
:key="(session.session_id || '') + idx"
|
||||
class="session-item" :class="[`fade-in-up-${Math.min(idx % 5 + 1, 5)}`]"
|
||||
>
|
||||
<view class="session-item__header">
|
||||
<view class="session-item__icon">
|
||||
<text class="session-item__icon-text">
|
||||
{{ getDeviceIcon(session.os) }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="session-item__info">
|
||||
<text class="session-item__os">
|
||||
{{ session.os || '未知设备' }}
|
||||
</text>
|
||||
<text class="session-item__browser">
|
||||
{{ getBrowserLabel(session.browser) }}
|
||||
</text>
|
||||
</view>
|
||||
<view
|
||||
v-if="idx === 0"
|
||||
class="session-item__current-tag"
|
||||
>
|
||||
<text class="session-item__current-text">
|
||||
当前
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="session-item__details">
|
||||
<view class="detail-row">
|
||||
<text class="detail-row__label">
|
||||
IP 地址
|
||||
</text>
|
||||
<text class="detail-row__value">
|
||||
{{ session.ipaddr || '—' }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="detail-row">
|
||||
<text class="detail-row__label">
|
||||
登录位置
|
||||
</text>
|
||||
<text class="detail-row__value">
|
||||
{{ session.login_location || '—' }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="detail-row">
|
||||
<text class="detail-row__label">
|
||||
登录时间
|
||||
</text>
|
||||
<text class="detail-row__value">
|
||||
{{ session.login_time || '—' }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="idx !== 0" class="session-item__action">
|
||||
<view class="logout-btn btn-press" @click="handleForceLogout(session.session_id || '')">
|
||||
<text class="logout-btn__text">
|
||||
强制下线
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Tips -->
|
||||
<view class="tips fade-in-up-3">
|
||||
<text class="tips__text">
|
||||
💡 如发现异常登录设备,请立即强制下线并修改密码。
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view style="height: 80rpx;" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.list-page {
|
||||
padding: 0 32rpx;
|
||||
padding-bottom: 40rpx;
|
||||
background: var(--page-bg-color, #F9F9F9);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* ===== Summary Card ===== */
|
||||
.summary-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
background: linear-gradient(135deg, var(--primary-color, #4F8CFF), #005CD6);
|
||||
border-radius: 24rpx;
|
||||
padding: 32rpx;
|
||||
margin: 24rpx 0;
|
||||
box-shadow: 0 8rpx 24rpx rgba(1, 77, 178, 0.2);
|
||||
|
||||
&__icon {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 20rpx;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__icon-text {
|
||||
font-size: 40rpx;
|
||||
}
|
||||
|
||||
&__body {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__title {
|
||||
display: block;
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #FFFFFF;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
&__desc {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Sessions List ===== */
|
||||
.sessions-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.session-item {
|
||||
background: var(--card-bg-color, #FFFFFF);
|
||||
border-radius: 24rpx;
|
||||
padding: 24rpx;
|
||||
box-shadow: var(--shadow-sm, 0 1rpx 2rpx rgba(1, 77, 178,0.06));
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 18rpx;
|
||||
background: var(--bg-color-2, #F5F6F8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__icon-text {
|
||||
font-size: 36rpx;
|
||||
}
|
||||
|
||||
&__info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__os {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text-color, #0A1628);
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
&__browser {
|
||||
font-size: 24rpx;
|
||||
color: var(--text-color-3, #6B7280);
|
||||
}
|
||||
|
||||
&__current-tag {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
padding: 4rpx 16rpx;
|
||||
border-radius: 8rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__current-text {
|
||||
font-size: 22rpx;
|
||||
color: #10B981;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&__details {
|
||||
background: var(--page-bg-color, #F9F9F9);
|
||||
border-radius: 16rpx;
|
||||
padding: 16rpx;
|
||||
}
|
||||
|
||||
&__action {
|
||||
margin-top: 16rpx;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 6rpx 0;
|
||||
|
||||
&__label {
|
||||
font-size: 24rpx;
|
||||
color: var(--text-color-3, #6B7280);
|
||||
}
|
||||
|
||||
&__value {
|
||||
font-size: 24rpx;
|
||||
color: var(--text-color, #0A1628);
|
||||
font-weight: 500;
|
||||
font-family: 'SF Mono', 'Consolas', monospace;
|
||||
}
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 56rpx;
|
||||
padding: 0 32rpx;
|
||||
border-radius: 12rpx;
|
||||
background: var(--danger-color-light, #FEF2F2);
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&:active { opacity: 0.7; transform: scale(0.95); }
|
||||
|
||||
&__text {
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
color: var(--danger-color, #EF4444);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Tips ===== */
|
||||
.tips {
|
||||
margin-top: 32rpx;
|
||||
padding: 24rpx;
|
||||
background: var(--warning-color-light, rgba(245, 158, 11, 0.08));
|
||||
border-radius: 16rpx;
|
||||
|
||||
&__text {
|
||||
font-size: 24rpx;
|
||||
color: var(--text-color-3, #6B7280);
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,229 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { ResourceItem } from '@/api/module_monitor/resource'
|
||||
import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app'
|
||||
import { ref } from 'vue'
|
||||
import { ResourceAPI } from '@/api/module_monitor/resource'
|
||||
|
||||
definePage({ name: 'work-file-manager', style: { navigationBarTitleText: '文件管理' } })
|
||||
|
||||
const toast = useToast()
|
||||
const currentPath = ref('')
|
||||
const showMkdir = ref(false)
|
||||
const newDirName = ref('')
|
||||
|
||||
const { list: files, total, loading, pageParams, loadData, toFirst, loadPrev, loadNext } = useListPage<ResourceItem>({
|
||||
fetcher: p => ResourceAPI.getPage({ ...p, path: currentPath.value || undefined }),
|
||||
onError: e => toast.error(getErrorMessage(e, '加载失败')),
|
||||
})
|
||||
|
||||
function enterDir(path: string) {
|
||||
currentPath.value = path
|
||||
toFirst()
|
||||
loadData()
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
const parts = currentPath.value.split('/').filter(Boolean)
|
||||
parts.pop()
|
||||
currentPath.value = parts.length ? `/${parts.join('/')}` : ''
|
||||
toFirst()
|
||||
loadData()
|
||||
}
|
||||
|
||||
function goHome() {
|
||||
currentPath.value = ''
|
||||
toFirst()
|
||||
loadData()
|
||||
}
|
||||
|
||||
// Upload
|
||||
function handleUpload() {
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
success: async (res) => {
|
||||
const filePath = res.tempFilePaths[0]
|
||||
const fileName = filePath.split('/').pop() || 'unknown'
|
||||
try {
|
||||
await ResourceAPI.upload(currentPath.value || '/', filePath, fileName)
|
||||
toast.success('上传成功')
|
||||
loadData()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '上传失败')) }
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Create directory
|
||||
async function handleMkdir() {
|
||||
if (!newDirName.value.trim()) {
|
||||
toast.error('请输入目录名')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await ResourceAPI.mkdir(currentPath.value || '/', newDirName.value.trim())
|
||||
toast.success('目录创建成功')
|
||||
showMkdir.value = false
|
||||
newDirName.value = ''
|
||||
loadData()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '创建失败')) }
|
||||
}
|
||||
|
||||
// Delete
|
||||
function handleDelete(id: number) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确认删除该文件/目录?',
|
||||
success: async (r) => {
|
||||
if (r.confirm) {
|
||||
try {
|
||||
await ResourceAPI.remove([id])
|
||||
toast.success('已删除')
|
||||
loadData()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '删除失败')) }
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Download
|
||||
async function handleDownload(id: number) {
|
||||
try {
|
||||
const res = await ResourceAPI.download(id)
|
||||
if (res?.url) {
|
||||
uni.downloadFile({
|
||||
url: res.url,
|
||||
success: () => { toast.success('下载成功') },
|
||||
fail: () => { toast.error('下载失败') },
|
||||
})
|
||||
}
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '下载失败')) }
|
||||
}
|
||||
|
||||
function getFileIcon(name: string, is_dir?: boolean): string {
|
||||
if (is_dir)
|
||||
return 'folder'
|
||||
const ext = name?.split('.').pop()?.toLowerCase() || ''
|
||||
const iconMap: Record<string, string> = {
|
||||
png: 'file-image',
|
||||
jpg: 'file-image',
|
||||
jpeg: 'file-image',
|
||||
gif: 'file-image',
|
||||
svg: 'file-image',
|
||||
webp: 'file-image',
|
||||
mp4: 'file-video',
|
||||
avi: 'file-video',
|
||||
mov: 'file-video',
|
||||
mkv: 'file-video',
|
||||
mp3: 'file-audio',
|
||||
wav: 'file-audio',
|
||||
pdf: 'file-pdf',
|
||||
}
|
||||
return iconMap[ext] || 'file'
|
||||
}
|
||||
|
||||
onReachBottom(() => {
|
||||
if (!loading.value)
|
||||
loadNext()
|
||||
})
|
||||
onPullDownRefresh(() => loadData())
|
||||
onLoad(() => loadData())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="page-wraper">
|
||||
<!-- Breadcrumb navigation -->
|
||||
<view class="breadcrumb-bar">
|
||||
<view class="flex items-center gap-xs">
|
||||
<wd-button size="small" plain :disabled="!currentPath" @click="goHome">
|
||||
<wd-icon name="home" size="14px" />
|
||||
</wd-button>
|
||||
<text class="text-xs text-muted">
|
||||
/
|
||||
</text>
|
||||
<template v-if="currentPath">
|
||||
<wd-button size="small" plain @click="goBack">
|
||||
<wd-icon name="arrow-left" size="14px" />
|
||||
</wd-button>
|
||||
<text class="text-sm text-muted truncate flex-1">
|
||||
{{ currentPath }}
|
||||
</text>
|
||||
</template>
|
||||
<text v-else class="text-sm text-muted flex-1">
|
||||
根目录
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Actions -->
|
||||
<view class="action-bar">
|
||||
<text class="text-md font-bold text-muted">
|
||||
共 {{ total }} 项
|
||||
</text>
|
||||
<view class="flex gap-sm">
|
||||
<wd-button size="small" plain @click="showMkdir = true">新建目录</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleUpload">上传文件</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- File list -->
|
||||
<SkeletonPage v-if="loading" :rows="5" />
|
||||
<template v-else>
|
||||
<view class="px-sm">
|
||||
<view class="admin-card">
|
||||
<ListEmpty v-if="!loading && files.length === 0" text="当前目录为空" />
|
||||
<wd-cell-group v-else>
|
||||
<wd-cell
|
||||
v-for="item in files"
|
||||
:key="item.id"
|
||||
:is-link="item.is_dir"
|
||||
@click="item.is_dir ? enterDir(currentPath ? `${currentPath}/${item.name}` : `/${item.name}`) : null"
|
||||
>
|
||||
<template #title>
|
||||
<view class="flex items-center gap-sm">
|
||||
<wd-icon :name="getFileIcon(item.name, item.is_dir)" size="22px" :color="item.is_dir ? 'var(--warning-color)' : 'var(--primary-color)'" />
|
||||
<view>
|
||||
<text class="font-medium text-md">
|
||||
{{ item.name }}
|
||||
</text>
|
||||
<text class="text-xs text-muted block mt-xs">
|
||||
{{ item.size || '-' }}{{ item.created_time ? ` · ${item.created_time}` : '' }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<template #default>
|
||||
<view class="flex items-center gap-sm">
|
||||
<template v-if="!item.is_dir">
|
||||
<wd-icon name="download" size="18px" color="var(--primary-color)" @click.stop="handleDownload(item.id)" />
|
||||
</template>
|
||||
<wd-icon name="delete" size="18px" color="var(--danger-color)" @click.stop="handleDelete(item.id)" />
|
||||
</view>
|
||||
</template>
|
||||
</wd-cell>
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
</view>
|
||||
<PaginationBar :current="pageParams.page_no" :page-size="pageParams.page_size" :total="total" @prev="loadPrev" @next="loadNext" />
|
||||
</template>
|
||||
|
||||
<!-- Mkdir Popup -->
|
||||
<wd-popup v-model="showMkdir" position="bottom" round @close="showMkdir = false">
|
||||
<view class="p-xl" style="max-height:40vh;">
|
||||
<wd-navbar title="新建目录" left-arrow @click-left="showMkdir = false" />
|
||||
<view class="mt-lg">
|
||||
<view class="flex items-center gap-sm">
|
||||
<text>目录名</text>
|
||||
<wd-input v-model="newDirName" placeholder="请输入目录名称" class="flex-1" />
|
||||
</view>
|
||||
<view class="flex gap-md mt-lg">
|
||||
<wd-button block plain @click="showMkdir = false">取消</wd-button>
|
||||
<wd-button block type="primary" @click="handleMkdir">创建</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</view>
|
||||
</template>
|
||||
@@ -11,7 +11,7 @@ const searchName = ref('')
|
||||
|
||||
const { list, total, loading, pageParams, loadData, toFirst, loadPrev, loadNext } = useListPage<LoginLogItem>({
|
||||
fetcher: p => LoginLogAPI.getPage({ ...p, username: searchName.value || undefined }),
|
||||
onError: e => toast.error(getErrorMessage(e, '加载失败')),
|
||||
onError: () => toast.error('加载失败'),
|
||||
})
|
||||
|
||||
function onSearch() {
|
||||
@@ -34,7 +34,7 @@ function handleDelete(id: number) {
|
||||
toast.success('删除成功')
|
||||
loadData()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '删除失败')) }
|
||||
catch { toast.error('删除失败') }
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -56,12 +56,16 @@ onLoad(() => loadData())
|
||||
<view class="search-bar">
|
||||
<view class="flex items-center gap-sm">
|
||||
<wd-input v-model="searchName" placeholder="搜索用户名" clearable class="flex-1" />
|
||||
<wd-button size="small" type="primary" plain @click="onSearch">搜索</wd-button>
|
||||
<wd-button size="small" plain @click="onReset">重置</wd-button>
|
||||
<wd-button size="small" type="primary" variant="plain" @click="onSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
<wd-button size="small" variant="plain" @click="onReset">
|
||||
重置
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="action-bar">
|
||||
<text class="text-md font-bold text-muted">
|
||||
<text class="text-md text-muted font-bold">
|
||||
共 {{ total }} 条
|
||||
</text>
|
||||
</view>
|
||||
@@ -74,16 +78,16 @@ onLoad(() => loadData())
|
||||
<wd-cell v-for="item in list" :key="item.id">
|
||||
<template #title>
|
||||
<view>
|
||||
<text class="font-medium text-md">
|
||||
<text class="text-md font-medium">
|
||||
{{ item.username || '-' }}
|
||||
</text>
|
||||
<text class="text-xs text-muted block mt-xs">
|
||||
<text class="text-muted mt-xs block text-xs">
|
||||
IP: {{ item.login_ip || '-' }} · {{ item.login_location || '-' }}
|
||||
</text>
|
||||
</view>
|
||||
</template>
|
||||
<template #label>
|
||||
<text class="text-xs text-muted">
|
||||
<text class="text-muted text-xs">
|
||||
{{ item.request_browser || '' }}{{ item.request_browser && item.request_os ? ' / ' : '' }}{{ item.request_os || '' }}{{ item.msg ? ` · ${item.msg}` : '' }}
|
||||
</text>
|
||||
</template>
|
||||
|
||||
@@ -9,7 +9,7 @@ const toast = useToast()
|
||||
|
||||
const { list, total, loading, pageParams, loadData, loadPrev, loadNext } = useListPage<OnlineUserItem>({
|
||||
fetcher: p => OnlineAPI.getPage(p),
|
||||
onError: e => toast.error(getErrorMessage(e, '加载失败')),
|
||||
onError: () => toast.error('加载失败'),
|
||||
})
|
||||
|
||||
function forceLogout(sessionId: string) {
|
||||
@@ -23,7 +23,7 @@ function forceLogout(sessionId: string) {
|
||||
toast.success('已强制下线')
|
||||
loadData()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '操作失败')) }
|
||||
catch { toast.error('操作失败') }
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -42,7 +42,7 @@ onLoad(() => loadData())
|
||||
<template>
|
||||
<view class="page-wraper">
|
||||
<view class="action-bar">
|
||||
<text class="text-md font-bold text-muted">
|
||||
<text class="text-md text-muted font-bold">
|
||||
共 {{ total }} 在线
|
||||
</text>
|
||||
</view>
|
||||
@@ -53,23 +53,25 @@ onLoad(() => loadData())
|
||||
<wd-cell v-for="item in list" :key="item.session_id || item.user_id">
|
||||
<template #title>
|
||||
<view>
|
||||
<text class="font-medium text-md">
|
||||
<text class="text-md font-medium">
|
||||
{{ item.user_name || item.name || '-' }}
|
||||
</text>
|
||||
<text class="text-xs text-muted block mt-xs">
|
||||
<text class="text-muted mt-xs block text-xs">
|
||||
IP: {{ item.ipaddr || '-' }} · {{ item.login_location || '' }}
|
||||
</text>
|
||||
</view>
|
||||
</template>
|
||||
<template #label>
|
||||
<text class="text-xs text-muted">
|
||||
<text class="text-muted text-xs">
|
||||
{{ item.browser || '' }}{{ item.browser && item.os ? ' / ' : '' }}{{ item.os || '' }}
|
||||
</text>
|
||||
</template>
|
||||
<template #default>
|
||||
<view class="flex items-center gap-sm">
|
||||
<StatusBadge status="active" />
|
||||
<wd-button size="small" type="danger" plain @click="forceLogout(item.session_id!)">下线</wd-button>
|
||||
<wd-button size="small" type="danger" variant="plain" @click="forceLogout(item.session_id!)">
|
||||
下线
|
||||
</wd-button>
|
||||
</view>
|
||||
</template>
|
||||
</wd-cell>
|
||||
|
||||
@@ -13,7 +13,7 @@ const detailItem = ref<OplogItem>()
|
||||
|
||||
const { list, total, loading, pageParams, loadData, toFirst, loadPrev, loadNext } = useListPage<OplogItem>({
|
||||
fetcher: p => OperationLogAPI.getPage({ ...p, request_path: searchPath.value || undefined }),
|
||||
onError: e => toast.error(getErrorMessage(e, '加载失败')),
|
||||
onError: () => toast.error('加载失败'),
|
||||
})
|
||||
|
||||
function onSearch() {
|
||||
@@ -40,7 +40,7 @@ function handleDelete(id: number) {
|
||||
toast.success('删除成功')
|
||||
loadData()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '删除失败')) }
|
||||
catch { toast.error('删除失败') }
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -62,12 +62,16 @@ onLoad(() => loadData())
|
||||
<view class="search-bar">
|
||||
<view class="flex items-center gap-sm">
|
||||
<wd-input v-model="searchPath" placeholder="搜索请求路径" clearable class="flex-1" />
|
||||
<wd-button size="small" type="primary" plain @click="onSearch">搜索</wd-button>
|
||||
<wd-button size="small" plain @click="onReset">重置</wd-button>
|
||||
<wd-button size="small" type="primary" variant="plain" @click="onSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
<wd-button size="small" variant="plain" @click="onReset">
|
||||
重置
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="action-bar">
|
||||
<text class="text-md font-bold text-muted">
|
||||
<text class="text-md text-muted font-bold">
|
||||
共 {{ total }} 条
|
||||
</text>
|
||||
</view>
|
||||
@@ -79,16 +83,16 @@ onLoad(() => loadData())
|
||||
<wd-cell-group v-else border>
|
||||
<wd-cell v-for="item in list" :key="item.id" is-link @click="viewDetail(item)">
|
||||
<template #title>
|
||||
<view class="flex items-center gap-sm flex-wrap">
|
||||
<view class="flex flex-wrap items-center gap-sm">
|
||||
<StatusBadge :status="(item.response_code || 0) < 300 ? 'success' : 'failed'" dot />
|
||||
<text class="font-mono text-sm font-medium">
|
||||
<text class="text-sm font-medium font-mono">
|
||||
{{ item.request_method }}
|
||||
</text>
|
||||
<text class="text-sm text-muted truncate" style="max-width: 300rpx;">
|
||||
<text class="text-muted truncate text-sm" style="max-width: 300rpx;">
|
||||
{{ item.request_path }}
|
||||
</text>
|
||||
</view>
|
||||
<text class="text-xs text-muted block">
|
||||
<text class="text-muted block text-xs">
|
||||
{{ item.description || '-' }} · {{ item.request_ip }} · {{ item.process_time }}
|
||||
</text>
|
||||
</template>
|
||||
@@ -116,13 +120,15 @@ onLoad(() => loadData())
|
||||
<wd-cell title="操作说明" :value="detailItem.description || '-'" />
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
<view class="mt-sm px-sm py-sm bg-light rounded-md">
|
||||
<text class="text-xs text-muted">
|
||||
<view class="mt-sm rounded-md bg-light px-sm py-sm">
|
||||
<text class="text-muted text-xs">
|
||||
请求参数:{{ detailItem.request_payload || '无' }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="mt-lg">
|
||||
<wd-button block @click="showDetail = false">关闭</wd-button>
|
||||
<wd-button block @click="showDetail = false">
|
||||
关闭
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import type { ServerInfo } from '@/api/module_monitor/server'
|
||||
import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app'
|
||||
import { ref } from 'vue'
|
||||
import { ServerAPI } from '@/api/module_monitor/server'
|
||||
|
||||
definePage({ name: 'work-server', style: { navigationBarTitleText: '服务监控' } })
|
||||
|
||||
const toast = useToast()
|
||||
const loading = ref(false)
|
||||
const info = ref<ServerInfo>()
|
||||
|
||||
function pc(v = 0) {
|
||||
return Math.round(v)
|
||||
}
|
||||
function pcColor(v = 0) {
|
||||
if (v > 80)
|
||||
return 'var(--danger-color)'
|
||||
if (v > 60)
|
||||
return 'var(--warning-color)'
|
||||
return 'var(--success-color)'
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
info.value = await ServerAPI.getInfo()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '加载失败')) }
|
||||
finally {
|
||||
loading.value = false
|
||||
uni.stopPullDownRefresh()
|
||||
}
|
||||
}
|
||||
|
||||
onPullDownRefresh(() => {
|
||||
loadData()
|
||||
})
|
||||
|
||||
onLoad(() => loadData())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="page-wraper p-md">
|
||||
<wd-loading v-if="loading && !info" />
|
||||
<template v-else-if="info">
|
||||
<view class="stats-grid server-stats">
|
||||
<view class="stat-card">
|
||||
<text class="stat-card__label">
|
||||
CPU
|
||||
</text><text class="stat-card__value" :style="{ color: pcColor(info.cpu?.used_percent) }">
|
||||
{{ pc(info.cpu?.used_percent) }}%
|
||||
</text><text class="stat-card__trend text-muted">
|
||||
{{ info.cpu?.cores || '-' }} 核心
|
||||
</text>
|
||||
</view>
|
||||
<view class="stat-card">
|
||||
<text class="stat-card__label">
|
||||
内存
|
||||
</text><text class="stat-card__value" :style="{ color: pcColor(info.memory?.used_percent) }">
|
||||
{{ pc(info.memory?.used_percent) }}%
|
||||
</text><text class="stat-card__trend text-muted">
|
||||
{{ info.memory?.used || '-' }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="stat-card">
|
||||
<text class="stat-card__label">
|
||||
磁盘
|
||||
</text><text class="stat-card__value" :style="{ color: pcColor(info.disk?.used_percent) }">
|
||||
{{ pc(info.disk?.used_percent) }}%
|
||||
</text><text class="stat-card__trend text-muted">
|
||||
{{ info.disk?.used || '-' }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="admin-card">
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="操作系统" :value="info.os?.name || '-'" />
|
||||
<wd-cell title="主机名" :value="info.os?.hostname || '-'" />
|
||||
<wd-cell title="运行时间" :value="info.os?.uptime || '-'" />
|
||||
<wd-cell title="CPU 型号" :value="info.cpu?.model || '-'" />
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
</template>
|
||||
<view v-else class="empty-state">
|
||||
<text class="empty-state__icon">
|
||||
⚠️
|
||||
</text>
|
||||
<text class="empty-state__text">
|
||||
加载失败,请下拉刷新重试
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -31,7 +31,7 @@ async function loadTree() {
|
||||
treeList.value = await DeptAPI.getTree() || []
|
||||
flatList.value = flatten(treeList.value)
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '加载失败')) }
|
||||
catch { toast.error('加载失败') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
function resetForm() {
|
||||
@@ -66,7 +66,7 @@ async function handleSubmit() {
|
||||
showForm.value = false
|
||||
loadTree()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '操作失败')) }
|
||||
catch { toast.error('操作失败') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
function handleDelete(id: number) {
|
||||
@@ -77,7 +77,7 @@ function handleDelete(id: number) {
|
||||
toast.success('删除成功')
|
||||
loadTree()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '删除失败')) }
|
||||
catch { toast.error('删除失败') }
|
||||
}
|
||||
} })
|
||||
}
|
||||
@@ -149,7 +149,7 @@ onLoad(() => loadTree())
|
||||
</wd-form-item>
|
||||
</wd-form>
|
||||
<view class="gap-md mt-xl flex">
|
||||
<wd-button plain block @click="showForm = false">
|
||||
<wd-button variant="plain" block @click="showForm = false">
|
||||
取消
|
||||
</wd-button><wd-button block type="primary" :loading="loading" @click="handleSubmit">
|
||||
保存
|
||||
|
||||
@@ -34,7 +34,7 @@ async function loadTree() {
|
||||
treeList.value = await MenuAPI.getTree() || []
|
||||
flatList.value = flatten(treeList.value)
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '加载失败')) }
|
||||
catch { toast.error('加载失败') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
function resetForm() {
|
||||
@@ -61,7 +61,7 @@ async function handleSubmit() {
|
||||
showForm.value = false
|
||||
loadTree()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '操作失败')) }
|
||||
catch { toast.error('操作失败') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
function handleDelete(id: number) {
|
||||
@@ -72,7 +72,7 @@ function handleDelete(id: number) {
|
||||
toast.success('删除成功')
|
||||
loadTree()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '删除失败')) }
|
||||
catch { toast.error('删除失败') }
|
||||
}
|
||||
} })
|
||||
}
|
||||
@@ -150,7 +150,7 @@ onLoad(() => loadTree())
|
||||
</wd-form-item>
|
||||
</wd-form>
|
||||
<view class="gap-md mt-xl flex">
|
||||
<wd-button plain block @click="showForm = false">
|
||||
<wd-button variant="plain" block @click="showForm = false">
|
||||
取消
|
||||
</wd-button><wd-button block type="primary" :loading="loading" @click="handleSubmit">
|
||||
保存
|
||||
|
||||
@@ -15,7 +15,6 @@ const formData = reactive<PositionForm>({ name: '', code: '', order: 1, status:
|
||||
|
||||
const { list, total, loading, pageParams, loadData, toFirst, loadPrev, loadNext } = useListPage<PositionItem>({
|
||||
fetcher: p => PositionAPI.getPage({ ...p, name: searchName.value || undefined }),
|
||||
onError: e => toast.error(getErrorMessage(e, '加载失败')),
|
||||
})
|
||||
|
||||
function onSearch() {
|
||||
@@ -44,7 +43,7 @@ async function openEdit(id: number) {
|
||||
Object.assign(formData, { name: detail.name, code: detail.code || '', order: detail.order || 1, status: detail.status ?? 0, description: detail.description || '' })
|
||||
showForm.value = true
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '获取详情失败')) }
|
||||
catch { toast.error('获取详情失败') }
|
||||
}
|
||||
async function handleSubmit() {
|
||||
loading.value = true
|
||||
@@ -60,7 +59,7 @@ async function handleSubmit() {
|
||||
showForm.value = false
|
||||
loadData()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '操作失败')) }
|
||||
catch { toast.error('操作失败') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
function handleDelete(id: number) {
|
||||
@@ -74,7 +73,7 @@ function handleDelete(id: number) {
|
||||
toast.success('删除成功')
|
||||
loadData()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '删除失败')) }
|
||||
catch { toast.error('删除失败') }
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -96,10 +95,10 @@ onLoad(() => loadData())
|
||||
<view class="search-bar">
|
||||
<view class="flex items-center gap-sm">
|
||||
<wd-input v-model="searchName" placeholder="搜索名称" clearable class="flex-1" />
|
||||
<wd-button size="small" type="primary" plain @click="onSearch">
|
||||
<wd-button size="small" type="primary" variant="plain" @click="onSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
<wd-button size="small" plain @click="onReset">
|
||||
<wd-button size="small" variant="plain" @click="onReset">
|
||||
重置
|
||||
</wd-button>
|
||||
</view>
|
||||
@@ -155,7 +154,7 @@ onLoad(() => loadData())
|
||||
</wd-form-item>
|
||||
</wd-form>
|
||||
<view class="gap-md mt-xl flex">
|
||||
<wd-button plain block @click="showForm = false">
|
||||
<wd-button variant="plain" block @click="showForm = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button block type="primary" :loading="loading" @click="handleSubmit">
|
||||
|
||||
@@ -16,7 +16,7 @@ const formData = reactive<RoleForm>({ name: '', code: '', order: 1, data_scope:
|
||||
|
||||
const { list, total, loading, pageParams, loadData, toFirst, loadPrev, loadNext } = useListPage<RoleItem>({
|
||||
fetcher: p => RoleAPI.getPage({ ...p, name: searchName.value || undefined }),
|
||||
onError: e => toast.error(getErrorMessage(e, '加载失败')),
|
||||
onError: () => toast.error('加载失败'),
|
||||
})
|
||||
|
||||
function onSearch() {
|
||||
@@ -50,7 +50,7 @@ async function openEdit(id: number) {
|
||||
Object.assign(formData, { name: detail.name, code: detail.code, order: detail.order || 1, data_scope: detail.data_scope || 4, status: detail.status ?? 0, description: detail.description || '' })
|
||||
showForm.value = true
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '获取详情失败')) }
|
||||
catch { toast.error('获取详情失败') }
|
||||
}
|
||||
async function handleSubmit() {
|
||||
loading.value = true
|
||||
@@ -66,7 +66,7 @@ async function handleSubmit() {
|
||||
showForm.value = false
|
||||
loadData()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '操作失败')) }
|
||||
catch { toast.error('操作失败') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
function handleDelete(id: number) {
|
||||
@@ -80,7 +80,7 @@ function handleDelete(id: number) {
|
||||
toast.success('删除成功')
|
||||
loadData()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '删除失败')) }
|
||||
catch { toast.error('删除失败') }
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -102,10 +102,10 @@ onLoad(() => loadData())
|
||||
<view class="search-bar">
|
||||
<view class="flex items-center gap-sm">
|
||||
<wd-input v-model="searchName" placeholder="搜索角色名称" clearable class="flex-1" />
|
||||
<wd-button size="small" type="primary" plain @click="onSearch">
|
||||
<wd-button size="small" type="primary" variant="plain" @click="onSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
<wd-button size="small" plain @click="onReset">
|
||||
<wd-button size="small" variant="plain" @click="onReset">
|
||||
重置
|
||||
</wd-button>
|
||||
</view>
|
||||
@@ -162,7 +162,7 @@ onLoad(() => loadData())
|
||||
</wd-form-item>
|
||||
</wd-form>
|
||||
<view class="gap-md mt-xl flex">
|
||||
<wd-button plain block @click="showForm = false">
|
||||
<wd-button variant="plain" block @click="showForm = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button block type="primary" :loading="loading" @click="handleSubmit">
|
||||
|
||||
@@ -92,7 +92,7 @@ async function loadOptions() {
|
||||
|
||||
const { list, total, loading, pageParams, loadData, toFirst, loadPrev, loadNext } = useListPage<UserInfo>({
|
||||
fetcher: p => UserAPI.getUserPage({ ...p, name: searchName.value || undefined }),
|
||||
onError: e => toast.error(getErrorMessage(e, '加载失败')),
|
||||
onError: () => toast.error('加载失败'),
|
||||
})
|
||||
|
||||
function onSearch() {
|
||||
@@ -111,7 +111,7 @@ function openCreate() {
|
||||
showForm.value = true
|
||||
}
|
||||
function openEdit(id: number) {
|
||||
router.push({ name: 'work-user-detail', query: { id } })
|
||||
router.push({ name: 'work-user-detail', query: { id: String(id) } })
|
||||
}
|
||||
async function handleSubmit() {
|
||||
loading.value = true
|
||||
@@ -128,7 +128,7 @@ async function handleSubmit() {
|
||||
showForm.value = false
|
||||
loadData()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '操作失败')) }
|
||||
catch { toast.error('操作失败') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
function handleDelete(id: number) {
|
||||
@@ -139,7 +139,7 @@ function handleDelete(id: number) {
|
||||
toast.success('删除成功')
|
||||
loadData()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '删除失败')) }
|
||||
catch { toast.error('删除失败') }
|
||||
}
|
||||
} })
|
||||
}
|
||||
@@ -282,7 +282,7 @@ onLoad(() => loadData())
|
||||
</wd-form-item>
|
||||
</wd-form>
|
||||
<view class="gap-md mt-xl flex">
|
||||
<wd-button plain block @click="showForm = false">
|
||||
<wd-button variant="plain" block @click="showForm = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button block type="primary" :loading="loading" @click="handleSubmit">
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import BarChart from '@/subEcharts/echarts/components/BarChart.vue'
|
||||
import DonutChart from '@/subEcharts/echarts/components/DonutChart.vue'
|
||||
import FunnelChart from '@/subEcharts/echarts/components/FunnelChart.vue'
|
||||
import GaugeChart from '@/subEcharts/echarts/components/GaugeChart.vue'
|
||||
import LineChart from '@/subEcharts/echarts/components/LineChart.vue'
|
||||
import LiquidFillChart from '@/subEcharts/echarts/components/LiquidFillChart.vue'
|
||||
import MiniLineChart from '@/subEcharts/echarts/components/MiniLineChart.vue'
|
||||
import PieChart from '@/subEcharts/echarts/components/PieChart.vue'
|
||||
import RadarChart from '@/subEcharts/echarts/components/RadarChart.vue'
|
||||
import ScatterChart from '@/subEcharts/echarts/components/ScatterChart.vue'
|
||||
import StackedBarChart from '@/subEcharts/echarts/components/StackedBarChart.vue'
|
||||
import BarChart from '@/subPages/wot-demo/echarts/components/BarChart.vue'
|
||||
import DonutChart from '@/subPages/wot-demo/echarts/components/DonutChart.vue'
|
||||
import FunnelChart from '@/subPages/wot-demo/echarts/components/FunnelChart.vue'
|
||||
import GaugeChart from '@/subPages/wot-demo/echarts/components/GaugeChart.vue'
|
||||
import LineChart from '@/subPages/wot-demo/echarts/components/LineChart.vue'
|
||||
import LiquidFillChart from '@/subPages/wot-demo/echarts/components/LiquidFillChart.vue'
|
||||
import MiniLineChart from '@/subPages/wot-demo/echarts/components/MiniLineChart.vue'
|
||||
import PieChart from '@/subPages/wot-demo/echarts/components/PieChart.vue'
|
||||
import RadarChart from '@/subPages/wot-demo/echarts/components/RadarChart.vue'
|
||||
import ScatterChart from '@/subPages/wot-demo/echarts/components/ScatterChart.vue'
|
||||
import StackedBarChart from '@/subPages/wot-demo/echarts/components/StackedBarChart.vue'
|
||||
|
||||
defineOptions({
|
||||
componentPlaceholder: {
|
||||
@@ -30,7 +30,7 @@ defineOptions({
|
||||
definePage({
|
||||
name: 'echartsAsync',
|
||||
style: {
|
||||
navigationBarTitleText: 'echarts 异步跨包示例',
|
||||
navigationBarTitleText: 'echarts 异步加载示例',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -70,7 +70,7 @@ function getIconBg(color: string) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="pb-safe box-border min-h-screen wot-bg-bg">
|
||||
<view class="pb-safe wot-bg-bg box-border min-h-screen">
|
||||
<!-- Header -->
|
||||
<view class="px-6 pb-6 pt-8">
|
||||
<view class="mb-2 text-3xl font-bold leading-tight wot-text-text-main">
|
||||
|
||||
Reference in New Issue
Block a user