mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-10-16 05:45:35 +00:00
chore: 初始化uniapp前端项目基础框架
新增项目配置、静态资源、工具类、状态管理、API接口、组件库集成等全套基础代码,完成项目初始化搭建
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
<script setup lang="ts">
|
||||
import { useTeam } from '@/composables/useTeam'
|
||||
|
||||
definePage({
|
||||
name: 'about',
|
||||
style: {
|
||||
navigationBarTitleText: '关于',
|
||||
},
|
||||
})
|
||||
|
||||
const { data: coreTeam } = useTeam()
|
||||
|
||||
function openUrl(url?: string) {
|
||||
if (!url) {
|
||||
return
|
||||
}
|
||||
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
// 打开公众号二维码
|
||||
function openWeChat() {
|
||||
uni.previewImage({
|
||||
urls: ['https://wot-ui.cn/wechatPublicAccount.png'],
|
||||
})
|
||||
}
|
||||
|
||||
// 打开捐赠二维码
|
||||
function donate() {
|
||||
uni.previewImage({
|
||||
urls: ['https://wot-ui.cn/weixinQrcode.jpg'],
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="min-h-screen py-3">
|
||||
<!-- 头部介绍 -->
|
||||
<view class="mx-3 mb-3 flex flex-col gap-2">
|
||||
<text class="text-6 font-bold wot-text-text-main">
|
||||
关于我们
|
||||
</text>
|
||||
<text class="text-3.5 leading-snug wot-text-text-secondary">
|
||||
我是不如摸鱼去,一个前端打工仔,我和我的小伙伴们正在致力于开发轻量、高效的uni-app组件库和高效、易用的uni-app快速开发模板。
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 核心团队 -->
|
||||
<demo-block title="核心团队" transparent>
|
||||
<view class="grid grid-cols-2 gap-3">
|
||||
<view
|
||||
v-for="member in coreTeam"
|
||||
:key="member.name"
|
||||
class="rounded-2 p-4 text-center wot-bg-filled-oppo"
|
||||
@click="openUrl(member.github)"
|
||||
>
|
||||
<image
|
||||
:src="member.avatar"
|
||||
class="mx-auto mb-2 h-16 w-16 border-2 border-blue-200 rounded-full dark:border-blue-800"
|
||||
/>
|
||||
<view class="mb-1 text-3.5 font-bold wot-text-text-main">
|
||||
{{ member.name }}
|
||||
</view>
|
||||
<view class="mb-2 text-2.5 text-blue-600 dark:text-blue-400">
|
||||
{{ member.title }}
|
||||
</view>
|
||||
<view class="text-2.5 leading-snug wot-text-text-secondary">
|
||||
{{ member.desc }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 关于 uni-helper -->
|
||||
<demo-block title="关于 uni-helper 团队" transparent>
|
||||
<view class="rounded-3 p-5 wot-bg-filled-oppo">
|
||||
<text class="mb-3 block text-3.5 leading-relaxed wot-text-text-secondary">
|
||||
<text class="text-blue-600" @click="openUrl('https://uni-helper.cn/')">
|
||||
uni-helper
|
||||
</text>
|
||||
是一个旨在增强 uni-app 系列产品的开发体验为爱发电的非官方组织。作为靠爱发电的非官方项目,uni-helper 提供了打包工具插件支持、编辑器扩展支持、NPM 包等并尽力维护它们。
|
||||
</text>
|
||||
<text class="text-3.5 leading-relaxed wot-text-text-secondary">
|
||||
在此我们特别向 uni-helper 团队表示感谢,他们为 uni-app 系列产品提供了强大的支持,包括打包工具插件支持、编辑器扩展支持等,这使我们得以站在巨人的巨人的肩膀上完成此项目。
|
||||
</text>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 更多信息 -->
|
||||
<demo-block title="更多信息" transparent>
|
||||
<wd-cell-group border custom-class="rounded-2! overflow-hidden">
|
||||
<wd-cell
|
||||
title="关注公众号"
|
||||
title-width="200px"
|
||||
label="uni-app教程、组件库讯息一手掌握!"
|
||||
is-link
|
||||
@click="openWeChat"
|
||||
/>
|
||||
<wd-cell
|
||||
title="捐赠"
|
||||
title-width="200px"
|
||||
label="每一份捐赠都是对我们莫大的鼓励!"
|
||||
is-link
|
||||
@click="donate"
|
||||
/>
|
||||
</wd-cell-group>
|
||||
</demo-block>
|
||||
</view>
|
||||
</template>
|
||||
@@ -0,0 +1,448 @@
|
||||
<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 { DashboardAPI } from '@/api/module_monitor/dashboard'
|
||||
import { ServerAPI } from '@/api/module_monitor/server'
|
||||
import SkeletonPage from '@/components/SkeletonPage.vue'
|
||||
|
||||
definePage({
|
||||
name: 'dashboard',
|
||||
style: { navigationBarTitleText: '数据概览' },
|
||||
})
|
||||
|
||||
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([
|
||||
{ day: '一', value: 45, label: '128' },
|
||||
{ day: '二', value: 60, label: '186' },
|
||||
{ day: '三', value: 85, label: '245' },
|
||||
{ day: '四', value: 72, label: '210' },
|
||||
{ day: '五', value: 100, label: '312' },
|
||||
{ day: '六', value: 68, label: '198' },
|
||||
{ day: '日', value: 38, label: '95' },
|
||||
])
|
||||
|
||||
const alerts = ref([
|
||||
{ id: 1, level: 'danger', text: '磁盘使用率超过 85%', time: '10分钟前' },
|
||||
{ id: 2, level: 'warning', text: '内存使用率持续升高', time: '30分钟前' },
|
||||
{ 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
|
||||
}
|
||||
catch (e) {
|
||||
toast.error(getErrorMessage(e, '加载失败'))
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
uni.stopPullDownRefresh()
|
||||
}
|
||||
}
|
||||
|
||||
onLoad(() => loadData())
|
||||
onShow(() => {
|
||||
const pages = getCurrentPages()
|
||||
if (pages.length > 0 && pages[pages.length - 1].route === 'subPages/biz/dashboard/index') {
|
||||
uni.$emit('updateTabbar', 'work')
|
||||
}
|
||||
})
|
||||
onPullDownRefresh(() => loadData())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="dashboard-page">
|
||||
<!-- Nav header -->
|
||||
<view class="dash-nav">
|
||||
<text class="dash-nav__title">
|
||||
数据概览
|
||||
</text>
|
||||
<view class="dash-nav__badge">
|
||||
<text class="dash-nav__badge-text">
|
||||
今日
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<SkeletonPage v-if="loading && !stats" :rows="6" />
|
||||
|
||||
<template v-else>
|
||||
<!-- Stats row -->
|
||||
<view class="stats-row">
|
||||
<view class="stat-card card-pressable">
|
||||
<text class="stat-card__value" style="color: var(--primary-color);">
|
||||
{{ stats?.total_users?.toLocaleString() ?? '-' }}
|
||||
</text>
|
||||
<text class="stat-card__label">
|
||||
总用户数
|
||||
</text>
|
||||
</view>
|
||||
<view class="stat-card card-pressable">
|
||||
<text class="stat-card__value" style="color: var(--success-color);">
|
||||
{{ stats?.today_login_count ?? '-' }}
|
||||
</text>
|
||||
<text class="stat-card__label">
|
||||
今日活跃
|
||||
</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 -->
|
||||
<view class="chart-card">
|
||||
<view class="chart-card__header">
|
||||
<text class="chart-card__title">
|
||||
登录趋势 (近7天)
|
||||
</text>
|
||||
<text class="chart-card__period">
|
||||
7天
|
||||
</text>
|
||||
</view>
|
||||
<view class="chart-bars">
|
||||
<view v-for="item in loginTrend" :key="item.day" class="chart-bar-wrap">
|
||||
<view class="chart-bar" :style="{ height: `${item.value}%` }">
|
||||
<text class="chart-bar__tip">
|
||||
{{ item.label }}
|
||||
</text>
|
||||
</view>
|
||||
<text class="chart-bar__label">
|
||||
{{ item.day }}
|
||||
</text>
|
||||
</view>
|
||||
</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">
|
||||
<text class="alert-card__title">
|
||||
系统告警
|
||||
</text>
|
||||
<view class="alert-card__badge">
|
||||
<text class="alert-card__badge-text">
|
||||
{{ alerts.length }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
v-for="alert in alerts" :key="alert.id"
|
||||
class="alert-item"
|
||||
:class="`alert-item--${alert.level}`"
|
||||
>
|
||||
<view class="alert-item__dot" :class="`alert-item__dot--${alert.level}`" />
|
||||
<text class="alert-item__text">
|
||||
{{ alert.text }}
|
||||
</text>
|
||||
<text class="alert-item__time">
|
||||
{{ alert.time }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dashboard-page {
|
||||
min-height: 100vh;
|
||||
background: var(--page-bg-color, #F9F9F9);
|
||||
padding: var(--spacing-2xl, 32rpx);
|
||||
padding-bottom: calc(40rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.dash-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 88rpx;
|
||||
margin-bottom: var(--spacing-lg, 20rpx);
|
||||
|
||||
&__title {
|
||||
font-size: var(--font-2xl, 40rpx);
|
||||
font-weight: 600;
|
||||
color: var(--text-color, #0A1628);
|
||||
}
|
||||
|
||||
&__badge {
|
||||
background: var(--bg-color-2, #F2F3F5);
|
||||
border-radius: var(--radius-full, 9999rpx);
|
||||
padding: 8rpx 24rpx;
|
||||
}
|
||||
|
||||
&__badge-text {
|
||||
font-size: var(--font-sm, 24rpx);
|
||||
color: var(--text-color-3, #6B7280);
|
||||
}
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
display: flex;
|
||||
gap: var(--spacing-md, 16rpx);
|
||||
margin-bottom: var(--spacing-lg, 20rpx);
|
||||
}
|
||||
|
||||
.stat-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;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
|
||||
&__value {
|
||||
font-size: var(--font-2xl, 40rpx);
|
||||
font-weight: 700;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: var(--font-xs, 20rpx);
|
||||
color: var(--text-color-3, #6B7280);
|
||||
}
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
background: var(--card-bg-color, #FFFFFF);
|
||||
border-radius: var(--radius-md, 16rpx);
|
||||
padding: var(--spacing-lg, 20rpx);
|
||||
box-shadow: var(--shadow-sm, 0 1rpx 2rpx rgba(1, 77, 178, 0.06));
|
||||
margin-bottom: var(--spacing-lg, 20rpx);
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: var(--spacing-lg, 20rpx);
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: var(--font-md, 28rpx);
|
||||
font-weight: 600;
|
||||
color: var(--text-color, #0A1628);
|
||||
}
|
||||
|
||||
&__period {
|
||||
font-size: var(--font-sm, 24rpx);
|
||||
color: var(--text-color-3, #6B7280);
|
||||
}
|
||||
}
|
||||
|
||||
.chart-bars {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
height: 280rpx;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.chart-bar-wrap {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
height: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.chart-bar {
|
||||
width: 52rpx;
|
||||
min-height: 16rpx;
|
||||
background: linear-gradient(180deg, var(--primary-color-light, #E8F0FF) 0%, var(--primary-color, #4F8CFF) 100%);
|
||||
border-radius: 8rpx;
|
||||
position: relative;
|
||||
transition: height 0.6s ease;
|
||||
|
||||
&__tip {
|
||||
position: absolute;
|
||||
top: -36rpx;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: var(--font-xs, 20rpx);
|
||||
color: var(--text-color-3, #6B7280);
|
||||
white-space: nowrap;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: var(--font-xs, 20rpx);
|
||||
color: var(--text-color-3, #6B7280);
|
||||
}
|
||||
}
|
||||
|
||||
.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);
|
||||
padding: var(--spacing-lg, 20rpx);
|
||||
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: var(--spacing-md, 16rpx);
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: var(--font-md, 28rpx);
|
||||
font-weight: 600;
|
||||
color: var(--text-color, #0A1628);
|
||||
}
|
||||
|
||||
&__badge {
|
||||
background: var(--danger-color, #EF4444);
|
||||
border-radius: 50%;
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&__badge-text {
|
||||
font-size: var(--font-sm, 24rpx);
|
||||
font-weight: 700;
|
||||
color: var(--text-color-inverse, #FFFFFF);
|
||||
}
|
||||
}
|
||||
|
||||
.alert-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
padding: 20rpx;
|
||||
border-radius: var(--radius-sm, 8rpx);
|
||||
margin-bottom: 12rpx;
|
||||
|
||||
&:last-child { margin-bottom: 0; }
|
||||
|
||||
&--danger { background: var(--danger-color-light, #FEF2F2); }
|
||||
&--warning { background: var(--warning-color-light, #FFF8E6); }
|
||||
&--info { background: var(--info-color-light, #F2F3F5); }
|
||||
|
||||
&__dot {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
|
||||
&--danger { background: var(--danger-color, #EF4444); }
|
||||
&--warning { background: var(--warning-color, #F59E0B); }
|
||||
&--info { background: var(--info-color, #8892A3); }
|
||||
}
|
||||
|
||||
&__text {
|
||||
flex: 1;
|
||||
font-size: var(--font-sm, 24rpx);
|
||||
color: var(--text-color, #0A1628);
|
||||
}
|
||||
|
||||
&__time {
|
||||
font-size: var(--font-xs, 20rpx);
|
||||
color: var(--text-color-3, #6B7280);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,301 @@
|
||||
<script setup lang="ts">
|
||||
import type { DictDataForm, DictDataItem, DictTypeForm, DictTypeItem } from '@/api/module_system/dict'
|
||||
import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app'
|
||||
import { ref } from 'vue'
|
||||
import { DictAPI } from '@/api/module_system/dict'
|
||||
|
||||
definePage({ name: 'work-dicts', style: { navigationBarTitleText: '字典管理' } })
|
||||
|
||||
const toast = useToast()
|
||||
const searchName = ref('')
|
||||
const showForm = ref(false)
|
||||
const formTitle = ref('')
|
||||
const currentId = ref<number>()
|
||||
const formData = reactive<DictTypeForm>({ dict_name: '', dict_type: '', status: 0, description: '' })
|
||||
|
||||
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, '加载失败')),
|
||||
})
|
||||
function onSearch() {
|
||||
toFirst()
|
||||
loadData()
|
||||
}
|
||||
function onReset() {
|
||||
searchName.value = ''
|
||||
toFirst()
|
||||
loadData()
|
||||
}
|
||||
function resetForm() {
|
||||
Object.assign(formData, { dict_name: '', dict_type: '', status: 0, description: '' })
|
||||
}
|
||||
function openCreate() {
|
||||
formTitle.value = '新增字典'
|
||||
currentId.value = undefined
|
||||
resetForm()
|
||||
showForm.value = true
|
||||
}
|
||||
async function handleSubmit() {
|
||||
loading.value = true
|
||||
try {
|
||||
if (currentId.value) {
|
||||
await DictAPI.updateType(currentId.value, formData)
|
||||
toast.success('更新成功')
|
||||
}
|
||||
else {
|
||||
await DictAPI.createType(formData)
|
||||
toast.success('创建成功')
|
||||
}
|
||||
showForm.value = false
|
||||
loadData()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '操作失败')) }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
function handleDelete(id: number) {
|
||||
uni.showModal({ title: '提示', content: '确认删除?', success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await DictAPI.removeType([id])
|
||||
toast.success('删除成功')
|
||||
loadData()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '删除失败')) }
|
||||
}
|
||||
} })
|
||||
}
|
||||
|
||||
/* ===== Data sub-list ===== */
|
||||
const showDataSection = ref(false)
|
||||
const showDataForm = ref(false)
|
||||
const dataFormTitle = ref('')
|
||||
const dataCurrentId = ref<number>()
|
||||
const currentTypeId = ref(0)
|
||||
const currentTypeCode = ref('')
|
||||
const dataFormData = reactive<DictDataForm>({ dict_label: '', dict_value: '', dict_type: '', dict_type_id: 0, dict_sort: 1, status: 0, description: '', is_default: false })
|
||||
|
||||
const {
|
||||
list: dataList,
|
||||
total: dataTotal,
|
||||
loading: dataLoading,
|
||||
pageParams: dataPageParams,
|
||||
loadData: loadDataList,
|
||||
toFirst: dataToFirst,
|
||||
loadPrev: dataLoadPrev,
|
||||
loadNext: dataLoadNext,
|
||||
} = useListPage<DictDataItem>({
|
||||
fetcher: p => DictAPI.getDataPage({ ...p, dict_type_id: currentTypeId.value }),
|
||||
onError: e => toast.error(getErrorMessage(e, '加载失败')),
|
||||
})
|
||||
|
||||
function loadDictData(item: DictTypeItem) {
|
||||
currentTypeCode.value = item.dict_type
|
||||
currentTypeId.value = item.id
|
||||
dataFormData.dict_type = item.dict_type
|
||||
dataFormData.dict_type_id = item.id
|
||||
dataToFirst()
|
||||
loadDataList()
|
||||
showDataSection.value = true
|
||||
}
|
||||
function resetDataForm() {
|
||||
Object.assign(dataFormData, { dict_label: '', dict_value: '', dict_sort: 1, status: 0, description: '', is_default: false, dict_type: currentTypeCode.value, dict_type_id: currentTypeId.value })
|
||||
}
|
||||
function openCreateData() {
|
||||
dataFormTitle.value = '新增字典项'
|
||||
dataCurrentId.value = undefined
|
||||
resetDataForm()
|
||||
showDataForm.value = true
|
||||
}
|
||||
async function openEditData(id: number) {
|
||||
dataFormTitle.value = '编辑字典项'
|
||||
dataCurrentId.value = id
|
||||
try {
|
||||
const detail = await DictAPI.getDataDetail(id)
|
||||
Object.assign(dataFormData, {
|
||||
dict_label: detail.dict_label || '',
|
||||
dict_value: detail.dict_value || '',
|
||||
dict_sort: detail.dict_sort ?? 1,
|
||||
status: detail.status ?? 0,
|
||||
description: detail.description || '',
|
||||
is_default: detail.is_default ?? false,
|
||||
dict_type: detail.dict_type || currentTypeCode.value,
|
||||
dict_type_id: detail.dict_type_id || currentTypeId.value,
|
||||
})
|
||||
showDataForm.value = true
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '获取详情失败')) }
|
||||
}
|
||||
async function handleSubmitData() {
|
||||
dataLoading.value = true
|
||||
try {
|
||||
if (dataCurrentId.value) {
|
||||
await DictAPI.updateData(dataCurrentId.value, dataFormData)
|
||||
toast.success('更新成功')
|
||||
}
|
||||
else {
|
||||
await DictAPI.createData(dataFormData)
|
||||
toast.success('创建成功')
|
||||
}
|
||||
showDataForm.value = false
|
||||
loadDataList()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '操作失败')) }
|
||||
finally { dataLoading.value = false }
|
||||
}
|
||||
function handleDeleteData(id: number) {
|
||||
uni.showModal({ title: '提示', content: '确认删除?', success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await DictAPI.removeData([id])
|
||||
toast.success('删除成功')
|
||||
loadDataList()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '删除失败')) }
|
||||
}
|
||||
} })
|
||||
}
|
||||
function closeDataSection() {
|
||||
showDataSection.value = false
|
||||
}
|
||||
|
||||
onReachBottom(() => {
|
||||
if (!showDataSection.value && !loading.value)
|
||||
loadNext()
|
||||
})
|
||||
onPullDownRefresh(() => {
|
||||
loadData()
|
||||
})
|
||||
onLoad(() => loadData())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="page-wraper">
|
||||
<view v-if="!showDataSection">
|
||||
<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>
|
||||
</view>
|
||||
</view>
|
||||
<view class="action-bar">
|
||||
<text class="text-md font-bold text-muted">
|
||||
共 {{ total }} 条
|
||||
</text>
|
||||
<wd-button size="small" type="primary" @click="openCreate">+ 新增</wd-button>
|
||||
</view>
|
||||
<view class="px-sm">
|
||||
<view class="admin-card">
|
||||
<SkeletonPage v-if="loading" />
|
||||
<ListEmpty v-else-if="list.length === 0" text="暂无字典" />
|
||||
<wd-cell-group v-else>
|
||||
<wd-cell v-for="item in list" :key="item.id" is-link @click="loadDictData(item)">
|
||||
<template #title>
|
||||
<view>
|
||||
<text class="font-medium text-md">
|
||||
{{ item.dict_name }}
|
||||
</text><text class="text-xs text-muted block mt-xs">
|
||||
类型: {{ item.dict_type }}
|
||||
</text>
|
||||
</view>
|
||||
</template>
|
||||
<view class="flex items-center gap-xs">
|
||||
<StatusBadge :status="item.status" />
|
||||
<wd-icon name="delete" size="18px" color="var(--danger-color)" @click.stop="handleDelete(item.id!)" />
|
||||
</view>
|
||||
</wd-cell>
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
</view>
|
||||
<PaginationBar :current="pageParams.page_no" :page-size="pageParams.page_size" :total="total" @prev="loadPrev" @next="loadNext" />
|
||||
</view>
|
||||
|
||||
<view v-else>
|
||||
<view class="search-bar">
|
||||
<view class="flex items-center gap-sm">
|
||||
<u-icon name="arrow-left" size="20" @click="closeDataSection" /><text class="font-bold text-md">
|
||||
{{ currentTypeCode }} - 字典项
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="action-bar">
|
||||
<text class="text-md font-bold text-muted">
|
||||
共 {{ dataTotal }} 项
|
||||
</text><wd-button size="small" type="primary" @click="openCreateData">+ 新增</wd-button>
|
||||
</view>
|
||||
<view class="px-sm">
|
||||
<view class="admin-card">
|
||||
<ListEmpty v-if="!dataLoading && dataList.length === 0" text="暂无字典项" /><wd-cell-group v-else>
|
||||
<wd-cell v-for="item in dataList" :key="item.id" is-link @click="openEditData(item.id!)">
|
||||
<template #title>
|
||||
<view>
|
||||
<text class="font-medium text-md">
|
||||
{{ item.dict_label }}
|
||||
</text><text class="text-xs text-muted block mt-xs">
|
||||
值: {{ item.dict_value }}
|
||||
</text>
|
||||
</view>
|
||||
</template>
|
||||
<view class="flex items-center gap-xs">
|
||||
<StatusBadge :status="item.status" />
|
||||
<wd-icon name="delete" size="18px" color="var(--danger-color)" @click.stop="handleDeleteData(item.id!)" />
|
||||
</view>
|
||||
</wd-cell>
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
</view>
|
||||
<PaginationBar :current="dataPageParams.page_no" :page-size="dataPageParams.page_size" :total="dataTotal" @prev="dataLoadPrev" @next="dataLoadNext" />
|
||||
|
||||
<!-- Data form -->
|
||||
<wd-popup v-model="showDataForm" position="bottom" round custom-style="max-height: 60vh; overflow-y: auto;" @close="showDataForm = false">
|
||||
<view class="p-xl">
|
||||
<wd-navbar :title="dataFormTitle" left-arrow @click-left="showDataForm = false" /><wd-form :model="dataFormData" class="mt-lg">
|
||||
<wd-form-item label="标签" prop="dict_label" border>
|
||||
<wd-input v-model="dataFormData.dict_label" placeholder="请输入" />
|
||||
</wd-form-item><wd-form-item label="值" prop="dict_value" border>
|
||||
<wd-input v-model="dataFormData.dict_value" placeholder="请输入" />
|
||||
</wd-form-item><wd-form-item label="排序" border>
|
||||
<wd-input v-model="dataFormData.dict_sort" type="number" placeholder="请输入" />
|
||||
</wd-form-item><wd-form-item label="状态" border>
|
||||
<wd-radio-group v-model="dataFormData.status">
|
||||
<wd-radio v-for="opt in STATUS_OPTIONS" :key="opt.value" :value="opt.value">
|
||||
{{ opt.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-form-item><wd-form-item label="备注" border>
|
||||
<wd-textarea v-model="dataFormData.description" placeholder="请输入" />
|
||||
</wd-form-item>
|
||||
</wd-form><view class="flex gap-md mt-xl">
|
||||
<wd-button block plain @click="showDataForm = false">取消</wd-button><wd-button block type="primary" :loading="dataLoading" @click="handleSubmitData">保存</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</view>
|
||||
|
||||
<!-- Type form (shared, outside v-if/v-else) -->
|
||||
<u-popup :show="showForm" mode="bottom" :round="10" custom-style="max-height: 60vh; overflow-y: auto;" @close="showForm = false">
|
||||
<view class="p-xl">
|
||||
<u-navbar :title="formTitle" left-icon="arrow-left" @left-click="showForm = false" /><u-form :model="formData" class="mt-lg">
|
||||
<u-form-item label="字典名称" prop="dict_name" :border-bottom="true">
|
||||
<u-input v-model="formData.dict_name" border="none" placeholder="请输入" />
|
||||
</u-form-item><u-form-item label="字典类型" prop="dict_type" :border-bottom="true">
|
||||
<u-input v-model="formData.dict_type" border="none" placeholder="请输入" />
|
||||
</u-form-item><u-form-item label="状态" :border-bottom="true">
|
||||
<u-radio-group v-model="formData.status">
|
||||
<u-radio v-for="opt in STATUS_OPTIONS" :key="opt.value" :name="opt.value" :label="opt.value">
|
||||
{{ opt.label }}
|
||||
</u-radio>
|
||||
</u-radio-group>
|
||||
</u-form-item><u-form-item label="备注" :border-bottom="true">
|
||||
<u-textarea v-model="formData.description" border="none" placeholder="请输入" />
|
||||
</u-form-item>
|
||||
</u-form><view class="flex gap-md mt-xl">
|
||||
<u-button :block="true" :plain="true" text="取消" @click="showForm = false" /><u-button :block="true" type="primary" :loading="loading" text="保存" @click="handleSubmit" />
|
||||
</view>
|
||||
</view>
|
||||
</u-popup>
|
||||
</view>
|
||||
</template>
|
||||
@@ -0,0 +1,186 @@
|
||||
<script setup lang="ts">
|
||||
import type { NoticeForm, NoticeItem } from '@/api/module_system/notice'
|
||||
import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app'
|
||||
import { reactive, ref } from 'vue'
|
||||
import { NoticeAPI } from '@/api/module_system/notice'
|
||||
|
||||
definePage({ name: 'work-notices', style: { navigationBarTitleText: '通知公告' } })
|
||||
|
||||
const toast = useToast()
|
||||
const searchTitle = ref('')
|
||||
const showForm = ref(false)
|
||||
const formTitle = ref('')
|
||||
const currentId = ref<number>()
|
||||
const formData = reactive<NoticeForm>({ notice_title: '', notice_content: '', notice_type: '1', status: 0, description: '' })
|
||||
|
||||
const NOTICE_TYPE_OPTIONS = [{ value: '1', label: '通知' }, { value: '2', label: '公告' }]
|
||||
const STATUS_OPTIONS = [{ value: 0, label: '草稿' }, { value: 1, label: '已发布' }, { value: 2, label: '已归档' }]
|
||||
|
||||
function statusLabel(s: number | string | undefined) {
|
||||
const n = Number(s)
|
||||
if (n === 1)
|
||||
return 'published'
|
||||
if (n === 2)
|
||||
return 'archived'
|
||||
return 'draft'
|
||||
}
|
||||
|
||||
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, '加载失败')),
|
||||
})
|
||||
|
||||
function onSearch() {
|
||||
toFirst()
|
||||
loadData()
|
||||
}
|
||||
function onReset() {
|
||||
searchTitle.value = ''
|
||||
toFirst()
|
||||
loadData()
|
||||
}
|
||||
function resetForm() {
|
||||
Object.assign(formData, { notice_title: '', notice_content: '', notice_type: '1', status: 0, description: '' })
|
||||
}
|
||||
function openCreate() {
|
||||
formTitle.value = '新增公告'
|
||||
currentId.value = undefined
|
||||
resetForm()
|
||||
showForm.value = true
|
||||
}
|
||||
async function openEdit(id: number) {
|
||||
formTitle.value = '编辑公告'
|
||||
currentId.value = id
|
||||
try {
|
||||
const detail = await NoticeAPI.getDetail(id)
|
||||
Object.assign(formData, {
|
||||
notice_title: detail.notice_title || '',
|
||||
notice_content: detail.notice_content || '',
|
||||
notice_type: detail.notice_type || '1',
|
||||
status: detail.status ?? 0,
|
||||
description: detail.description || '',
|
||||
})
|
||||
showForm.value = true
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '获取详情失败')) }
|
||||
}
|
||||
async function handleSubmit() {
|
||||
loading.value = true
|
||||
try {
|
||||
if (currentId.value) {
|
||||
await NoticeAPI.update(currentId.value, formData)
|
||||
toast.success('更新成功')
|
||||
}
|
||||
else {
|
||||
await NoticeAPI.create(formData)
|
||||
toast.success('创建成功')
|
||||
}
|
||||
showForm.value = false
|
||||
loadData()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '操作失败')) }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
function handleDelete(id: number) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确认删除?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await NoticeAPI.remove([id])
|
||||
toast.success('删除成功')
|
||||
loadData()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '删除失败')) }
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
onReachBottom(() => {
|
||||
if (!loading.value)
|
||||
loadNext()
|
||||
})
|
||||
onPullDownRefresh(() => {
|
||||
loadData()
|
||||
})
|
||||
onLoad(() => loadData())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="page-wraper">
|
||||
<view class="search-bar">
|
||||
<view class="flex items-center gap-sm">
|
||||
<u-input v-model="searchTitle" placeholder="搜索公告标题" clearable border="surround" class="flex-1" />
|
||||
<u-button size="mini" type="primary" :plain="true" text="搜索" @click="onSearch" />
|
||||
<u-button size="mini" :plain="true" text="重置" @click="onReset" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="action-bar">
|
||||
<text class="text-md font-bold text-muted">
|
||||
共 {{ total }} 条
|
||||
</text>
|
||||
<wd-button size="small" type="primary" @click="openCreate">+ 新增</wd-button>
|
||||
</view>
|
||||
<SkeletonPage v-if="loading" :rows="5" search />
|
||||
<template v-else>
|
||||
<view class="px-sm">
|
||||
<view class="admin-card">
|
||||
<ListEmpty v-if="!loading && list.length === 0" text="暂无公告" />
|
||||
<wd-cell-group v-else>
|
||||
<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">
|
||||
{{ item.notice_title }}
|
||||
</text><text class="text-xs text-muted block mt-xs">
|
||||
{{ item.description || '' }}
|
||||
</text>
|
||||
</view>
|
||||
</template>
|
||||
<view class="flex items-center gap-xs">
|
||||
<StatusBadge :status="statusLabel(item.status)" />
|
||||
<wd-icon name="delete" size="18px" color="var(--danger-color)" @click.stop="handleDelete(item.id!)" />
|
||||
</view>
|
||||
</wd-cell>
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
</view>
|
||||
<PaginationBar :current="pageParams.page_no" :page-size="pageParams.page_size" :total="total" @prev="loadPrev" @next="loadNext" />
|
||||
<wd-popup v-model="showForm" position="bottom" round custom-style="max-height: 80vh; overflow-y: auto;" @close="showForm = false">
|
||||
<view class="p-xl">
|
||||
<wd-navbar :title="formTitle" left-arrow @click-left="showForm = false" />
|
||||
<wd-form :model="formData" class="mt-lg">
|
||||
<wd-form-item label="公告标题" prop="notice_title" border>
|
||||
<wd-input v-model="formData.notice_title" placeholder="请输入" />
|
||||
</wd-form-item>
|
||||
<wd-form-item label="公告内容" prop="notice_content" border>
|
||||
<wd-textarea v-model="formData.notice_content" placeholder="请输入内容" />
|
||||
</wd-form-item>
|
||||
<wd-form-item label="公告类型" border>
|
||||
<wd-radio-group v-model="formData.notice_type">
|
||||
<wd-radio v-for="opt in NOTICE_TYPE_OPTIONS" :key="opt.value" :value="opt.value">
|
||||
{{ opt.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-form-item>
|
||||
<wd-form-item label="状态" border>
|
||||
<wd-radio-group v-model="formData.status">
|
||||
<wd-radio v-for="opt in STATUS_OPTIONS" :key="opt.value" :value="opt.value">
|
||||
{{ opt.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-form-item>
|
||||
<wd-form-item label="备注" border>
|
||||
<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>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
@@ -0,0 +1,157 @@
|
||||
<script setup lang="ts">
|
||||
import type { ParamsForm, ParamsItem } from '@/api/module_system/params'
|
||||
import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { ParamsAPI } from '@/api/module_system/params'
|
||||
|
||||
definePage({ name: 'work-params', style: { navigationBarTitleText: '参数管理' } })
|
||||
|
||||
const toast = useToast()
|
||||
const loading = ref(false)
|
||||
const list = ref<ParamsItem[]>([])
|
||||
const searchName = ref('')
|
||||
const showForm = ref(false)
|
||||
const currentId = ref<number>()
|
||||
const formData = reactive<ParamsForm>({ config_name: '', config_key: '', config_value: '', config_type: false, status: 0, description: '' })
|
||||
|
||||
const STATUS_OPTIONS = [{ value: 0, label: '正常' }, { value: 1, label: '禁用' }]
|
||||
|
||||
const filteredList = computed(() => {
|
||||
if (!searchName.value)
|
||||
return list.value
|
||||
const keyword = searchName.value.toLowerCase()
|
||||
return list.value.filter(item =>
|
||||
(item.config_name || '').toLowerCase().includes(keyword)
|
||||
|| (item.config_key || '').toLowerCase().includes(keyword),
|
||||
)
|
||||
})
|
||||
|
||||
function displayName(item: ParamsItem) {
|
||||
return item.config_name || '未知'
|
||||
}
|
||||
function displayKey(item: ParamsItem) {
|
||||
return item.config_key || ''
|
||||
}
|
||||
function displayValue(item: ParamsItem) {
|
||||
return item.config_value || ''
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await ParamsAPI.getInfo()
|
||||
list.value = res || []
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '加载失败')) }
|
||||
finally {
|
||||
loading.value = false
|
||||
uni.stopPullDownRefresh()
|
||||
}
|
||||
}
|
||||
function onSearch() { /* 前端实时过滤 */ }
|
||||
function onReset() {
|
||||
searchName.value = ''
|
||||
}
|
||||
function openEdit(item: ParamsItem) {
|
||||
currentId.value = item.id
|
||||
Object.assign(formData, {
|
||||
config_name: displayName(item),
|
||||
config_key: displayKey(item),
|
||||
config_value: displayValue(item),
|
||||
config_type: item.config_type ?? false,
|
||||
status: item.status ?? 0,
|
||||
description: item.description || '',
|
||||
})
|
||||
showForm.value = true
|
||||
}
|
||||
async function handleSubmit() {
|
||||
loading.value = true
|
||||
try {
|
||||
await ParamsAPI.update(currentId.value!, formData)
|
||||
toast.success('更新成功')
|
||||
showForm.value = false
|
||||
loadData()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '操作失败')) }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
onPullDownRefresh(() => {
|
||||
loadData()
|
||||
})
|
||||
onLoad(() => loadData())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="page-wraper">
|
||||
<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>
|
||||
</view>
|
||||
</view>
|
||||
<view class="action-bar">
|
||||
<text class="text-md font-bold text-muted">
|
||||
共 {{ filteredList.length }} 条
|
||||
</text>
|
||||
</view>
|
||||
<SkeletonPage v-if="loading" :rows="5" search />
|
||||
<template v-else>
|
||||
<view class="px-sm">
|
||||
<view class="admin-card">
|
||||
<ListEmpty v-if="filteredList.length === 0" text="暂无数据" />
|
||||
<wd-cell-group v-else>
|
||||
<wd-cell v-for="item in filteredList" :key="item.id" is-link @click="openEdit(item)">
|
||||
<template #title>
|
||||
<view>
|
||||
<text class="font-medium text-md">
|
||||
{{ displayName(item) }}
|
||||
</text><text class="text-xs text-muted block mt-xs">
|
||||
{{ `键: ${displayKey(item)}` }}
|
||||
</text>
|
||||
</view>
|
||||
</template>
|
||||
<template #label>
|
||||
<text class="text-xs text-muted">
|
||||
{{ displayValue(item) || '-' }}
|
||||
</text>
|
||||
</template>
|
||||
<StatusBadge :status="item.status" />
|
||||
</wd-cell>
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
</view>
|
||||
<wd-popup v-model="showForm" position="bottom" round custom-style="max-height: 80vh; overflow-y: auto;" @close="showForm = false">
|
||||
<view class="p-xl">
|
||||
<wd-navbar title="编辑参数" left-arrow @click-left="showForm = false" />
|
||||
<wd-form :model="formData" class="mt-lg">
|
||||
<wd-form-item label="参数名称" prop="config_name" border>
|
||||
<wd-input v-model="formData.config_name" placeholder="请输入" />
|
||||
</wd-form-item>
|
||||
<wd-form-item label="参数键名" prop="config_key" border>
|
||||
<wd-input v-model="formData.config_key" placeholder="请输入" />
|
||||
</wd-form-item>
|
||||
<wd-form-item label="参数键值" border>
|
||||
<wd-textarea v-model="formData.config_value" placeholder="请输入" />
|
||||
</wd-form-item>
|
||||
<wd-form-item label="状态" border>
|
||||
<wd-radio-group v-model="formData.status">
|
||||
<wd-radio v-for="opt in STATUS_OPTIONS" :key="opt.value" :value="opt.value">
|
||||
{{ opt.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-form-item>
|
||||
<wd-form-item label="备注" border>
|
||||
<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>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
@@ -0,0 +1,500 @@
|
||||
<script setup lang="ts">
|
||||
import type { TicketComment, TicketItem } from '@/api/module_system/ticket'
|
||||
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',
|
||||
style: { navigationBarTitleText: '工单详情' },
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const loading = ref(false)
|
||||
const ticket = ref<TicketItem | null>(null)
|
||||
const comments = ref<TicketComment[]>([])
|
||||
const commentTotal = ref(0)
|
||||
const commentPage = reactive({ page_no: 1, page_size: 20 })
|
||||
const commentText = ref('')
|
||||
const submitting = ref(false)
|
||||
const ticketId = ref(0)
|
||||
|
||||
async function loadTicket() {
|
||||
if (!ticketId.value)
|
||||
return
|
||||
loading.value = true
|
||||
try {
|
||||
ticket.value = await TicketAPI.getDetail(ticketId.value)
|
||||
}
|
||||
catch (e) {
|
||||
toast.error(getErrorMessage(e, '加载工单详情失败'))
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadComments() {
|
||||
if (!ticketId.value)
|
||||
return
|
||||
try {
|
||||
const res = await TicketAPI.getComments(ticketId.value, { page_no: commentPage.page_no, page_size: commentPage.page_size })
|
||||
comments.value = res.list || []
|
||||
commentTotal.value = res.total || 0
|
||||
}
|
||||
catch (e) {
|
||||
logger.error('加载评论失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function submitComment() {
|
||||
if (!commentText.value.trim())
|
||||
return toast.warning('请输入评论内容')
|
||||
submitting.value = true
|
||||
try {
|
||||
await TicketAPI.createComment(ticketId.value, { content: commentText.value.trim() })
|
||||
commentText.value = ''
|
||||
toast.success('评论成功')
|
||||
loadComments()
|
||||
}
|
||||
catch (e) {
|
||||
toast.error(getErrorMessage(e, '评论失败'))
|
||||
}
|
||||
finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function getTypeLabel(type?: string) {
|
||||
const map: Record<string, string> = { suggestion: '建议', bug: '缺陷', optimize: '优化', other: '其他' }
|
||||
return map[type || ''] || type || '其他'
|
||||
}
|
||||
function getTypeColor(type?: string) {
|
||||
const map: Record<string, string> = { suggestion: '#4F8CFF', bug: '#EF4444', optimize: '#F59E0B', other: '#6B7280' }
|
||||
return map[type || ''] || '#6B7280'
|
||||
}
|
||||
function getStatusLabel(status?: string | number) {
|
||||
const map: Record<string, string> = { 0: '待处理', 1: '处理中', 2: '已完成', 3: '已关闭' }
|
||||
return map[String(status ?? '')] || '未知'
|
||||
}
|
||||
function getStatusColor(status?: string | number) {
|
||||
const map: Record<string, string> = { 0: '#F59E0B', 1: '#4F8CFF', 2: '#10B981', 3: '#6B7280' }
|
||||
return map[String(status ?? '')] || '#6B7280'
|
||||
}
|
||||
function parseImages(images?: string): string[] {
|
||||
if (!images)
|
||||
return []
|
||||
try {
|
||||
return JSON.parse(images)
|
||||
}
|
||||
catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function previewImages(images: string[], current: number) {
|
||||
uni.previewImage({ urls: images, current })
|
||||
}
|
||||
|
||||
onPullDownRefresh(async () => {
|
||||
try {
|
||||
await Promise.all([loadTicket(), loadComments()])
|
||||
}
|
||||
finally {
|
||||
uni.stopPullDownRefresh()
|
||||
}
|
||||
})
|
||||
|
||||
onLoad((options) => {
|
||||
ticketId.value = Number(options?.id || 0)
|
||||
if (ticketId.value) {
|
||||
loadTicket()
|
||||
loadComments()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="detail-page">
|
||||
<SkeletonPage v-if="loading && !ticket" :rows="5" />
|
||||
|
||||
<template v-else-if="ticket">
|
||||
<!-- Ticket Header -->
|
||||
<view class="ticket-header fade-in-up">
|
||||
<text class="ticket-header__title">
|
||||
{{ ticket.title || '未命名工单' }}
|
||||
</text>
|
||||
<view class="ticket-header__badges">
|
||||
<view class="badge" :style="{ background: `${getTypeColor(ticket.ticket_type)}18`, color: getTypeColor(ticket.ticket_type) }">
|
||||
{{ getTypeLabel(ticket.ticket_type) }}
|
||||
</view>
|
||||
<view class="badge" :style="{ background: `${getStatusColor(ticket.status)}18`, color: getStatusColor(ticket.status) }">
|
||||
{{ getStatusLabel(ticket.status) }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Ticket Info -->
|
||||
<view class="ticket-info fade-in-up-1">
|
||||
<view class="info-row">
|
||||
<text class="info-row__label">
|
||||
创建时间
|
||||
</text>
|
||||
<text class="info-row__value">
|
||||
{{ ticket.created_time || '—' }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-row__label">
|
||||
处理人
|
||||
</text>
|
||||
<text class="info-row__value">
|
||||
{{ ticket.assigned_by?.name || '未分配' }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-row__label">
|
||||
更新时间
|
||||
</text>
|
||||
<text class="info-row__value">
|
||||
{{ ticket.updated_time || '—' }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Ticket Content -->
|
||||
<view v-if="ticket.ticket_content || ticket.summary" class="ticket-content fade-in-up-2">
|
||||
<text class="section-title">
|
||||
工单内容
|
||||
</text>
|
||||
<text class="ticket-content__text">
|
||||
{{ ticket.summary || ticket.ticket_content }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- Images -->
|
||||
<view v-if="parseImages(ticket.images).length > 0" class="ticket-images fade-in-up-2">
|
||||
<text class="section-title">
|
||||
附件图片
|
||||
</text>
|
||||
<view class="images-grid">
|
||||
<image
|
||||
v-for="(img, idx) in parseImages(ticket.images)"
|
||||
:key="img"
|
||||
:src="img"
|
||||
class="images-grid__item"
|
||||
mode="aspectFill"
|
||||
lazy-load
|
||||
@click="previewImages(parseImages(ticket.images), idx)"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Reply -->
|
||||
<view v-if="ticket.reply" class="ticket-reply fade-in-up-3">
|
||||
<text class="section-title">
|
||||
处理回复
|
||||
</text>
|
||||
<view class="reply-box">
|
||||
<text class="reply-box__text">
|
||||
{{ ticket.reply }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Comments -->
|
||||
<view class="comments-section fade-in-up-3">
|
||||
<view class="comments-header">
|
||||
<text class="section-title">
|
||||
评论 ({{ commentTotal }})
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<ListEmpty v-if="comments.length === 0" text="暂无评论,快来抢沙发" />
|
||||
|
||||
<view v-else class="comments-list">
|
||||
<view v-for="comment in comments" :key="comment.id" class="comment-item">
|
||||
<view class="comment-item__avatar">
|
||||
<text class="comment-item__avatar-text">
|
||||
{{ (comment.username || '匿').charAt(0) }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="comment-item__body">
|
||||
<view class="comment-item__header">
|
||||
<text class="comment-item__name">
|
||||
{{ comment.username || '匿名用户' }}
|
||||
</text>
|
||||
<text class="comment-item__time">
|
||||
{{ comment.created_time || '' }}
|
||||
</text>
|
||||
</view>
|
||||
<text class="comment-item__content">
|
||||
{{ comment.content }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="commentTotal > comments.length" class="load-more" @click="commentPage.page_no++; loadComments()">
|
||||
<text class="load-more__text">
|
||||
加载更多
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<ListEmpty v-else text="工单不存在或已删除" />
|
||||
|
||||
<!-- Comment Input -->
|
||||
<view v-if="ticket" class="comment-bar">
|
||||
<u-input
|
||||
v-model="commentText"
|
||||
placeholder="写下你的评论..."
|
||||
clearable
|
||||
border="surround"
|
||||
style="flex:1"
|
||||
/>
|
||||
<u-button
|
||||
size="mini"
|
||||
type="primary"
|
||||
:loading="submitting"
|
||||
:disabled="!commentText.trim()"
|
||||
text="发送"
|
||||
@click="submitComment"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.detail-page {
|
||||
padding: 0 32rpx 140rpx;
|
||||
background: var(--page-bg-color, #F9F9F9);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* ===== Ticket Header ===== */
|
||||
.ticket-header {
|
||||
background: var(--card-bg-color, #FFFFFF);
|
||||
border-radius: 24rpx;
|
||||
padding: 24rpx;
|
||||
margin: 24rpx 0 16rpx;
|
||||
box-shadow: var(--shadow-sm, 0 1rpx 2rpx rgba(1, 77, 178,0.06));
|
||||
|
||||
&__title {
|
||||
display: block;
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
color: var(--text-color, #0A1628);
|
||||
line-height: 1.4;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
&__badges {
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.badge {
|
||||
font-size: 22rpx;
|
||||
padding: 4rpx 16rpx;
|
||||
border-radius: 8rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ===== Ticket Info ===== */
|
||||
.ticket-info {
|
||||
background: var(--card-bg-color, #FFFFFF);
|
||||
border-radius: 24rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 16rpx;
|
||||
box-shadow: var(--shadow-sm, 0 1rpx 2rpx rgba(1, 77, 178,0.06));
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8rpx 0;
|
||||
|
||||
&__label {
|
||||
font-size: 26rpx;
|
||||
color: var(--text-color-3, #6B7280);
|
||||
}
|
||||
|
||||
&__value {
|
||||
font-size: 26rpx;
|
||||
color: var(--text-color, #0A1628);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Sections ===== */
|
||||
.section-title {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text-color, #0A1628);
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
/* ===== Content ===== */
|
||||
.ticket-content {
|
||||
background: var(--card-bg-color, #FFFFFF);
|
||||
border-radius: 24rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 16rpx;
|
||||
box-shadow: var(--shadow-sm, 0 1rpx 2rpx rgba(1, 77, 178,0.06));
|
||||
|
||||
&__text {
|
||||
font-size: 28rpx;
|
||||
color: var(--text-color-2, #4B5563);
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Images ===== */
|
||||
.ticket-images {
|
||||
background: var(--card-bg-color, #FFFFFF);
|
||||
border-radius: 24rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 16rpx;
|
||||
box-shadow: var(--shadow-sm, 0 1rpx 2rpx rgba(1, 77, 178,0.06));
|
||||
}
|
||||
|
||||
.images-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx;
|
||||
|
||||
&__item {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
border-radius: 16rpx;
|
||||
background: var(--page-bg-color, #F9F9F9);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Reply ===== */
|
||||
.ticket-reply {
|
||||
background: var(--card-bg-color, #FFFFFF);
|
||||
border-radius: 24rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 16rpx;
|
||||
box-shadow: var(--shadow-sm, 0 1rpx 2rpx rgba(1, 77, 178,0.06));
|
||||
}
|
||||
|
||||
.reply-box {
|
||||
background: var(--primary-color-light, rgba(1,77,178,0.06));
|
||||
border-radius: 16rpx;
|
||||
padding: 16rpx;
|
||||
|
||||
&__text {
|
||||
font-size: 28rpx;
|
||||
color: var(--text-color-2, #4B5563);
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Comments ===== */
|
||||
.comments-section {
|
||||
background: var(--card-bg-color, #FFFFFF);
|
||||
border-radius: 24rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 16rpx;
|
||||
box-shadow: var(--shadow-sm, 0 1rpx 2rpx rgba(1, 77, 178,0.06));
|
||||
}
|
||||
|
||||
.comments-header {
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.comments-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.comment-item {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
|
||||
&__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;
|
||||
}
|
||||
|
||||
&__avatar-text {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
&__body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text-color, #0A1628);
|
||||
}
|
||||
|
||||
&__time {
|
||||
font-size: 22rpx;
|
||||
color: var(--text-color-4, #B0B0B0);
|
||||
}
|
||||
|
||||
&__content {
|
||||
font-size: 28rpx;
|
||||
color: var(--text-color-2, #4B5563);
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Load more ===== */
|
||||
.load-more {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 72rpx;
|
||||
|
||||
&__text {
|
||||
font-size: 26rpx;
|
||||
color: var(--primary-color, #4F8CFF);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Comment Bar ===== */
|
||||
.comment-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
padding: 16rpx 32rpx;
|
||||
padding-bottom: calc(16rpx + env(safe-area-inset-bottom));
|
||||
background: var(--card-bg-color, #FFFFFF);
|
||||
border-top: 1rpx solid var(--border-color, #F0F0F0);
|
||||
z-index: 100;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,196 @@
|
||||
<script setup lang="ts">
|
||||
import type { TicketForm, TicketItem } from '@/api/module_system/ticket'
|
||||
import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app'
|
||||
import { reactive, ref } from 'vue'
|
||||
import { TicketAPI } from '@/api/module_system/ticket'
|
||||
|
||||
definePage({ name: 'work-tickets', style: { navigationBarTitleText: '工单管理' } })
|
||||
|
||||
const router = useRouter()
|
||||
const toast = useToast()
|
||||
const searchTitle = ref('')
|
||||
const showForm = ref(false)
|
||||
const formTitle = ref('')
|
||||
const currentId = ref<number>()
|
||||
const initForm: TicketForm = { title: '', ticket_content: '', ticket_type: '', status: undefined, description: '' }
|
||||
const formData = reactive<TicketForm>({ ...initForm })
|
||||
const showPickerType = ref(false)
|
||||
const showPickerStatus = ref(false)
|
||||
|
||||
const TYPE_OPTIONS = [{ value: 'suggestion', label: '建议' }, { value: 'bug', label: '缺陷' }, { value: 'optimize', label: '优化' }, { value: 'other', label: '其他' }]
|
||||
|
||||
const STATUS_OPTIONS = [{ value: 0, label: '待处理' }, { value: 1, label: '处理中' }, { value: 2, label: '已完成' }, { value: 3, label: '已关闭' }]
|
||||
|
||||
interface PickerConfirmEvent { value: Array<string | number>, selectedOptions: any[] }
|
||||
function handleTypeConfirm(e: PickerConfirmEvent) {
|
||||
const item = TYPE_OPTIONS.find(o => o.value === e.value[0])
|
||||
if (item)
|
||||
formData.ticket_type = item.value
|
||||
showPickerType.value = false
|
||||
}
|
||||
function handleStatusConfirm(e: PickerConfirmEvent) {
|
||||
const item = STATUS_OPTIONS.find(o => o.value === e.value[0])
|
||||
if (item)
|
||||
formData.status = item.value
|
||||
showPickerStatus.value = false
|
||||
}
|
||||
|
||||
const TICKET_STATUS_MAP: Record<number, string> = { 0: 'pending', 1: 'processing', 2: 'completed', 3: 'closed' }
|
||||
function ticketStatus(status: number | string | undefined) {
|
||||
return TICKET_STATUS_MAP[Number(status)] || String(status)
|
||||
}
|
||||
|
||||
function displayContent(item: TicketItem) {
|
||||
return item.ticket_content || ''
|
||||
}
|
||||
function displayType(item: TicketItem) {
|
||||
return item.ticket_type || ''
|
||||
}
|
||||
|
||||
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, '加载失败')),
|
||||
})
|
||||
|
||||
function onSearch() {
|
||||
toFirst()
|
||||
loadData()
|
||||
}
|
||||
function onReset() {
|
||||
searchTitle.value = ''
|
||||
toFirst()
|
||||
loadData()
|
||||
}
|
||||
function resetForm() {
|
||||
Object.assign(formData, { ...initForm })
|
||||
}
|
||||
function openCreate() {
|
||||
formTitle.value = '新增'
|
||||
currentId.value = undefined
|
||||
resetForm()
|
||||
showForm.value = true
|
||||
}
|
||||
function navigateToDetail(id: number) {
|
||||
router.push({ name: 'work-ticket-detail', query: { id } })
|
||||
}
|
||||
async function handleSubmit() {
|
||||
loading.value = true
|
||||
try {
|
||||
if (currentId.value) {
|
||||
await TicketAPI.update(currentId.value, { ...formData })
|
||||
toast.success('更新成功')
|
||||
}
|
||||
else {
|
||||
await TicketAPI.create({ ...formData })
|
||||
toast.success('创建成功')
|
||||
}
|
||||
showForm.value = false
|
||||
loadData()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '操作失败')) }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
function handleDelete(id: number) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确认删除?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await TicketAPI.remove([id])
|
||||
toast.success('删除成功')
|
||||
loadData()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '删除失败')) }
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
onReachBottom(() => {
|
||||
if (!loading.value)
|
||||
loadNext()
|
||||
})
|
||||
onPullDownRefresh(() => {
|
||||
loadData()
|
||||
})
|
||||
onLoad(() => loadData())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="page-wraper">
|
||||
<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>
|
||||
</view>
|
||||
</view>
|
||||
<view class="action-bar">
|
||||
<text class="text-md font-bold text-muted">
|
||||
共 {{ total }} 条
|
||||
</text>
|
||||
<wd-button size="small" type="primary" @click="openCreate">+ 新增</wd-button>
|
||||
</view>
|
||||
<SkeletonPage v-if="loading" :rows="5" search />
|
||||
<template v-else>
|
||||
<view class="px-sm">
|
||||
<view class="admin-card">
|
||||
<ListEmpty v-if="!loading && list.length === 0" text="暂无数据" />
|
||||
<wd-cell-group v-else>
|
||||
<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">
|
||||
{{ item.title }}
|
||||
</text>
|
||||
</view>
|
||||
</template>
|
||||
<template #label>
|
||||
<text class="text-xs text-muted">
|
||||
{{ displayType(item) ? `${displayType(item)} · ` : '' }}{{ displayContent(item) }}
|
||||
</text>
|
||||
</template>
|
||||
<view class="flex items-center gap-xs">
|
||||
<StatusBadge :status="ticketStatus(item.status)" />
|
||||
<wd-icon name="delete" size="18px" color="var(--danger-color)" @click.stop="handleDelete(item.id!)" />
|
||||
</view>
|
||||
</wd-cell>
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
</view>
|
||||
<PaginationBar :current="pageParams.page_no" :page-size="pageParams.page_size" :total="total" @prev="loadPrev" @next="loadNext" />
|
||||
</template>
|
||||
<wd-popup v-model="showForm" position="bottom" round custom-style="max-height: 80vh; overflow-y: auto;" @close="showForm = false">
|
||||
<view class="p-xl">
|
||||
<wd-navbar :title="formTitle" left-arrow @click-left="showForm = false" />
|
||||
<wd-form :model="formData" class="mt-lg">
|
||||
<wd-form-item label="工单标题">
|
||||
<wd-input v-model="formData.title" placeholder="请输入" />
|
||||
</wd-form-item>
|
||||
<wd-form-item label="内容">
|
||||
<wd-textarea v-model="formData.ticket_content" placeholder="请输入" />
|
||||
</wd-form-item>
|
||||
<wd-form-item label="类型">
|
||||
<view @click="showPickerType = true">
|
||||
<wd-cell title="类型" :value="TYPE_OPTIONS.find(o => o.value === formData.ticket_type)?.label || '请选择'" is-link :border="false" />
|
||||
</view>
|
||||
<wd-picker :visible="showPickerType" :columns="[TYPE_OPTIONS]" @confirm="handleTypeConfirm" @cancel="showPickerType = false" />
|
||||
</wd-form-item>
|
||||
<wd-form-item v-if="currentId" label="状态">
|
||||
<view @click="showPickerStatus = true">
|
||||
<wd-cell title="状态" :value="STATUS_OPTIONS.find(o => o.value === formData.status)?.label || '请选择'" is-link :border="false" />
|
||||
</view>
|
||||
<wd-picker :visible="showPickerStatus" :columns="[STATUS_OPTIONS]" @confirm="handleStatusConfirm" @cancel="showPickerStatus = false" />
|
||||
</wd-form-item>
|
||||
<wd-form-item label="备注">
|
||||
<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>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</view>
|
||||
</template>
|
||||
@@ -0,0 +1,497 @@
|
||||
<script setup lang="ts">
|
||||
import type { DeptItem } from '@/api/module_system/dept'
|
||||
import type { UserForm } from '@/api/module_system/user'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { ref } from 'vue'
|
||||
import { DeptAPI } from '@/api/module_system/dept'
|
||||
import { RoleAPI } from '@/api/module_system/role'
|
||||
import UserAPI from '@/api/module_system/user'
|
||||
import SkeletonPage from '@/components/SkeletonPage.vue'
|
||||
|
||||
definePage({
|
||||
name: 'work-user-detail',
|
||||
style: { navigationBarTitleText: '用户详情' },
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const userId = ref<number>()
|
||||
|
||||
const user = ref<UserForm>({})
|
||||
const deptOptions = ref<{ value: number, label: string }[]>([])
|
||||
const roleOptions = ref<{ value: number, label: string }[]>([])
|
||||
const showPickerDept = ref(false)
|
||||
const showPickerRole = ref(false)
|
||||
|
||||
interface PickerConfirmEvent { value: Array<string | number>, selectedOptions: any[] }
|
||||
function handleDeptConfirm(e: PickerConfirmEvent) {
|
||||
const item = deptOptions.value.find(o => o.value === e.value[0])
|
||||
if (item)
|
||||
user.value.dept_id = item.value
|
||||
showPickerDept.value = false
|
||||
}
|
||||
function handleRoleConfirm(e: PickerConfirmEvent) {
|
||||
const item = roleOptions.value.find(o => o.value === e.value[0])
|
||||
if (item)
|
||||
user.value.role_ids = [item.value]
|
||||
showPickerRole.value = false
|
||||
}
|
||||
|
||||
const STATUS_OPTIONS = [
|
||||
{ value: 1, label: '正常', color: '#10B981', bg: '#E8F8F1' },
|
||||
{ value: 0, label: '禁用', color: '#EF4444', bg: '#FEF2F2' },
|
||||
]
|
||||
|
||||
const GENDER_OPTIONS = [
|
||||
{ value: 0, label: '男' },
|
||||
{ value: 1, label: '女' },
|
||||
{ value: 2, label: '未知' },
|
||||
]
|
||||
|
||||
function statusBadge(status?: number) {
|
||||
return STATUS_OPTIONS.find(s => s.value === status) || STATUS_OPTIONS[1]
|
||||
}
|
||||
|
||||
function genderLabel(gender?: number) {
|
||||
return GENDER_OPTIONS.find(g => g.value === gender)?.label || '未知'
|
||||
}
|
||||
|
||||
function flattenDept(items: DeptItem[], prefix = ''): { value: number, label: string }[] {
|
||||
let r: { value: number, label: string }[] = []
|
||||
for (const item of items) {
|
||||
r.push({ value: item.id, label: prefix + item.name })
|
||||
if (item.children?.length)
|
||||
r = r.concat(flattenDept(item.children, `${prefix} `))
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
async function loadOptions() {
|
||||
try {
|
||||
const [deptTree, roleRes] = await Promise.all([
|
||||
DeptAPI.getTree().catch(() => [] as DeptItem[]),
|
||||
RoleAPI.getOptions().catch(() => []),
|
||||
])
|
||||
deptOptions.value = flattenDept(deptTree || [])
|
||||
roleOptions.value = (roleRes || []).map(r => ({ value: r.value, label: r.label }))
|
||||
}
|
||||
catch { /* silent */ }
|
||||
}
|
||||
|
||||
async function loadUser() {
|
||||
if (!userId.value)
|
||||
return
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await UserAPI.getUserDetail(userId.value)
|
||||
user.value = res
|
||||
}
|
||||
catch (e) {
|
||||
toast.error(getErrorMessage(e, '加载用户详情失败'))
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!user.value.id)
|
||||
return
|
||||
saving.value = true
|
||||
try {
|
||||
await UserAPI.updateUser(user.value)
|
||||
toast.success('保存成功')
|
||||
isEdit.value = false
|
||||
}
|
||||
catch (e) {
|
||||
toast.error(getErrorMessage(e, '保存失败'))
|
||||
}
|
||||
finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleToggleStatus() {
|
||||
const newStatus = user.value.status === 1 ? 0 : 1
|
||||
const action = newStatus === 0 ? '禁用' : '启用'
|
||||
uni.showModal({
|
||||
title: '确认操作',
|
||||
content: `确认${action}该用户吗?`,
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
user.value.status = newStatus
|
||||
await UserAPI.updateUser(user.value)
|
||||
toast.success(`${action}成功`)
|
||||
}
|
||||
catch (e) {
|
||||
toast.error(getErrorMessage(e, '操作失败'))
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
router.back()
|
||||
}
|
||||
|
||||
onLoad((query) => {
|
||||
userId.value = Number(query?.id)
|
||||
loadOptions()
|
||||
loadUser()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="user-detail-page">
|
||||
<SkeletonPage v-if="loading" :rows="6" />
|
||||
|
||||
<template v-else>
|
||||
<!-- Nav -->
|
||||
<view class="detail-nav">
|
||||
<text class="detail-nav__back" @click="goBack">
|
||||
← 返回
|
||||
</text>
|
||||
<text class="detail-nav__title">
|
||||
用户详情
|
||||
</text>
|
||||
<view class="detail-nav__edit" @click="isEdit = !isEdit">
|
||||
<text class="detail-nav__edit-text">
|
||||
{{ isEdit ? '取消' : '编辑' }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Avatar -->
|
||||
<view class="detail-avatar-area">
|
||||
<view class="detail-avatar">
|
||||
<text class="detail-avatar__text">
|
||||
{{ (user.name || user.username || '?').charAt(0) }}
|
||||
</text>
|
||||
</view>
|
||||
<text class="detail-avatar__name">
|
||||
{{ user.name || user.username || '未知用户' }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- View mode -->
|
||||
<view v-if="!isEdit" class="detail-card">
|
||||
<view class="detail-row">
|
||||
<text class="detail-row__label">
|
||||
用户名
|
||||
</text>
|
||||
<text class="detail-row__value">
|
||||
{{ user.username || '-' }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="detail-divider" />
|
||||
<view class="detail-row">
|
||||
<text class="detail-row__label">
|
||||
邮箱
|
||||
</text>
|
||||
<text class="detail-row__value">
|
||||
{{ user.email || '-' }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="detail-divider" />
|
||||
<view class="detail-row">
|
||||
<text class="detail-row__label">
|
||||
手机号
|
||||
</text>
|
||||
<text class="detail-row__value">
|
||||
{{ user.mobile || '-' }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="detail-divider" />
|
||||
<view class="detail-row">
|
||||
<text class="detail-row__label">
|
||||
性别
|
||||
</text>
|
||||
<text class="detail-row__value">
|
||||
{{ genderLabel(user.gender) }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="detail-divider" />
|
||||
<view class="detail-row">
|
||||
<text class="detail-row__label">
|
||||
角色
|
||||
</text>
|
||||
<text class="detail-row__value" style="color: var(--primary-color);">
|
||||
{{ user.role_names?.join(', ') || roleOptions.find(r => user.role_ids?.includes(r.value))?.label || '-' }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="detail-divider" />
|
||||
<view class="detail-row">
|
||||
<text class="detail-row__label">
|
||||
部门
|
||||
</text>
|
||||
<text class="detail-row__value">
|
||||
{{ user.dept_name || deptOptions.find(d => d.value === user.dept_id)?.label || '-' }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="detail-divider" />
|
||||
<view class="detail-row">
|
||||
<text class="detail-row__label">
|
||||
状态
|
||||
</text>
|
||||
<view class="detail-status" :style="{ background: statusBadge(user.status).bg, color: statusBadge(user.status).color }">
|
||||
<text class="detail-status__text">
|
||||
{{ statusBadge(user.status).label }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Edit mode -->
|
||||
<view v-else class="detail-card">
|
||||
<view class="form-row">
|
||||
<text class="form-row__label">
|
||||
用户名
|
||||
</text>
|
||||
<wd-input v-model="user.username" placeholder="请输入用户名" />
|
||||
</view>
|
||||
<view class="detail-divider" />
|
||||
<view class="form-row">
|
||||
<text class="form-row__label">
|
||||
姓名
|
||||
</text>
|
||||
<wd-input v-model="user.name" placeholder="请输入姓名" />
|
||||
</view>
|
||||
<view class="detail-divider" />
|
||||
<view class="form-row">
|
||||
<text class="form-row__label">
|
||||
邮箱
|
||||
</text>
|
||||
<wd-input v-model="user.email" placeholder="请输入邮箱" />
|
||||
</view>
|
||||
<view class="detail-divider" />
|
||||
<view class="form-row">
|
||||
<text class="form-row__label">
|
||||
手机号
|
||||
</text>
|
||||
<wd-input v-model="user.mobile" placeholder="请输入手机号" />
|
||||
</view>
|
||||
<view class="detail-divider" />
|
||||
<view class="form-row">
|
||||
<text class="form-row__label">
|
||||
性别
|
||||
</text>
|
||||
<wd-radio-group v-model="user.gender">
|
||||
<wd-radio v-for="g in GENDER_OPTIONS" :key="g.value" :value="g.value">
|
||||
{{ g.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
<view class="detail-divider" />
|
||||
<view class="form-row">
|
||||
<text class="form-row__label">
|
||||
部门
|
||||
</text>
|
||||
<view style="flex:1" @click="showPickerDept = true">
|
||||
<wd-cell :value="deptOptions.find(o => o.value === user.dept_id)?.label || '选择部门'" is-link :border="false" />
|
||||
</view>
|
||||
<wd-picker :visible="showPickerDept" :columns="[deptOptions]" @confirm="handleDeptConfirm" @cancel="showPickerDept = false" />
|
||||
</view>
|
||||
<view class="detail-divider" />
|
||||
<view class="form-row">
|
||||
<text class="form-row__label">
|
||||
角色
|
||||
</text>
|
||||
<view style="flex:1" @click="showPickerRole = true">
|
||||
<wd-cell :value="roleOptions.find(o => o.value === user.role_ids?.[0])?.label || '选择角色'" is-link :border="false" />
|
||||
</view>
|
||||
<wd-picker :visible="showPickerRole" :columns="[roleOptions]" @confirm="handleRoleConfirm" @cancel="showPickerRole = false" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Actions -->
|
||||
<view v-if="isEdit" class="detail-actions">
|
||||
<view class="detail-btn detail-btn--primary" @click="handleSave">
|
||||
<text class="detail-btn__text">
|
||||
保存修改
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else class="detail-actions">
|
||||
<view class="detail-btn detail-btn--primary" @click="isEdit = true">
|
||||
<text class="detail-btn__text">
|
||||
编辑资料
|
||||
</text>
|
||||
</view>
|
||||
<view
|
||||
class="detail-btn detail-btn--danger"
|
||||
@click="handleToggleStatus"
|
||||
>
|
||||
<text class="detail-btn__text">
|
||||
{{ user.status === 1 ? '禁用账号' : '启用账号' }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.user-detail-page {
|
||||
min-height: 100vh;
|
||||
background: var(--page-bg-color, #F9F9F9);
|
||||
padding: var(--spacing-lg, 16px);
|
||||
padding-bottom: calc(var(--spacing-lg, 16px) + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.detail-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 44px;
|
||||
margin-bottom: var(--spacing-lg, 16px);
|
||||
|
||||
&__back {
|
||||
font-size: var(--font-md, 28rpx);
|
||||
color: var(--primary-color, #4F8CFF);
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: var(--font-xl, 36rpx);
|
||||
font-weight: 600;
|
||||
color: var(--text-color, #0A1628);
|
||||
}
|
||||
|
||||
&__edit {
|
||||
background: var(--primary-color, #4F8CFF);
|
||||
border-radius: var(--radius-full, 9999rpx);
|
||||
padding: 4px 14px;
|
||||
}
|
||||
|
||||
&__edit-text {
|
||||
font-size: var(--font-sm, 24rpx);
|
||||
color: #FFFFFF;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-avatar-area {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: var(--spacing-lg, 16px);
|
||||
}
|
||||
|
||||
.detail-avatar {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, var(--primary-color, #4F8CFF), var(--primary-color-dark, #2970FF));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&__text {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-size: var(--font-xl, 36rpx);
|
||||
font-weight: 600;
|
||||
color: var(--text-color, #0A1628);
|
||||
}
|
||||
}
|
||||
|
||||
.detail-card {
|
||||
background: var(--card-bg-color, #FFFFFF);
|
||||
border-radius: var(--radius-md, 16rpx);
|
||||
box-shadow: var(--shadow-sm, 0 1rpx 2rpx rgba(1, 77, 178,0.06));
|
||||
margin-bottom: var(--spacing-lg, 16px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 16px;
|
||||
|
||||
&__label {
|
||||
font-size: var(--font-md, 28rpx);
|
||||
color: var(--text-color-3, #6B7280);
|
||||
}
|
||||
|
||||
&__value {
|
||||
font-size: var(--font-md, 28rpx);
|
||||
color: var(--text-color, #0A1628);
|
||||
}
|
||||
}
|
||||
|
||||
.detail-divider {
|
||||
height: 1px;
|
||||
background: var(--border-color, #EAEAEA);
|
||||
margin: 0 16px;
|
||||
}
|
||||
|
||||
.detail-status {
|
||||
padding: 4px 12px;
|
||||
border-radius: var(--radius-full, 9999rpx);
|
||||
font-size: var(--font-xs, 20rpx);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 16px;
|
||||
gap: 12px;
|
||||
|
||||
&__label {
|
||||
font-size: var(--font-md, 28rpx);
|
||||
color: var(--text-color-3, #6B7280);
|
||||
flex-shrink: 0;
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
:deep(.wd-input) {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md, 12px);
|
||||
}
|
||||
|
||||
.detail-btn {
|
||||
height: 48px;
|
||||
border-radius: var(--radius-full, 9999rpx);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&--primary {
|
||||
background: linear-gradient(135deg, var(--primary-color, #4F8CFF), var(--primary-color-dark, #2970FF));
|
||||
box-shadow: 0 4px 12px rgba(1, 77, 178, 0.25);
|
||||
}
|
||||
|
||||
&--danger {
|
||||
background: var(--danger-color-light, #FEF2F2);
|
||||
}
|
||||
|
||||
&__text {
|
||||
font-size: var(--font-lg, 32rpx);
|
||||
font-weight: 600;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
&--danger &__text {
|
||||
color: var(--danger-color, #EF4444);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,196 @@
|
||||
<script setup lang="ts">
|
||||
import type { VersionForm, VersionItem } from '@/api/module_system/version'
|
||||
import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app'
|
||||
import { reactive, ref } from 'vue'
|
||||
import { VersionAPI } from '@/api/module_system/version'
|
||||
|
||||
definePage({ name: 'work-versions', style: { navigationBarTitleText: '版本管理' } })
|
||||
|
||||
const toast = useToast()
|
||||
const searchName = ref('')
|
||||
const showForm = ref(false)
|
||||
const formTitle = ref('')
|
||||
const currentId = ref<number>()
|
||||
const initForm: VersionForm = { version: '', title: '', date: '', content: '', description: '', sort: 0, status: 0 }
|
||||
const formData = reactive<VersionForm>({ ...initForm })
|
||||
|
||||
const STATUS_MAP: Record<number, string> = { 0: 'draft', 1: 'published', 2: 'deprecated' }
|
||||
const STATUS_LABELS: Record<number, string> = { 0: '草稿', 1: '已发布', 2: '已废弃' }
|
||||
const STATUS_OPTIONS = [{ value: 0, label: '草稿' }, { value: 1, label: '已发布' }, { value: 2, label: '已废弃' }]
|
||||
|
||||
function getStatus(status: number | string | undefined) {
|
||||
return STATUS_MAP[Number(status)] || 'draft'
|
||||
}
|
||||
function getStatusLabel(status: number | string | undefined) {
|
||||
return STATUS_LABELS[Number(status)] || '未知'
|
||||
}
|
||||
|
||||
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, '加载失败')),
|
||||
})
|
||||
|
||||
function onSearch() {
|
||||
toFirst()
|
||||
loadData()
|
||||
}
|
||||
function onReset() {
|
||||
searchName.value = ''
|
||||
toFirst()
|
||||
loadData()
|
||||
}
|
||||
function resetForm() {
|
||||
Object.assign(formData, { ...initForm })
|
||||
}
|
||||
function openCreate() {
|
||||
formTitle.value = '新增'
|
||||
currentId.value = undefined
|
||||
resetForm()
|
||||
showForm.value = true
|
||||
}
|
||||
async function openEdit(id: number) {
|
||||
formTitle.value = '编辑'
|
||||
currentId.value = id
|
||||
try {
|
||||
const detail = await VersionAPI.getDetail(id)
|
||||
Object.assign(formData, {
|
||||
version: detail.version || '',
|
||||
title: detail.title || '',
|
||||
date: detail.date || '',
|
||||
content: detail.content || '',
|
||||
description: detail.description || '',
|
||||
sort: detail.sort ?? 0,
|
||||
status: detail.status ?? 0,
|
||||
})
|
||||
showForm.value = true
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '获取详情失败')) }
|
||||
}
|
||||
async function handleSubmit() {
|
||||
loading.value = true
|
||||
try {
|
||||
if (currentId.value) {
|
||||
await VersionAPI.update(currentId.value, { ...formData })
|
||||
toast.success('更新成功')
|
||||
}
|
||||
else {
|
||||
await VersionAPI.create({ ...formData })
|
||||
toast.success('创建成功')
|
||||
}
|
||||
showForm.value = false
|
||||
loadData()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '操作失败')) }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
function handleDelete(id: number) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确认删除?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await VersionAPI.remove([id])
|
||||
toast.success('删除成功')
|
||||
loadData()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '删除失败')) }
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
onReachBottom(() => {
|
||||
if (!loading.value)
|
||||
loadNext()
|
||||
})
|
||||
onPullDownRefresh(() => {
|
||||
loadData()
|
||||
})
|
||||
onLoad(() => loadData())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="page-wraper">
|
||||
<view class="search-bar">
|
||||
<view class="flex items-center gap-sm">
|
||||
<u-input v-model="searchName" placeholder="搜索版本标题" clearable border="surround" class="flex-1" />
|
||||
<u-button size="mini" type="primary" :plain="true" text="搜索" @click="onSearch" />
|
||||
<u-button size="mini" :plain="true" text="重置" @click="onReset" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="action-bar">
|
||||
<text class="text-md font-bold text-muted">
|
||||
共 {{ total }} 条
|
||||
</text>
|
||||
<wd-button size="small" type="primary" @click="openCreate">+ 新增</wd-button>
|
||||
</view>
|
||||
<SkeletonPage v-if="loading" :rows="5" search />
|
||||
<template v-else>
|
||||
<view class="px-sm">
|
||||
<view class="admin-card">
|
||||
<ListEmpty v-if="!loading && list.length === 0" text="暂无数据" />
|
||||
<wd-cell-group v-else>
|
||||
<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">
|
||||
{{ item.title || '未命名' }}
|
||||
</text><text class="text-xs text-muted block mt-xs">
|
||||
v{{ item.version || '-' }}{{ item.date ? ` · ${item.date}` : '' }}
|
||||
</text>
|
||||
</view>
|
||||
</template>
|
||||
<template #label>
|
||||
<text class="text-xs text-muted">
|
||||
{{ getStatusLabel(item.status) }}
|
||||
</text>
|
||||
</template>
|
||||
<view class="flex items-center gap-xs">
|
||||
<StatusBadge :status="getStatus(item.status)" />
|
||||
<wd-icon name="delete" size="18px" color="var(--danger-color)" @click.stop="handleDelete(item.id!)" />
|
||||
</view>
|
||||
</wd-cell>
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
</view>
|
||||
<PaginationBar :current="pageParams.page_no" :page-size="pageParams.page_size" :total="total" @prev="loadPrev" @next="loadNext" />
|
||||
<wd-popup v-model="showForm" position="bottom" round custom-style="max-height: 80vh; overflow-y: auto;" @close="showForm = false">
|
||||
<view class="p-xl">
|
||||
<wd-navbar :title="formTitle" left-arrow @click-left="showForm = false" />
|
||||
<wd-form :model="formData" class="mt-lg">
|
||||
<wd-form-item label="版本号">
|
||||
<wd-input v-model="formData.version" placeholder="如 1.0.0" />
|
||||
</wd-form-item>
|
||||
<wd-form-item label="版本标题">
|
||||
<wd-input v-model="formData.title" placeholder="请输入" />
|
||||
</wd-form-item>
|
||||
<wd-form-item label="发布日期">
|
||||
<wd-input v-model="formData.date" placeholder="如 2024-01-01" />
|
||||
</wd-form-item>
|
||||
<wd-form-item label="排序">
|
||||
<wd-input v-model="formData.sort" type="number" placeholder="请输入" />
|
||||
</wd-form-item>
|
||||
<wd-form-item label="状态">
|
||||
<wd-radio-group v-model="formData.status">
|
||||
<wd-radio v-for="opt in STATUS_OPTIONS" :key="opt.value" :value="opt.value">
|
||||
{{ opt.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-form-item>
|
||||
<wd-form-item label="更新内容">
|
||||
<wd-textarea v-model="formData.content" placeholder="请输入" />
|
||||
</wd-form-item>
|
||||
<wd-form-item label="备注">
|
||||
<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>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
@@ -0,0 +1,457 @@
|
||||
<script setup lang="ts">
|
||||
import type { AIModelConfig, AIModelForm } from '@/api/module_ai/chat'
|
||||
import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app'
|
||||
import { reactive, ref } from 'vue'
|
||||
import { ChatAPI } from '@/api/module_ai/chat'
|
||||
import SkeletonPage from '@/components/SkeletonPage.vue'
|
||||
|
||||
definePage({
|
||||
name: 'work-ai-models',
|
||||
style: { navigationBarTitleText: 'AI 模型配置' },
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const loading = ref(false)
|
||||
const models = ref<AIModelConfig[]>([])
|
||||
|
||||
const showForm = ref(false)
|
||||
const formTitle = ref('新建模型配置')
|
||||
const editingId = ref<number | null>(null)
|
||||
const submitting = ref(false)
|
||||
const form = reactive<AIModelForm>({
|
||||
name: '',
|
||||
base_url: '',
|
||||
api_key: '',
|
||||
model_id: '',
|
||||
temperature: 0.7,
|
||||
})
|
||||
|
||||
async function loadModels() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await ChatAPI.getModels()
|
||||
models.value = res || []
|
||||
}
|
||||
catch (e) {
|
||||
toast.error(getErrorMessage(e, '加载模型列表失败'))
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
uni.stopPullDownRefresh()
|
||||
}
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
formTitle.value = '新建模型配置'
|
||||
editingId.value = null
|
||||
Object.assign(form, { name: '', base_url: '', api_key: '', model_id: '', temperature: 0.7 })
|
||||
showForm.value = true
|
||||
}
|
||||
|
||||
function openEdit(model: AIModelConfig) {
|
||||
formTitle.value = '编辑模型配置'
|
||||
editingId.value = model.config_id
|
||||
Object.assign(form, {
|
||||
name: model.name,
|
||||
base_url: model.base_url,
|
||||
api_key: model.api_key,
|
||||
model_id: model.model_id,
|
||||
temperature: model.temperature ?? 0.7,
|
||||
})
|
||||
showForm.value = true
|
||||
}
|
||||
|
||||
async function submitForm() {
|
||||
if (!form.name.trim())
|
||||
return toast.warning('请输入配置名称')
|
||||
if (!form.base_url.trim())
|
||||
return toast.warning('请输入 API 地址')
|
||||
if (!form.model_id.trim())
|
||||
return toast.warning('请输入模型 ID')
|
||||
submitting.value = true
|
||||
try {
|
||||
if (editingId.value) {
|
||||
await ChatAPI.updateModel(editingId.value, { ...form })
|
||||
toast.success('更新成功')
|
||||
}
|
||||
else {
|
||||
await ChatAPI.createModel({ ...form })
|
||||
toast.success('创建成功')
|
||||
}
|
||||
showForm.value = false
|
||||
loadModels()
|
||||
}
|
||||
catch (e) {
|
||||
toast.error(getErrorMessage(e, '保存失败'))
|
||||
}
|
||||
finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(configId: number) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确认删除此模型配置?',
|
||||
success: async (res) => {
|
||||
if (!res.confirm)
|
||||
return
|
||||
try {
|
||||
await ChatAPI.deleteModel(configId)
|
||||
toast.success('删除成功')
|
||||
loadModels()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '删除失败')) }
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function handleActivate(configId: number) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确认切换到此模型配置?',
|
||||
success: async (res) => {
|
||||
if (!res.confirm)
|
||||
return
|
||||
try {
|
||||
await ChatAPI.activateModel(configId)
|
||||
toast.success('已切换激活模型')
|
||||
loadModels()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '切换失败')) }
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
onPullDownRefresh(() => {
|
||||
loadModels()
|
||||
})
|
||||
onLoad(() => {
|
||||
loadModels()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="list-page">
|
||||
<view class="create-btn fade-in-up" @click="openCreate">
|
||||
<text class="create-btn__icon">
|
||||
+
|
||||
</text>
|
||||
<text class="create-btn__text">
|
||||
新建模型配置
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<SkeletonPage v-if="loading && models.length === 0" :rows="3" />
|
||||
<ListEmpty v-else-if="!loading && models.length === 0" text="暂无模型配置" />
|
||||
<view v-else class="models-list">
|
||||
<view
|
||||
v-for="(model, idx) in models"
|
||||
:key="model.config_id"
|
||||
class="model-item" :class="[`fade-in-up-${Math.min(idx % 5 + 1, 5)}`, model.is_active && 'model-item--active']"
|
||||
>
|
||||
<view class="model-item__header">
|
||||
<view class="model-item__info">
|
||||
<text class="model-item__name">
|
||||
{{ model.name || '未命名' }}
|
||||
</text>
|
||||
<view v-if="model.is_active" class="model-item__active-tag">
|
||||
<text class="model-item__active-text">
|
||||
● 使用中
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="model-item__model-id">
|
||||
{{ model.model_id }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="model-item__meta">
|
||||
<text class="model-item__url">
|
||||
{{ model.base_url }}
|
||||
</text>
|
||||
<text class="model-item__temp">
|
||||
温度: {{ model.temperature ?? '—' }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="model-item__actions">
|
||||
<view
|
||||
v-if="!model.is_active"
|
||||
class="model-action model-action--primary btn-press"
|
||||
@click="handleActivate(model.config_id)"
|
||||
>
|
||||
<text class="model-action__text">
|
||||
⚡ 激活
|
||||
</text>
|
||||
</view>
|
||||
<view class="model-action btn-press" @click="openEdit(model)">
|
||||
<text class="model-action__text">
|
||||
✎ 编辑
|
||||
</text>
|
||||
</view>
|
||||
<view class="model-action model-action--danger btn-press" @click="handleDelete(model.config_id)">
|
||||
<text class="model-action__text">
|
||||
✕ 删除
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view style="height: 120rpx;" />
|
||||
|
||||
<!-- Form Popup -->
|
||||
<u-popup
|
||||
:show="showForm"
|
||||
mode="bottom"
|
||||
custom-style="border-radius: 32rpx 32rpx 0 0; padding-bottom: 40rpx;"
|
||||
@close="showForm = false"
|
||||
>
|
||||
<view class="form-panel">
|
||||
<view class="form-panel__header">
|
||||
<text class="form-panel__title">
|
||||
{{ formTitle }}
|
||||
</text>
|
||||
<text class="form-panel__close" @click="showForm = false">
|
||||
✕
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="form-group">
|
||||
<text class="form-group__label">
|
||||
配置名称 *
|
||||
</text>
|
||||
<u-input v-model="form.name" placeholder="如:生产环境 GPT-4" clearable border="surround" />
|
||||
</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" />
|
||||
</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" />
|
||||
</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" />
|
||||
</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" />
|
||||
</view>
|
||||
|
||||
<view class="form-actions">
|
||||
<u-button :block="true" type="primary" :loading="submitting" :text="editingId ? '更新' : '创建'" @click="submitForm" />
|
||||
</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;
|
||||
}
|
||||
|
||||
/* ===== 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: 24rpx 0;
|
||||
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; }
|
||||
}
|
||||
|
||||
/* ===== Models list ===== */
|
||||
.models-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.model-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));
|
||||
border: 2rpx solid transparent;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&--active {
|
||||
border-color: var(--primary-color, #4F8CFF);
|
||||
box-shadow: 0 4rpx 16rpx rgba(1, 77, 178, 0.12);
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
&__info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: var(--text-color, #0A1628);
|
||||
}
|
||||
|
||||
&__active-tag {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
padding: 2rpx 10rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
&__active-text {
|
||||
font-size: 20rpx;
|
||||
color: #10B981;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&__model-id {
|
||||
font-size: 22rpx;
|
||||
color: var(--primary-color, #4F8CFF);
|
||||
background: var(--primary-color-light, rgba(1,77,178,0.08));
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
font-family: 'SF Mono', 'Consolas', monospace;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
&__url {
|
||||
font-size: 24rpx;
|
||||
color: var(--text-color-3, #6B7280);
|
||||
font-family: 'SF Mono', 'Consolas', monospace;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__temp {
|
||||
font-size: 22rpx;
|
||||
color: var(--text-color-4, #B0B0B0);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
padding-top: 12rpx;
|
||||
border-top: 1rpx solid var(--border-color, #F0F0F0);
|
||||
}
|
||||
}
|
||||
|
||||
.model-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 .model-action__text { color: var(--primary-color, #4F8CFF); }
|
||||
&--danger .model-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>
|
||||
@@ -0,0 +1,248 @@
|
||||
<script setup lang="ts">
|
||||
import type { ChatMessage, ChatSession } from '@/api/module_ai/chat'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
import { ChatAPI } from '@/api/module_ai/chat'
|
||||
|
||||
definePage({ name: 'work-chat', style: { navigationBarTitleText: 'AI 助手' } })
|
||||
|
||||
const toast = useToast()
|
||||
const sessions = ref<ChatSession[]>([])
|
||||
const currentSession = ref<number | null>(null)
|
||||
const currentTitle = ref('AI 助手')
|
||||
const messages = ref<ChatMessage[]>([])
|
||||
const inputText = ref('')
|
||||
const loading = ref(false)
|
||||
const showSessions = ref(false)
|
||||
|
||||
// ===== 消息窗口化渲染 =====
|
||||
// 长会话只渲染最近 MAX_VISIBLE_MESSAGES 条,避免消息过多时 DOM 节点与 setData 数据量膨胀导致卡顿;
|
||||
// 通过"加载更早消息"按步长扩大渲染窗口,保持浏览位置不跳变。
|
||||
const MAX_VISIBLE_MESSAGES = 60
|
||||
const INCREMENT = 40
|
||||
const messageWindow = ref(MAX_VISIBLE_MESSAGES)
|
||||
const scrollTarget = ref('')
|
||||
const visibleMessages = computed(() => messages.value.slice(-messageWindow.value))
|
||||
const hasOlderMessages = computed(() => messages.value.length > messageWindow.value)
|
||||
|
||||
function loadOlderMessages() {
|
||||
messageWindow.value += INCREMENT
|
||||
// 滚动到新增窗口边界,保持已读消息位置不跳变
|
||||
scrollTarget.value = `msg-${INCREMENT - 1}`
|
||||
nextTick(() => {
|
||||
scrollTarget.value = ''
|
||||
})
|
||||
}
|
||||
|
||||
function resetWindow() {
|
||||
messageWindow.value = MAX_VISIBLE_MESSAGES
|
||||
scrollTarget.value = ''
|
||||
}
|
||||
|
||||
async function loadSessions() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await ChatAPI.getSessions()
|
||||
sessions.value = res.list || []
|
||||
}
|
||||
catch (e) {
|
||||
sessions.value = []
|
||||
toast.error(getErrorMessage(e, '加载会话列表失败'))
|
||||
}
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
async function createSession() {
|
||||
try {
|
||||
const res = await ChatAPI.createSession('新对话')
|
||||
// 兼容后端可能返回 { data: ChatSession } 或直接 ChatSession 两种结构
|
||||
const newSession = (res as { data?: ChatSession })?.data ?? (res as ChatSession)
|
||||
if (newSession?.id) {
|
||||
sessions.value.unshift(newSession)
|
||||
selectSession(newSession.id, newSession.title)
|
||||
}
|
||||
toast.success('新会话已创建')
|
||||
}
|
||||
catch (e) {
|
||||
toast.error(getErrorMessage(e, '创建会话失败'))
|
||||
}
|
||||
}
|
||||
|
||||
async function selectSession(id: number, title?: string) {
|
||||
currentSession.value = id
|
||||
currentTitle.value = title || `会话 ${id}`
|
||||
showSessions.value = false
|
||||
loading.value = true
|
||||
try {
|
||||
const detail = await ChatAPI.getDetail(id)
|
||||
// 兼容后端可能返回 { messages: [] } 或 { data: { messages: [] } } 两种结构
|
||||
const detailRes = detail as { messages?: ChatMessage[], data?: { messages?: ChatMessage[] } }
|
||||
const msgs = detailRes?.messages || detailRes?.data?.messages || []
|
||||
resetWindow()
|
||||
messages.value = msgs.map(m => ({ role: m.role || 'ai', content: m.content || '', time: String(m.created_at || m.time || '') }))
|
||||
}
|
||||
catch (e) {
|
||||
messages.value = []
|
||||
toast.error(getErrorMessage(e, '加载会话消息失败'))
|
||||
}
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
async function deleteSession(id: number) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确认删除该会话?',
|
||||
success: async (r) => {
|
||||
if (r.confirm) {
|
||||
try {
|
||||
await ChatAPI.removeSession([id])
|
||||
sessions.value = sessions.value.filter(s => s.id !== id)
|
||||
if (currentSession.value === id) {
|
||||
currentSession.value = null
|
||||
currentTitle.value = 'AI 助手'
|
||||
messages.value = []
|
||||
}
|
||||
toast.success('已删除')
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '删除失败')) }
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function sendMessage() {
|
||||
if (!inputText.value.trim())
|
||||
return
|
||||
if (!currentSession.value) {
|
||||
toast.info('请先创建或选择会话')
|
||||
return
|
||||
}
|
||||
|
||||
const text = inputText.value.trim()
|
||||
messages.value.push({ role: 'user', content: text, time: new Date().toLocaleTimeString() })
|
||||
inputText.value = ''
|
||||
loading.value = true
|
||||
|
||||
// Scroll to bottom
|
||||
nextTick(() => {
|
||||
// scroll-view auto-scroll
|
||||
})
|
||||
|
||||
try {
|
||||
const res = await ChatAPI.sendMessage(currentSession.value, text)
|
||||
// 兼容后端可能返回 ChatMessage 或 { data: { content } } 或 { reply } 多种结构
|
||||
const replyRes = res as { content?: string, reply?: string, data?: { content?: string } }
|
||||
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() })
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onLoad(() => {
|
||||
loadSessions()
|
||||
messages.value = [{ role: 'ai', content: '你好!我是 FastapiAdmin AI 助手,有什么可以帮助你的?' }]
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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">
|
||||
{{ currentTitle }}
|
||||
</text>
|
||||
<u-button size="mini" type="primary" :plain="true" @click="createSession">
|
||||
<u-icon name="plus" size="16" />
|
||||
</u-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">
|
||||
会话列表
|
||||
</text>
|
||||
<u-button size="mini" type="primary" text="新建" @click="createSession" />
|
||||
</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)" />
|
||||
</template>
|
||||
</u-cell>
|
||||
</view>
|
||||
<ListEmpty v-if="!loading && sessions.length === 0" text="暂无会话,点击右上角新建" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Messages -->
|
||||
<scroll-view
|
||||
scroll-y
|
||||
class="chat-messages"
|
||||
style="flex:1;padding:var(--spacing-md,16rpx);"
|
||||
:scroll-into-view="scrollTarget || `msg-${visibleMessages.length - 1}`"
|
||||
:scroll-with-animation="true"
|
||||
>
|
||||
<!-- 窗口化渲染:仅渲染最近 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" />
|
||||
</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
|
||||
class="chat-bubble"
|
||||
:style="{
|
||||
background: msg.role === 'user' ? 'var(--primary-color)' : 'var(--card-bg-color)',
|
||||
color: msg.role === 'user' ? 'var(--text-color-inverse)' : 'var(--text-color)',
|
||||
borderRadius: msg.role === 'user' ? '16rpx 16rpx 4rpx 16rpx' : '16rpx 16rpx 16rpx 4rpx',
|
||||
border: msg.role === 'user' ? 'none' : '1px solid var(--border-color)',
|
||||
}"
|
||||
>
|
||||
<text style="white-space:pre-wrap;font-size:var(--font-sm,24rpx);line-height:1.7;">
|
||||
{{ msg.content }}
|
||||
</text>
|
||||
<text v-if="msg.time" style="font-size:var(--font-xs,20rpx);opacity:0.6;margin-top:8rpx;display:block;" :style="msg.role === 'user' ? 'text-align:right' : 'text-align:left'">
|
||||
{{ msg.time }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 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" />
|
||||
<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 />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 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" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.chat-bubble {
|
||||
padding: 16rpx 24rpx;
|
||||
max-width: 72%;
|
||||
word-break: break-word;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,230 @@
|
||||
<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="flex items-center gap-sm p-md" style="background:var(--card-bg-color);border-bottom:1px solid var(--border-color);">
|
||||
<u-button :type="activeTab === 'biztables' ? 'primary' : 'default'" size="mini" text="业务表" @click="activeTab = 'biztables'; loadBizTables()" />
|
||||
<u-button :type="activeTab === 'dbtables' ? 'primary' : 'default'" size="mini" text="数据库表" @click="activeTab = 'dbtables'; loadDbTables()" />
|
||||
</view>
|
||||
|
||||
<!-- Business Tables -->
|
||||
<template v-if="activeTab === 'biztables'">
|
||||
<view class="action-bar">
|
||||
<text class="text-md font-bold text-muted">
|
||||
共 {{ bizTotal }} 张表
|
||||
</text>
|
||||
<u-button size="mini" type="primary" text="导入表" @click="showDbPicker = true" />
|
||||
</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="暂无业务表,请从数据库导入" />
|
||||
<u-cell-group v-else>
|
||||
<u-cell v-for="item in bizTables" :key="item.id || item.table_name">
|
||||
<template #title>
|
||||
<view class="flex items-center gap-sm">
|
||||
<u-icon name="list" size="18" color="var(--primary-color)" />
|
||||
<view>
|
||||
<text class="font-medium text-md">
|
||||
{{ item.table_name || '-' }}
|
||||
</text>
|
||||
<text class="text-xs text-muted block">
|
||||
{{ item.table_comment || '' }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<template #right>
|
||||
<u-button size="mini" type="primary" :plain="true" text="生成" @click="batchGenerate([item.id])" />
|
||||
<u-button size="mini" :plain="true" text="预览" @click="previewCode(item.id)" />
|
||||
</template>
|
||||
</u-cell>
|
||||
</u-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 font-bold text-muted">
|
||||
数据库表列表
|
||||
</text>
|
||||
<u-button size="mini" type="primary" :loading="dbLoading" text="刷新" @click="loadDbTables" />
|
||||
</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="暂无数据库表" />
|
||||
<u-cell-group v-else>
|
||||
<u-cell v-for="item in dbTables" :key="item.table_name">
|
||||
<template #title>
|
||||
<view class="flex items-center gap-sm">
|
||||
<u-icon name="list" size="18" color="var(--success-color)" />
|
||||
<view>
|
||||
<text class="font-medium text-md font-mono">
|
||||
{{ item.table_name || item.name || '-' }}
|
||||
</text>
|
||||
<text class="text-xs text-muted block">
|
||||
{{ item.table_comment || item.comment || '' }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<template #right>
|
||||
<u-button size="mini" type="success" :plain="true" text="导入" @click="importTable(item.table_name || '')" />
|
||||
</template>
|
||||
</u-cell>
|
||||
</u-cell-group>
|
||||
</view>
|
||||
</view>
|
||||
<PaginationBar :current="dbPageParams.page_no" :page-size="dbPageParams.page_size" :total="dbTotal" @prev="dbLoadPrev" @next="dbLoadNext" />
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- Preview Popup -->
|
||||
<u-popup :show="showPreview" mode="bottom" custom-style="max-height:80vh;overflow-y:auto;" :round="10" @close="showPreview = false">
|
||||
<view class="p-xl">
|
||||
<u-navbar title="代码预览" left-icon="arrow-left" @left-click="showPreview = false" />
|
||||
<view class="flex items-center justify-between mt-lg mb-md">
|
||||
<text class="font-bold text-md">
|
||||
预览 ID: {{ previewId }}
|
||||
</text>
|
||||
<view class="flex gap-sm">
|
||||
<u-button size="mini" type="primary" text="生成下载" @click="batchGenerate([previewId])" />
|
||||
<u-button size="mini" type="success" text="同步数据库" @click="syncDb(bizTables.find(t => t.id === previewId)?.table_name || '')" />
|
||||
</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>
|
||||
</u-popup>
|
||||
|
||||
<!-- Import DB Table Picker -->
|
||||
<u-popup :show="showDbPicker" mode="bottom" custom-style="max-height:70vh;overflow-y:auto;" :round="10" @close="showDbPicker = false">
|
||||
<view class="p-xl">
|
||||
<u-navbar title="选择数据库表" left-icon="arrow-left" @left-click="showDbPicker = false" />
|
||||
<view class="mt-lg">
|
||||
<u-cell-group :border="true">
|
||||
<u-cell v-for="item in dbTables" :key="item.table_name" :title="item.table_name || item.name" :label="item.table_comment || item.comment || ''" center>
|
||||
<template #right>
|
||||
<u-button size="mini" type="success" :plain="true" text="导入" @click="importTable(item.table_name || '')" />
|
||||
</template>
|
||||
</u-cell>
|
||||
</u-cell-group>
|
||||
<view v-if="dbTables.length === 0" class="empty-state mt-xl">
|
||||
<text class="empty-state__text">
|
||||
点击"数据库表"Tab 刷新表列表
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</u-popup>
|
||||
</view>
|
||||
</template>
|
||||
@@ -0,0 +1,524 @@
|
||||
<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>
|
||||
@@ -0,0 +1,94 @@
|
||||
<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>
|
||||
@@ -0,0 +1,986 @@
|
||||
<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>
|
||||
@@ -0,0 +1,373 @@
|
||||
<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>
|
||||
@@ -0,0 +1,229 @@
|
||||
<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>
|
||||
@@ -0,0 +1,103 @@
|
||||
<script setup lang="ts">
|
||||
import type { LoginLogItem } from '@/api/module_system/log'
|
||||
import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app'
|
||||
import { ref } from 'vue'
|
||||
import { LoginLogAPI } from '@/api/module_system/log'
|
||||
|
||||
definePage({ name: 'work-loginlogs', style: { navigationBarTitleText: '登录日志' } })
|
||||
|
||||
const toast = useToast()
|
||||
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, '加载失败')),
|
||||
})
|
||||
|
||||
function onSearch() {
|
||||
toFirst()
|
||||
loadData()
|
||||
}
|
||||
function onReset() {
|
||||
searchName.value = ''
|
||||
toFirst()
|
||||
loadData()
|
||||
}
|
||||
function handleDelete(id: number) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确认删除?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await LoginLogAPI.remove([id])
|
||||
toast.success('删除成功')
|
||||
loadData()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '删除失败')) }
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
onReachBottom(() => {
|
||||
if (!loading.value)
|
||||
loadNext()
|
||||
})
|
||||
onPullDownRefresh(() => {
|
||||
loadData()
|
||||
})
|
||||
|
||||
onLoad(() => loadData())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="page-wraper">
|
||||
<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>
|
||||
</view>
|
||||
</view>
|
||||
<view class="action-bar">
|
||||
<text class="text-md font-bold text-muted">
|
||||
共 {{ total }} 条
|
||||
</text>
|
||||
</view>
|
||||
<SkeletonPage v-if="loading" :rows="5" search />
|
||||
<template v-else>
|
||||
<view class="px-sm">
|
||||
<view class="admin-card">
|
||||
<ListEmpty v-if="!loading && list.length === 0" text="暂无登录日志" />
|
||||
<wd-cell-group v-else>
|
||||
<wd-cell v-for="item in list" :key="item.id">
|
||||
<template #title>
|
||||
<view>
|
||||
<text class="font-medium text-md">
|
||||
{{ item.username || '-' }}
|
||||
</text>
|
||||
<text class="text-xs text-muted block mt-xs">
|
||||
IP: {{ item.login_ip || '-' }} · {{ item.login_location || '-' }}
|
||||
</text>
|
||||
</view>
|
||||
</template>
|
||||
<template #label>
|
||||
<text class="text-xs text-muted">
|
||||
{{ item.request_browser || '' }}{{ item.request_browser && item.request_os ? ' / ' : '' }}{{ item.request_os || '' }}{{ item.msg ? ` · ${item.msg}` : '' }}
|
||||
</text>
|
||||
</template>
|
||||
<template #default>
|
||||
<view class="flex items-center gap-sm">
|
||||
<StatusBadge :status="String(item.status) === '1' ? 'success' : 'failed'" />
|
||||
<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>
|
||||
</view>
|
||||
</template>
|
||||
@@ -0,0 +1,81 @@
|
||||
<script setup lang="ts">
|
||||
import type { OnlineUserItem } from '@/api/module_monitor/online'
|
||||
import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app'
|
||||
import { OnlineAPI } from '@/api/module_monitor/online'
|
||||
|
||||
definePage({ name: 'work-online', style: { navigationBarTitleText: '在线用户' } })
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
const { list, total, loading, pageParams, loadData, loadPrev, loadNext } = useListPage<OnlineUserItem>({
|
||||
fetcher: p => OnlineAPI.getPage(p),
|
||||
onError: e => toast.error(getErrorMessage(e, '加载失败')),
|
||||
})
|
||||
|
||||
function forceLogout(sessionId: string) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确认强制下线该用户?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await OnlineAPI.forceLogout(sessionId)
|
||||
toast.success('已强制下线')
|
||||
loadData()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '操作失败')) }
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
onReachBottom(() => {
|
||||
if (!loading.value)
|
||||
loadNext()
|
||||
})
|
||||
onPullDownRefresh(() => {
|
||||
loadData()
|
||||
})
|
||||
onLoad(() => loadData())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="page-wraper">
|
||||
<view class="action-bar">
|
||||
<text class="text-md font-bold text-muted">
|
||||
共 {{ total }} 在线
|
||||
</text>
|
||||
</view>
|
||||
<view class="px-sm">
|
||||
<view class="admin-card">
|
||||
<ListEmpty v-if="!loading && list.length === 0" text="暂无在线用户" />
|
||||
<wd-cell-group v-else>
|
||||
<wd-cell v-for="item in list" :key="item.session_id || item.user_id">
|
||||
<template #title>
|
||||
<view>
|
||||
<text class="font-medium text-md">
|
||||
{{ item.user_name || item.name || '-' }}
|
||||
</text>
|
||||
<text class="text-xs text-muted block mt-xs">
|
||||
IP: {{ item.ipaddr || '-' }} · {{ item.login_location || '' }}
|
||||
</text>
|
||||
</view>
|
||||
</template>
|
||||
<template #label>
|
||||
<text class="text-xs text-muted">
|
||||
{{ 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>
|
||||
</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" />
|
||||
</view>
|
||||
</template>
|
||||
@@ -0,0 +1,131 @@
|
||||
<script setup lang="ts">
|
||||
import type { OplogItem } from '@/api/module_system/log'
|
||||
import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app'
|
||||
import { ref } from 'vue'
|
||||
import { OperationLogAPI } from '@/api/module_system/log'
|
||||
|
||||
definePage({ name: 'work-oplogs', style: { navigationBarTitleText: '操作日志' } })
|
||||
|
||||
const toast = useToast()
|
||||
const searchPath = ref('')
|
||||
const showDetail = ref(false)
|
||||
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, '加载失败')),
|
||||
})
|
||||
|
||||
function onSearch() {
|
||||
toFirst()
|
||||
loadData()
|
||||
}
|
||||
function onReset() {
|
||||
searchPath.value = ''
|
||||
toFirst()
|
||||
loadData()
|
||||
}
|
||||
function viewDetail(item: OplogItem) {
|
||||
detailItem.value = item
|
||||
showDetail.value = true
|
||||
}
|
||||
function handleDelete(id: number) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确认删除?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await OperationLogAPI.remove([id])
|
||||
toast.success('删除成功')
|
||||
loadData()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '删除失败')) }
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
onReachBottom(() => {
|
||||
if (!loading.value)
|
||||
loadNext()
|
||||
})
|
||||
onPullDownRefresh(() => {
|
||||
loadData()
|
||||
})
|
||||
|
||||
onLoad(() => loadData())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="page-wraper">
|
||||
<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>
|
||||
</view>
|
||||
</view>
|
||||
<view class="action-bar">
|
||||
<text class="text-md font-bold text-muted">
|
||||
共 {{ total }} 条
|
||||
</text>
|
||||
</view>
|
||||
<SkeletonPage v-if="loading" :rows="5" search />
|
||||
<template v-else>
|
||||
<view class="px-sm">
|
||||
<view class="admin-card">
|
||||
<ListEmpty v-if="!loading && list.length === 0" text="暂无日志" />
|
||||
<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">
|
||||
<StatusBadge :status="(item.response_code || 0) < 300 ? 'success' : 'failed'" dot />
|
||||
<text class="font-mono text-sm font-medium">
|
||||
{{ item.request_method }}
|
||||
</text>
|
||||
<text class="text-sm text-muted truncate" style="max-width: 300rpx;">
|
||||
{{ item.request_path }}
|
||||
</text>
|
||||
</view>
|
||||
<text class="text-xs text-muted block">
|
||||
{{ item.description || '-' }} · {{ item.request_ip }} · {{ item.process_time }}
|
||||
</text>
|
||||
</template>
|
||||
<template #default>
|
||||
<wd-icon name="delete" size="18px" color="var(--danger-color)" @click.stop="handleDelete(item.id!)" />
|
||||
</template>
|
||||
</wd-cell>
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
</view>
|
||||
<PaginationBar :current="pageParams.page_no" :page-size="pageParams.page_size" :total="total" @prev="loadPrev" @next="loadNext" />
|
||||
<wd-popup v-model="showDetail" position="bottom" round closable @close="showDetail = false">
|
||||
<view v-if="detailItem" class="p-xl">
|
||||
<wd-navbar title="日志详情" left-arrow @click-left="showDetail = false" />
|
||||
<view class="admin-card mt-lg">
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="操作人" :value="detailItem.username || '-'" />
|
||||
<wd-cell title="请求路径" :value="detailItem.request_path || '-'" />
|
||||
<wd-cell title="请求方法" :value="detailItem.request_method || '-'" />
|
||||
<wd-cell title="请求 IP" :value="detailItem.request_ip || '-'" />
|
||||
<wd-cell title="状态码">
|
||||
<StatusBadge :status="(detailItem.response_code || 0) < 300 ? 'success' : 'failed'" />
|
||||
</wd-cell>
|
||||
<wd-cell title="响应时间" :value="detailItem.process_time || '-'" />
|
||||
<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">
|
||||
请求参数:{{ detailItem.request_payload || '无' }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="mt-lg">
|
||||
<wd-button block @click="showDetail = false">关闭</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
@@ -0,0 +1,94 @@
|
||||
<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>
|
||||
@@ -0,0 +1,155 @@
|
||||
<script setup lang="ts">
|
||||
import type { DeptForm, DeptItem } from '@/api/module_system/dept'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { reactive, ref } from 'vue'
|
||||
import { DeptAPI } from '@/api/module_system/dept'
|
||||
|
||||
definePage({ name: 'work-depts', style: { navigationBarTitleText: '部门管理' } })
|
||||
|
||||
const toast = useToast()
|
||||
const loading = ref(false)
|
||||
const treeList = ref<DeptItem[]>([])
|
||||
const flatList = ref<(DeptItem & { _depth: number })[]>([])
|
||||
const showForm = ref(false)
|
||||
const formTitle = ref('')
|
||||
const currentId = ref<number>()
|
||||
const formData = reactive<DeptForm>({ name: '', code: '', order: 1, parent_id: undefined, leader: '', phone: '', email: '', status: 0, description: '' })
|
||||
|
||||
function flatten(items: DeptItem[], depth = 0): (DeptItem & { _depth: number })[] {
|
||||
let r: (DeptItem & { _depth: number })[] = []
|
||||
for (const item of items) {
|
||||
r.push({ ...item, _depth: depth })
|
||||
if (item.children?.length)
|
||||
r = r.concat(flatten(item.children, depth + 1))
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
async function loadTree() {
|
||||
loading.value = true
|
||||
try {
|
||||
treeList.value = await DeptAPI.getTree() || []
|
||||
flatList.value = flatten(treeList.value)
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '加载失败')) }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
function resetForm() {
|
||||
formData.name = ''
|
||||
formData.code = ''
|
||||
formData.order = 1
|
||||
formData.parent_id = undefined
|
||||
formData.leader = ''
|
||||
formData.phone = ''
|
||||
formData.email = ''
|
||||
formData.status = 0
|
||||
formData.description = ''
|
||||
}
|
||||
function openCreate(parentId?: number) {
|
||||
formTitle.value = '新增部门'
|
||||
currentId.value = undefined
|
||||
resetForm()
|
||||
formData.parent_id = parentId
|
||||
showForm.value = true
|
||||
}
|
||||
async function handleSubmit() {
|
||||
loading.value = true
|
||||
try {
|
||||
if (currentId.value) {
|
||||
await DeptAPI.update(currentId.value, formData)
|
||||
toast.success('更新成功')
|
||||
}
|
||||
else {
|
||||
await DeptAPI.create(formData)
|
||||
toast.success('创建成功')
|
||||
}
|
||||
showForm.value = false
|
||||
loadTree()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '操作失败')) }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
function handleDelete(id: number) {
|
||||
uni.showModal({ title: '提示', content: '确认删除该部门?', success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await DeptAPI.remove([id])
|
||||
toast.success('删除成功')
|
||||
loadTree()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '删除失败')) }
|
||||
}
|
||||
} })
|
||||
}
|
||||
onLoad(() => loadTree())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="page-wraper">
|
||||
<view class="action-bar">
|
||||
<text class="font-bold text-lg">
|
||||
部门列表
|
||||
</text>
|
||||
<u-button size="mini" type="primary" text="+ 新增" @click="openCreate()" />
|
||||
</view>
|
||||
<view class="px-sm">
|
||||
<view class="admin-card">
|
||||
<SkeletonPage v-if="loading" />
|
||||
<ListEmpty v-else-if="flatList.length === 0" text="暂无部门" />
|
||||
<u-cell-group v-else>
|
||||
<u-cell v-for="item in flatList" :key="item.id">
|
||||
<template #title>
|
||||
<view class="flex items-center gap-xs" :style="{ marginLeft: `${(item._depth || 0) * 36}rpx` }">
|
||||
<text v-if="item._depth > 0" class="text-muted" style="font-size: 20rpx;">
|
||||
└─
|
||||
</text>
|
||||
<StatusBadge :status="item.status" dot />
|
||||
<text class="font-medium text-md">
|
||||
{{ item.name }}
|
||||
</text>
|
||||
<text class="text-xs text-muted">
|
||||
[{{ item.code }}]
|
||||
</text>
|
||||
</view>
|
||||
</template>
|
||||
<template #right>
|
||||
<u-icon name="plus" size="18" color="var(--success-color)" @click.stop="openCreate(item.id)" />
|
||||
<u-icon name="trash" size="18" color="var(--danger-color)" @click.stop="handleDelete(item.id)" />
|
||||
</template>
|
||||
</u-cell>
|
||||
</u-cell-group>
|
||||
</view>
|
||||
</view>
|
||||
<u-popup :show="showForm" mode="bottom" :round="10" custom-style="max-height: 80vh; overflow-y: auto;" @close="showForm = false">
|
||||
<view class="p-xl">
|
||||
<u-navbar :title="formTitle" left-icon="arrow-left" @left-click="showForm = false" />
|
||||
<u-form :model="formData" class="mt-lg">
|
||||
<u-form-item label="部门名称" prop="name" :border-bottom="true">
|
||||
<u-input v-model="formData.name" border="none" placeholder="请输入" />
|
||||
</u-form-item>
|
||||
<u-form-item label="部门编码" prop="code" :border-bottom="true">
|
||||
<u-input v-model="formData.code" border="none" placeholder="请输入" />
|
||||
</u-form-item>
|
||||
<u-form-item label="排序" :border-bottom="true">
|
||||
<u-input v-model="formData.order" border="none" type="number" placeholder="请输入" />
|
||||
</u-form-item>
|
||||
<u-form-item label="负责人" :border-bottom="true">
|
||||
<u-input v-model="formData.leader" border="none" placeholder="请输入" />
|
||||
</u-form-item>
|
||||
<u-form-item label="联系电话" :border-bottom="true">
|
||||
<u-input v-model="formData.phone" border="none" placeholder="请输入" />
|
||||
</u-form-item>
|
||||
<u-form-item label="邮箱" :border-bottom="true">
|
||||
<u-input v-model="formData.email" border="none" placeholder="请输入" />
|
||||
</u-form-item>
|
||||
<u-form-item label="备注" :border-bottom="true">
|
||||
<u-textarea v-model="formData.description" border="none" placeholder="请输入" />
|
||||
</u-form-item>
|
||||
</u-form>
|
||||
<view class="flex gap-md mt-xl">
|
||||
<u-button :block="true" :plain="true" text="取消" @click="showForm = false" /><u-button :block="true" type="primary" :loading="loading" text="保存" @click="handleSubmit" />
|
||||
</view>
|
||||
</view>
|
||||
</u-popup>
|
||||
</view>
|
||||
</template>
|
||||
@@ -0,0 +1,156 @@
|
||||
<script setup lang="ts">
|
||||
import type { MenuForm, MenuItem } from '@/api/module_system/menu'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { reactive, ref } from 'vue'
|
||||
import { MenuAPI } from '@/api/module_system/menu'
|
||||
|
||||
definePage({ name: 'work-menus', style: { navigationBarTitleText: '菜单管理' } })
|
||||
|
||||
const toast = useToast()
|
||||
const loading = ref(false)
|
||||
const treeList = ref<MenuItem[]>([])
|
||||
const flatList = ref<(MenuItem & { _depth: number })[]>([])
|
||||
const showForm = ref(false)
|
||||
const formTitle = ref('')
|
||||
const currentId = ref<number>()
|
||||
const formData = reactive<MenuForm>({ name: '', type: 2, order: 1, parent_id: undefined, permission: '', icon: '', route_name: '', route_path: '', component_path: '', redirect: '', hidden: false, keep_alive: true, always_show: false, title: '', affix: false, status: 0, description: '', client: 'pc' })
|
||||
|
||||
const typeLabels: Record<number, string> = { 1: '目录', 2: '菜单', 3: '按钮', 4: '外链' }
|
||||
const typeColors: Record<number, string> = { 1: 'primary', 2: 'enabled', 3: 'draft', 4: 'failed' }
|
||||
|
||||
function flatten(items: MenuItem[], depth = 0): (MenuItem & { _depth: number })[] {
|
||||
let r: (MenuItem & { _depth: number })[] = []
|
||||
for (const item of items) {
|
||||
r.push({ ...item, _depth: depth })
|
||||
if (item.children?.length)
|
||||
r = r.concat(flatten(item.children, depth + 1))
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
async function loadTree() {
|
||||
loading.value = true
|
||||
try {
|
||||
treeList.value = await MenuAPI.getTree() || []
|
||||
flatList.value = flatten(treeList.value)
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '加载失败')) }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
function resetForm() {
|
||||
Object.assign(formData, { name: '', type: 2, order: 1, parent_id: undefined, permission: '', icon: '', route_name: '', route_path: '', component_path: '', redirect: '', hidden: false, keep_alive: true, always_show: false, title: '', affix: false, status: 0, description: '', client: 'pc' })
|
||||
}
|
||||
function openCreate(parentId?: number) {
|
||||
formTitle.value = '新增菜单'
|
||||
currentId.value = undefined
|
||||
resetForm()
|
||||
formData.parent_id = parentId
|
||||
showForm.value = true
|
||||
}
|
||||
async function handleSubmit() {
|
||||
loading.value = true
|
||||
try {
|
||||
if (currentId.value) {
|
||||
await MenuAPI.update(currentId.value, formData)
|
||||
toast.success('更新成功')
|
||||
}
|
||||
else {
|
||||
await MenuAPI.create(formData)
|
||||
toast.success('创建成功')
|
||||
}
|
||||
showForm.value = false
|
||||
loadTree()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '操作失败')) }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
function handleDelete(id: number) {
|
||||
uni.showModal({ title: '提示', content: '确认删除该菜单?', success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await MenuAPI.remove([id])
|
||||
toast.success('删除成功')
|
||||
loadTree()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '删除失败')) }
|
||||
}
|
||||
} })
|
||||
}
|
||||
onLoad(() => loadTree())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="page-wraper">
|
||||
<view class="action-bar">
|
||||
<text class="font-bold text-lg">
|
||||
菜单列表
|
||||
</text>
|
||||
<u-button size="mini" type="primary" text="+ 新增" @click="openCreate()" />
|
||||
</view>
|
||||
<view class="px-sm">
|
||||
<view class="admin-card">
|
||||
<SkeletonPage v-if="loading" />
|
||||
<ListEmpty v-else-if="flatList.length === 0" text="暂无菜单" />
|
||||
<u-cell-group v-else>
|
||||
<u-cell v-for="item in flatList" :key="item.id">
|
||||
<template #title>
|
||||
<view class="flex items-center gap-xs" :style="{ marginLeft: `${(item._depth || 0) * 36}rpx` }">
|
||||
<text v-if="item._depth > 0" class="text-muted" style="font-size: 20rpx;">
|
||||
└─
|
||||
</text>
|
||||
<text v-if="item.icon" class="text-primary" style="font-size: 24rpx;">
|
||||
{{ item.icon }}
|
||||
</text>
|
||||
<text class="font-medium text-md">
|
||||
{{ item.name }}
|
||||
</text>
|
||||
<span class="status-badge" :class="`status-badge--${typeColors[item.type] || 'disabled'}`" style="font-size: 18rpx; padding: 0 10rpx;">
|
||||
{{ typeLabels[item.type] || '未知' }}
|
||||
</span>
|
||||
</view>
|
||||
</template>
|
||||
<template #right-icon>
|
||||
<StatusBadge :status="item.status" />
|
||||
<u-icon name="plus" size="18" color="var(--success-color)" @click.stop="openCreate(item.id)" />
|
||||
<u-icon name="trash" size="18" color="var(--danger-color)" @click.stop="handleDelete(item.id)" />
|
||||
</template>
|
||||
</u-cell>
|
||||
</u-cell-group>
|
||||
</view>
|
||||
</view>
|
||||
<u-popup :show="showForm" mode="bottom" :round="10" custom-style="max-height: 80vh; overflow-y: auto;" @close="showForm = false">
|
||||
<view class="p-xl">
|
||||
<u-navbar :title="formTitle" left-icon="arrow-left" @left-click="showForm = false" />
|
||||
<u-form :model="formData" class="mt-lg">
|
||||
<u-form-item label="菜单名称">
|
||||
<u-input v-model="formData.name" placeholder="请输入" border="surround" />
|
||||
</u-form-item>
|
||||
<u-form-item label="类型(1-4)">
|
||||
<u-input v-model="formData.type" type="number" placeholder="目录/菜单/按钮/外链" border="surround" />
|
||||
</u-form-item>
|
||||
<u-form-item label="排序">
|
||||
<u-input v-model="formData.order" type="number" placeholder="请输入" border="surround" />
|
||||
</u-form-item>
|
||||
<u-form-item label="权限标识">
|
||||
<u-input v-model="formData.permission" placeholder="如 sys:user:query" border="surround" />
|
||||
</u-form-item>
|
||||
<u-form-item label="路由地址">
|
||||
<u-input v-model="formData.route_path" placeholder="如 /system/user" border="surround" />
|
||||
</u-form-item>
|
||||
<u-form-item label="组件路径">
|
||||
<u-input v-model="formData.component_path" placeholder="system/user/index" border="surround" />
|
||||
</u-form-item>
|
||||
<u-form-item label="图标">
|
||||
<u-input v-model="formData.icon" placeholder="图标名称" border="surround" />
|
||||
</u-form-item>
|
||||
<u-form-item label="备注">
|
||||
<u-textarea v-model="formData.description" placeholder="请输入" />
|
||||
</u-form-item>
|
||||
</u-form>
|
||||
<view class="flex gap-md mt-xl">
|
||||
<u-button :block="true" :plain="true" text="取消" @click="showForm = false" /><u-button :block="true" type="primary" :loading="loading" text="保存" @click="handleSubmit" />
|
||||
</view>
|
||||
</view>
|
||||
</u-popup>
|
||||
</view>
|
||||
</template>
|
||||
@@ -0,0 +1,161 @@
|
||||
<script setup lang="ts">
|
||||
import type { PositionForm, PositionItem } from '@/api/module_system/position'
|
||||
import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app'
|
||||
import { reactive, ref } from 'vue'
|
||||
import { PositionAPI } from '@/api/module_system/position'
|
||||
|
||||
definePage({ name: 'work-positions', style: { navigationBarTitleText: '岗位管理' } })
|
||||
|
||||
const toast = useToast()
|
||||
const searchName = ref('')
|
||||
const showForm = ref(false)
|
||||
const formTitle = ref('')
|
||||
const currentId = ref<number>()
|
||||
const formData = reactive<PositionForm>({ name: '', code: '', order: 1, status: 0, description: '' })
|
||||
|
||||
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() {
|
||||
toFirst()
|
||||
loadData()
|
||||
}
|
||||
function onReset() {
|
||||
searchName.value = ''
|
||||
toFirst()
|
||||
loadData()
|
||||
}
|
||||
function resetForm() {
|
||||
Object.assign(formData, { name: '', code: '', order: 1, status: 0, description: '' })
|
||||
}
|
||||
function openCreate() {
|
||||
formTitle.value = '新增'
|
||||
currentId.value = undefined
|
||||
resetForm()
|
||||
showForm.value = true
|
||||
}
|
||||
async function openEdit(id: number) {
|
||||
formTitle.value = '编辑'
|
||||
currentId.value = id
|
||||
try {
|
||||
const detail = await PositionAPI.getDetail(id)
|
||||
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, '获取详情失败')) }
|
||||
}
|
||||
async function handleSubmit() {
|
||||
loading.value = true
|
||||
try {
|
||||
if (currentId.value) {
|
||||
await PositionAPI.update(currentId.value, formData)
|
||||
toast.success('更新成功')
|
||||
}
|
||||
else {
|
||||
await PositionAPI.create(formData)
|
||||
toast.success('创建成功')
|
||||
}
|
||||
showForm.value = false
|
||||
loadData()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '操作失败')) }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
function handleDelete(id: number) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确认删除?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await PositionAPI.remove([id])
|
||||
toast.success('删除成功')
|
||||
loadData()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '删除失败')) }
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
onReachBottom(() => {
|
||||
if (!loading.value)
|
||||
loadNext()
|
||||
})
|
||||
onPullDownRefresh(() => {
|
||||
loadData()
|
||||
})
|
||||
|
||||
onLoad(() => loadData())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="page-wraper">
|
||||
<view class="search-bar">
|
||||
<view class="flex items-center gap-sm">
|
||||
<u-input v-model="searchName" placeholder="搜索名称" clearable border="surround" class="flex-1" />
|
||||
<u-button size="mini" type="primary" :plain="true" text="搜索" @click="onSearch" />
|
||||
<u-button size="mini" :plain="true" text="重置" @click="onReset" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="action-bar">
|
||||
<text class="text-md font-bold text-muted">
|
||||
共 {{ total }} 条
|
||||
</text>
|
||||
<u-button size="mini" type="primary" text="+ 新增" @click="openCreate" />
|
||||
</view>
|
||||
<SkeletonPage v-if="loading" :rows="5" search />
|
||||
<template v-else>
|
||||
<view class="px-sm">
|
||||
<view class="admin-card">
|
||||
<ListEmpty v-if="!loading && list.length === 0" text="暂无数据" />
|
||||
<u-cell-group v-else>
|
||||
<u-cell v-for="item in list" :key="item.id" :is-link="true" @click="openEdit(item.id!)">
|
||||
<template #title>
|
||||
<view>
|
||||
<text class="font-medium text-md">
|
||||
{{ item.name }}
|
||||
</text><text class="text-xs text-muted block mt-xs">
|
||||
{{ `编码: ${item.code || '-'}` }}
|
||||
</text>
|
||||
</view>
|
||||
</template>
|
||||
<template #value>
|
||||
<StatusBadge :status="item.status" />
|
||||
</template>
|
||||
<template #right>
|
||||
<u-icon name="trash" size="18" color="var(--danger-color)" @click.stop="handleDelete(item.id!)" />
|
||||
</template>
|
||||
</u-cell>
|
||||
</u-cell-group>
|
||||
</view>
|
||||
</view>
|
||||
<PaginationBar :current="pageParams.page_no" :page-size="pageParams.page_size" :total="total" @prev="loadPrev" @next="loadNext" />
|
||||
<u-popup :show="showForm" mode="bottom" :round="10" custom-style="max-height: 80vh; overflow-y: auto;" @close="showForm = false">
|
||||
<view class="p-xl">
|
||||
<u-navbar :title="formTitle" left-icon="arrow-left" @left-click="showForm = false" />
|
||||
<u-form :model="formData" class="mt-lg">
|
||||
<u-form-item label="岗位名称" prop="name" :border-bottom="true">
|
||||
<u-input v-model="formData.name" border="none" placeholder="请输入" />
|
||||
</u-form-item>
|
||||
<u-form-item label="岗位编码" prop="code" :border-bottom="true">
|
||||
<u-input v-model="formData.code" border="none" placeholder="请输入" />
|
||||
</u-form-item>
|
||||
<u-form-item label="排序" :border-bottom="true">
|
||||
<u-input v-model="formData.order" border="none" type="number" placeholder="请输入" />
|
||||
</u-form-item>
|
||||
<u-form-item label="备注" :border-bottom="true">
|
||||
<u-textarea v-model="formData.description" border="none" placeholder="请输入" />
|
||||
</u-form-item>
|
||||
</u-form>
|
||||
<view class="flex gap-md mt-xl">
|
||||
<u-button :block="true" :plain="true" text="取消" @click="showForm = false" />
|
||||
<u-button :block="true" type="primary" :loading="loading" text="保存" @click="handleSubmit" />
|
||||
</view>
|
||||
</view>
|
||||
</u-popup>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
@@ -0,0 +1,166 @@
|
||||
<script setup lang="ts">
|
||||
import type { RoleForm, RoleItem } from '@/api/module_system/role'
|
||||
import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app'
|
||||
import { reactive, ref } from 'vue'
|
||||
import { RoleAPI } from '@/api/module_system/role'
|
||||
|
||||
definePage({ name: 'work-roles', style: { navigationBarTitleText: '角色管理' } })
|
||||
|
||||
const toast = useToast()
|
||||
const searchName = ref('')
|
||||
const showForm = ref(false)
|
||||
const formTitle = ref('')
|
||||
const currentId = ref<number>()
|
||||
const formRef = ref()
|
||||
const formData = reactive<RoleForm>({ name: '', code: '', order: 1, data_scope: 4, status: 0, description: '' })
|
||||
|
||||
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, '加载失败')),
|
||||
})
|
||||
|
||||
function onSearch() {
|
||||
toFirst()
|
||||
loadData()
|
||||
}
|
||||
function onReset() {
|
||||
searchName.value = ''
|
||||
toFirst()
|
||||
loadData()
|
||||
}
|
||||
function resetForm() {
|
||||
formData.name = ''
|
||||
formData.code = ''
|
||||
formData.order = 1
|
||||
formData.data_scope = 4
|
||||
formData.status = 0
|
||||
formData.description = ''
|
||||
}
|
||||
function openCreate() {
|
||||
formTitle.value = '新增角色'
|
||||
currentId.value = undefined
|
||||
resetForm()
|
||||
showForm.value = true
|
||||
}
|
||||
async function openEdit(id: number) {
|
||||
formTitle.value = '编辑角色'
|
||||
currentId.value = id
|
||||
try {
|
||||
const detail = await RoleAPI.getDetail(id)
|
||||
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, '获取详情失败')) }
|
||||
}
|
||||
async function handleSubmit() {
|
||||
loading.value = true
|
||||
try {
|
||||
if (currentId.value) {
|
||||
await RoleAPI.update(currentId.value, formData)
|
||||
toast.success('更新成功')
|
||||
}
|
||||
else {
|
||||
await RoleAPI.create(formData)
|
||||
toast.success('创建成功')
|
||||
}
|
||||
showForm.value = false
|
||||
loadData()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '操作失败')) }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
function handleDelete(id: number) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确认删除该角色?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await RoleAPI.remove([id])
|
||||
toast.success('删除成功')
|
||||
loadData()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '删除失败')) }
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
onReachBottom(() => {
|
||||
if (!loading.value)
|
||||
loadNext()
|
||||
})
|
||||
onPullDownRefresh(() => {
|
||||
loadData()
|
||||
})
|
||||
|
||||
onLoad(() => loadData())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="page-wraper">
|
||||
<view class="search-bar">
|
||||
<view class="flex items-center gap-sm">
|
||||
<u-input v-model="searchName" placeholder="搜索角色名称" clearable border="surround" class="flex-1" />
|
||||
<u-button size="mini" type="primary" :plain="true" text="搜索" @click="onSearch" />
|
||||
<u-button size="mini" :plain="true" text="重置" @click="onReset" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="action-bar">
|
||||
<text class="text-md font-bold text-muted">
|
||||
共 {{ total }} 条
|
||||
</text>
|
||||
<wd-button size="small" type="primary" @click="openCreate">+ 新增</wd-button>
|
||||
</view>
|
||||
<SkeletonPage v-if="loading" :rows="5" search />
|
||||
<template v-else>
|
||||
<view class="px-sm">
|
||||
<view class="admin-card">
|
||||
<ListEmpty v-if="!loading && list.length === 0" text="暂无角色" />
|
||||
<wd-cell-group v-else>
|
||||
<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">
|
||||
{{ item.name }}
|
||||
</text>
|
||||
<text class="text-xs text-muted block">
|
||||
{{ item.code }}{{ item.description ? ` · ${item.description}` : '' }}
|
||||
</text>
|
||||
</view>
|
||||
</template>
|
||||
<template #default>
|
||||
<StatusBadge :status="item.status" />
|
||||
<wd-icon name="delete" size="18px" color="var(--danger-color)" @click.stop="handleDelete(item.id!)" />
|
||||
</template>
|
||||
</wd-cell>
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
</view>
|
||||
<PaginationBar :current="pageParams.page_no" :page-size="pageParams.page_size" :total="total" @prev="loadPrev" @next="loadNext" />
|
||||
<wd-popup v-model="showForm" position="bottom" round custom-style="max-height: 80vh; overflow-y: auto;" @close="showForm = false">
|
||||
<view class="p-xl">
|
||||
<wd-navbar :title="formTitle" left-arrow @click-left="showForm = false" />
|
||||
<wd-form ref="formRef" :model="formData" class="mt-lg">
|
||||
<wd-form-item label="角色名称" prop="name" border>
|
||||
<wd-input v-model="formData.name" placeholder="请输入" />
|
||||
</wd-form-item>
|
||||
<wd-form-item label="角色编码" prop="code" border>
|
||||
<wd-input v-model="formData.code" placeholder="请输入" />
|
||||
</wd-form-item>
|
||||
<wd-form-item label="排序" border>
|
||||
<wd-input v-model="formData.order" type="number" placeholder="请输入" />
|
||||
</wd-form-item>
|
||||
<wd-form-item label="备注" border>
|
||||
<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>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
@@ -0,0 +1,415 @@
|
||||
<script setup lang="ts">
|
||||
import type { DeptItem } from '@/api/module_system/dept'
|
||||
import type { PositionItem } from '@/api/module_system/position'
|
||||
import type { RoleItem } from '@/api/module_system/role'
|
||||
import type { UserForm, UserInfo } from '@/api/module_system/user'
|
||||
import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app'
|
||||
import { ref } from 'vue'
|
||||
import { DeptAPI } from '@/api/module_system/dept'
|
||||
import { PositionAPI } from '@/api/module_system/position'
|
||||
import { RoleAPI } from '@/api/module_system/role'
|
||||
import UserAPI from '@/api/module_system/user'
|
||||
|
||||
definePage({ name: 'work-users', style: { navigationBarTitleText: '用户管理' } })
|
||||
|
||||
const router = useRouter()
|
||||
const toast = useToast()
|
||||
const searchName = ref('')
|
||||
const showForm = ref(false)
|
||||
const formTitle = ref('')
|
||||
const currentId = ref<number>()
|
||||
const formData = reactive<UserForm>({
|
||||
username: '',
|
||||
name: '',
|
||||
password: '',
|
||||
dept_id: undefined,
|
||||
role_ids: [],
|
||||
position_ids: [],
|
||||
email: '',
|
||||
mobile: '',
|
||||
gender: 2,
|
||||
status: 1,
|
||||
description: '',
|
||||
})
|
||||
|
||||
const GENDER_OPTIONS = [{ value: 0, label: '男' }, { value: 1, label: '女' }, { value: 2, label: '未知' }]
|
||||
const STATUS_OPTIONS = [{ value: 0, label: '正常' }, { value: 1, label: '禁用' }]
|
||||
|
||||
const deptOptions = ref<{ value: number, label: string }[]>([])
|
||||
const roleOptions = ref<{ value: number, label: string }[]>([])
|
||||
const positionOptions = ref<{ value: number, label: string }[]>([])
|
||||
const showPickerDept = ref(false)
|
||||
const showPickerRole = ref(false)
|
||||
const showPickerPosition = ref(false)
|
||||
|
||||
interface PickerConfirmEvent { indexs: number[] }
|
||||
function handleDeptConfirm(e: PickerConfirmEvent) {
|
||||
const item = deptOptions.value[e.indexs[0]]
|
||||
if (item)
|
||||
formData.dept_id = item.value
|
||||
showPickerDept.value = false
|
||||
}
|
||||
function handleRoleConfirm(e: PickerConfirmEvent) {
|
||||
const item = roleOptions.value[e.indexs[0]]
|
||||
if (item)
|
||||
formData.role_ids = [item.value]
|
||||
showPickerRole.value = false
|
||||
}
|
||||
function handlePositionConfirm(e: PickerConfirmEvent) {
|
||||
const item = positionOptions.value[e.indexs[0]]
|
||||
if (item)
|
||||
formData.position_ids = [item.value]
|
||||
showPickerPosition.value = false
|
||||
}
|
||||
|
||||
function flattenDept(items: DeptItem[], prefix = ''): { value: number, label: string }[] {
|
||||
let r: { value: number, label: string }[] = []
|
||||
for (const item of items) {
|
||||
r.push({ value: item.id, label: prefix + item.name })
|
||||
if (item.children?.length)
|
||||
r = r.concat(flattenDept(item.children, `${prefix} `))
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
async function loadOptions() {
|
||||
try {
|
||||
const [depts, roles, positions] = await Promise.all([
|
||||
DeptAPI.getTree(),
|
||||
RoleAPI.getPage({ page_no: 1, page_size: 1000 }),
|
||||
PositionAPI.getPage({ page_no: 1, page_size: 1000 }),
|
||||
])
|
||||
deptOptions.value = flattenDept(depts || [])
|
||||
roleOptions.value = (roles.list || []).map((r: RoleItem) => ({ value: r.id, label: r.name || '' }))
|
||||
positionOptions.value = (positions.list || []).map((p: PositionItem) => ({ value: p.id, label: p.name || '' }))
|
||||
}
|
||||
catch {
|
||||
deptOptions.value = []
|
||||
roleOptions.value = []
|
||||
positionOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
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, '加载失败')),
|
||||
})
|
||||
|
||||
function onSearch() {
|
||||
toFirst()
|
||||
loadData()
|
||||
}
|
||||
|
||||
const initForm: UserForm = { username: '', name: '', password: '', dept_id: undefined, role_ids: [], position_ids: [], email: '', mobile: '', gender: 2, status: 1, description: '' }
|
||||
|
||||
function openCreate() {
|
||||
formTitle.value = '新增用户'
|
||||
currentId.value = undefined
|
||||
Object.assign(formData, { ...initForm })
|
||||
if (deptOptions.value.length === 0)
|
||||
loadOptions()
|
||||
showForm.value = true
|
||||
}
|
||||
function openEdit(id: number) {
|
||||
router.push({ name: 'work-user-detail', query: { id } })
|
||||
}
|
||||
async function handleSubmit() {
|
||||
loading.value = true
|
||||
try {
|
||||
const payload = { ...formData }
|
||||
if (currentId.value) {
|
||||
await UserAPI.updateUser({ ...payload, id: currentId.value })
|
||||
toast.success('更新成功')
|
||||
}
|
||||
else {
|
||||
await UserAPI.addUser(payload)
|
||||
toast.success('创建成功')
|
||||
}
|
||||
showForm.value = false
|
||||
loadData()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '操作失败')) }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
function handleDelete(id: number) {
|
||||
uni.showModal({ title: '提示', content: '确认删除该用户?', success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await UserAPI.deleteUser([id])
|
||||
toast.success('删除成功')
|
||||
loadData()
|
||||
}
|
||||
catch (e) { toast.error(getErrorMessage(e, '删除失败')) }
|
||||
}
|
||||
} })
|
||||
}
|
||||
|
||||
// Avatar color generator
|
||||
const avatarColors = ['#014DB2', '#F59E0B', '#10B981', '#8B5CF6', '#EF4444', '#EC4899', '#06B6D4']
|
||||
function getAvatarColor(index: number) {
|
||||
return avatarColors[index % avatarColors.length]
|
||||
}
|
||||
function getAvatarInitial(name: string) {
|
||||
return (name || '?').charAt(0)
|
||||
}
|
||||
|
||||
onReachBottom(() => {
|
||||
if (!loading.value)
|
||||
loadNext()
|
||||
})
|
||||
onPullDownRefresh(() => {
|
||||
loadData()
|
||||
})
|
||||
onLoad(() => loadData())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="list-page">
|
||||
<!-- Search bar -->
|
||||
<view class="list-search">
|
||||
<view class="list-search__box">
|
||||
<text class="list-search__icon">
|
||||
🔍
|
||||
</text>
|
||||
<u-input
|
||||
v-model="searchName"
|
||||
placeholder="搜索姓名或用户名"
|
||||
clearable
|
||||
border="none"
|
||||
@confirm="onSearch"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Meta bar -->
|
||||
<view class="list-meta">
|
||||
<text class="list-meta__count">
|
||||
共 {{ total }} 条
|
||||
</text>
|
||||
<view class="list-meta__add" @click="openCreate">
|
||||
<text class="list-meta__add-icon">
|
||||
+
|
||||
</text>
|
||||
<text class="list-meta__add-text">
|
||||
新增
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- List -->
|
||||
<SkeletonPage v-if="loading && list.length === 0" :rows="5" search />
|
||||
<template v-else>
|
||||
<ListEmpty v-if="!loading && list.length === 0" text="暂无用户" />
|
||||
|
||||
<view v-for="(item, index) in list" :key="item.id" class="list-item" hover-class="list-item--hover" @click="openEdit(item.id!)">
|
||||
<view class="list-item__avatar" :style="{ background: getAvatarColor(index) }">
|
||||
<text class="list-item__avatar-text">
|
||||
{{ getAvatarInitial(item.name || item.username || '') }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="list-item__info">
|
||||
<text class="list-item__name">
|
||||
{{ item.name || item.username }}
|
||||
</text>
|
||||
<text class="list-item__sub">
|
||||
{{ item.username }} · {{ item.email || '-' }}
|
||||
</text>
|
||||
</view>
|
||||
<StatusBadge :status="item.status" />
|
||||
<view class="list-item__delete" @click.stop="handleDelete(item.id!)">
|
||||
<text class="list-item__delete-icon">
|
||||
🗑
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<PaginationBar :current="pageParams.page_no" :page-size="pageParams.page_size" :total="total" @prev="loadPrev" @next="loadNext" />
|
||||
</template>
|
||||
|
||||
<!-- Form popup -->
|
||||
<u-popup :show="showForm" mode="bottom" :round="10" custom-style="max-height: 80vh; overflow-y: auto;" @close="showForm = false">
|
||||
<view class="p-xl">
|
||||
<u-navbar :title="formTitle" left-icon="arrow-left" @left-click="showForm = false" />
|
||||
<u-form :model="formData" class="mt-lg">
|
||||
<u-form-item label="用户名">
|
||||
<u-input v-model="formData.username" placeholder="请输入" border="surround" />
|
||||
</u-form-item>
|
||||
<u-form-item label="姓名">
|
||||
<u-input v-model="formData.name" placeholder="请输入" border="surround" />
|
||||
</u-form-item>
|
||||
<u-form-item v-if="!currentId" label="密码">
|
||||
<u-input v-model="formData.password" placeholder="请输入" border="surround" />
|
||||
</u-form-item>
|
||||
<u-form-item label="部门">
|
||||
<view @click="showPickerDept = true">
|
||||
<u-cell title="部门" :value="deptOptions.find(o => o.value === formData.dept_id)?.label || '请选择部门'" :is-link="true" :border="false" />
|
||||
</view>
|
||||
<u-picker :show="showPickerDept" :columns="[deptOptions.map(o => o.label)]" key-name="label" @confirm="handleDeptConfirm" @cancel="showPickerDept = false" />
|
||||
</u-form-item>
|
||||
<u-form-item label="角色">
|
||||
<view @click="showPickerRole = true">
|
||||
<u-cell title="角色" :value="roleOptions.find(o => o.value === formData.role_ids?.[0])?.label || '请选择角色'" :is-link="true" :border="false" />
|
||||
</view>
|
||||
<u-picker :show="showPickerRole" :columns="[roleOptions.map(o => o.label)]" key-name="label" @confirm="handleRoleConfirm" @cancel="showPickerRole = false" />
|
||||
</u-form-item>
|
||||
<u-form-item label="岗位">
|
||||
<view @click="showPickerPosition = true">
|
||||
<u-cell title="岗位" :value="positionOptions.find(o => o.value === formData.position_ids?.[0])?.label || '请选择岗位'" :is-link="true" :border="false" />
|
||||
</view>
|
||||
<u-picker :show="showPickerPosition" :columns="[positionOptions.map(o => o.label)]" key-name="label" @confirm="handlePositionConfirm" @cancel="showPickerPosition = false" />
|
||||
</u-form-item>
|
||||
<u-form-item label="性别">
|
||||
<u-radio-group v-model="formData.gender">
|
||||
<u-radio v-for="opt in GENDER_OPTIONS" :key="opt.value" :name="opt.value">
|
||||
{{ opt.label }}
|
||||
</u-radio>
|
||||
</u-radio-group>
|
||||
</u-form-item>
|
||||
<u-form-item label="状态">
|
||||
<u-radio-group v-model="formData.status">
|
||||
<u-radio v-for="opt in STATUS_OPTIONS" :key="opt.value" :name="opt.value">
|
||||
{{ opt.label }}
|
||||
</u-radio>
|
||||
</u-radio-group>
|
||||
</u-form-item>
|
||||
<u-form-item label="邮箱">
|
||||
<u-input v-model="formData.email" placeholder="请输入" border="surround" />
|
||||
</u-form-item>
|
||||
<u-form-item label="手机号">
|
||||
<u-input v-model="formData.mobile" placeholder="请输入" border="surround" />
|
||||
</u-form-item>
|
||||
<u-form-item label="备注">
|
||||
<u-textarea v-model="formData.description" placeholder="请输入" />
|
||||
</u-form-item>
|
||||
</u-form>
|
||||
<view class="flex gap-md mt-xl">
|
||||
<u-button :block="true" :plain="true" text="取消" @click="showForm = false" />
|
||||
<u-button :block="true" type="primary" :loading="loading" text="保存" @click="handleSubmit" />
|
||||
</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;
|
||||
}
|
||||
|
||||
/* ===== Search ===== */
|
||||
.list-search {
|
||||
padding: 24rpx 0;
|
||||
|
||||
&__box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
background: var(--bg-color-2, #F5F6F8);
|
||||
border-radius: 16rpx;
|
||||
padding: 0 24rpx;
|
||||
height: 80rpx;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
font-size: 28rpx;
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Meta ===== */
|
||||
.list-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8rpx 0 24rpx;
|
||||
|
||||
&__count {
|
||||
font-size: 26rpx;
|
||||
color: var(--text-color-3, #6B7280);
|
||||
}
|
||||
|
||||
&__add {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
background: var(--primary-color, #4F8CFF);
|
||||
color: #FFFFFF;
|
||||
padding: 8rpx 24rpx;
|
||||
border-radius: 32rpx;
|
||||
|
||||
&-icon { font-size: 36rpx; font-weight: 300; line-height: 1; }
|
||||
|
||||
&-text { font-size: 24rpx; font-weight: 500; }
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== List item ===== */
|
||||
.list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
background: var(--card-bg-color, #FFFFFF);
|
||||
border-radius: 24rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 16rpx;
|
||||
box-shadow: var(--shadow-sm, 0 1rpx 2rpx rgba(1, 77, 178,0.06));
|
||||
transition: all 0.15s ease;
|
||||
|
||||
&--hover {
|
||||
opacity: 0.9;
|
||||
box-shadow: var(--shadow-md, 0 4rpx 12rpx rgba(1, 77, 178,0.10));
|
||||
}
|
||||
|
||||
&__avatar {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
&-text {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
}
|
||||
|
||||
&__info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
color: var(--text-color, #0A1628);
|
||||
}
|
||||
|
||||
&__sub {
|
||||
font-size: 24rpx;
|
||||
color: var(--text-color-3, #6B7280);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__delete {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&-icon { font-size: 28rpx; opacity: 0.4; }
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,428 @@
|
||||
<script setup lang="ts">
|
||||
definePage({
|
||||
name: 'ci',
|
||||
style: {
|
||||
navigationBarTitleText: 'CI 持续集成',
|
||||
},
|
||||
})
|
||||
|
||||
const { success: showSuccess } = useGlobalToast()
|
||||
|
||||
// 配置示例数据
|
||||
const configExample = ref(`{
|
||||
"weixin": {
|
||||
"appid": "微信小程序appid",
|
||||
"privateKeyPath": "key/private.appid.key",
|
||||
"projectPath": "dist/build/mp-weixin"
|
||||
},
|
||||
"alipay": {
|
||||
"appid": "支付宝小程序appid",
|
||||
"toolId": "支付宝开放平台工具ID",
|
||||
"privateKey": "私钥文本内容",
|
||||
"projectPath": "dist/build/mp-alipay",
|
||||
"autoincrement": true
|
||||
},
|
||||
"dd": {
|
||||
"appid": "钉钉小程序appid",
|
||||
"token": "钉钉开发者后台的 API Token",
|
||||
"projectPath": "dist/build/mp-alipay",
|
||||
"autoincrement": true
|
||||
},
|
||||
"version": "0.0.1",
|
||||
"desc": "版本描述"
|
||||
}`)
|
||||
|
||||
const packageScripts = ref(`{
|
||||
"scripts": {
|
||||
"upload:weixin": "minici --platform weixin",
|
||||
"upload:alipay": "minici --platform alipay",
|
||||
"upload:dd": "minici --platform dd",
|
||||
"upload:mp-weixin": "uni build -p mp-weixin && minici --platform weixin",
|
||||
"upload:mp-alipay": "uni build -p mp-alipay && minici --platform alipay",
|
||||
"upload:mp-dingtalk": "uni build -p mp-dingtalk && minici --platform dd"
|
||||
}
|
||||
}`)
|
||||
|
||||
// 特性列表
|
||||
const features = ref([
|
||||
{
|
||||
icon: '📱',
|
||||
title: '多平台支持',
|
||||
desc: '支持微信、支付宝、钉钉小程序的上传',
|
||||
},
|
||||
{
|
||||
icon: '🔢',
|
||||
title: '版本管理',
|
||||
desc: '支持指定版本号和版本号自增',
|
||||
},
|
||||
{
|
||||
icon: '⚙️',
|
||||
title: '简单配置',
|
||||
desc: '通过配置文件轻松管理多平台参数',
|
||||
},
|
||||
{
|
||||
icon: '🔄',
|
||||
title: 'CI/CD集成',
|
||||
desc: '完美集成持续集成和持续部署流程',
|
||||
},
|
||||
{
|
||||
icon: '🛠️',
|
||||
title: '命令行工具',
|
||||
desc: '支持全局和局部安装,灵活使用',
|
||||
},
|
||||
{
|
||||
icon: '📦',
|
||||
title: '打包上传',
|
||||
desc: '支持打包和上传命令组合使用',
|
||||
},
|
||||
])
|
||||
|
||||
// 平台配置
|
||||
const platforms = ref([
|
||||
{
|
||||
name: 'weixin',
|
||||
title: '微信小程序',
|
||||
icon: '💚',
|
||||
color: 'text-green-600',
|
||||
bgColor: 'bg-green-50',
|
||||
borderColor: 'border-green-200',
|
||||
features: ['支持企业微信', 'CI机器人选择', '编译设置', '代码压缩保护'],
|
||||
},
|
||||
{
|
||||
name: 'alipay',
|
||||
title: '支付宝小程序',
|
||||
icon: '💙',
|
||||
color: 'text-blue-600',
|
||||
bgColor: 'bg-blue-50',
|
||||
borderColor: 'border-blue-200',
|
||||
features: ['多终端支持', '版本号自增', '私钥认证', '开放平台集成'],
|
||||
},
|
||||
{
|
||||
name: 'dd',
|
||||
title: '钉钉小程序',
|
||||
icon: '💜',
|
||||
color: 'text-purple-600',
|
||||
bgColor: 'bg-purple-50',
|
||||
borderColor: 'border-purple-200',
|
||||
features: ['企业应用', '版本号自增', 'Token认证', '多应用类型'],
|
||||
},
|
||||
])
|
||||
|
||||
// 复制代码到剪贴板
|
||||
function copyCode(code: string) {
|
||||
uni.setClipboardData({
|
||||
data: code,
|
||||
showToast: false,
|
||||
success: () => {
|
||||
uni.hideToast()
|
||||
showSuccess({ msg: '代码已复制到剪贴板' })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 链接导航处理
|
||||
function handleNavigate(url: string) {
|
||||
// #ifdef H5
|
||||
window.open(url, '_blank')
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
uni.setClipboardData({
|
||||
data: url,
|
||||
showToast: false,
|
||||
success: () => {
|
||||
uni.hideToast()
|
||||
showSuccess({ msg: `${url} 已复制到剪贴板` })
|
||||
},
|
||||
})
|
||||
// #endif
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="min-h-screen py-3">
|
||||
<!-- 头部介绍 -->
|
||||
<view class="mx-3 mb-3">
|
||||
<view class="rounded-3 px-5 py-8 text-center wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-10">
|
||||
🚀
|
||||
</view>
|
||||
<view class="mb-2 text-6 font-bold wot-text-text-main">
|
||||
uni-mini-ci
|
||||
</view>
|
||||
<view class="mb-2 text-3.5 leading-relaxed wot-text-text-secondary">
|
||||
小程序持续集成的插件
|
||||
</view>
|
||||
<view class="text-3 wot-text-text-secondary">
|
||||
支持上传微信、企业微信、钉钉、支付宝平台的小程序
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 核心特性 -->
|
||||
<demo-block title="核心特性" transparent>
|
||||
<view class="grid grid-cols-2 gap-3">
|
||||
<view
|
||||
v-for="feature in features"
|
||||
:key="feature.title"
|
||||
class="rounded-2 p-4 text-center wot-bg-filled-oppo"
|
||||
>
|
||||
<view class="mb-2 text-6">
|
||||
{{ feature.icon }}
|
||||
</view>
|
||||
<view class="mb-1 text-3.5 font-bold wot-text-text-main">
|
||||
{{ feature.title }}
|
||||
</view>
|
||||
<view class="text-3 leading-snug wot-text-text-secondary">
|
||||
{{ feature.desc }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 安装方式 -->
|
||||
<demo-block title="安装方式" transparent>
|
||||
<view class="space-y-4">
|
||||
<!-- 全局安装 -->
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-4 font-bold wot-text-text-main">
|
||||
全局安装
|
||||
</view>
|
||||
<view class="mb-3 text-3 wot-text-text-secondary">
|
||||
推荐在开发环境中全局安装,方便在任何项目中使用
|
||||
</view>
|
||||
<view class="space-y-2">
|
||||
<view class="wot-bg-bg flex items-center justify-between border rounded-2 p-3 wot-border-border-main" @click="copyCode('npm i uni-mini-ci -g')">
|
||||
<text class="flex-1 text-3 font-mono wot-text-text-secondary">
|
||||
npm i uni-mini-ci -g
|
||||
</text>
|
||||
<wd-icon name="copy" size="16px" color="#666" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 局部安装 -->
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-4 font-bold wot-text-text-main">
|
||||
局部安装
|
||||
</view>
|
||||
<view class="mb-3 text-3 wot-text-text-secondary">
|
||||
在项目中作为开发依赖安装,适合团队协作
|
||||
</view>
|
||||
<view class="space-y-2">
|
||||
<view class="wot-bg-bg flex items-center justify-between border rounded-2 p-3 wot-border-border-main" @click="copyCode('npm i uni-mini-ci --save-dev')">
|
||||
<text class="flex-1 text-3 font-mono wot-text-text-secondary">
|
||||
npm i uni-mini-ci --save-dev
|
||||
</text>
|
||||
<wd-icon name="copy" size="16px" color="#666" />
|
||||
</view>
|
||||
<view class="wot-bg-bg flex items-center justify-between border rounded-2 p-3 wot-border-border-main" @click="copyCode('pnpm add uni-mini-ci -D')">
|
||||
<text class="flex-1 text-3 font-mono wot-text-text-secondary">
|
||||
pnpm add uni-mini-ci -D
|
||||
</text>
|
||||
<wd-icon name="copy" size="16px" color="#666" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 配置文件 -->
|
||||
<demo-block title="配置文件" transparent>
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-4 font-bold wot-text-text-main">
|
||||
.minicirc 配置文件
|
||||
</view>
|
||||
<view class="mb-3 text-3 leading-relaxed wot-text-text-secondary">
|
||||
在项目根目录创建 <text class="rounded px-1 font-mono">
|
||||
.minicirc
|
||||
</text> 文件,配置各平台的上传参数
|
||||
</view>
|
||||
|
||||
<view class="wot-bg-bg mb-3 border rounded-2 p-3 wot-border-border-main" @click="copyCode(configExample)">
|
||||
<view class="mb-2 flex items-center justify-between">
|
||||
<text class="text-3 font-bold wot-text-text-secondary">
|
||||
配置示例:
|
||||
</text>
|
||||
<wd-icon name="copy" size="16px" color="#666" />
|
||||
</view>
|
||||
<view class="max-h-60 overflow-y-auto">
|
||||
<pre class="whitespace-pre-wrap text-2.5 leading-relaxed font-mono wot-text-text-secondary">{{ configExample }}</pre>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="border border-orange-200 rounded-2 bg-orange-50 p-3 dark:bg-orange-900/20">
|
||||
<view class="mb-2 text-3.5 text-orange-700 font-bold dark:text-orange-300">
|
||||
💡 配置说明
|
||||
</view>
|
||||
<view class="text-3 text-orange-600 leading-relaxed space-y-1 dark:text-orange-200">
|
||||
<view>• version: 上传版本号,不传时默认读取 package.json 的 version 字段</view>
|
||||
<view>• desc: 上传时的描述信息</view>
|
||||
<view>• autoincrement: 版本号自增,建议测试环境开启</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 支持平台 -->
|
||||
<demo-block title="支持平台" transparent>
|
||||
<view class="space-y-3">
|
||||
<view
|
||||
v-for="platform in platforms"
|
||||
:key="platform.name"
|
||||
class="rounded-2 p-4 wot-bg-filled-oppo"
|
||||
>
|
||||
<view class="mb-3 flex items-center">
|
||||
<view class="mr-2 text-6">
|
||||
{{ platform.icon }}
|
||||
</view>
|
||||
<view class="text-4 font-bold wot-text-text-main">
|
||||
{{ platform.title }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="grid grid-cols-2 mb-3 gap-2">
|
||||
<view
|
||||
v-for="feature in platform.features"
|
||||
:key="feature"
|
||||
class="rounded px-2 py-1 text-center text-2.5 wot-text-text-secondary"
|
||||
>
|
||||
{{ feature }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="wot-bg-bg flex items-center justify-between border rounded-2 p-3 wot-border-border-main" @click="copyCode(`minici --platform ${platform.name}`)">
|
||||
<text class="flex-1 text-3 font-mono wot-text-text-secondary">
|
||||
minici --platform {{ platform.name }}
|
||||
</text>
|
||||
<wd-icon name="copy" size="16px" color="#666" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 添加命令 -->
|
||||
<demo-block title="添加命令" transparent>
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-4 font-bold wot-text-text-main">
|
||||
package.json 脚本配置
|
||||
</view>
|
||||
<view class="mb-3 text-3 leading-relaxed wot-text-text-secondary">
|
||||
在 package.json 中添加上传脚本,支持单独上传和打包上传组合
|
||||
</view>
|
||||
|
||||
<view class="wot-bg-bg mb-3 border rounded-2 p-3 wot-border-border-main" @click="copyCode(packageScripts)">
|
||||
<view class="mb-2 flex items-center justify-between">
|
||||
<text class="text-3 font-bold wot-text-text-secondary">
|
||||
脚本配置:
|
||||
</text>
|
||||
<wd-icon name="copy" size="16px" color="#666" />
|
||||
</view>
|
||||
<view class="max-h-60 overflow-y-auto">
|
||||
<pre class="whitespace-pre-wrap text-2.5 leading-relaxed font-mono wot-text-text-secondary">{{ packageScripts }}</pre>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="space-y-2">
|
||||
<view class="text-3.5 font-bold wot-text-text-main">
|
||||
使用命令:
|
||||
</view>
|
||||
<view class="grid grid-cols-1 gap-2">
|
||||
<view class="wot-bg-bg flex items-center justify-between border rounded-2 p-3 wot-border-border-main" @click="copyCode('pnpm upload:weixin')">
|
||||
<text class="flex-1 text-3 font-mono wot-text-text-secondary">
|
||||
pnpm upload:weixin
|
||||
</text>
|
||||
<wd-icon name="copy" size="16px" color="#666" />
|
||||
</view>
|
||||
<view class="wot-bg-bg flex items-center justify-between border rounded-2 p-3 wot-border-border-main" @click="copyCode('pnpm upload:mp-weixin')">
|
||||
<text class="flex-1 text-3 font-mono wot-text-text-secondary">
|
||||
pnpm upload:mp-weixin
|
||||
</text>
|
||||
<wd-icon name="copy" size="16px" color="#666" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 使用步骤 -->
|
||||
<demo-block title="使用步骤" transparent>
|
||||
<view class="space-y-3">
|
||||
<view
|
||||
v-for="(step, index) in [
|
||||
{ title: '安装插件', desc: '选择全局或局部安装方式', icon: '📦' },
|
||||
{ title: '创建配置', desc: '在根目录创建 .minicirc 配置文件', icon: '⚙️' },
|
||||
{ title: '配置平台', desc: '填写各平台的 appid、密钥等信息', icon: '🔑' },
|
||||
{ title: '添加脚本', desc: '在 package.json 中添加上传命令', icon: '📝' },
|
||||
{ title: '执行上传', desc: '运行命令上传到对应平台', icon: '🚀' },
|
||||
]"
|
||||
:key="index"
|
||||
class="rounded-2 p-4 wot-bg-filled-oppo"
|
||||
>
|
||||
<view class="flex items-center">
|
||||
<view class="mr-3 h-8 w-8 flex items-center justify-center rounded-full bg-blue-100 text-3.5 text-blue-600 font-bold dark:bg-blue-900/20 dark:text-blue-400">
|
||||
{{ index + 1 }}
|
||||
</view>
|
||||
<view class="mr-2 text-5">
|
||||
{{ step.icon }}
|
||||
</view>
|
||||
<view class="flex-1">
|
||||
<view class="text-3.5 font-bold wot-text-text-main">
|
||||
{{ step.title }}
|
||||
</view>
|
||||
<view class="text-3 wot-text-text-secondary">
|
||||
{{ step.desc }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 注意事项 -->
|
||||
<demo-block title="注意事项" transparent>
|
||||
<view class="space-y-3">
|
||||
<view class="border border-red-200 rounded-2 bg-red-50 p-4 dark:bg-red-900/20">
|
||||
<view class="mb-2 text-3.5 text-red-700 font-bold dark:text-red-300">
|
||||
⚠️ 版本号限制
|
||||
</view>
|
||||
<view class="text-3 text-red-600 leading-relaxed dark:text-red-200">
|
||||
支付宝和钉钉小程序不支持上传的版本号小于或等于现有版本号。建议测试环境开启 autoincrement 字段,生产环境手动指定版本号。
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="border border-blue-200 rounded-2 bg-blue-50 p-4 dark:bg-blue-900/20">
|
||||
<view class="mb-2 text-3.5 text-blue-700 font-bold dark:text-blue-300">
|
||||
💡 最佳实践
|
||||
</view>
|
||||
<view class="text-3 text-blue-600 leading-relaxed space-y-1 dark:text-blue-200">
|
||||
<view>• 测试环境:开启版本号自增,频繁调试更方便</view>
|
||||
<view>• 生产环境:手动指定版本号,便于版本管理</view>
|
||||
<view>• 密钥安全:不要将私钥文件提交到代码仓库</view>
|
||||
<view>• CI/CD:结合打包命令,实现自动化部署</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="border border-green-200 rounded-2 bg-green-50 p-4 dark:bg-green-900/20">
|
||||
<view class="mb-2 text-3.5 text-green-700 font-bold dark:text-green-300">
|
||||
🔒 安全建议
|
||||
</view>
|
||||
<view class="text-3 text-green-600 leading-relaxed space-y-1 dark:text-green-200">
|
||||
<view>• 使用环境变量存储敏感信息</view>
|
||||
<view>• 将配置文件添加到 .gitignore</view>
|
||||
<view>• 定期更换密钥和 Token</view>
|
||||
<view>• 限制 CI 机器人权限范围</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 相关链接 -->
|
||||
<demo-block title="相关链接" transparent>
|
||||
<wd-cell-group border custom-class="rounded-2! overflow-hidden">
|
||||
<wd-cell title="📦 NPM 包" value="uni-mini-ci" is-link @click="handleNavigate('https://www.npmjs.com/package/uni-mini-ci')" />
|
||||
<wd-cell title="🐙 GitHub 仓库" value="源码仓库" is-link @click="handleNavigate('https://github.com/Moonofweisheng/uni-mini-ci')" />
|
||||
<wd-cell title="📱 微信小程序 CI" value="官方文档" is-link @click="handleNavigate('https://developers.weixin.qq.com/miniprogram/dev/devtools/ci.html')" />
|
||||
<wd-cell title="💙 支付宝小程序 CI" value="官方文档" is-link @click="handleNavigate('https://opendocs.alipay.com/mini/02q29z')" />
|
||||
<wd-cell title="💜 钉钉小程序 CI" value="官方文档" is-link @click="handleNavigate('https://github.com/open-dingtalk/dingtalk-design-cli')" />
|
||||
</wd-cell-group>
|
||||
</demo-block>
|
||||
</view>
|
||||
</template>
|
||||
@@ -0,0 +1,252 @@
|
||||
<script setup lang="ts">
|
||||
definePage({
|
||||
name: 'create-uni',
|
||||
style: {
|
||||
navigationBarTitleText: 'CreateUni 脚手架',
|
||||
},
|
||||
})
|
||||
|
||||
const { success: showSuccess } = useGlobalToast()
|
||||
// 核心优势
|
||||
const advantages = ref([
|
||||
{
|
||||
icon: '🚀',
|
||||
title: '简单易用',
|
||||
desc: '一行命令即可创建项目,无需手动配置开发环境',
|
||||
},
|
||||
{
|
||||
icon: '🔧',
|
||||
title: '功能强大',
|
||||
desc: '内置多种实用工具和插件,支持一键集成 WotUI',
|
||||
},
|
||||
{
|
||||
icon: '🔄',
|
||||
title: '持续更新',
|
||||
desc: '由 uni-helper 团队积极维护,及时跟进最新技术栈',
|
||||
},
|
||||
{
|
||||
icon: '👥',
|
||||
title: '社区活跃',
|
||||
desc: 'GitHub 获得众多 star,活跃的开发者社区',
|
||||
},
|
||||
{
|
||||
icon: '🌐',
|
||||
title: '网络稳定',
|
||||
desc: '解决官方 CLI 网络不稳定问题,支持国内环境',
|
||||
},
|
||||
{
|
||||
icon: '⚡️',
|
||||
title: '开发效率',
|
||||
desc: '提升开发效率 100%,快速搭建现代化项目',
|
||||
},
|
||||
])
|
||||
|
||||
// 解决的问题
|
||||
const problems = ref([
|
||||
{
|
||||
problem: '网络问题',
|
||||
desc: 'degit 命令只支持 GitHub,国内网络不稳定',
|
||||
},
|
||||
{
|
||||
problem: '维护问题',
|
||||
desc: '官方模板维护不够积极,版本往往落后',
|
||||
},
|
||||
{
|
||||
problem: '工程化问题',
|
||||
desc: '项目配置过于基础,缺少现代工程化支持',
|
||||
},
|
||||
{
|
||||
problem: '集成问题',
|
||||
desc: '需要手动集成组件库,配置过程繁琐',
|
||||
},
|
||||
])
|
||||
|
||||
// WotUI 快速创建
|
||||
const wotQuickStart = ref({
|
||||
title: '快速创建 WotUI 项目',
|
||||
command: 'pnpm create uni@latest -t wot-starter-v2 <你的项目名称>',
|
||||
desc: '一键创建集成了 WotUI 的完整项目',
|
||||
features: [
|
||||
'✅ TypeScript 项目',
|
||||
'✅ 集成 Pinia 状态管理',
|
||||
'✅ 自动配置 WotUI 组件库',
|
||||
'✅ 添加 ESLint 支持',
|
||||
],
|
||||
})
|
||||
|
||||
// 其他使用方式
|
||||
const quickMethods = ref([
|
||||
{
|
||||
title: 'GUI 模式',
|
||||
command: 'pnpm create uni --gui',
|
||||
desc: '使用图形界面创建项目',
|
||||
},
|
||||
{
|
||||
title: '环境信息',
|
||||
command: 'npx @create-uni/info@latest',
|
||||
desc: '获取当前项目环境信息',
|
||||
},
|
||||
])
|
||||
|
||||
// 复制命令到剪贴板
|
||||
function copyCommand(command: string) {
|
||||
uni.setClipboardData({
|
||||
data: command,
|
||||
showToast: false,
|
||||
success: () => {
|
||||
uni.hideToast()
|
||||
showSuccess({
|
||||
msg: `${command}已成功复制到剪贴板`,
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function handleNavigate(url: string) {
|
||||
// #ifdef H5
|
||||
window.open(url, '_blank')
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
copyCommand(url)
|
||||
// #endif
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="min-h-screen py-3">
|
||||
<!-- 头部介绍 -->
|
||||
<view class="mx-3 mb-3">
|
||||
<view class="rounded-3 px-5 py-8 text-center wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-10">
|
||||
🌱
|
||||
</view>
|
||||
<view class="mb-2 text-6 font-bold wot-text-text-main">
|
||||
CreateUni 脚手架
|
||||
</view>
|
||||
<view class="mb-2 text-3.5 leading-relaxed wot-text-text-secondary">
|
||||
由 uni-helper 团队开发的轻量级脚手架工具
|
||||
</view>
|
||||
<view class="text-3 wot-text-text-secondary">
|
||||
提升开发效率 100%,告别繁琐配置,一键集成 WotUI 组件库
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 为什么选择 create-uni -->
|
||||
<demo-block title="为什么选择 create-uni?" transparent>
|
||||
<view class="mb-3 rounded-3 p-3 wot-bg-filled-oppo">
|
||||
<text class="text-3.5 leading-relaxed wot-text-text-secondary">
|
||||
在使用 uni-app 官方 CLI 工具时,我们经常会遇到以下问题:
|
||||
</text>
|
||||
</view>
|
||||
<view class="mb-3">
|
||||
<view
|
||||
v-for="item in problems"
|
||||
:key="item.problem"
|
||||
class="mb-2 rounded-3 p-3 wot-bg-filled-oppo"
|
||||
>
|
||||
<view class="mb-1 text-3.5 text-red-600 font-bold dark:text-red-400">
|
||||
❌ {{ item.problem }}
|
||||
</view>
|
||||
<view class="text-3 leading-snug wot-text-text-secondary">
|
||||
{{ item.desc }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="rounded-3 p-3 wot-bg-filled-oppo">
|
||||
<text class="text-3.5 text-green-600 font-bold dark:text-green-400">
|
||||
✅ 而 create-uni 的出现,完美解决了这些问题!
|
||||
</text>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 核心优势 -->
|
||||
<demo-block title="核心优势" transparent>
|
||||
<view class="grid grid-cols-2 gap-3">
|
||||
<view
|
||||
v-for="advantage in advantages"
|
||||
:key="advantage.title"
|
||||
class="rounded-2 p-4 text-center wot-bg-filled-oppo"
|
||||
>
|
||||
<view class="mb-2 text-6">
|
||||
{{ advantage.icon }}
|
||||
</view>
|
||||
<view class="mb-1 text-3.5 font-bold wot-text-text-main">
|
||||
{{ advantage.title }}
|
||||
</view>
|
||||
<view class="text-3 leading-snug wot-text-text-secondary">
|
||||
{{ advantage.desc }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 快速创建 WotUI 项目 -->
|
||||
<demo-block title="快速创建 WotUI 项目" transparent>
|
||||
<view class="rounded-3 p-5 wot-bg-filled-oppo">
|
||||
<view class="mb-3 flex items-center">
|
||||
<view class="mr-2 text-7">
|
||||
🎨
|
||||
</view>
|
||||
<view class="text-4.5 font-bold wot-text-text-main">
|
||||
{{ wotQuickStart.title }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="mb-3 text-3.5 leading-relaxed wot-text-text-secondary">
|
||||
{{ wotQuickStart.desc }}
|
||||
</view>
|
||||
<view class="wot-bg-bg mb-3 flex items-center justify-between border rounded-2 p-3 wot-border-border-main" @click="copyCommand(wotQuickStart.command)">
|
||||
<text class="flex-1 break-all text-3 font-mono wot-text-text-secondary">
|
||||
{{ wotQuickStart.command }}
|
||||
</text>
|
||||
<wd-icon name="copy" size="16px" color="#666" />
|
||||
</view>
|
||||
<view>
|
||||
<view class="mb-2 text-3.5 font-bold wot-text-text-main">
|
||||
这个命令会:
|
||||
</view>
|
||||
<view
|
||||
v-for="feature in wotQuickStart.features"
|
||||
:key="feature"
|
||||
class="mb-1 text-3 leading-snug wot-text-text-secondary"
|
||||
>
|
||||
{{ feature }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 其他使用方式 -->
|
||||
<demo-block title="其他使用方式" transparent>
|
||||
<view
|
||||
v-for="method in quickMethods"
|
||||
:key="method.title"
|
||||
class="mb-3 rounded-2 p-4 wot-bg-filled-oppo last:mb-0"
|
||||
>
|
||||
<view class="mb-2 text-4 font-bold wot-text-text-main">
|
||||
{{ method.title }}
|
||||
</view>
|
||||
<view class="mb-2 text-3.5 leading-relaxed wot-text-text-secondary">
|
||||
{{ method.desc }}
|
||||
</view>
|
||||
<view class="wot-bg-bg flex items-center justify-between border rounded-2 p-2 wot-border-border-main" @click="copyCommand(method.command)">
|
||||
<text class="flex-1 break-all text-3 font-mono wot-text-text-secondary">
|
||||
{{ method.command }}
|
||||
</text>
|
||||
<wd-icon name="copy" size="16px" color="#666" />
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 相关链接 -->
|
||||
<demo-block title="相关链接" transparent>
|
||||
<wd-cell-group border custom-class="rounded-2! overflow-hidden">
|
||||
<wd-cell title="📦 NPM 包" value="create uni" is-link @click="handleNavigate('https://www.npmjs.com/package/create-uni')" />
|
||||
<wd-cell title="🐙 GitHub 仓库" value="create uni" is-link @click="handleNavigate('https://github.com/uni-helper/create-uni')" />
|
||||
<wd-cell title="🎨 Wot UI" value="UI 组件库" is-link @click="handleNavigate('https://wot-ui.cn/')" />
|
||||
<wd-cell title="🛠️ Uni Helper" value="先进工具集合" is-link @click="handleNavigate('https://github.com/uni-helper')" />
|
||||
<wd-cell title="💝 赞助CreateUni" value="支持开发者" is-link @click="handleNavigate('https://github.com/uni-helper/create-uni')" />
|
||||
</wd-cell-group>
|
||||
</demo-block>
|
||||
</view>
|
||||
</template>
|
||||
@@ -0,0 +1,581 @@
|
||||
<script setup lang="ts">
|
||||
definePage({
|
||||
name: 'feedback',
|
||||
style: {
|
||||
navigationBarTitleText: '全局反馈',
|
||||
},
|
||||
})
|
||||
|
||||
const { success: showSuccess } = useGlobalToast()
|
||||
const { show: showToast, success, error, warning, info } = useGlobalToast()
|
||||
const { loading, close: closeLoading } = useGlobalLoading()
|
||||
const { confirm, alert, prompt } = useGlobalDialog()
|
||||
|
||||
// 表单数据
|
||||
const customMessage = ref('这是一个自定义消息')
|
||||
const loadingText = ref('正在处理中...')
|
||||
|
||||
// Toast 演示方法
|
||||
function demoToastSuccess() {
|
||||
success({ msg: '操作成功!' })
|
||||
}
|
||||
|
||||
function demoToastError() {
|
||||
error({ msg: '操作失败!' })
|
||||
}
|
||||
|
||||
function demoToastWarning() {
|
||||
warning({ msg: '警告提示!' })
|
||||
}
|
||||
|
||||
function demoToastInfo() {
|
||||
info({ msg: '信息提示!' })
|
||||
}
|
||||
|
||||
function demoCustomToast() {
|
||||
if (!customMessage.value.trim()) {
|
||||
warning({ msg: '请输入自定义消息' })
|
||||
return
|
||||
}
|
||||
showToast({
|
||||
msg: customMessage.value,
|
||||
duration: 3000,
|
||||
position: 'middle',
|
||||
})
|
||||
}
|
||||
|
||||
function demoToastPosition(position: 'top' | 'middle' | 'bottom') {
|
||||
showToast({
|
||||
msg: `显示在${position === 'top' ? '顶部' : position === 'middle' ? '中间' : '底部'}的提示`,
|
||||
position,
|
||||
duration: 2000,
|
||||
})
|
||||
}
|
||||
|
||||
// Loading 演示方法
|
||||
function demoBasicLoading() {
|
||||
loading('加载中...')
|
||||
setTimeout(() => {
|
||||
closeLoading()
|
||||
success({ msg: '加载完成!' })
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
function demoCustomLoading() {
|
||||
if (!loadingText.value.trim()) {
|
||||
warning({ msg: '请输入加载文本' })
|
||||
return
|
||||
}
|
||||
loading({
|
||||
msg: loadingText.value,
|
||||
cover: true,
|
||||
})
|
||||
setTimeout(() => {
|
||||
closeLoading()
|
||||
success({ msg: '自定义加载完成!' })
|
||||
}, 2500)
|
||||
}
|
||||
|
||||
function demoLoadingNoCover() {
|
||||
loading({
|
||||
msg: '无遮罩加载',
|
||||
cover: false,
|
||||
})
|
||||
setTimeout(() => {
|
||||
closeLoading()
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
// Message 演示方法
|
||||
function demoConfirm() {
|
||||
confirm({
|
||||
title: '确认操作',
|
||||
msg: '您确定要执行此操作吗?',
|
||||
success: (res) => {
|
||||
if (res.action === 'confirm') {
|
||||
success({ msg: '用户确认了操作' })
|
||||
}
|
||||
else {
|
||||
info({ msg: '用户取消了操作' })
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function demoAlert() {
|
||||
alert({
|
||||
title: '重要提醒',
|
||||
msg: '这是一个重要的提醒信息,请仔细阅读!',
|
||||
success: () => {
|
||||
success({ msg: '用户已阅读提醒' })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function demoPrompt() {
|
||||
prompt({
|
||||
title: '输入信息',
|
||||
msg: '请输入您的姓名:',
|
||||
success: (res) => {
|
||||
if (res.action === 'confirm') {
|
||||
if (res.value && String(res.value).trim()) {
|
||||
success({ msg: `您好,${res.value}!` })
|
||||
}
|
||||
else {
|
||||
warning({ msg: '输入不能为空' })
|
||||
}
|
||||
}
|
||||
else {
|
||||
info({ msg: '用户取消了输入' })
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function demoCustomConfirm() {
|
||||
confirm({
|
||||
title: '自定义确认',
|
||||
msg: '这是一个自定义的确认弹窗,可以配置按钮文字和样式',
|
||||
confirmButtonText: '好的',
|
||||
cancelButtonText: '算了',
|
||||
success: (res) => {
|
||||
if (res.action === 'confirm') {
|
||||
success({ msg: '点击了"好的"' })
|
||||
}
|
||||
else {
|
||||
info({ msg: '点击了"算了"' })
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 组合演示
|
||||
function demoComplex() {
|
||||
loading('准备演示...')
|
||||
|
||||
setTimeout(() => {
|
||||
closeLoading()
|
||||
confirm({
|
||||
title: '组合演示',
|
||||
msg: '这是一个组合演示,将依次展示不同的反馈效果。是否继续?',
|
||||
success: (res) => {
|
||||
if (res.action === 'confirm') {
|
||||
success({ msg: '开始演示' })
|
||||
|
||||
setTimeout(() => {
|
||||
loading('演示进行中...')
|
||||
|
||||
setTimeout(() => {
|
||||
closeLoading()
|
||||
info({ msg: '演示完成!' })
|
||||
}, 2000)
|
||||
}, 1000)
|
||||
}
|
||||
else {
|
||||
info({ msg: '已取消演示' })
|
||||
}
|
||||
},
|
||||
})
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// 代码示例常量
|
||||
const codeExamples = {
|
||||
basicToast: `const { success, error, warning, info } = useGlobalToast()
|
||||
|
||||
// 使用方法
|
||||
success({ msg: '操作成功!' })
|
||||
error({ msg: '操作失败!' })
|
||||
warning({ msg: '警告提示!' })
|
||||
info({ msg: '信息提示!' })`,
|
||||
|
||||
customToast: `const { show } = useGlobalToast()
|
||||
|
||||
show({
|
||||
msg: '自定义消息',
|
||||
duration: 3000,
|
||||
position: 'middle' // 'top' | 'middle' | 'bottom'
|
||||
})`,
|
||||
|
||||
basicLoading: `const { loading, close } = useGlobalLoading()
|
||||
|
||||
// 基础用法
|
||||
loading('加载中...')
|
||||
|
||||
// 手动关闭
|
||||
close()`,
|
||||
|
||||
customLoading: `loading({
|
||||
msg: '正在处理中...',
|
||||
cover: true // 是否显示遮罩
|
||||
})`,
|
||||
|
||||
basicMessage: `const { confirm, alert, prompt } = useGlobalDialog()
|
||||
|
||||
// 确认弹窗
|
||||
confirm({
|
||||
title: '确认操作',
|
||||
msg: '您确定要执行此操作吗?',
|
||||
success: (res) => {
|
||||
if (res.action === 'confirm') {
|
||||
// 用户确认
|
||||
}
|
||||
}
|
||||
})`,
|
||||
|
||||
customMessage: `confirm({
|
||||
title: '自定义确认',
|
||||
msg: '自定义的确认弹窗',
|
||||
confirmButtonText: '好的',
|
||||
cancelButtonText: '算了',
|
||||
success: (res) => {
|
||||
// res.action: 'confirm' | 'cancel'
|
||||
}
|
||||
})`,
|
||||
}
|
||||
|
||||
// 复制代码到剪贴板
|
||||
function copyCode(code: string) {
|
||||
uni.setClipboardData({
|
||||
data: code,
|
||||
showToast: false,
|
||||
success: () => {
|
||||
uni.hideToast()
|
||||
showSuccess({ msg: '代码已复制到剪贴板' })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 链接导航处理
|
||||
function handleNavigate(url: string) {
|
||||
// #ifdef H5
|
||||
window.open(url, '_blank')
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
uni.setClipboardData({
|
||||
data: url,
|
||||
showToast: false,
|
||||
success: () => {
|
||||
uni.hideToast()
|
||||
showSuccess({ msg: `${url} 已复制到剪贴板` })
|
||||
},
|
||||
})
|
||||
// #endif
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="min-h-screen py-3">
|
||||
<!-- 头部介绍 -->
|
||||
<view class="mx-3 mb-3">
|
||||
<view class="rounded-3 px-5 py-8 text-center wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-10">
|
||||
💬
|
||||
</view>
|
||||
<view class="mb-2 text-6 font-bold wot-text-text-main">
|
||||
全局反馈组件
|
||||
</view>
|
||||
<view class="mb-2 text-3.5 leading-relaxed wot-text-text-secondary">
|
||||
GlobalToast / GlobalLoading / GlobalDialog
|
||||
</view>
|
||||
<view class="text-3 wot-text-text-secondary">
|
||||
统一的用户反馈交互体验,支持在路由导航守卫和网络请求拦截器中使用,扩展 WotUI 交互组件能力。
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- GlobalToast 演示 -->
|
||||
<demo-block title="GlobalToast 轻提示" transparent>
|
||||
<view class="space-y-4">
|
||||
<!-- 基础用法 -->
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-4 font-bold wot-text-text-main">
|
||||
基础用法
|
||||
</view>
|
||||
<view class="grid grid-cols-2 mb-3 gap-2">
|
||||
<wd-button type="success" @click="demoToastSuccess">
|
||||
成功提示
|
||||
</wd-button>
|
||||
<wd-button type="danger" @click="demoToastError">
|
||||
错误提示
|
||||
</wd-button>
|
||||
<wd-button type="warning" @click="demoToastWarning">
|
||||
警告提示
|
||||
</wd-button>
|
||||
<wd-button type="info" @click="demoToastInfo">
|
||||
信息提示
|
||||
</wd-button>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="rounded p-3" @click="copyCode(codeExamples.basicToast)"
|
||||
>
|
||||
<view class="mb-2 text-3 font-bold wot-text-text-secondary">
|
||||
代码示例:
|
||||
</view>
|
||||
<view class="whitespace-pre-line text-2.5 leading-relaxed font-mono wot-text-text-secondary">
|
||||
const { success, error, warning, info } = useGlobalToast()
|
||||
|
||||
success({ msg: '操作成功!' })
|
||||
error({ msg: '操作失败!' })
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 自定义用法 -->
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-4 font-bold wot-text-text-main">
|
||||
自定义配置
|
||||
</view>
|
||||
<view class="mb-3">
|
||||
<wd-input v-model="customMessage" placeholder="请输入自定义消息" />
|
||||
</view>
|
||||
<view class="grid grid-cols-2 mb-3 gap-2">
|
||||
<wd-button type="primary" @click="demoCustomToast">
|
||||
自定义消息
|
||||
</wd-button>
|
||||
<wd-button type="success" @click="demoToastPosition('top')">
|
||||
顶部显示
|
||||
</wd-button>
|
||||
<wd-button type="warning" @click="demoToastPosition('middle')">
|
||||
中间显示
|
||||
</wd-button>
|
||||
<wd-button type="info" @click="demoToastPosition('bottom')">
|
||||
底部显示
|
||||
</wd-button>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="rounded p-3" @click="copyCode(codeExamples.customToast)"
|
||||
>
|
||||
<view class="mb-2 text-3 font-bold wot-text-text-secondary">
|
||||
自定义配置:
|
||||
</view>
|
||||
<view class="whitespace-pre-line text-2.5 leading-relaxed font-mono wot-text-text-secondary">
|
||||
show({
|
||||
msg: '自定义消息',
|
||||
duration: 3000,
|
||||
position: 'middle'
|
||||
})
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- GlobalLoading 演示 -->
|
||||
<demo-block title="GlobalLoading 加载提示" transparent>
|
||||
<view class="space-y-4">
|
||||
<!-- 基础用法 -->
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-4 font-bold wot-text-text-main">
|
||||
基础用法
|
||||
</view>
|
||||
<view class="grid grid-cols-2 mb-3 gap-2">
|
||||
<wd-button type="primary" @click="demoBasicLoading">
|
||||
显示加载(3秒)
|
||||
</wd-button>
|
||||
<wd-button type="warning" @click="closeLoading">
|
||||
手动关闭
|
||||
</wd-button>
|
||||
<wd-button type="success" @click="demoLoadingNoCover">
|
||||
无遮罩加载
|
||||
</wd-button>
|
||||
<wd-button type="info" @click="demoComplex">
|
||||
组合演示
|
||||
</wd-button>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="rounded p-3" @click="copyCode(codeExamples.basicLoading)"
|
||||
>
|
||||
<view class="mb-2 text-3 font-bold wot-text-text-secondary">
|
||||
代码示例:
|
||||
</view>
|
||||
<view class="whitespace-pre-line text-2.5 leading-relaxed font-mono wot-text-text-secondary">
|
||||
const { loading, close } = useGlobalLoading()
|
||||
|
||||
loading('加载中...')
|
||||
close()
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 自定义用法 -->
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-4 font-bold wot-text-text-main">
|
||||
自定义配置
|
||||
</view>
|
||||
<view class="mb-3">
|
||||
<wd-input v-model="loadingText" placeholder="请输入加载文本" />
|
||||
</view>
|
||||
<view class="mb-3">
|
||||
<wd-button type="primary" block @click="demoCustomLoading">
|
||||
自定义加载文本
|
||||
</wd-button>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="rounded p-3" @click="copyCode(codeExamples.customLoading)"
|
||||
>
|
||||
<view class="mb-2 text-3 font-bold wot-text-text-secondary">
|
||||
自定义配置:
|
||||
</view>
|
||||
<view class="whitespace-pre-line text-2.5 leading-relaxed font-mono wot-text-text-secondary">
|
||||
loading({
|
||||
msg: '正在处理中...',
|
||||
cover: true // 是否显示遮罩
|
||||
})
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- GlobalDialog 演示 -->
|
||||
<demo-block title="GlobalDialog 弹窗提示" transparent>
|
||||
<view class="space-y-4">
|
||||
<!-- 基础用法 -->
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-4 font-bold wot-text-text-main">
|
||||
基础用法
|
||||
</view>
|
||||
<view class="grid grid-cols-3 mb-3 gap-2">
|
||||
<wd-button type="primary" @click="demoConfirm">
|
||||
确认弹窗
|
||||
</wd-button>
|
||||
<wd-button type="warning" @click="demoAlert">
|
||||
警告弹窗
|
||||
</wd-button>
|
||||
<wd-button type="success" @click="demoPrompt">
|
||||
输入弹窗
|
||||
</wd-button>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="rounded p-3" @click="copyCode(codeExamples.basicMessage)"
|
||||
>
|
||||
<view class="mb-2 text-3 font-bold wot-text-text-secondary">
|
||||
代码示例:
|
||||
</view>
|
||||
<view class="whitespace-pre-line text-2.5 leading-relaxed font-mono wot-text-text-secondary">
|
||||
confirm({
|
||||
title: '确认操作',
|
||||
msg: '您确定要执行此操作吗?',
|
||||
success: (res) => { /* 处理结果 */ }
|
||||
})
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 自定义用法 -->
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-4 font-bold wot-text-text-main">
|
||||
自定义配置
|
||||
</view>
|
||||
<view class="mb-3">
|
||||
<wd-button type="primary" block @click="demoCustomConfirm">
|
||||
自定义按钮文字
|
||||
</wd-button>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="rounded p-3" @click="copyCode(codeExamples.customMessage)"
|
||||
>
|
||||
<view class="mb-2 text-3 font-bold wot-text-text-secondary">
|
||||
自定义按钮:
|
||||
</view>
|
||||
<view class="whitespace-pre-line text-2.5 leading-relaxed font-mono wot-text-text-secondary">
|
||||
confirm({
|
||||
confirmButtonText: '好的',
|
||||
cancelButtonText: '算了'
|
||||
})
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 组件特性 -->
|
||||
<demo-block title="组件特性" transparent>
|
||||
<view class="space-y-3">
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 flex items-center">
|
||||
<view class="mr-2 text-6">
|
||||
🎯
|
||||
</view>
|
||||
<view class="text-4 font-bold wot-text-text-main">
|
||||
设计理念
|
||||
</view>
|
||||
</view>
|
||||
<view class="text-3 leading-relaxed wot-text-text-secondary space-y-2">
|
||||
<view>
|
||||
• <text class="text-blue-600 font-bold dark:text-blue-400">
|
||||
全局单例
|
||||
</text>:整个应用只有一个实例,避免重复创建
|
||||
</view>
|
||||
<view>
|
||||
• <text class="text-green-600 font-bold dark:text-green-400">
|
||||
状态管理
|
||||
</text>:基于 Pinia 管理组件状态,响应式更新
|
||||
</view>
|
||||
<view>
|
||||
• <text class="text-purple-600 font-bold dark:text-purple-400">
|
||||
类型安全
|
||||
</text>:完整的 TypeScript 类型定义
|
||||
</view>
|
||||
<view>
|
||||
• <text class="text-orange-600 font-bold dark:text-orange-400">
|
||||
易于使用
|
||||
</text>:简洁的 API 设计,开箱即用
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 flex items-center">
|
||||
<view class="mr-2 text-6">
|
||||
⚡
|
||||
</view>
|
||||
<view class="text-4 font-bold wot-text-text-main">
|
||||
使用优势
|
||||
</view>
|
||||
</view>
|
||||
<view class="text-3 leading-relaxed wot-text-text-secondary space-y-2">
|
||||
<view>• 统一的用户反馈体验</view>
|
||||
<view>• 避免组件重复创建和销毁</view>
|
||||
<view>• 支持深色模式自动适配</view>
|
||||
<view>• 可在任意组件中调用</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 flex items-center">
|
||||
<view class="mr-2 text-6">
|
||||
📝
|
||||
</view>
|
||||
<view class="text-4 font-bold wot-text-text-main">
|
||||
最佳实践
|
||||
</view>
|
||||
</view>
|
||||
<view class="text-3 leading-relaxed wot-text-text-secondary space-y-2">
|
||||
<view>• Toast:用于简短的状态反馈,不打断用户操作</view>
|
||||
<view>• Loading:用于异步操作的等待提示</view>
|
||||
<view>• Message:用于重要信息确认和用户输入</view>
|
||||
<view>• 合理控制显示时长,避免信息过载</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 相关链接 -->
|
||||
<demo-block title="相关链接" transparent>
|
||||
<wd-cell-group border custom-class="rounded-2! overflow-hidden">
|
||||
<wd-cell title="📚 WotUI Toast" value="轻提示组件" is-link @click="handleNavigate('https://wot-ui.cn/component/toast.html')" />
|
||||
<wd-cell title="🔄 WotUI Loading" value="加载组件" is-link @click="handleNavigate('https://wot-ui.cn/component/loading.html')" />
|
||||
<wd-cell title="💬 WotUI MessageBox" value="弹窗组件" is-link @click="handleNavigate('https://wot-ui.cn/component/message-box.html')" />
|
||||
<wd-cell title="🍍 Pinia 状态管理" value="Vue 状态库" is-link @click="handleNavigate('https://pinia.vuejs.org/zh/')" />
|
||||
</wd-cell-group>
|
||||
</demo-block>
|
||||
</view>
|
||||
</template>
|
||||
@@ -0,0 +1,483 @@
|
||||
<script setup lang="ts">
|
||||
definePage({
|
||||
name: 'icon',
|
||||
style: {
|
||||
navigationBarTitleText: '图标演示',
|
||||
},
|
||||
})
|
||||
|
||||
const { success: showSuccess } = useGlobalToast()
|
||||
|
||||
// WotUI 内置图标列表(精选9个)
|
||||
const wotIcons = ref([
|
||||
{ name: 'add', desc: '添加' },
|
||||
{ name: 'close', desc: '关闭' },
|
||||
{ name: 'check', desc: '勾选' },
|
||||
{ name: 'arrow-right', desc: '右箭头' },
|
||||
{ name: 'bags', desc: '购物袋' },
|
||||
{ name: 'search', desc: '搜索' },
|
||||
{ name: 'star', desc: '星标' },
|
||||
{ name: 'heart', desc: '喜欢' },
|
||||
{ name: 'setting', desc: '设置' },
|
||||
])
|
||||
|
||||
// Carbon 图标是编译时的,不能动态渲染,所以直接在模板中静态写入
|
||||
|
||||
// 图标大小选项
|
||||
const iconSizes = ref([
|
||||
{ name: '小', value: '16px' },
|
||||
{ name: '中', value: '20px' },
|
||||
{ name: '大', value: '24px' },
|
||||
{ name: '超大', value: '32px' },
|
||||
])
|
||||
const currentIconSize = ref('20px')
|
||||
|
||||
// 复制图标代码
|
||||
function copyIconCode(iconName: string, isCarbon = false) {
|
||||
const code = isCarbon
|
||||
? `<div class="i-carbon:${iconName} text-xl"></div>`
|
||||
: `<wd-icon name="${iconName}" size="20px" />`
|
||||
|
||||
uni.setClipboardData({
|
||||
data: code,
|
||||
showToast: false,
|
||||
success: () => {
|
||||
uni.hideToast()
|
||||
showSuccess({ msg: '代码已复制到剪贴板' })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 链接导航处理
|
||||
function handleNavigate(url: string) {
|
||||
// #ifdef H5
|
||||
window.open(url, '_blank')
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
uni.setClipboardData({
|
||||
data: url,
|
||||
showToast: false,
|
||||
success: () => {
|
||||
uni.hideToast()
|
||||
showSuccess({ msg: `${url} 已复制到剪贴板` })
|
||||
},
|
||||
})
|
||||
// #endif
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="min-h-screen py-3">
|
||||
<!-- 头部介绍 -->
|
||||
<view class="mx-3 mb-3">
|
||||
<view class="rounded-3 px-5 py-8 text-center wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-10">
|
||||
🎯
|
||||
</view>
|
||||
<view class="mb-2 text-6 font-bold wot-text-text-main">
|
||||
图标使用演示
|
||||
</view>
|
||||
<view class="mb-2 text-3.5 leading-relaxed wot-text-text-secondary">
|
||||
WotUI 图标 & Iconify 图标集
|
||||
</view>
|
||||
<view class="text-3 wot-text-text-secondary">
|
||||
丰富的图标资源,满足各种使用场景
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 图标大小选择 -->
|
||||
<demo-block title="图标大小" transparent>
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-4 font-bold wot-text-text-main">
|
||||
选择图标大小
|
||||
</view>
|
||||
<view class="mb-4 flex flex-wrap gap-2">
|
||||
<view
|
||||
v-for="size in iconSizes"
|
||||
:key="size.value"
|
||||
class="rounded-2 px-3 py-2 text-3 transition-all duration-200"
|
||||
:class="[
|
||||
currentIconSize === size.value
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'wot-text-text-secondary hover:bg-gray-200 dark:hover:bg-[var(--wot-dark-border)]',
|
||||
]"
|
||||
@click="currentIconSize = size.value"
|
||||
>
|
||||
{{ size.name }} ({{ size.value }})
|
||||
</view>
|
||||
</view>
|
||||
<view class="wot-bg-bg flex items-center justify-center rounded-2 p-4 space-x-4">
|
||||
<wd-icon name="star" :size="currentIconSize" color="#f59e0b" />
|
||||
<view class="i-carbon:star text-yellow-500" :style="{ fontSize: currentIconSize }" />
|
||||
<text class="text-3 wot-text-text-secondary">
|
||||
当前大小: {{ currentIconSize }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- WotUI 图标 -->
|
||||
<demo-block title="WotUI 内置图标(精选示例)" transparent>
|
||||
<!-- 说明文字 -->
|
||||
<view class="mb-4 border border-blue-200 rounded-2 bg-blue-50 p-3 dark:border-blue-800 dark:bg-blue-900/20">
|
||||
<view class="mb-1 text-3.5 text-blue-700 font-bold dark:text-blue-300">
|
||||
💡 说明
|
||||
</view>
|
||||
<view class="text-3 text-blue-600 leading-relaxed dark:text-blue-200">
|
||||
这里只展示了部分常用图标作为示例。完整的图标列表请查看
|
||||
<text class="text-blue-700 underline dark:text-blue-300" @click="handleNavigate('https://wot-ui.cn/component/icon.html')">
|
||||
WotUI 官方文档
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 图标网格 -->
|
||||
<view class="grid grid-cols-3 gap-3 md:grid-cols-6 sm:grid-cols-4">
|
||||
<view
|
||||
v-for="icon in wotIcons"
|
||||
:key="icon.name"
|
||||
class="rounded-2 p-4 text-center transition-all duration-200 wot-bg-filled-oppo active:scale-95 hover:shadow-md"
|
||||
@click="copyIconCode(icon.name)"
|
||||
>
|
||||
<view class="mb-2 flex justify-center">
|
||||
<wd-icon :name="icon.name" :size="currentIconSize" color="#666" />
|
||||
</view>
|
||||
<view class="mb-1 text-2.5 font-mono wot-text-text-main">
|
||||
{{ icon.name }}
|
||||
</view>
|
||||
<view class="text-2 wot-text-text-secondary">
|
||||
{{ icon.desc }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 使用示例 -->
|
||||
<view class="mt-4 rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-4 font-bold wot-text-text-main">
|
||||
使用示例
|
||||
</view>
|
||||
<view class="space-y-3">
|
||||
<view class="rounded p-3">
|
||||
<view class="mb-2 text-3 font-bold wot-text-text-secondary">
|
||||
基础用法:
|
||||
</view>
|
||||
<view class="text-2.5 leading-relaxed font-mono wot-text-text-secondary">
|
||||
<wd-icon name="star" size="20px" color="#f59e0b" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="rounded p-3">
|
||||
<view class="mb-2 text-3 font-bold wot-text-text-secondary">
|
||||
自定义样式:
|
||||
</view>
|
||||
<view class="text-2.5 leading-relaxed font-mono wot-text-text-secondary">
|
||||
<wd-icon name="home" size="24px" color="var(--wot-color-theme)" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- Carbon 图标 -->
|
||||
<demo-block title="Carbon 图标集" transparent>
|
||||
<view class="mb-4 border border-blue-200 rounded-2 bg-blue-50 p-3 dark:border-blue-800 dark:bg-blue-900/20">
|
||||
<view class="mb-1 text-3.5 text-blue-700 font-bold dark:text-blue-300">
|
||||
💡 说明
|
||||
</view>
|
||||
<view class="text-3 text-blue-600 leading-relaxed dark:text-blue-200">
|
||||
Iconify 图标集资源异常丰富,这里我们以 Carbon 图标集为例,展示如何使用图标。
|
||||
<text class="text-blue-700 underline dark:text-blue-300" @click="handleNavigate('https://icones.js.org/collection/carbon')">
|
||||
Carbon 图标集
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 图标网格 - 静态渲染 -->
|
||||
<view class="grid grid-cols-3 gap-3 md:grid-cols-6 sm:grid-cols-4">
|
||||
<!-- add -->
|
||||
<view
|
||||
class="rounded-2 p-4 text-center transition-all duration-200 wot-bg-filled-oppo active:scale-95 hover:shadow-md"
|
||||
@click="copyIconCode('add', true)"
|
||||
>
|
||||
<view class="mb-2 flex justify-center">
|
||||
<view class="i-carbon:add wot-text-text-secondary" :style="{ fontSize: currentIconSize }" />
|
||||
</view>
|
||||
<view class="mb-1 text-2.5 font-mono wot-text-text-main">
|
||||
add
|
||||
</view>
|
||||
<view class="text-2 wot-text-text-secondary">
|
||||
添加
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- close -->
|
||||
<view
|
||||
class="rounded-2 p-4 text-center transition-all duration-200 wot-bg-filled-oppo active:scale-95 hover:shadow-md"
|
||||
@click="copyIconCode('close', true)"
|
||||
>
|
||||
<view class="mb-2 flex justify-center">
|
||||
<view class="i-carbon:close wot-text-text-secondary" :style="{ fontSize: currentIconSize }" />
|
||||
</view>
|
||||
<view class="mb-1 text-2.5 font-mono wot-text-text-main">
|
||||
close
|
||||
</view>
|
||||
<view class="text-2 wot-text-text-secondary">
|
||||
关闭
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- checkmark -->
|
||||
<view
|
||||
class="rounded-2 p-4 text-center transition-all duration-200 wot-bg-filled-oppo active:scale-95 hover:shadow-md"
|
||||
@click="copyIconCode('checkmark', true)"
|
||||
>
|
||||
<view class="mb-2 flex justify-center">
|
||||
<view class="i-carbon:checkmark wot-text-text-secondary" :style="{ fontSize: currentIconSize }" />
|
||||
</view>
|
||||
<view class="mb-1 text-2.5 font-mono wot-text-text-main">
|
||||
checkmark
|
||||
</view>
|
||||
<view class="text-2 wot-text-text-secondary">
|
||||
确认
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- arrow-right -->
|
||||
<view
|
||||
class="rounded-2 p-4 text-center transition-all duration-200 wot-bg-filled-oppo active:scale-95 hover:shadow-md"
|
||||
@click="copyIconCode('arrow-right', true)"
|
||||
>
|
||||
<view class="mb-2 flex justify-center">
|
||||
<view class="i-carbon:arrow-right wot-text-text-secondary" :style="{ fontSize: currentIconSize }" />
|
||||
</view>
|
||||
<view class="mb-1 text-2.5 font-mono wot-text-text-main">
|
||||
arrow-right
|
||||
</view>
|
||||
<view class="text-2 wot-text-text-secondary">
|
||||
右箭头
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- home -->
|
||||
<view
|
||||
class="rounded-2 p-4 text-center transition-all duration-200 wot-bg-filled-oppo active:scale-95 hover:shadow-md"
|
||||
@click="copyIconCode('home', true)"
|
||||
>
|
||||
<view class="mb-2 flex justify-center">
|
||||
<view class="i-carbon:home wot-text-text-secondary" :style="{ fontSize: currentIconSize }" />
|
||||
</view>
|
||||
<view class="mb-1 text-2.5 font-mono wot-text-text-main">
|
||||
home
|
||||
</view>
|
||||
<view class="text-2 wot-text-text-secondary">
|
||||
首页
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- search -->
|
||||
<view
|
||||
class="rounded-2 p-4 text-center transition-all duration-200 wot-bg-filled-oppo active:scale-95 hover:shadow-md"
|
||||
@click="copyIconCode('search', true)"
|
||||
>
|
||||
<view class="mb-2 flex justify-center">
|
||||
<view class="i-carbon:search wot-text-text-secondary" :style="{ fontSize: currentIconSize }" />
|
||||
</view>
|
||||
<view class="mb-1 text-2.5 font-mono wot-text-text-main">
|
||||
search
|
||||
</view>
|
||||
<view class="text-2 wot-text-text-secondary">
|
||||
搜索
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- star -->
|
||||
<view
|
||||
class="rounded-2 p-4 text-center transition-all duration-200 wot-bg-filled-oppo active:scale-95 hover:shadow-md"
|
||||
@click="copyIconCode('star', true)"
|
||||
>
|
||||
<view class="mb-2 flex justify-center">
|
||||
<view class="i-carbon:star wot-text-text-secondary" :style="{ fontSize: currentIconSize }" />
|
||||
</view>
|
||||
<view class="mb-1 text-2.5 font-mono wot-text-text-main">
|
||||
star
|
||||
</view>
|
||||
<view class="text-2 wot-text-text-secondary">
|
||||
星级
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- favorite -->
|
||||
<view
|
||||
class="rounded-2 p-4 text-center transition-all duration-200 wot-bg-filled-oppo active:scale-95 hover:shadow-md"
|
||||
@click="copyIconCode('favorite', true)"
|
||||
>
|
||||
<view class="mb-2 flex justify-center">
|
||||
<view class="i-carbon:favorite wot-text-text-secondary" :style="{ fontSize: currentIconSize }" />
|
||||
</view>
|
||||
<view class="mb-1 text-2.5 font-mono wot-text-text-main">
|
||||
favorite
|
||||
</view>
|
||||
<view class="text-2 wot-text-text-secondary">
|
||||
收藏
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- settings -->
|
||||
<view
|
||||
class="rounded-2 p-4 text-center transition-all duration-200 wot-bg-filled-oppo active:scale-95 hover:shadow-md"
|
||||
@click="copyIconCode('settings', true)"
|
||||
>
|
||||
<view class="mb-2 flex justify-center">
|
||||
<view class="i-carbon:settings wot-text-text-secondary" :style="{ fontSize: currentIconSize }" />
|
||||
</view>
|
||||
<view class="mb-1 text-2.5 font-mono wot-text-text-main">
|
||||
settings
|
||||
</view>
|
||||
<view class="text-2 wot-text-text-secondary">
|
||||
设置
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 使用示例 -->
|
||||
<view class="mt-4 rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-4 font-bold wot-text-text-main">
|
||||
使用示例
|
||||
</view>
|
||||
<view class="space-y-3">
|
||||
<view class="rounded p-3">
|
||||
<view class="mb-2 text-3 font-bold wot-text-text-secondary">
|
||||
UnoCSS 方式:
|
||||
</view>
|
||||
<view class="text-2.5 leading-relaxed font-mono wot-text-text-secondary">
|
||||
<div class="i-carbon:star text-xl text-yellow-500"></div>
|
||||
</view>
|
||||
</view>
|
||||
<view class="rounded p-3">
|
||||
<view class="mb-2 text-3 font-bold wot-text-text-secondary">
|
||||
自定义大小和颜色:
|
||||
</view>
|
||||
<view class="text-2.5 leading-relaxed font-mono wot-text-text-secondary">
|
||||
<div class="i-carbon:home" style="font-size: 24px; color: #3b82f6;"></div>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 对比演示 -->
|
||||
<demo-block title="图标对比" transparent>
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-4 text-4 font-bold wot-text-text-main">
|
||||
相同功能图标对比
|
||||
</view>
|
||||
<view class="space-y-4">
|
||||
<view class="grid grid-cols-2 gap-4">
|
||||
<view class="rounded-2 bg-blue-50 p-4 text-center dark:bg-blue-900/20">
|
||||
<view class="mb-2 text-3.5 text-blue-700 font-bold dark:text-blue-300">
|
||||
WotUI 图标
|
||||
</view>
|
||||
<view class="mb-3 flex justify-center space-x-4">
|
||||
<wd-icon name="home" :size="20" color="#3b82f6" />
|
||||
<wd-icon name="search1" :size="20" color="#3b82f6" />
|
||||
<wd-icon name="star" :size="20" color="#3b82f6" />
|
||||
<wd-icon name="heart" :size="20" color="#3b82f6" />
|
||||
</view>
|
||||
<view class="text-2.5 text-blue-600 dark:text-blue-200">
|
||||
组件化使用,API统一
|
||||
</view>
|
||||
</view>
|
||||
<view class="rounded-2 bg-green-50 p-4 text-center dark:bg-green-900/20">
|
||||
<view class="mb-2 text-3.5 text-green-700 font-bold dark:text-green-300">
|
||||
Iconify 图标
|
||||
</view>
|
||||
<view class="mb-3 flex justify-center space-x-4">
|
||||
<view class="i-carbon:home text-20px text-green-600" />
|
||||
<view class="i-carbon:search text-20px text-green-600" />
|
||||
<view class="i-carbon:star text-20px text-green-600" />
|
||||
<view class="i-carbon:favorite text-20px text-green-600" />
|
||||
</view>
|
||||
<view class="text-2.5 text-green-600 dark:text-green-200">
|
||||
原子化CSS,更加灵活
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 使用建议 -->
|
||||
<demo-block title="使用建议" transparent>
|
||||
<view class="space-y-3">
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 flex items-center">
|
||||
<view class="mr-2 text-6">
|
||||
💡
|
||||
</view>
|
||||
<view class="text-4 font-bold wot-text-text-main">
|
||||
WotUI 图标适用场景
|
||||
</view>
|
||||
</view>
|
||||
<view class="text-3 leading-relaxed wot-text-text-secondary space-y-2">
|
||||
<view>• 与WotUI组件配合使用时</view>
|
||||
<view>• 需要统一的图标API时</view>
|
||||
<view>• 快速开发,不需要过多自定义时</view>
|
||||
<view>• 移动端应用,需要保证兼容性时</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 flex items-center">
|
||||
<view class="mr-2 text-6">
|
||||
🎨
|
||||
</view>
|
||||
<view class="text-4 font-bold wot-text-text-main">
|
||||
Iconify 图标适用场景
|
||||
</view>
|
||||
</view>
|
||||
<view class="text-3 leading-relaxed wot-text-text-secondary space-y-2">
|
||||
<view>• 需要更多图标选择时</view>
|
||||
<view>• 使用UnoCSS原子化CSS时</view>
|
||||
<view>• 需要灵活自定义样式时</view>
|
||||
<view>• 追求更小的包体积时</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 flex items-center">
|
||||
<view class="mr-2 text-6">
|
||||
🤝
|
||||
</view>
|
||||
<view class="text-4 font-bold wot-text-text-main">
|
||||
结合使用建议
|
||||
</view>
|
||||
</view>
|
||||
<view class="text-3 leading-relaxed wot-text-text-secondary space-y-2">
|
||||
<view>
|
||||
• <text class="text-blue-600 font-bold dark:text-blue-400">
|
||||
WotUI图标
|
||||
</text>:用于组件内置图标,如按钮、表单等
|
||||
</view>
|
||||
<view>
|
||||
• <text class="text-green-600 font-bold dark:text-green-400">
|
||||
Iconify图标
|
||||
</text>:用于装饰性图标,如页面图标、状态图标等
|
||||
</view>
|
||||
<view>• 两者可以在同一个项目中混用,各取所长</view>
|
||||
<view>• 建议制定团队规范,明确使用场景</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 相关链接 -->
|
||||
<demo-block title="相关链接" transparent>
|
||||
<wd-cell-group border custom-class="rounded-2! overflow-hidden">
|
||||
<wd-cell title="📚 WotUI 图标文档" value="组件库文档" is-link @click="handleNavigate('https://wot-ui.cn/component/icon.html')" />
|
||||
<wd-cell title="🎯 Carbon 图标集" value="IBM设计系统" is-link @click="handleNavigate('https://icones.js.org/collection/carbon')" />
|
||||
<wd-cell title="🔧 Iconify 图标集" value="巨量图标合集" is-link @click="handleNavigate('https://iconify.design/')" />
|
||||
<wd-cell title="⚡ UnoCSS 图标" value="原子化CSS图标" is-link @click="handleNavigate('https://unocss.dev/presets/icons')" />
|
||||
</wd-cell-group>
|
||||
</demo-block>
|
||||
</view>
|
||||
</template>
|
||||
@@ -0,0 +1,137 @@
|
||||
<script setup lang="ts">
|
||||
definePage({
|
||||
name: 'home-demo',
|
||||
layout: 'default',
|
||||
style: {
|
||||
navigationBarTitleText: 'wot示例',
|
||||
},
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const {
|
||||
theme,
|
||||
toggleTheme,
|
||||
currentThemeColor,
|
||||
showThemeColorSheet,
|
||||
themeColorOptions,
|
||||
openThemeColorPicker,
|
||||
closeThemeColorPicker,
|
||||
selectThemeColor,
|
||||
setFollowSystem,
|
||||
} = useManualTheme()
|
||||
|
||||
const isDark = computed({
|
||||
get() {
|
||||
return theme.value === 'dark'
|
||||
},
|
||||
set() {
|
||||
toggleTheme()
|
||||
},
|
||||
})
|
||||
|
||||
// 页面跳转方法
|
||||
function navigateTo(name: string) {
|
||||
router.push({
|
||||
name,
|
||||
})
|
||||
}
|
||||
|
||||
// 处理主题色选择
|
||||
function handleThemeColorSelect(option: any) {
|
||||
selectThemeColor(option)
|
||||
}
|
||||
|
||||
function openUrl(url: string) {
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="box-border py-3">
|
||||
<view class="mx-3 box-border rounded-3 px-4 py-6 text-center wot-bg-filled-oppo">
|
||||
<text class="mb-3 block text-left text-5 font-bold wot-text-text-main">
|
||||
Wot Starter
|
||||
</text>
|
||||
<text class="mb-3 block text-left text-30rpx leading-relaxed wot-text-text-secondary">
|
||||
⚡️ 基于 vitesse-uni-app 由 vite & uni-app 驱动的、深度整合 Wot UI 组件库的快速启动模板
|
||||
</text>
|
||||
<text class="block text-left text-3 leading-relaxed wot-text-text-auxiliary">
|
||||
背靠 Uni Helper、Wot UI 团队,告别 HBuilderX ,拥抱现代前端开发工具链
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<demo-block title="基础设置" transparent>
|
||||
<wd-cell-group border custom-class="rounded-2! overflow-hidden">
|
||||
<wd-cell title="暗黑模式">
|
||||
<wd-switch v-model="isDark" size="18px" />
|
||||
</wd-cell>
|
||||
<wd-cell title="跟随系统">
|
||||
<wd-button size="small" @click="setFollowSystem(true)">
|
||||
跟随系统
|
||||
</wd-button>
|
||||
</wd-cell>
|
||||
<wd-cell title="选择主题色" is-link @click="openThemeColorPicker">
|
||||
<view class="flex items-center justify-end gap-2">
|
||||
<view
|
||||
class="h-4 w-4 rounded-full"
|
||||
:style="{ backgroundColor: currentThemeColor.primary }"
|
||||
/>
|
||||
<text>{{ currentThemeColor.name }}</text>
|
||||
</view>
|
||||
</wd-cell>
|
||||
</wd-cell-group>
|
||||
</demo-block>
|
||||
|
||||
<demo-block title="工具链介绍" transparent>
|
||||
<wd-cell-group border custom-class="rounded-2! overflow-hidden">
|
||||
<wd-cell title="🧩 WotUI组件库" is-link @click="openUrl('https://wot-ui.cn/')" />
|
||||
<!-- <wd-cell title="🧠 Agent Skills" is-link @click="navigateTo('skills')" /> -->
|
||||
|
||||
<wd-cell title="🚦 Router 路由管理" is-link @click="navigateTo('router')" />
|
||||
<wd-cell title="🌐 Alova 网络请求" is-link @click="navigateTo('request')" />
|
||||
<wd-cell title="🎨 Icon 图标" is-link @click="navigateTo('icon')" />
|
||||
<wd-cell title="✨ Unocss 原子化" is-link @click="navigateTo('styles')" />
|
||||
<wd-cell title="🍍 Pinia 持久化" is-link @click="navigateTo('pinia')" />
|
||||
<wd-cell title="💬 Fedback 反馈组件" is-link @click="navigateTo('feedback')" />
|
||||
<!-- <wd-cell title="🌱 CreateUni 脚手架" is-link @click="navigateTo('create-uni') " /> -->
|
||||
<wd-cell title="🔄 CI/CD 持续集成" is-link @click="navigateTo('ci')" />
|
||||
<wd-cell title="🦾 uni-ku/root" is-link @click="navigateTo('root')" />
|
||||
<wd-cell title="📊 uni-echarts" is-link @click="navigateTo('echarts')" />
|
||||
</wd-cell-group>
|
||||
</demo-block>
|
||||
|
||||
<!-- 主题色选择 ActionSheet -->
|
||||
<wd-action-sheet
|
||||
v-model="showThemeColorSheet"
|
||||
title="选择主题色"
|
||||
:close-on-click-action="true"
|
||||
@cancel="closeThemeColorPicker"
|
||||
>
|
||||
<view class="px-4 pb-4">
|
||||
<view
|
||||
v-for="option in themeColorOptions"
|
||||
:key="option.value"
|
||||
class="flex items-center justify-between border-b py-3 wot-border-border-main last:border-b-0"
|
||||
@click="handleThemeColorSelect(option)"
|
||||
>
|
||||
<view class="flex items-center gap-3">
|
||||
<view
|
||||
class="h-6 w-6 border-2 rounded-full wot-border-border-main"
|
||||
:style="{ backgroundColor: option.primary }"
|
||||
/>
|
||||
<text class="text-4 wot-text-text-main">
|
||||
{{ option.name }}
|
||||
</text>
|
||||
</view>
|
||||
<wd-icon
|
||||
v-if="currentThemeColor.value === option.value"
|
||||
name="check"
|
||||
:color="option.primary"
|
||||
size="20px"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<wd-gap :height="50" />
|
||||
</wd-action-sheet>
|
||||
</view>
|
||||
</template>
|
||||
@@ -0,0 +1,547 @@
|
||||
<script setup lang="ts">
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
definePage({
|
||||
name: 'pinia',
|
||||
style: {
|
||||
navigationBarTitleText: 'Pinia 演示',
|
||||
},
|
||||
})
|
||||
|
||||
const { success: showSuccess } = useGlobalToast()
|
||||
|
||||
// 定义一个基础的计数器store
|
||||
const useCounterStore = defineStore('counter', () => {
|
||||
const count = ref(0)
|
||||
const doubleCount = computed(() => count.value * 2)
|
||||
|
||||
function increment() {
|
||||
count.value++
|
||||
}
|
||||
|
||||
function decrement() {
|
||||
count.value--
|
||||
}
|
||||
|
||||
function reset() {
|
||||
count.value = 0
|
||||
}
|
||||
|
||||
return {
|
||||
count,
|
||||
doubleCount,
|
||||
increment,
|
||||
decrement,
|
||||
reset,
|
||||
}
|
||||
})
|
||||
|
||||
// 定义一个用户信息store(演示持久化)
|
||||
const useUserStore = defineStore('user', () => {
|
||||
const userInfo = ref({
|
||||
name: '',
|
||||
email: '',
|
||||
avatar: '',
|
||||
theme: 'light',
|
||||
})
|
||||
|
||||
const isLoggedIn = computed(() => !!userInfo.value.name)
|
||||
|
||||
function setUserInfo(info: Partial<typeof userInfo.value>) {
|
||||
userInfo.value = { ...userInfo.value, ...info }
|
||||
}
|
||||
|
||||
function clearUserInfo() {
|
||||
userInfo.value = {
|
||||
name: '',
|
||||
email: '',
|
||||
avatar: '',
|
||||
theme: 'light',
|
||||
}
|
||||
}
|
||||
|
||||
function toggleTheme() {
|
||||
userInfo.value.theme = userInfo.value.theme === 'light' ? 'dark' : 'light'
|
||||
}
|
||||
|
||||
return {
|
||||
userInfo,
|
||||
isLoggedIn,
|
||||
setUserInfo,
|
||||
clearUserInfo,
|
||||
toggleTheme,
|
||||
}
|
||||
})
|
||||
|
||||
// 定义一个临时store(不持久化)
|
||||
const useTempStore = defineStore('temp', () => {
|
||||
const tempData = ref({
|
||||
message: '',
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
|
||||
function setMessage(msg: string) {
|
||||
tempData.value.message = msg
|
||||
tempData.value.timestamp = Date.now()
|
||||
}
|
||||
|
||||
function clearMessage() {
|
||||
tempData.value.message = ''
|
||||
tempData.value.timestamp = Date.now()
|
||||
}
|
||||
|
||||
return {
|
||||
tempData,
|
||||
setMessage,
|
||||
clearMessage,
|
||||
}
|
||||
})
|
||||
|
||||
// 使用stores
|
||||
const counterStore = useCounterStore()
|
||||
const userStore = useUserStore()
|
||||
const tempStore = useTempStore()
|
||||
|
||||
// 表单数据
|
||||
const userForm = ref({
|
||||
name: '',
|
||||
email: '',
|
||||
})
|
||||
|
||||
const tempMessage = ref('')
|
||||
|
||||
// 复制代码到剪贴板
|
||||
function copyCode(code: string) {
|
||||
uni.setClipboardData({
|
||||
data: code,
|
||||
showToast: false,
|
||||
success: () => {
|
||||
uni.hideToast()
|
||||
showSuccess({ msg: '代码已复制到剪贴板' })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 保存用户信息
|
||||
function saveUserInfo() {
|
||||
if (!userForm.value.name) {
|
||||
uni.showToast({
|
||||
title: '请输入用户名',
|
||||
icon: 'none',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
userStore.setUserInfo({
|
||||
name: userForm.value.name,
|
||||
email: userForm.value.email,
|
||||
avatar: '😆',
|
||||
})
|
||||
|
||||
showSuccess({ msg: '用户信息已保存(会持久化)' })
|
||||
}
|
||||
|
||||
// 设置临时消息
|
||||
function setTempMessage() {
|
||||
if (!tempMessage.value) {
|
||||
uni.showToast({
|
||||
title: '请输入消息内容',
|
||||
icon: 'none',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
tempStore.setMessage(tempMessage.value)
|
||||
tempMessage.value = ''
|
||||
showSuccess({ msg: '临时消息已设置(不会持久化)' })
|
||||
}
|
||||
|
||||
// 链接导航处理
|
||||
function handleNavigate(url: string) {
|
||||
// #ifdef H5
|
||||
window.open(url, '_blank')
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
uni.setClipboardData({
|
||||
data: url,
|
||||
showToast: false,
|
||||
success: () => {
|
||||
uni.hideToast()
|
||||
showSuccess({ msg: `${url} 已复制到剪贴板` })
|
||||
},
|
||||
})
|
||||
// #endif
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="min-h-screen py-3">
|
||||
<!-- 头部介绍 -->
|
||||
<view class="mx-3 mb-3">
|
||||
<view class="rounded-3 px-5 py-8 text-center wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-10">
|
||||
🍍
|
||||
</view>
|
||||
<view class="mb-2 text-6 font-bold wot-text-text-main">
|
||||
Pinia 状态管理
|
||||
</view>
|
||||
<view class="mb-2 text-3.5 leading-relaxed wot-text-text-secondary">
|
||||
Vue 3 的直观状态管理库
|
||||
</view>
|
||||
<view class="mb-2 text-3 wot-text-text-secondary">
|
||||
轻量、类型安全、支持组合式API
|
||||
</view>
|
||||
<view class="text-3 wot-text-text-secondary">
|
||||
本项目默认持久化所有 Store 数据,开发者可以配置排除列表
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 基础计数器演示 -->
|
||||
<demo-block title="基础使用 - 计数器" transparent>
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-4 text-center">
|
||||
<view class="mb-2 text-8 text-blue-500">
|
||||
{{ counterStore.count }}
|
||||
</view>
|
||||
<view class="text-3 wot-text-text-secondary">
|
||||
双倍值: {{ counterStore.doubleCount }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="grid grid-cols-3 mb-4 gap-2">
|
||||
<wd-button type="success" size="small" @click="counterStore.increment">
|
||||
+1
|
||||
</wd-button>
|
||||
<wd-button type="warning" size="small" @click="counterStore.decrement">
|
||||
-1
|
||||
</wd-button>
|
||||
<wd-button type="info" size="small" @click="counterStore.reset">
|
||||
重置
|
||||
</wd-button>
|
||||
</view>
|
||||
|
||||
<!-- 代码示例 -->
|
||||
<view class="space-y-3">
|
||||
<view class="rounded p-3">
|
||||
<view class="mb-2 text-3 font-bold wot-text-text-secondary">
|
||||
定义 Store:
|
||||
</view>
|
||||
<view
|
||||
class="text-2.5 leading-relaxed font-mono wot-text-text-secondary" @click="copyCode(`const useCounterStore = defineStore('counter', () => {
|
||||
const count = ref(0)
|
||||
const doubleCount = computed(() => count.value * 2)
|
||||
|
||||
function increment() {
|
||||
count.value++
|
||||
}
|
||||
|
||||
return { count, doubleCount, increment }
|
||||
})`)"
|
||||
>
|
||||
const useCounterStore = defineStore('counter', () => {\n
|
||||
const count = ref(0)\n
|
||||
const doubleCount = computed(() => count.value * 2)\n
|
||||
function increment() { count.value++ }\n
|
||||
return { count, doubleCount, increment }\n
|
||||
})
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="rounded p-3">
|
||||
<view class="mb-2 text-3 font-bold wot-text-text-secondary">
|
||||
使用 Store:
|
||||
</view>
|
||||
<view
|
||||
class="text-2.5 leading-relaxed font-mono wot-text-text-secondary" @click="copyCode(`const counterStore = useCounterStore()
|
||||
// 直接使用
|
||||
counterStore.increment()
|
||||
// 响应式访问
|
||||
console.log(counterStore.count)`)"
|
||||
>
|
||||
const counterStore = useCounterStore()\n
|
||||
counterStore.increment() // 调用方法\n
|
||||
console.log(counterStore.count) // 访问状态
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 用户信息演示(持久化) -->
|
||||
<demo-block title="持久化演示 - 用户信息" transparent>
|
||||
<view class="mb-3 border border-green-200 rounded-2 bg-green-50 p-3 dark:border-green-800 dark:bg-green-900/20">
|
||||
<view class="mb-1 text-3.5 text-green-700 font-bold dark:text-green-300">
|
||||
✅ 持久化存储
|
||||
</view>
|
||||
<view class="text-3 text-green-600 leading-relaxed dark:text-green-200">
|
||||
这个 Store 的数据会自动保存到本地存储,刷新页面后数据不会丢失
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="space-y-4">
|
||||
<!-- 当前用户信息 -->
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-4 font-bold wot-text-text-main">
|
||||
当前用户信息
|
||||
</view>
|
||||
|
||||
<view v-if="userStore.isLoggedIn" class="flex items-center space-x-3">
|
||||
<view class="rounded-full text-12">
|
||||
{{ userStore.userInfo.avatar }}
|
||||
</view>
|
||||
<view class="flex-1">
|
||||
<view class="text-4 font-bold wot-text-text-main">
|
||||
{{ userStore.userInfo.name }}
|
||||
</view>
|
||||
<view class="text-3 wot-text-text-secondary">
|
||||
{{ userStore.userInfo.email }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else class="text-center wot-text-text-secondary">
|
||||
暂无用户信息
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 用户信息表单 -->
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-4 font-bold wot-text-text-main">
|
||||
设置用户信息
|
||||
</view>
|
||||
|
||||
<view class="space-y-3">
|
||||
<view>
|
||||
<view class="mb-1 text-3 wot-text-text-secondary">
|
||||
用户名:
|
||||
</view>
|
||||
<wd-input v-model="userForm.name" placeholder="请输入用户名" />
|
||||
</view>
|
||||
|
||||
<view>
|
||||
<view class="mb-1 text-3 wot-text-text-secondary">
|
||||
邮箱:
|
||||
</view>
|
||||
<wd-input v-model="userForm.email" placeholder="请输入邮箱" />
|
||||
</view>
|
||||
|
||||
<view class="grid grid-cols-2 gap-2">
|
||||
<wd-button type="primary" @click="saveUserInfo">
|
||||
保存信息
|
||||
</wd-button>
|
||||
<wd-button type="danger" @click="userStore.clearUserInfo">
|
||||
清除信息
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 临时数据演示(不持久化) -->
|
||||
<demo-block title="非持久化演示 - 临时数据" transparent>
|
||||
<view class="mb-3 border border-orange-200 rounded-2 bg-orange-50 p-3 dark:border-orange-800 dark:bg-orange-900/20">
|
||||
<view class="mb-1 text-3.5 text-orange-700 font-bold dark:text-orange-300">
|
||||
⚠️ 临时存储
|
||||
</view>
|
||||
<view class="text-3 text-orange-600 leading-relaxed dark:text-orange-200">
|
||||
这个 Store 的数据不会持久化,刷新页面后数据会丢失
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-4 font-bold wot-text-text-main">
|
||||
临时消息
|
||||
</view>
|
||||
|
||||
<view v-if="tempStore.tempData.message" class="mb-4 rounded-2 bg-blue-50 p-3 dark:bg-blue-900/20">
|
||||
<view class="text-3.5 text-blue-700 font-bold dark:text-blue-300">
|
||||
{{ tempStore.tempData.message }}
|
||||
</view>
|
||||
<view class="text-2.5 text-blue-600 dark:text-blue-200">
|
||||
时间戳: {{ new Date(tempStore.tempData.timestamp).toLocaleString() }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else class="mb-4 text-center wot-text-text-secondary">
|
||||
暂无临时消息
|
||||
</view>
|
||||
|
||||
<view class="space-y-3">
|
||||
<wd-input v-model="tempMessage" placeholder="请输入临时消息" />
|
||||
<view class="grid grid-cols-2 gap-2">
|
||||
<wd-button type="primary" size="small" @click="setTempMessage">
|
||||
设置消息
|
||||
</wd-button>
|
||||
<wd-button type="warning" size="small" @click="tempStore.clearMessage">
|
||||
清除消息
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 持久化原理 -->
|
||||
<demo-block title="持久化实现原理" transparent>
|
||||
<view class="space-y-4">
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-4 font-bold wot-text-text-main">
|
||||
持久化插件
|
||||
</view>
|
||||
<view class="mb-3 text-3 leading-relaxed wot-text-text-secondary">
|
||||
项目使用自定义的 Pinia 插件实现持久化,位于 <text class="text-blue-600 font-mono dark:text-blue-400">
|
||||
src/store/persist.ts
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="rounded p-3" @click="copyCode(`function persist({ store }, excludedIds) {
|
||||
// 检查是否需要排除
|
||||
if (excludedIds.includes(store.$id)) return
|
||||
|
||||
// 从本地存储读取数据
|
||||
const storageState = uni.getStorageSync(store.$id)
|
||||
if (storageState) {
|
||||
store.$state = storageState
|
||||
}
|
||||
|
||||
// 监听状态变化并保存
|
||||
store.$subscribe(() => {
|
||||
uni.setStorageSync(store.$id, store.$state)
|
||||
})
|
||||
}`)"
|
||||
>
|
||||
<view class="mb-2 text-3 font-bold wot-text-text-secondary">
|
||||
核心实现:
|
||||
</view>
|
||||
<view class="text-2.5 leading-relaxed font-mono wot-text-text-secondary">
|
||||
function persist({ store }, excludedIds) {\n
|
||||
// 检查是否需要排除\n
|
||||
if (excludedIds.includes(store.$id)) return\n
|
||||
// 从本地存储读取数据\n
|
||||
const storageState = uni.getStorageSync(store.$id)\n
|
||||
// 监听状态变化并保存\n
|
||||
store.$subscribe(() => {\n
|
||||
uni.setStorageSync(store.$id, store.$state)\n
|
||||
})\n
|
||||
}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-4 font-bold wot-text-text-main">
|
||||
排除列表
|
||||
</view>
|
||||
<view class="mb-3 text-3 leading-relaxed wot-text-text-secondary">
|
||||
可以通过排除列表控制哪些 Store 不需要持久化
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="rounded p-3" @click="copyCode(`// 在 persist.ts 中配置排除列表
|
||||
persist(context, ['global-register', 'temp'])`)"
|
||||
>
|
||||
<view class="text-2.5 leading-relaxed font-mono wot-text-text-secondary">
|
||||
// 当前排除的 Store ID:\n
|
||||
['global-register', 'temp']
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-4 font-bold wot-text-text-main">
|
||||
使用说明
|
||||
</view>
|
||||
<view class="text-3 leading-relaxed wot-text-text-secondary space-y-2">
|
||||
<view>
|
||||
• <text class="text-green-600 font-bold dark:text-green-400">
|
||||
自动持久化
|
||||
</text>:默认情况下,所有 Store 都会自动持久化
|
||||
</view>
|
||||
<view>
|
||||
• <text class="text-orange-600 font-bold dark:text-orange-400">
|
||||
排除机制
|
||||
</text>:通过 Store ID 控制哪些不需要持久化
|
||||
</view>
|
||||
<view>
|
||||
• <text class="text-blue-600 font-bold dark:text-blue-400">
|
||||
深拷贝保护
|
||||
</text>:使用深拷贝避免引用问题
|
||||
</view>
|
||||
<view>
|
||||
• <text class="text-purple-600 font-bold dark:text-purple-400">
|
||||
自动恢复
|
||||
</text>:页面刷新时自动从本地存储恢复数据
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 最佳实践 -->
|
||||
<demo-block title="最佳实践" transparent>
|
||||
<view class="space-y-3">
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 flex items-center">
|
||||
<view class="mr-2 text-6">
|
||||
💡
|
||||
</view>
|
||||
<view class="text-4 font-bold wot-text-text-main">
|
||||
Store 设计建议
|
||||
</view>
|
||||
</view>
|
||||
<view class="text-3 leading-relaxed wot-text-text-secondary space-y-2">
|
||||
<view>• 使用组合式 API 风格定义 Store</view>
|
||||
<view>• 合理拆分 Store,避免单个 Store 过于庞大</view>
|
||||
<view>• 使用 TypeScript 获得更好的类型支持</view>
|
||||
<view>• 为 Store 提供清晰的命名和注释</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 flex items-center">
|
||||
<view class="mr-2 text-6">
|
||||
🔒
|
||||
</view>
|
||||
<view class="text-4 font-bold wot-text-text-main">
|
||||
持久化建议
|
||||
</view>
|
||||
</view>
|
||||
<view class="text-3 leading-relaxed wot-text-text-secondary space-y-2">
|
||||
<view>• 敏感数据(如密码)不要持久化存储</view>
|
||||
<view>• 大量数据考虑使用数据库而非本地存储</view>
|
||||
<view>• 临时状态(如加载状态)不需要持久化</view>
|
||||
<view>• 定期清理无用的持久化数据</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 flex items-center">
|
||||
<view class="mr-2 text-6">
|
||||
⚡
|
||||
</view>
|
||||
<view class="text-4 font-bold wot-text-text-main">
|
||||
性能优化
|
||||
</view>
|
||||
</view>
|
||||
<view class="text-3 leading-relaxed wot-text-text-secondary space-y-2">
|
||||
<view>• 避免在 Store 中存储大量数据</view>
|
||||
<view>• 使用 computed 缓存复杂计算结果</view>
|
||||
<view>• 合理使用 $subscribe 监听状态变化</view>
|
||||
<view>• 考虑使用 $patch 批量更新状态</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 相关链接 -->
|
||||
<demo-block title="相关链接" transparent>
|
||||
<wd-cell-group border custom-class="rounded-2! overflow-hidden">
|
||||
<wd-cell title="📚 Pinia 官方文档" value="状态管理库" is-link @click="handleNavigate('https://pinia.vuejs.org/zh/')" />
|
||||
<wd-cell title="🎯 Vue 3 文档" value="组合式 API" is-link @click="handleNavigate('https://cn.vuejs.org/guide/extras/composition-api-faq.html')" />
|
||||
<wd-cell title="🔧 uni-app 存储" value="本地存储 API" is-link @click="handleNavigate('https://uniapp.dcloud.net.cn/api/storage/storage.html')" />
|
||||
</wd-cell-group>
|
||||
</demo-block>
|
||||
</view>
|
||||
</template>
|
||||
@@ -0,0 +1,290 @@
|
||||
<script setup lang="ts">
|
||||
definePage({
|
||||
name: 'request',
|
||||
style: {
|
||||
navigationBarTitleText: '网络请求',
|
||||
},
|
||||
})
|
||||
|
||||
const GlobalToast = useGlobalToast()
|
||||
|
||||
// Alova 核心特性
|
||||
const alovaFeatures = [
|
||||
{
|
||||
icon: '⚡',
|
||||
title: '简单易用',
|
||||
description: '观看视频5分钟上手',
|
||||
details: '极简的API设计,直观的使用方式,让开发者快速掌握并投入使用',
|
||||
color: 'blue',
|
||||
},
|
||||
{
|
||||
icon: '🔧',
|
||||
title: '完美兼容',
|
||||
description: '兼容你最喜欢的技术栈',
|
||||
details: '支持Vue、React、Angular等主流框架,以及各种HTTP客户端库',
|
||||
color: 'green',
|
||||
},
|
||||
{
|
||||
icon: '🚀',
|
||||
title: '高性能模块',
|
||||
description: '20+ 高性能的业务模块',
|
||||
details: '提供分页、实时数据、文件上传下载等开箱即用的高性能解决方案',
|
||||
color: 'purple',
|
||||
},
|
||||
{
|
||||
icon: '🔗',
|
||||
title: 'OpenAPI 解决方案',
|
||||
description: '更先进的 openAPI 解决方案',
|
||||
details: '在代码中和API信息高效交互,自动生成类型定义和请求函数',
|
||||
color: 'orange',
|
||||
},
|
||||
{
|
||||
icon: '📦',
|
||||
title: '请求共享缓存',
|
||||
description: '请求共享和响应缓存',
|
||||
details: '智能的请求去重和响应缓存机制,显著提升应用性能',
|
||||
color: 'cyan',
|
||||
},
|
||||
{
|
||||
icon: '🛡️',
|
||||
title: '类型安全',
|
||||
description: '完整的 TypeScript 支持',
|
||||
details: '从请求到响应的全链路类型安全,减少运行时错误',
|
||||
color: 'red',
|
||||
},
|
||||
]
|
||||
|
||||
// 使用真正的 useRequest hook - 获取宠物列表
|
||||
const {
|
||||
data: petData,
|
||||
loading: petLoading,
|
||||
error: petError,
|
||||
send: loadPetData,
|
||||
} = useRequest(
|
||||
(status: 'available' | 'pending' | 'sold' = 'available') => Apis.pet.findPetsByStatus({
|
||||
params: { status },
|
||||
}),
|
||||
{
|
||||
immediate: false,
|
||||
},
|
||||
).onError((error) => {
|
||||
GlobalToast.error(error.error?.message || '获取宠物列表失败')
|
||||
})
|
||||
|
||||
// 使用真正的 useRequest hook - 用户登录演示
|
||||
const {
|
||||
data: loginData,
|
||||
loading: loginLoading,
|
||||
error: loginError,
|
||||
send: performLogin,
|
||||
} = useRequest(
|
||||
(username: string, password: string) => Apis.user.loginUser({
|
||||
params: {
|
||||
username,
|
||||
password,
|
||||
},
|
||||
}),
|
||||
{
|
||||
immediate: false,
|
||||
},
|
||||
).onError((error) => {
|
||||
GlobalToast.error(error.error?.message || '用户登录失败')
|
||||
})
|
||||
|
||||
// useRequest 演示函数
|
||||
function demoLoadPets() {
|
||||
loadPetData('available')
|
||||
}
|
||||
|
||||
function demoLogin() {
|
||||
performLogin('testuser', 'testpass')
|
||||
}
|
||||
|
||||
// 链接导航处理
|
||||
function handleNavigate(url: string) {
|
||||
// #ifdef H5
|
||||
window.open(url, '_blank')
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
uni.setClipboardData({
|
||||
data: url,
|
||||
showToast: false,
|
||||
success: () => {
|
||||
uni.hideToast()
|
||||
GlobalToast.success(`${url} 已复制到剪贴板`)
|
||||
},
|
||||
})
|
||||
// #endif
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="min-h-screen py-3">
|
||||
<!-- 头部介绍 -->
|
||||
<view class="mx-3 mb-3">
|
||||
<view class="rounded-3 px-5 py-8 text-center wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-10">
|
||||
🌐
|
||||
</view>
|
||||
<view class="mb-2 text-6 font-bold wot-text-text-main">
|
||||
Alova 网络请求
|
||||
</view>
|
||||
<view class="mb-2 text-3.5 leading-relaxed wot-text-text-secondary">
|
||||
极致高效的请求工具集
|
||||
</view>
|
||||
<view class="text-3 leading-relaxed wot-text-text-secondary">
|
||||
alova完美兼容你最喜欢的HTTP client和UI框架,快速开发客户端和服务的应用的业务逻辑,同时让API信息与代码进行交互,像虫洞一样拉近后端协作距离,极致高效地集成你的APIs
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Alova 核心特性 -->
|
||||
<demo-block title="Alova 核心特性" transparent>
|
||||
<view class="grid grid-cols-2 gap-3">
|
||||
<view
|
||||
v-for="feature in alovaFeatures"
|
||||
:key="feature.title"
|
||||
class="rounded-2 p-3 wot-bg-filled-oppo"
|
||||
>
|
||||
<view class="mb-2 flex items-center">
|
||||
<view class="mr-2 text-5">
|
||||
{{ feature.icon }}
|
||||
</view>
|
||||
<view class="flex-1">
|
||||
<view class="text-3.5 font-bold wot-text-text-main">
|
||||
{{ feature.title }}
|
||||
</view>
|
||||
<view class="text-2.5 wot-text-text-secondary">
|
||||
{{ feature.description }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- Alova useRequest 演示 -->
|
||||
<demo-block title="useRequest Hook 演示" transparent>
|
||||
<view class="space-y-3">
|
||||
<!-- 宠物列表请求 -->
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 flex items-center">
|
||||
<view class="mr-2 text-5">
|
||||
🐾
|
||||
</view>
|
||||
<view class="text-4 font-bold wot-text-text-main">
|
||||
宠物列表请求
|
||||
</view>
|
||||
</view>
|
||||
<view class="mb-3 text-3 leading-relaxed wot-text-text-secondary">
|
||||
使用 useRequest 获取宠物列表,支持参数传递和错误处理
|
||||
</view>
|
||||
|
||||
<view class="mb-3">
|
||||
<wd-button
|
||||
type="primary"
|
||||
block
|
||||
:loading="petLoading"
|
||||
@click="demoLoadPets"
|
||||
>
|
||||
获取宠物列表
|
||||
</wd-button>
|
||||
</view>
|
||||
|
||||
<!-- 请求状态显示 -->
|
||||
<view class="space-y-2">
|
||||
<view v-if="petLoading" class="flex items-center text-3 text-blue-600">
|
||||
<wd-icon name="loading" size="14px" class="mr-1" />
|
||||
正在加载宠物数据...
|
||||
</view>
|
||||
<view v-if="petError" class="text-3 text-red-600">
|
||||
❌ 请求失败: {{ petError.message }}
|
||||
</view>
|
||||
<view v-if="petData && !petLoading" class="text-3 text-green-600">
|
||||
✅ 成功获取 {{ petData.length }} 只宠物数据
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 代码示例 -->
|
||||
<view class="wot-bg-bg mt-3 rounded-2 p-3">
|
||||
<view class="mb-2 text-3 font-bold wot-text-text-secondary">
|
||||
代码示例:
|
||||
</view>
|
||||
<view class="text-2.5 leading-relaxed font-mono wot-text-text-secondary">
|
||||
const { data, loading, send } = useRequest(\n
|
||||
(status) => Apis.pet.findPetsByStatus({ params: { status } }),\n
|
||||
{ immediate: false }\n
|
||||
).onError((error) => { ... })
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 用户登录请求 -->
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 flex items-center">
|
||||
<view class="mr-2 text-5">
|
||||
👤
|
||||
</view>
|
||||
<view class="text-4 font-bold wot-text-text-main">
|
||||
用户登录请求
|
||||
</view>
|
||||
</view>
|
||||
<view class="mb-3 text-3 leading-relaxed wot-text-text-secondary">
|
||||
演示多参数传递和POST请求处理
|
||||
</view>
|
||||
|
||||
<view class="mb-3">
|
||||
<wd-button
|
||||
type="warning"
|
||||
block
|
||||
:loading="loginLoading"
|
||||
@click="demoLogin"
|
||||
>
|
||||
模拟用户登录
|
||||
</wd-button>
|
||||
</view>
|
||||
|
||||
<!-- 请求状态显示 -->
|
||||
<view class="space-y-2">
|
||||
<view v-if="loginLoading" class="flex items-center text-3 text-blue-600">
|
||||
<wd-icon name="loading" size="14px" class="mr-1" />
|
||||
正在登录中...
|
||||
</view>
|
||||
<view v-if="loginError" class="text-3 text-red-600">
|
||||
❌ 登录失败: {{ loginError.message }}
|
||||
</view>
|
||||
<view v-if="loginData && !loginLoading" class="text-3 text-green-600">
|
||||
✅ 登录成功!
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 代码示例 -->
|
||||
<view class="wot-bg-bg mt-3 rounded-2 p-3">
|
||||
<view class="mb-2 text-3 font-bold wot-text-text-secondary">
|
||||
代码示例:
|
||||
</view>
|
||||
<view class="text-2.5 leading-relaxed font-mono wot-text-text-secondary">
|
||||
const { data, loading, send } = useRequest(\n
|
||||
(username, password) => Apis.user.loginUser({\n
|
||||
params: { username, password }\n
|
||||
}),\n
|
||||
{ immediate: false }\n
|
||||
).onError((error) => { ... })
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 相关链接 -->
|
||||
<demo-block title="相关链接" transparent>
|
||||
<wd-cell-group border custom-class="rounded-2! overflow-hidden">
|
||||
<wd-cell title="📚 Alova 官方文档" value="alova.js.org" is-link @click="handleNavigate('https://alova.js.org/')" />
|
||||
<wd-cell title="🐙 GitHub 仓库" value="alovajs/alova" is-link @click="handleNavigate('https://github.com/alovajs/alova')" />
|
||||
<wd-cell title="📖 uni-app 文档" value="网络请求" is-link @click="handleNavigate('https://uniapp.dcloud.net.cn/api/request/request.html')" />
|
||||
<wd-cell title="🎯 Alova Hooks" value="请求策略" is-link @click="handleNavigate('https://alova.js.org/zh-CN/tutorial/client/strategy/')" />
|
||||
<wd-cell title="💡 最佳实践" value="使用指南" is-link @click="handleNavigate('https://alova.js.org/zh-CN/tutorial/project/best-practice/')" />
|
||||
</wd-cell-group>
|
||||
</demo-block>
|
||||
</view>
|
||||
</template>
|
||||
@@ -0,0 +1,54 @@
|
||||
<script setup lang="ts">
|
||||
definePage({
|
||||
name: 'demo-aftereach',
|
||||
style: {
|
||||
navigationBarTitleText: 'afterEach 钩子演示',
|
||||
},
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
function goBack() {
|
||||
router.back()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="min-h-screen py-3">
|
||||
<!-- 头部 -->
|
||||
<view class="mx-3 mb-3">
|
||||
<view class="rounded-3 px-5 py-6 text-center wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-8">
|
||||
📊
|
||||
</view>
|
||||
<view class="mb-2 text-5 font-bold wot-text-text-main">
|
||||
afterEach 钩子演示
|
||||
</view>
|
||||
<view class="text-3.5 wot-text-text-secondary">
|
||||
这个页面用于演示 afterEach 钩子的触发
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 内容 -->
|
||||
<demo-block title="afterEach 演示成功" transparent>
|
||||
<view class="border border-green-200 rounded-2 bg-green-50 p-4 dark:bg-green-900/20">
|
||||
<view class="mb-2 text-4 text-green-700 font-bold dark:text-green-300">
|
||||
🎉 恭喜!
|
||||
</view>
|
||||
<view class="text-3.5 text-green-600 leading-relaxed dark:text-green-200">
|
||||
如果你能看到这个页面,说明 afterEach 钩子已经触发了!\n
|
||||
打开浏览器控制台(F12 → Console)可以看到相关的日志输出。\n
|
||||
afterEach 钩子常用于页面统计、埋点上报等导航后处理。
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<demo-block title="导航" transparent>
|
||||
<wd-button type="warning" block @click="goBack">
|
||||
返回上一页
|
||||
</wd-button>
|
||||
</demo-block>
|
||||
</view>
|
||||
</template>
|
||||
@@ -0,0 +1,83 @@
|
||||
<script setup lang="ts">
|
||||
definePage({
|
||||
name: 'demo-guard',
|
||||
style: {
|
||||
navigationBarTitleText: '导航守卫演示',
|
||||
},
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
function goBack() {
|
||||
router.back()
|
||||
}
|
||||
|
||||
// 演示权限拦截
|
||||
function handleBeforeEach() {
|
||||
router.push({
|
||||
name: 'demo-protected',
|
||||
})
|
||||
}
|
||||
|
||||
// 跳转到afterEach演示页面
|
||||
function demoAfterEachPage() {
|
||||
router.push('/subPages/router/demo-aftereach')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="py-3">
|
||||
<!-- 头部 -->
|
||||
<view class="mx-3 mb-3">
|
||||
<view class="rounded-3 px-5 py-6 text-center wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-8">
|
||||
🛡️
|
||||
</view>
|
||||
<view class="mb-2 text-5 font-bold wot-text-text-main">
|
||||
导航守卫演示
|
||||
</view>
|
||||
<view class="text-3.5 wot-text-text-secondary">
|
||||
演示 beforeEach 和 afterEach 守卫功能
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 守卫演示 -->
|
||||
<demo-block title="守卫演示" transparent>
|
||||
<view class="space-y-3">
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-4 font-bold wot-text-text-main">
|
||||
前置导航守卫拦截
|
||||
</view>
|
||||
<view class="mb-3 text-3.5 wot-text-text-secondary">
|
||||
跳转前交互,可以拦截导航
|
||||
</view>
|
||||
<wd-button type="danger" block @click="handleBeforeEach">
|
||||
📊 beforeEach 演示
|
||||
</wd-button>
|
||||
</view>
|
||||
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-4 font-bold wot-text-text-main">
|
||||
后置导航钩子演示
|
||||
</view>
|
||||
<view class="mb-3 text-3.5 wot-text-text-secondary">
|
||||
跳转到专门的 afterEach 演示页面,体验页面统计、埋点上报等功能
|
||||
</view>
|
||||
<wd-button type="success" block @click="demoAfterEachPage">
|
||||
📊 afterEach 演示
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<demo-block title="导航" transparent>
|
||||
<view class="px-3">
|
||||
<wd-button type="warning" block @click="goBack">
|
||||
返回上一页
|
||||
</wd-button>
|
||||
</view>
|
||||
</demo-block>
|
||||
</view>
|
||||
</template>
|
||||
@@ -0,0 +1,123 @@
|
||||
<script setup lang="ts">
|
||||
definePage({
|
||||
name: 'demo-object',
|
||||
style: {
|
||||
navigationBarTitleText: '对象路径跳转',
|
||||
},
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
function goBack() {
|
||||
router.back()
|
||||
}
|
||||
|
||||
function pushToParams() {
|
||||
router.push({ name: 'demo-params', params: { username: 'eduardo' } })
|
||||
}
|
||||
|
||||
function pushToQuery() {
|
||||
router.push({ path: '/subPages/router/demo-query', query: { keyword: 'vue', type: 'framework' } })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="min-h-screen py-3">
|
||||
<!-- 头部 -->
|
||||
<view class="mx-3 mb-3">
|
||||
<view class="rounded-3 px-5 py-6 text-center wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-8">
|
||||
🎯
|
||||
</view>
|
||||
<view class="mb-2 text-5 font-bold wot-text-text-main">
|
||||
对象路径跳转演示
|
||||
</view>
|
||||
<view class="text-3.5 wot-text-text-secondary">
|
||||
使用 router.push({ path: '/path' }) 进行跳转
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 跳转信息 -->
|
||||
<demo-block title="跳转信息" transparent>
|
||||
<view class="rounded-3 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-4 font-bold wot-text-text-main">
|
||||
当前页面信息
|
||||
</view>
|
||||
<view class="space-y-2">
|
||||
<view class="flex items-center justify-between border-b py-2 wot-border-border-main">
|
||||
<text class="text-3.5 wot-text-text-secondary">
|
||||
路径:
|
||||
</text>
|
||||
<text class="text-3.5 font-mono wot-text-text-main">
|
||||
{{ route.path }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="flex items-center justify-between border-b py-2 wot-border-border-main">
|
||||
<text class="text-3.5 wot-text-text-secondary">
|
||||
跳转方式:
|
||||
</text>
|
||||
<text class="text-3.5 wot-text-text-main">
|
||||
对象路径
|
||||
</text>
|
||||
</view>
|
||||
<view class="flex items-center justify-between py-2">
|
||||
<text class="text-3.5 wot-text-text-secondary">
|
||||
代码:
|
||||
</text>
|
||||
<text class="text-3.5 font-mono wot-text-text-main">
|
||||
router.push({ path: '/subPages/router/demo-object' })
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 参数传递演示 -->
|
||||
<demo-block title="参数传递演示" transparent>
|
||||
<view class="space-y-3">
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-4 font-bold wot-text-text-main">
|
||||
params 参数传递
|
||||
</view>
|
||||
<view class="mb-3 text-3.5 wot-text-text-secondary">
|
||||
使用 name + params 组合传递参数
|
||||
</view>
|
||||
<view class="wot-bg-bg mb-3 border rounded-2 p-3 wot-border-border-main">
|
||||
<text class="text-3 font-mono wot-text-text-secondary">
|
||||
router.push({ name: 'demo-params', params: { username: 'eduardo' } })
|
||||
</text>
|
||||
</view>
|
||||
<wd-button type="primary" block @click="pushToParams">
|
||||
跳转并传递 params
|
||||
</wd-button>
|
||||
</view>
|
||||
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-4 font-bold wot-text-text-main">
|
||||
query 参数传递
|
||||
</view>
|
||||
<view class="mb-3 text-3.5 wot-text-text-secondary">
|
||||
使用 path + query 组合传递参数
|
||||
</view>
|
||||
<view class="wot-bg-bg mb-3 border rounded-2 p-3 wot-border-border-main">
|
||||
<text class="text-3 font-mono wot-text-text-secondary">
|
||||
router.push({ path: '/demo-query', query: { keyword: 'vue' } })
|
||||
</text>
|
||||
</view>
|
||||
<wd-button type="success" block @click="pushToQuery">
|
||||
跳转并传递 query
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<demo-block title="导航" transparent>
|
||||
<wd-button type="warning" block @click="goBack">
|
||||
返回上一页
|
||||
</wd-button>
|
||||
</demo-block>
|
||||
</view>
|
||||
</template>
|
||||
@@ -0,0 +1,182 @@
|
||||
<script setup lang="ts">
|
||||
definePage({
|
||||
name: 'demo-params',
|
||||
style: {
|
||||
navigationBarTitleText: '参数接收演示',
|
||||
},
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
// 接收参数
|
||||
const receivedParams = ref({})
|
||||
const decodedUser = ref(null)
|
||||
|
||||
onLoad((option) => {
|
||||
console.log('onLoad option:', option)
|
||||
receivedParams.value = option || {}
|
||||
|
||||
// 如果有user参数,尝试解码对象
|
||||
if (option && option.user) {
|
||||
try {
|
||||
decodedUser.value = JSON.parse(decodeURIComponent(decodeURIComponent(option.user)))
|
||||
}
|
||||
catch (e) {
|
||||
console.error('解码user参数失败:', e)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function goBack() {
|
||||
router.back()
|
||||
}
|
||||
|
||||
function pushToGuard() {
|
||||
router.push('/subPages/router/demo-guard')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="min-h-screen py-3">
|
||||
<!-- 头部 -->
|
||||
<view class="mx-3 mb-3">
|
||||
<view class="rounded-3 px-5 py-6 text-center wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-8">
|
||||
📥
|
||||
</view>
|
||||
<view class="mb-2 text-5 font-bold wot-text-text-main">
|
||||
params 参数接收演示
|
||||
</view>
|
||||
<view class="text-3.5 wot-text-text-secondary">
|
||||
演示如何接收和处理路由参数
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 路由信息 -->
|
||||
<demo-block title="当前路由信息" transparent>
|
||||
<view class="rounded-3 p-4 wot-bg-filled-oppo">
|
||||
<view class="space-y-2">
|
||||
<view class="flex items-center justify-between border-b py-2 wot-border-border-main">
|
||||
<text class="text-3.5 wot-text-text-secondary">
|
||||
路径:
|
||||
</text>
|
||||
<text class="text-3.5 font-mono wot-text-text-main">
|
||||
{{ route.path }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="flex items-center justify-between border-b py-2 wot-border-border-main">
|
||||
<text class="text-3.5 wot-text-text-secondary">
|
||||
名称:
|
||||
</text>
|
||||
<text class="text-3.5 font-mono wot-text-text-main">
|
||||
{{ route.name }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="flex items-center justify-between py-2">
|
||||
<text class="text-3.5 wot-text-text-secondary">
|
||||
接收方式:
|
||||
</text>
|
||||
<text class="text-3.5 wot-text-text-main">
|
||||
onLoad(option)
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 参数信息 -->
|
||||
<demo-block title="接收到的参数" transparent>
|
||||
<view class="space-y-3">
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-4 font-bold wot-text-text-main">
|
||||
原始参数 (option)
|
||||
</view>
|
||||
<view class="wot-bg-bg border rounded-2 p-3 wot-border-border-main">
|
||||
<text class="text-3 font-mono wot-text-text-secondary">
|
||||
{{ JSON.stringify(receivedParams, null, 2) }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="decodedUser" class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-4 font-bold wot-text-text-main">
|
||||
解码后的对象参数
|
||||
</view>
|
||||
<view class="wot-bg-bg border rounded-2 p-3 wot-border-border-main">
|
||||
<text class="text-3 font-mono wot-text-text-secondary">
|
||||
{{ JSON.stringify(decodedUser, null, 2) }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="mt-3 text-3.5 wot-text-text-secondary">
|
||||
使用 JSON.parse(decodeURIComponent(decodeURIComponent(option.user))) 解码
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- API 说明 -->
|
||||
<demo-block title="API 说明" transparent>
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-4 font-bold wot-text-text-main">
|
||||
Params vs Query 的区别
|
||||
</view>
|
||||
<view class="mb-3 border border-orange-200 rounded-2 bg-orange-50 p-3 dark:bg-orange-900/20">
|
||||
<view class="mb-2 text-3.5 text-orange-700 font-bold dark:text-orange-300">
|
||||
⚠️ 重要说明
|
||||
</view>
|
||||
<view class="text-3 text-orange-600 leading-relaxed dark:text-orange-200">
|
||||
在 @wot-ui/router 中,params 和 query 参数在实际效果上并无区别,都会以查询字符串形式放在 URL 中。这种 API 设计主要是为了与 vue-router 保持一致。
|
||||
</view>
|
||||
</view>
|
||||
<view class="wot-bg-bg border rounded-2 p-3 wot-border-border-main">
|
||||
<text class="text-3 leading-relaxed font-mono wot-text-text-secondary">
|
||||
// Params 写法 (当前演示)
|
||||
router.push({ name: 'demo-params', params: { username: 'eduardo' } })
|
||||
// 结果: /demo-params?username=eduardo
|
||||
|
||||
// Query 写法 (效果相同)
|
||||
router.push({ path: '/demo-params', query: { username: 'eduardo' } })
|
||||
// 结果: /demo-params?username=eduardo
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 代码示例 -->
|
||||
<demo-block title="代码示例" transparent>
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-4 font-bold wot-text-text-main">
|
||||
接收参数的标准写法
|
||||
</view>
|
||||
<view class="wot-bg-bg border rounded-2 p-3 wot-border-border-main">
|
||||
<text class="text-3 leading-relaxed font-mono wot-text-text-secondary">
|
||||
onLoad((option) => {
|
||||
if (option && option.username) {
|
||||
const username = option.username
|
||||
}
|
||||
|
||||
// 对象参数需要解码
|
||||
if (option && option.user) {
|
||||
const user = JSON.parse(decodeURIComponent(decodeURIComponent(option.user)))
|
||||
}
|
||||
})
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<demo-block title="继续演示" transparent>
|
||||
<view class="space-y-3">
|
||||
<wd-button type="primary" block @click="pushToGuard">
|
||||
跳转到导航守卫演示
|
||||
</wd-button>
|
||||
<wd-button type="warning" block @click="goBack">
|
||||
返回上一页
|
||||
</wd-button>
|
||||
</view>
|
||||
</demo-block>
|
||||
</view>
|
||||
</template>
|
||||
@@ -0,0 +1,54 @@
|
||||
<script setup lang="ts">
|
||||
definePage({
|
||||
name: 'demo-protected',
|
||||
style: {
|
||||
navigationBarTitleText: '受保护页面',
|
||||
},
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
function goBack() {
|
||||
router.back()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="min-h-screen py-3">
|
||||
<!-- 头部 -->
|
||||
<view class="mx-3 mb-3">
|
||||
<view class="rounded-3 px-5 py-6 text-center wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-8">
|
||||
🔒
|
||||
</view>
|
||||
<view class="mb-2 text-5 font-bold wot-text-text-main">
|
||||
受保护的页面
|
||||
</view>
|
||||
<view class="text-3.5 wot-text-text-secondary">
|
||||
这个页面被路由守卫保护,只有登录用户才能访问
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 内容 -->
|
||||
<demo-block title="守卫拦截成功" transparent>
|
||||
<view class="border border-green-200 rounded-2 bg-green-50 p-4 dark:bg-green-900/20">
|
||||
<view class="mb-2 text-4 text-green-700 font-bold dark:text-green-300">
|
||||
🎉 恭喜!
|
||||
</view>
|
||||
<view class="text-3.5 text-green-600 leading-relaxed dark:text-green-200">
|
||||
如果你能看到这个页面,说明路由守卫验证通过了!\n
|
||||
但实际上,这个页面被配置为需要登录才能访问,\n
|
||||
守卫应该会拦截并重定向你到首页。
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<demo-block title="导航" transparent>
|
||||
<wd-button type="warning" block @click="goBack">
|
||||
返回上一页
|
||||
</wd-button>
|
||||
</demo-block>
|
||||
</view>
|
||||
</template>
|
||||
@@ -0,0 +1,201 @@
|
||||
<script setup lang="ts">
|
||||
definePage({
|
||||
name: 'demo-query',
|
||||
style: {
|
||||
navigationBarTitleText: '查询参数接收演示',
|
||||
},
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
// 接收参数
|
||||
const receivedQuery = ref({})
|
||||
const decodedUser = ref(null)
|
||||
|
||||
onLoad((option) => {
|
||||
console.log('onLoad option:', option)
|
||||
receivedQuery.value = option || {}
|
||||
|
||||
// 如果有user参数,尝试解码对象
|
||||
if (option && option.user) {
|
||||
try {
|
||||
decodedUser.value = JSON.parse(decodeURIComponent(decodeURIComponent(option.user)))
|
||||
}
|
||||
catch (e) {
|
||||
console.error('解码user参数失败:', e)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function goBack() {
|
||||
router.back()
|
||||
}
|
||||
|
||||
function pushToGuard() {
|
||||
router.push('/subPages/router/demo-guard')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="min-h-screen py-3">
|
||||
<!-- 头部 -->
|
||||
<view class="mx-3 mb-3">
|
||||
<view class="rounded-3 px-5 py-6 text-center wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-8">
|
||||
📨
|
||||
</view>
|
||||
<view class="mb-2 text-5 font-bold wot-text-text-main">
|
||||
query 参数接收演示
|
||||
</view>
|
||||
<view class="text-3.5 wot-text-text-secondary">
|
||||
演示如何接收和处理查询参数
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 路由信息 -->
|
||||
<demo-block title="当前路由信息" transparent>
|
||||
<view class="rounded-3 p-4 wot-bg-filled-oppo">
|
||||
<view class="space-y-2">
|
||||
<view class="flex items-center justify-between border-b py-2 wot-border-border-main">
|
||||
<text class="text-3.5 wot-text-text-secondary">
|
||||
路径:
|
||||
</text>
|
||||
<text class="text-3.5 font-mono wot-text-text-main">
|
||||
{{ route.path }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="flex items-center justify-between border-b py-2 wot-border-border-main">
|
||||
<text class="text-3.5 wot-text-text-secondary">
|
||||
查询字符串:
|
||||
</text>
|
||||
<text class="text-3.5 font-mono wot-text-text-main">
|
||||
{{ JSON.stringify(route.query) }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="flex items-center justify-between py-2">
|
||||
<text class="text-3.5 wot-text-text-secondary">
|
||||
接收方式:
|
||||
</text>
|
||||
<text class="text-3.5 wot-text-text-main">
|
||||
onLoad(option)
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 参数信息 -->
|
||||
<demo-block title="接收到的查询参数" transparent>
|
||||
<view class="space-y-3">
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-4 font-bold wot-text-text-main">
|
||||
原始查询参数 (option)
|
||||
</view>
|
||||
<view class="wot-bg-bg border rounded-2 p-3 wot-border-border-main">
|
||||
<text class="text-3 font-mono wot-text-text-secondary">
|
||||
{{ JSON.stringify(receivedQuery, null, 2) }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="mt-3 text-3.5 wot-text-text-secondary">
|
||||
URL: /demo-query?keyword={{ (receivedQuery as any).keyword }}&type={{ (receivedQuery as any).type }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="decodedUser" class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-4 font-bold wot-text-text-main">
|
||||
解码后的对象参数
|
||||
</view>
|
||||
<view class="wot-bg-bg border rounded-2 p-3 wot-border-border-main">
|
||||
<text class="text-3 font-mono wot-text-text-secondary">
|
||||
{{ JSON.stringify(decodedUser, null, 2) }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="mt-3 text-3.5 wot-text-text-secondary">
|
||||
使用 JSON.parse(decodeURIComponent(decodeURIComponent(option.user))) 解码
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- Query vs Params 说明 -->
|
||||
<demo-block title="Query vs Params 说明" transparent>
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-4 font-bold wot-text-text-main">
|
||||
两种参数传递方式说明
|
||||
</view>
|
||||
<view class="mb-3 border border-orange-200 rounded-2 bg-orange-50 p-3 dark:bg-orange-900/20">
|
||||
<view class="mb-2 text-3.5 text-orange-700 font-bold dark:text-orange-300">
|
||||
⚠️ 重要说明
|
||||
</view>
|
||||
<view class="text-3 text-orange-600 leading-relaxed dark:text-orange-200">
|
||||
在 @wot-ui/router 中,query 和 params 参数都会放在 URL 中,两者在实际效果上并无区别。这种 API 设计主要是为了与 vue-router 保持一致。
|
||||
</view>
|
||||
</view>
|
||||
<view class="space-y-3">
|
||||
<view class="border border-blue-200 rounded-2 bg-blue-50 p-3 dark:bg-blue-900/20">
|
||||
<view class="mb-2 text-3.5 text-blue-700 font-bold dark:text-blue-300">
|
||||
Query 写法 (当前演示)
|
||||
</view>
|
||||
<view class="text-3 text-blue-600 leading-relaxed dark:text-blue-200">
|
||||
• 使用 path + query 组合\n
|
||||
• 参数以查询字符串形式出现\n
|
||||
• 写法: router.push({ path: '/page', query: { key: 'value' } })\n
|
||||
• 结果: /page?key=value
|
||||
</view>
|
||||
</view>
|
||||
<view class="border border-green-200 rounded-2 bg-green-50 p-3 dark:bg-green-900/20">
|
||||
<view class="mb-2 text-3.5 text-green-700 font-bold dark:text-green-300">
|
||||
Params 写法
|
||||
</view>
|
||||
<view class="text-3 text-green-600 leading-relaxed dark:text-green-200">
|
||||
• 使用 name + params 组合\n
|
||||
• 参数同样以查询字符串形式出现\n
|
||||
• 写法: router.push({ name: 'page', params: { key: 'value' } })\n
|
||||
• 结果: /page?key=value (效果相同)
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 代码示例 -->
|
||||
<demo-block title="代码示例" transparent>
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-4 font-bold wot-text-text-main">
|
||||
接收查询参数的标准写法
|
||||
</view>
|
||||
<view class="wot-bg-bg border rounded-2 p-3 wot-border-border-main">
|
||||
<text class="text-3 leading-relaxed font-mono wot-text-text-secondary">
|
||||
// 发送方
|
||||
router.push({
|
||||
path: '/demo-query',
|
||||
query: { keyword: 'vue', type: 'framework' }
|
||||
})
|
||||
|
||||
// 接收方
|
||||
onLoad((option) => {
|
||||
if (option && option.keyword) {
|
||||
const keyword = option.keyword // 'vue'
|
||||
const type = option.type // 'framework'
|
||||
}
|
||||
})
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<demo-block title="继续演示" transparent>
|
||||
<view class="space-y-3">
|
||||
<wd-button type="primary" block @click="pushToGuard">
|
||||
跳转到导航守卫演示
|
||||
</wd-button>
|
||||
<wd-button type="warning" block @click="goBack">
|
||||
返回上一页
|
||||
</wd-button>
|
||||
</view>
|
||||
</demo-block>
|
||||
</view>
|
||||
</template>
|
||||
@@ -0,0 +1,85 @@
|
||||
<script setup lang="ts">
|
||||
definePage({
|
||||
name: 'demo-string',
|
||||
style: {
|
||||
navigationBarTitleText: '字符串路径跳转',
|
||||
},
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
function goBack() {
|
||||
router.back()
|
||||
}
|
||||
|
||||
function pushToObject() {
|
||||
router.push({ path: '/subPages/router/demo-object' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="min-h-screen py-3">
|
||||
<!-- 头部 -->
|
||||
<view class="mx-3 mb-3">
|
||||
<view class="rounded-3 px-5 py-6 text-center wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-8">
|
||||
📝
|
||||
</view>
|
||||
<view class="mb-2 text-5 font-bold wot-text-text-main">
|
||||
字符串路径跳转演示
|
||||
</view>
|
||||
<view class="text-3.5 wot-text-text-secondary">
|
||||
使用 router.push('/path') 进行跳转
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 跳转信息 -->
|
||||
<demo-block title="跳转信息" transparent>
|
||||
<view class="rounded-3 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-4 font-bold wot-text-text-main">
|
||||
当前页面信息
|
||||
</view>
|
||||
<view class="space-y-2">
|
||||
<view class="flex items-center justify-between border-b py-2 wot-border-border-main">
|
||||
<text class="text-3.5 wot-text-text-secondary">
|
||||
路径:
|
||||
</text>
|
||||
<text class="text-3.5 font-mono wot-text-text-main">
|
||||
{{ route.path }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="flex items-center justify-between border-b py-2 wot-border-border-main">
|
||||
<text class="text-3.5 wot-text-text-secondary">
|
||||
跳转方式:
|
||||
</text>
|
||||
<text class="text-3.5 wot-text-text-main">
|
||||
字符串路径
|
||||
</text>
|
||||
</view>
|
||||
<view class="flex items-center justify-between py-2">
|
||||
<text class="text-3.5 wot-text-text-secondary">
|
||||
代码:
|
||||
</text>
|
||||
<text class="text-3.5 font-mono wot-text-text-main">
|
||||
router.push('/subPages/router/demo-string')
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<demo-block title="继续演示" transparent>
|
||||
<view class="space-y-3">
|
||||
<wd-button type="primary" block @click="pushToObject">
|
||||
跳转到对象路径演示
|
||||
</wd-button>
|
||||
<wd-button type="warning" block @click="goBack">
|
||||
返回上一页
|
||||
</wd-button>
|
||||
</view>
|
||||
</demo-block>
|
||||
</view>
|
||||
</template>
|
||||
@@ -0,0 +1,469 @@
|
||||
<script setup lang="ts">
|
||||
definePage({
|
||||
name: 'router',
|
||||
style: {
|
||||
navigationBarTitleText: '路由管理',
|
||||
},
|
||||
})
|
||||
|
||||
const { success: showSuccess } = useGlobalToast()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
// 表单数据
|
||||
const userId = ref('eduardo')
|
||||
const searchKeyword = ref('vue')
|
||||
const userName = ref('小星星')
|
||||
const userLabel = ref('小熊熊')
|
||||
|
||||
// 基础导航方法
|
||||
function pushByString() {
|
||||
// 字符串路径
|
||||
router.push('/subPages/router/demo-string')
|
||||
showSuccess({ msg: '使用字符串路径跳转' })
|
||||
}
|
||||
|
||||
function pushByPath() {
|
||||
// 带有路径的对象
|
||||
router.push({ path: '/subPages/router/demo-object' })
|
||||
showSuccess({ msg: '使用path对象跳转' })
|
||||
}
|
||||
|
||||
function pushByName() {
|
||||
// 命名的路由
|
||||
router.push({ name: 'demo-object' })
|
||||
showSuccess({ msg: '使用name跳转' })
|
||||
}
|
||||
|
||||
// 参数传递示例
|
||||
function pushWithParams() {
|
||||
if (!userId.value) {
|
||||
uni.showToast({
|
||||
title: '请输入用户ID',
|
||||
icon: 'none',
|
||||
})
|
||||
return
|
||||
}
|
||||
// 命名的路由,并加上参数
|
||||
router.push({ name: 'demo-params', params: { username: userId.value } })
|
||||
showSuccess({ msg: `传递参数: ${userId.value}` })
|
||||
}
|
||||
|
||||
function pushWithQuery() {
|
||||
if (!searchKeyword.value) {
|
||||
uni.showToast({
|
||||
title: '请输入搜索关键词',
|
||||
icon: 'none',
|
||||
})
|
||||
return
|
||||
}
|
||||
// 带查询参数
|
||||
router.push({ path: '/subPages/router/demo-query', query: { username: searchKeyword.value } })
|
||||
showSuccess({ msg: `传递查询参数: ${searchKeyword.value}` })
|
||||
}
|
||||
|
||||
// 传递对象参数
|
||||
function pushWithObjectParams() {
|
||||
const user = {
|
||||
name: userName.value,
|
||||
label: userLabel.value,
|
||||
}
|
||||
// 命名路由传递对象参数
|
||||
router.push({
|
||||
name: 'demo-params',
|
||||
params: { user: encodeURIComponent(JSON.stringify(user)) },
|
||||
})
|
||||
showSuccess({ msg: '传递对象参数(params)' })
|
||||
}
|
||||
|
||||
function pushWithObjectQuery() {
|
||||
const user = {
|
||||
name: userName.value,
|
||||
label: userLabel.value,
|
||||
}
|
||||
// path+query传递对象参数
|
||||
router.push({
|
||||
path: '/subPages/router/demo-query',
|
||||
query: { user: encodeURIComponent(JSON.stringify(user)) },
|
||||
})
|
||||
showSuccess({ msg: '传递对象参数(query)' })
|
||||
}
|
||||
|
||||
// 导航守卫演示
|
||||
function demoNavigationGuards() {
|
||||
showSuccess({ msg: '跳转到完整的导航守卫演示页面' })
|
||||
router.push({
|
||||
name: 'demo-guard',
|
||||
})
|
||||
}
|
||||
|
||||
// Router方法演示
|
||||
function demoPush() {
|
||||
router.push('/subPages/router/demo-string')
|
||||
showSuccess({ msg: 'router.push() - 保留当前页面跳转' })
|
||||
}
|
||||
|
||||
function demoPushTab() {
|
||||
router.pushTab('/pages/index/index')
|
||||
showSuccess({ msg: 'router.pushTab() - 跳转到tabBar页面' })
|
||||
}
|
||||
|
||||
function demoReplace() {
|
||||
router.replace('/subPages/router/demo-object')
|
||||
showSuccess({ msg: 'router.replace() - 替换当前页面' })
|
||||
}
|
||||
|
||||
function demoReplaceAll() {
|
||||
router.replaceAll('/pages/index/index')
|
||||
showSuccess({ msg: 'router.replaceAll() - 关闭所有页面重新开始' })
|
||||
}
|
||||
|
||||
function demoBack() {
|
||||
router.back()
|
||||
showSuccess({ msg: 'router.back() - 返回上一页' })
|
||||
}
|
||||
|
||||
// 复制代码到剪贴板
|
||||
function copyCode(code: string) {
|
||||
uni.setClipboardData({
|
||||
data: code,
|
||||
showToast: false,
|
||||
success: () => {
|
||||
uni.hideToast()
|
||||
showSuccess({ msg: '代码已复制到剪贴板' })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 链接导航处理
|
||||
function handleNavigate(url: string) {
|
||||
// #ifdef H5
|
||||
window.open(url, '_blank')
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
uni.setClipboardData({
|
||||
data: url,
|
||||
showToast: false,
|
||||
success: () => {
|
||||
uni.hideToast()
|
||||
showSuccess({ msg: `${url} 已复制到剪贴板` })
|
||||
},
|
||||
})
|
||||
// #endif
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="min-h-screen py-3">
|
||||
<!-- 头部介绍 -->
|
||||
<view class="mx-3 mb-3">
|
||||
<view class="rounded-3 px-5 py-8 text-center wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-10">
|
||||
🚀
|
||||
</view>
|
||||
<view class="mb-2 text-6 font-bold wot-text-text-main">
|
||||
@wot-ui/router
|
||||
</view>
|
||||
<view class="mb-2 text-3.5 leading-relaxed wot-text-text-secondary">
|
||||
轻量级路由管理解决方案
|
||||
</view>
|
||||
<view class="text-3 wot-text-text-secondary">
|
||||
支持编程式导航、参数传递、导航守卫等功能
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 当前路由信息 -->
|
||||
<demo-block title="当前路由信息" transparent>
|
||||
<view class="rounded-3 p-4 wot-bg-filled-oppo">
|
||||
<view class="flex items-center justify-between border-b py-2 wot-border-border-main last:border-b-0">
|
||||
<text class="text-3.5 wot-text-text-secondary">
|
||||
路径:
|
||||
</text>
|
||||
<text class="text-3.5 font-mono wot-text-text-main">
|
||||
{{ route.path }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="flex items-center justify-between border-b py-2 wot-border-border-main last:border-b-0">
|
||||
<text class="text-3.5 wot-text-text-secondary">
|
||||
名称:
|
||||
</text>
|
||||
<text class="text-3.5 font-mono wot-text-text-main">
|
||||
{{ route.name || '未设置' }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="flex items-center justify-between border-b py-2 wot-border-border-main last:border-b-0">
|
||||
<text class="text-3.5 wot-text-text-secondary">
|
||||
参数:
|
||||
</text>
|
||||
<text class="break-all text-3.5 font-mono wot-text-text-main">
|
||||
{{ JSON.stringify(route.params) }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="flex items-center justify-between py-2">
|
||||
<text class="text-3.5 wot-text-text-secondary">
|
||||
查询:
|
||||
</text>
|
||||
<text class="break-all text-3.5 font-mono wot-text-text-main">
|
||||
{{ JSON.stringify(route.query) }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 编程式导航 -->
|
||||
<demo-block title="编程式导航" transparent>
|
||||
<view class="space-y-3">
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-4 font-bold wot-text-text-main">
|
||||
基础用法
|
||||
</view>
|
||||
<view class="wot-bg-bg mb-3 flex items-center justify-between border rounded-2 p-3 wot-border-border-main" @click="copyCode('router.push(\'/user\')')">
|
||||
<text class="flex-1 text-3 font-mono wot-text-text-secondary">
|
||||
router.push('/user')
|
||||
</text>
|
||||
<wd-icon name="copy" size="16px" color="#666" />
|
||||
</view>
|
||||
<view class="grid grid-cols-1 gap-2">
|
||||
<wd-button type="primary" size="small" @click="pushByString">
|
||||
字符串路径跳转
|
||||
</wd-button>
|
||||
<wd-button type="success" size="small" @click="pushByPath">
|
||||
对象路径跳转
|
||||
</wd-button>
|
||||
<wd-button type="warning" size="small" @click="pushByName">
|
||||
命名路由跳转
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- Router方法演示 -->
|
||||
<demo-block title="Router方法演示" transparent>
|
||||
<view class="space-y-3">
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-4 font-bold wot-text-text-main">
|
||||
push 方法
|
||||
</view>
|
||||
<view class="mb-3 text-3.5 leading-relaxed wot-text-text-secondary">
|
||||
保留当前页面,跳转到应用内的某个页面,相当于 uni.navigateTo()
|
||||
</view>
|
||||
<view class="wot-bg-bg mb-3 flex items-center justify-between border rounded-2 p-3 wot-border-border-main" @click="copyCode('router.push(target)')">
|
||||
<text class="flex-1 text-3 font-mono wot-text-text-secondary">
|
||||
router.push(target)
|
||||
</text>
|
||||
<wd-icon name="copy" size="16px" color="#666" />
|
||||
</view>
|
||||
<wd-button type="primary" block @click="demoPush">
|
||||
演示 push 方法
|
||||
</wd-button>
|
||||
</view>
|
||||
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-4 font-bold wot-text-text-main">
|
||||
pushTab 方法
|
||||
</view>
|
||||
<view class="mb-3 text-3.5 leading-relaxed wot-text-text-secondary">
|
||||
跳转到 tabBar 页面,并关闭其他所有非 tabBar 页面,相当于 uni.switchTab()
|
||||
</view>
|
||||
<view class="wot-bg-bg mb-3 flex items-center justify-between border rounded-2 p-3 wot-border-border-main" @click="copyCode('router.pushTab(target)')">
|
||||
<text class="flex-1 text-3 font-mono wot-text-text-secondary">
|
||||
router.pushTab(target)
|
||||
</text>
|
||||
<wd-icon name="copy" size="16px" color="#666" />
|
||||
</view>
|
||||
<wd-button type="success" block @click="demoPushTab">
|
||||
演示 pushTab 方法
|
||||
</wd-button>
|
||||
</view>
|
||||
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-4 font-bold wot-text-text-main">
|
||||
replace 方法
|
||||
</view>
|
||||
<view class="mb-3 text-3.5 leading-relaxed wot-text-text-secondary">
|
||||
关闭当前页面,跳转到应用内的某个页面,相当于 uni.redirectTo()
|
||||
</view>
|
||||
<view class="wot-bg-bg mb-3 flex items-center justify-between border rounded-2 p-3 wot-border-border-main" @click="copyCode('router.replace(target)')">
|
||||
<text class="flex-1 text-3 font-mono wot-text-text-secondary">
|
||||
router.replace(target)
|
||||
</text>
|
||||
<wd-icon name="copy" size="16px" color="#666" />
|
||||
</view>
|
||||
<wd-button type="warning" block @click="demoReplace">
|
||||
演示 replace 方法
|
||||
</wd-button>
|
||||
</view>
|
||||
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-4 font-bold wot-text-text-main">
|
||||
replaceAll 方法
|
||||
</view>
|
||||
<view class="mb-3 text-3.5 leading-relaxed wot-text-text-secondary">
|
||||
关闭所有页面,打开到应用内的某个页面,相当于 uni.reLaunch()
|
||||
</view>
|
||||
<view class="wot-bg-bg mb-3 flex items-center justify-between border rounded-2 p-3 wot-border-border-main" @click="copyCode('router.replaceAll(target)')">
|
||||
<text class="flex-1 text-3 font-mono wot-text-text-secondary">
|
||||
router.replaceAll(target)
|
||||
</text>
|
||||
<wd-icon name="copy" size="16px" color="#666" />
|
||||
</view>
|
||||
<wd-button type="danger" block @click="demoReplaceAll">
|
||||
演示 replaceAll 方法
|
||||
</wd-button>
|
||||
</view>
|
||||
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-4 font-bold wot-text-text-main">
|
||||
back 方法
|
||||
</view>
|
||||
<view class="mb-3 text-3.5 leading-relaxed wot-text-text-secondary">
|
||||
关闭当前页面,返回上一页面或多级页面,相当于 uni.navigateBack()
|
||||
</view>
|
||||
<view class="mb-3 space-y-2">
|
||||
<view class="wot-bg-bg flex items-center justify-between border rounded-2 p-3 wot-border-border-main" @click="copyCode('router.back()')">
|
||||
<text class="flex-1 text-3 font-mono wot-text-text-secondary">
|
||||
router.back()
|
||||
</text>
|
||||
<wd-icon name="copy" size="16px" color="#666" />
|
||||
</view>
|
||||
<view class="wot-bg-bg flex items-center justify-between border rounded-2 p-3 wot-border-border-main" @click="copyCode('router.back({ delta: 2 })')">
|
||||
<text class="flex-1 text-3 font-mono wot-text-text-secondary">
|
||||
router.back({ delta: 2 })
|
||||
</text>
|
||||
<wd-icon name="copy" size="16px" color="#666" />
|
||||
</view>
|
||||
</view>
|
||||
<wd-button type="info" block @click="demoBack">
|
||||
返回上一页
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 参数传递 -->
|
||||
<demo-block title="参数传递" transparent>
|
||||
<view class="mb-3 border border-orange-200 rounded-2 bg-orange-50 p-3 dark:bg-orange-900/20">
|
||||
<view class="mb-2 text-3.5 text-orange-700 font-bold dark:text-orange-300">
|
||||
⚠️ 重要提示
|
||||
</view>
|
||||
<view class="text-3 text-orange-600 leading-relaxed dark:text-orange-200">
|
||||
在 @wot-ui/router 中,params 和 query 参数都会以查询字符串形式放在 URL 中,两者在实际效果上并无区别。这种 API 设计主要是为了与 vue-router 保持一致。
|
||||
</view>
|
||||
</view>
|
||||
<view class="space-y-3">
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-4 font-bold wot-text-text-main">
|
||||
params 参数
|
||||
</view>
|
||||
<view class="mb-3 text-3.5 wot-text-text-secondary">
|
||||
注意:name 和 params 搭配使用,与 query 效果相同
|
||||
</view>
|
||||
<view class="mb-3">
|
||||
<wd-input v-model="userId" placeholder="请输入用户名" />
|
||||
</view>
|
||||
<view class="wot-bg-bg mb-3 flex items-center justify-between border rounded-2 p-3 wot-border-border-main" @click="copyCode('router.push({ name: \'user\', params: { username: \'eduardo\' } })')">
|
||||
<text class="flex-1 text-3 font-mono wot-text-text-secondary">
|
||||
router.push({ name: 'user', params: { username: 'eduardo' } })
|
||||
</text>
|
||||
<wd-icon name="copy" size="16px" color="#666" />
|
||||
</view>
|
||||
<wd-button type="primary" block @click="pushWithParams">
|
||||
传递 params 参数
|
||||
</wd-button>
|
||||
</view>
|
||||
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-4 font-bold wot-text-text-main">
|
||||
query 参数
|
||||
</view>
|
||||
<view class="mb-3 text-3.5 wot-text-text-secondary">
|
||||
注意:path 可以与 query 一起使用,与 params 效果相同
|
||||
</view>
|
||||
<view class="mb-3">
|
||||
<wd-input v-model="searchKeyword" placeholder="请输入搜索关键词" />
|
||||
</view>
|
||||
<view class="wot-bg-bg mb-3 flex items-center justify-between border rounded-2 p-3 wot-border-border-main" @click="copyCode('router.push({ path: \'/user\', query: { username: \'eduardo\' } })')">
|
||||
<text class="flex-1 text-3 font-mono wot-text-text-secondary">
|
||||
router.push({ path: '/user', query: { username: 'eduardo' } })
|
||||
</text>
|
||||
<wd-icon name="copy" size="16px" color="#666" />
|
||||
</view>
|
||||
<wd-button type="success" block @click="pushWithQuery">
|
||||
传递 query 参数
|
||||
</wd-button>
|
||||
</view>
|
||||
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-4 font-bold wot-text-text-main">
|
||||
传递对象参数
|
||||
</view>
|
||||
<view class="mb-3 text-3.5 leading-relaxed wot-text-text-secondary">
|
||||
URL有长度限制,复杂对象需要使用 encodeURIComponent 编码
|
||||
</view>
|
||||
<view class="grid grid-cols-2 mb-3 gap-2">
|
||||
<wd-input v-model="userName" placeholder="姓名" />
|
||||
<wd-input v-model="userLabel" placeholder="标签" />
|
||||
</view>
|
||||
<view class="grid grid-cols-2 gap-2">
|
||||
<wd-button type="primary" size="small" @click="pushWithObjectParams">
|
||||
对象 params
|
||||
</wd-button>
|
||||
<wd-button type="success" size="small" @click="pushWithObjectQuery">
|
||||
对象 query
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 导航守卫 -->
|
||||
<demo-block title="导航守卫" transparent>
|
||||
<view class="mb-3 border border-blue-200 rounded-2 bg-blue-50 p-3 dark:bg-blue-900/20">
|
||||
<view class="mb-2 text-3.5 text-blue-700 font-bold dark:text-blue-300">
|
||||
💡 完整演示
|
||||
</view>
|
||||
<view class="text-3 text-blue-600 leading-relaxed dark:text-blue-200">
|
||||
导航守卫功能比较复杂,我们准备了专门的演示页面,包含实时日志、权限验证、参数检查等完整功能演示。
|
||||
</view>
|
||||
</view>
|
||||
<view class="space-y-3">
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-4 font-bold wot-text-text-main">
|
||||
基础守卫 API
|
||||
</view>
|
||||
<view class="mb-3 text-3.5 leading-relaxed wot-text-text-secondary">
|
||||
beforeEach 和 afterEach 是最常用的导航守卫
|
||||
</view>
|
||||
<view class="mb-3 space-y-2">
|
||||
<view class="wot-bg-bg flex items-center justify-between border rounded-2 p-3 wot-border-border-main" @click="copyCode('router.beforeEach((to, from, next) => { next() })')">
|
||||
<text class="flex-1 text-3 font-mono wot-text-text-secondary">
|
||||
router.beforeEach((to, from, next) => { next() })
|
||||
</text>
|
||||
<wd-icon name="copy" size="16px" color="#666" />
|
||||
</view>
|
||||
<view class="wot-bg-bg flex items-center justify-between border rounded-2 p-3 wot-border-border-main" @click="copyCode('router.afterEach((to, from) => { })')">
|
||||
<text class="flex-1 text-3 font-mono wot-text-text-secondary">
|
||||
router.afterEach((to, from) => { console.log(to) })
|
||||
</text>
|
||||
<wd-icon name="copy" size="16px" color="#666" />
|
||||
</view>
|
||||
</view>
|
||||
<wd-button type="primary" block @click="demoNavigationGuards">
|
||||
🛡️ 进入完整守卫演示
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 相关链接 -->
|
||||
<demo-block title="相关链接" transparent>
|
||||
<wd-cell-group border custom-class="rounded-2! overflow-hidden">
|
||||
<wd-cell title="📚 @wot-ui/router 文档" value="路由管理" is-link @click="handleNavigate('https://moonofweisheng.github.io/@wot-ui/router/')" />
|
||||
<wd-cell title="🐙 GitHub 仓库" value="@wot-ui/router" is-link @click="handleNavigate('https://my-uni.wot-ui.cn/')" />
|
||||
<wd-cell title="📖 uni-app 路由文档" value="页面路由" is-link @click="handleNavigate('https://uniapp.dcloud.net.cn/tutorial/page.html')" />
|
||||
</wd-cell-group>
|
||||
</demo-block>
|
||||
</view>
|
||||
</template>
|
||||
@@ -0,0 +1,162 @@
|
||||
<script setup lang="ts">
|
||||
definePage({
|
||||
name: 'skills',
|
||||
layout: 'default',
|
||||
style: {
|
||||
navigationBarTitleText: 'Agent Skills',
|
||||
navigationBarBackgroundColor: '#f9fafb',
|
||||
navigationBarTextStyle: 'black',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
const skills = [
|
||||
{
|
||||
name: 'alova-api-module',
|
||||
desc: '快速创建 Alova 请求模块和 Mock 数据',
|
||||
icon: 'swap',
|
||||
color: '#007AFF',
|
||||
},
|
||||
{
|
||||
name: 'global-feedback',
|
||||
desc: '全局反馈组件(Toast/Message/Loading)使用指南',
|
||||
icon: 'chat',
|
||||
color: '#34C759',
|
||||
},
|
||||
{
|
||||
name: 'pinia-store-generator',
|
||||
desc: '创建符合项目规范的 Pinia Store',
|
||||
icon: 'folder',
|
||||
color: '#FFCC00',
|
||||
},
|
||||
{
|
||||
name: 'uni-page-generator',
|
||||
desc: '基于项目规范快速生成 uni-app 页面',
|
||||
icon: 'file',
|
||||
color: '#FF9500',
|
||||
},
|
||||
{
|
||||
name: 'vue-composable-creator',
|
||||
desc: '快速创建 Vue 3 组合式函数',
|
||||
icon: 'setting',
|
||||
color: '#5856D6',
|
||||
},
|
||||
{
|
||||
name: 'wot-router-usage',
|
||||
desc: '@wot-ui/router 轻量级路由库使用指南',
|
||||
icon: 'location',
|
||||
color: '#FF2D55',
|
||||
},
|
||||
{
|
||||
name: 'wot-ui',
|
||||
desc: 'wot-ui uni-app 组件库开发指南',
|
||||
icon: 'app',
|
||||
color: '#FE5900',
|
||||
},
|
||||
]
|
||||
|
||||
function handleClick(skill: string) {
|
||||
toast.success(`已选择: ${skill}`)
|
||||
}
|
||||
|
||||
function getIconBg(color: string) {
|
||||
// Simple hex to rgba with low opacity
|
||||
const r = Number.parseInt(color.slice(1, 3), 16)
|
||||
const g = Number.parseInt(color.slice(3, 5), 16)
|
||||
const b = Number.parseInt(color.slice(5, 7), 16)
|
||||
return `rgba(${r}, ${g}, ${b}, 0.1)`
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="pb-safe box-border min-h-screen wot-bg-bg">
|
||||
<!-- Header -->
|
||||
<view class="px-6 pb-6 pt-8">
|
||||
<view class="mb-2 text-3xl font-bold leading-tight wot-text-text-main">
|
||||
Agent Skills
|
||||
</view>
|
||||
<view class="text-base leading-relaxed wot-text-text-secondary">
|
||||
利用智能工具赋能开发效率,<br>打造极致开发体验。
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Content -->
|
||||
<view class="px-5 space-y-4">
|
||||
<view
|
||||
v-for="(skill, index) in skills"
|
||||
:key="skill.name"
|
||||
class="group relative animate-fade-in-up overflow-hidden rounded-[24px] p-5 shadow-sm transition-all duration-300 wot-bg-filled-oppo active:scale-[0.98]"
|
||||
:style="{ animationDelay: `${index * 100}ms` }"
|
||||
@click="handleClick(skill.name)"
|
||||
>
|
||||
<view class="flex items-start gap-4">
|
||||
<!-- Icon Container -->
|
||||
<view
|
||||
class="h-14 w-14 flex shrink-0 items-center justify-center rounded-[18px] transition-transform duration-300 group-active:scale-110"
|
||||
:style="{ backgroundColor: getIconBg(skill.color) }"
|
||||
>
|
||||
<wd-icon
|
||||
:name="skill.icon"
|
||||
size="28px"
|
||||
:color="skill.color"
|
||||
custom-style="display: block;"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- Text Content -->
|
||||
<view class="min-w-0 flex-1 pt-0.5">
|
||||
<view class="flex items-center justify-between">
|
||||
<view class="truncate pr-2 text-lg font-bold wot-text-text-main">
|
||||
{{ skill.name }}
|
||||
</view>
|
||||
<wd-icon name="arrow-right" color="#E5E7EB" size="20px" />
|
||||
</view>
|
||||
<view class="line-clamp-2 mt-1 text-sm leading-relaxed wot-text-text-secondary">
|
||||
{{ skill.desc }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Decoration (optional glow or highlight) -->
|
||||
<view
|
||||
class="pointer-events-none absolute h-24 w-24 rounded-full opacity-[0.03] -bottom-4 -right-4"
|
||||
:style="{ backgroundColor: skill.color }"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<wd-toast />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-in-up {
|
||||
opacity: 0; /* meaningful for delay */
|
||||
animation: fadeInUp 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||
}
|
||||
|
||||
/* Utilities */
|
||||
.line-clamp-2 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pb-safe {
|
||||
padding-bottom: constant(safe-area-inset-bottom);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,493 @@
|
||||
<script setup lang="ts">
|
||||
definePage({
|
||||
name: 'styles',
|
||||
style: {
|
||||
navigationBarTitleText: 'UnoCSS 演示',
|
||||
},
|
||||
})
|
||||
|
||||
const { success: showSuccess } = useGlobalToast()
|
||||
|
||||
// 演示状态
|
||||
const animationState = ref(false)
|
||||
|
||||
// UnoCSS 核心特性
|
||||
const unocssFeatures = ref([
|
||||
{
|
||||
icon: '⚡',
|
||||
title: '即时按需',
|
||||
desc: '只生成你使用的CSS',
|
||||
example: 'text-red-500 bg-blue-100',
|
||||
},
|
||||
{
|
||||
icon: '🎨',
|
||||
title: '完全可定制',
|
||||
desc: '灵活的配置和预设',
|
||||
example: 'custom-color-brand',
|
||||
},
|
||||
{
|
||||
icon: '🔧',
|
||||
title: '预设丰富',
|
||||
desc: '内置Tailwind、Windi等预设',
|
||||
example: '@unocss/preset-wind',
|
||||
},
|
||||
{
|
||||
icon: '🚀',
|
||||
title: '性能优异',
|
||||
desc: '极快的构建速度',
|
||||
example: '< 200ms',
|
||||
},
|
||||
])
|
||||
|
||||
// 颜色系统演示
|
||||
const colorCategories = ref([
|
||||
{
|
||||
name: '灰色系',
|
||||
colors: [
|
||||
{ name: 'gray-50', class: 'wot-bg-bg', value: '#f9fafb' },
|
||||
{ name: 'gray-100', class: 'bg-gray-100', value: '#f3f4f6' },
|
||||
{ name: 'gray-200', class: 'bg-gray-200', value: '#e5e7eb' },
|
||||
{ name: 'gray-300', class: 'bg-gray-300', value: '#d1d5db' },
|
||||
{ name: 'gray-400', class: 'bg-gray-400', value: '#9ca3af' },
|
||||
{ name: 'gray-500', class: 'wot-bg-bg0', value: '#6b7280' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '主题色',
|
||||
colors: [
|
||||
{ name: 'blue-500', class: 'bg-blue-500', value: '#3b82f6' },
|
||||
{ name: 'green-500', class: 'bg-green-500', value: '#10b981' },
|
||||
{ name: 'red-500', class: 'bg-red-500', value: '#ef4444' },
|
||||
{ name: 'yellow-500', class: 'bg-yellow-500', value: '#f59e0b' },
|
||||
{ name: 'purple-500', class: 'bg-purple-500', value: '#8b5cf6' },
|
||||
{ name: 'pink-500', class: 'bg-pink-500', value: '#ec4899' },
|
||||
],
|
||||
},
|
||||
])
|
||||
|
||||
// 布局演示数据
|
||||
const layoutExamples = ref([
|
||||
{
|
||||
title: 'Flex 居中',
|
||||
code: 'flex items-center justify-center',
|
||||
class: 'flex items-center justify-center h-20 bg-blue-100 rounded-2 text-blue-800',
|
||||
},
|
||||
{
|
||||
title: 'Grid 网格',
|
||||
code: 'grid grid-cols-3 gap-4',
|
||||
class: 'grid grid-cols-3 gap-2',
|
||||
},
|
||||
{
|
||||
title: 'Space Between',
|
||||
code: 'flex justify-between items-center',
|
||||
class: 'flex justify-between items-center h-16 bg-green-100 rounded-2 px-4 text-green-800',
|
||||
},
|
||||
])
|
||||
|
||||
// 间距系统
|
||||
const spacingExamples = ref([
|
||||
{ name: 'p-1', value: '4px', class: 'p-1 bg-red-100 inline-block m-1 rounded' },
|
||||
{ name: 'p-2', value: '8px', class: 'p-2 bg-blue-100 inline-block m-1 rounded' },
|
||||
{ name: 'p-4', value: '16px', class: 'p-4 bg-green-100 inline-block m-1 rounded' },
|
||||
{ name: 'p-6', value: '24px', class: 'p-6 bg-yellow-100 inline-block m-1 rounded' },
|
||||
{ name: 'p-8', value: '32px', class: 'p-8 bg-purple-100 inline-block m-1 rounded' },
|
||||
])
|
||||
|
||||
// 字体系统
|
||||
const fontExamples = ref([
|
||||
{ name: 'text-xs', size: '12px', class: 'text-xs' },
|
||||
{ name: 'text-sm', size: '14px', class: 'text-sm' },
|
||||
{ name: 'text-base', size: '16px', class: 'text-base' },
|
||||
{ name: 'text-lg', size: '18px', class: 'text-lg' },
|
||||
{ name: 'text-xl', size: '20px', class: 'text-xl' },
|
||||
{ name: 'text-2xl', size: '24px', class: 'text-2xl' },
|
||||
{ name: 'text-3xl', size: '30px', class: 'text-3xl' },
|
||||
])
|
||||
|
||||
// 阴影和效果
|
||||
const effectExamples = ref([
|
||||
{ name: 'shadow-sm', class: 'shadow-sm wot-bg-filled-oppo p-4 rounded-2 mb-3' },
|
||||
{ name: 'shadow', class: 'shadow wot-bg-filled-oppo p-4 rounded-2 mb-3' },
|
||||
{ name: 'shadow-md', class: 'shadow-md wot-bg-filled-oppo p-4 rounded-2 mb-3' },
|
||||
{ name: 'shadow-lg', class: 'shadow-lg wot-bg-filled-oppo p-4 rounded-2 mb-3' },
|
||||
{ name: 'shadow-xl', class: 'shadow-xl wot-bg-filled-oppo p-4 rounded-2 mb-3' },
|
||||
])
|
||||
|
||||
// 切换动画
|
||||
function toggleAnimation() {
|
||||
animationState.value = !animationState.value
|
||||
showSuccess({ msg: `动画状态: ${animationState.value ? '开启' : '关闭'}` })
|
||||
}
|
||||
|
||||
// 复制代码
|
||||
function copyCode(code: string) {
|
||||
uni.setClipboardData({
|
||||
data: code,
|
||||
showToast: false,
|
||||
success: () => {
|
||||
uni.hideToast()
|
||||
showSuccess({ msg: '代码已复制到剪贴板' })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 链接导航处理
|
||||
function handleNavigate(url: string) {
|
||||
// #ifdef H5
|
||||
window.open(url, '_blank')
|
||||
// #endif
|
||||
// #ifndef H5
|
||||
uni.setClipboardData({
|
||||
data: url,
|
||||
showToast: false,
|
||||
success: () => {
|
||||
uni.hideToast()
|
||||
showSuccess({ msg: `${url} 已复制到剪贴板` })
|
||||
},
|
||||
})
|
||||
// #endif
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="min-h-screen py-3">
|
||||
<!-- 头部介绍 -->
|
||||
<view class="mx-3 mb-3">
|
||||
<view class="rounded-3 px-5 py-8 text-center wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-10">
|
||||
🎨
|
||||
</view>
|
||||
<view class="mb-2 text-6 font-bold wot-text-text-main">
|
||||
UnoCSS 原子化CSS
|
||||
</view>
|
||||
<view class="mb-2 text-3.5 leading-relaxed wot-text-text-secondary">
|
||||
即时按需的原子化CSS引擎
|
||||
</view>
|
||||
<view class="text-3 wot-text-text-secondary">
|
||||
具有高性能且极具灵活性的即时原子化CSS引擎
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- UnoCSS 核心特性 -->
|
||||
<demo-block title="UnoCSS 核心特性" transparent>
|
||||
<view class="grid grid-cols-2 gap-3">
|
||||
<view
|
||||
v-for="feature in unocssFeatures"
|
||||
:key="feature.title"
|
||||
class="rounded-2 p-4 wot-bg-filled-oppo"
|
||||
>
|
||||
<view class="mb-2 text-6">
|
||||
{{ feature.icon }}
|
||||
</view>
|
||||
<view class="mb-1 text-3.5 font-bold wot-text-text-main">
|
||||
{{ feature.title }}
|
||||
</view>
|
||||
<view class="mb-2 text-3 leading-snug wot-text-text-secondary">
|
||||
{{ feature.desc }}
|
||||
</view>
|
||||
<view class="rounded px-2 py-1 text-2.5 font-mono wot-text-text-secondary">
|
||||
{{ feature.example }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 颜色系统 -->
|
||||
<demo-block title="颜色系统" transparent>
|
||||
<view
|
||||
v-for="category in colorCategories"
|
||||
:key="category.name"
|
||||
class="mb-4 last:mb-0"
|
||||
>
|
||||
<view class="mb-3 text-4 font-bold wot-text-text-main">
|
||||
{{ category.name }}
|
||||
</view>
|
||||
<view class="grid grid-cols-3 gap-2">
|
||||
<view
|
||||
v-for="color in category.colors"
|
||||
:key="color.name"
|
||||
class="overflow-hidden rounded-2 shadow-sm wot-bg-filled-oppo"
|
||||
@click="copyCode(color.class)"
|
||||
>
|
||||
<view :class="color.class" class="h-12 w-full" />
|
||||
<view class="p-2">
|
||||
<view class="text-2.5 font-mono wot-text-text-main">
|
||||
{{ color.name }}
|
||||
</view>
|
||||
<view class="text-2 wot-text-text-secondary">
|
||||
{{ color.value }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 布局系统 -->
|
||||
<demo-block title="布局系统" transparent>
|
||||
<view class="space-y-4">
|
||||
<view
|
||||
v-for="example in layoutExamples"
|
||||
:key="example.title"
|
||||
class="rounded-2 p-4 wot-bg-filled-oppo"
|
||||
>
|
||||
<view class="mb-3 flex items-center justify-between">
|
||||
<view class="text-4 font-bold wot-text-text-main">
|
||||
{{ example.title }}
|
||||
</view>
|
||||
<view class="rounded px-2 py-1 text-2.5 font-mono wot-text-text-secondary" @click="copyCode(example.code)">
|
||||
{{ example.code }}
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="example.title === 'Flex 居中'" :class="example.class">
|
||||
<text class="text-3.5">
|
||||
居中内容
|
||||
</text>
|
||||
</view>
|
||||
<view v-else-if="example.title === 'Grid 网格'" :class="example.class">
|
||||
<view
|
||||
v-for="n in 6"
|
||||
:key="n"
|
||||
class="rounded bg-blue-100 p-2 text-center text-3 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300"
|
||||
>
|
||||
{{ n }}
|
||||
</view>
|
||||
</view>
|
||||
<view v-else :class="example.class">
|
||||
<text class="text-3.5">
|
||||
左侧
|
||||
</text>
|
||||
<text class="text-3.5">
|
||||
右侧
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 间距系统 -->
|
||||
<demo-block title="间距系统" transparent>
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-4 font-bold wot-text-text-main">
|
||||
内边距 (Padding)
|
||||
</view>
|
||||
<view class="mb-4 text-3 leading-relaxed wot-text-text-secondary">
|
||||
点击下方色块复制对应的 padding 类名
|
||||
</view>
|
||||
<view class="flex flex-wrap items-center gap-2">
|
||||
<view
|
||||
v-for="spacing in spacingExamples"
|
||||
:key="spacing.name"
|
||||
:class="spacing.class"
|
||||
class="cursor-pointer"
|
||||
@click="copyCode(spacing.name)"
|
||||
>
|
||||
<text class="text-2.5 font-mono wot-text-text-secondary">
|
||||
{{ spacing.name }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="mt-4 text-2.5 wot-text-text-secondary">
|
||||
💡 提示:p-1 = 4px, p-2 = 8px, p-4 = 16px, p-6 = 24px, p-8 = 32px
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 字体系统 -->
|
||||
<demo-block title="字体系统" transparent>
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-4 font-bold wot-text-text-main">
|
||||
字体大小
|
||||
</view>
|
||||
<view class="space-y-3">
|
||||
<view
|
||||
v-for="font in fontExamples"
|
||||
:key="font.name"
|
||||
class="flex items-center justify-between border-b pb-2 wot-border-border-main last:border-b-0"
|
||||
@click="copyCode(font.name)"
|
||||
>
|
||||
<text :class="font.class" class="wot-text-text-main">
|
||||
字体演示文字
|
||||
</text>
|
||||
<view class="flex items-center space-x-2">
|
||||
<text class="text-2.5 wot-text-text-secondary">
|
||||
{{ font.size }}
|
||||
</text>
|
||||
<text class="rounded px-2 py-1 text-2.5 font-mono wot-text-text-secondary">
|
||||
{{ font.name }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 阴影效果 -->
|
||||
<demo-block title="阴影效果" transparent>
|
||||
<view class="space-y-3">
|
||||
<view
|
||||
v-for="effect in effectExamples"
|
||||
:key="effect.name"
|
||||
:class="effect.class"
|
||||
@click="copyCode(effect.name)"
|
||||
>
|
||||
<view class="flex items-center justify-between">
|
||||
<text class="text-3.5 wot-text-text-main">
|
||||
阴影效果演示
|
||||
</text>
|
||||
<text class="rounded px-2 py-1 text-2.5 font-mono wot-text-text-secondary">
|
||||
{{ effect.name }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 交互效果 -->
|
||||
<demo-block title="交互效果" transparent>
|
||||
<view class="space-y-4">
|
||||
<!-- 悬停效果 -->
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-4 font-bold wot-text-text-main">
|
||||
悬停效果
|
||||
</view>
|
||||
<view class="grid grid-cols-2 gap-3">
|
||||
<view class="rounded-2 bg-blue-100 p-4 text-center transition-all duration-300 active:scale-95 hover:scale-105 hover:bg-blue-200">
|
||||
<text class="text-3 text-blue-800">
|
||||
悬停变色
|
||||
</text>
|
||||
</view>
|
||||
<view class="rounded-2 bg-green-100 p-4 text-center transition-all duration-300 hover:shadow-lg hover:-translate-y-1">
|
||||
<text class="text-3 text-green-800">
|
||||
悬停上浮
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="mt-3 rounded p-2 text-2.5 font-mono wot-text-text-secondary" @click="copyCode('hover:bg-blue-200 hover:scale-105 transition-all duration-300')">
|
||||
hover:bg-blue-200 hover:scale-105 transition-all duration-300
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 动画效果 -->
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-4 font-bold wot-text-text-main">
|
||||
动画效果
|
||||
</view>
|
||||
<wd-button type="primary" block @click="toggleAnimation">
|
||||
切换动画状态
|
||||
</wd-button>
|
||||
<view class="mt-4 flex justify-center">
|
||||
<view
|
||||
class="rounded-2 p-6 text-center transition-all duration-500 ease-in-out"
|
||||
:class="[
|
||||
animationState
|
||||
? 'bg-gradient-to-r from-purple-500 to-pink-500 text-white transform rotate-12 scale-110 shadow-lg'
|
||||
: 'wot-text-text-main transform rotate-0 scale-100 shadow-sm ',
|
||||
]"
|
||||
>
|
||||
<text class="text-4 font-bold">
|
||||
动画演示
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="mt-3 rounded p-2 text-2.5 leading-relaxed font-mono wot-text-text-secondary" @click="copyCode('transition-all duration-500 ease-in-out transform rotate-12 scale-110')">
|
||||
transition-all duration-500 ease-in-out\n
|
||||
transform rotate-12 scale-110
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 响应式设计 -->
|
||||
<demo-block title="响应式设计" transparent>
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-4 font-bold wot-text-text-main">
|
||||
响应式断点
|
||||
</view>
|
||||
<view class="mb-4 text-3 leading-relaxed wot-text-text-secondary">
|
||||
UnoCSS 支持响应式断点,可以根据屏幕尺寸应用不同样式
|
||||
</view>
|
||||
<view class="mb-4 rounded-2 bg-red-100 p-4 text-center dark:bg-red-900/30 lg:bg-yellow-100 md:bg-blue-100 sm:bg-green-100 dark:lg:bg-yellow-900/30 dark:md:bg-blue-900/30 dark:sm:bg-green-900/30">
|
||||
<text class="text-3.5 font-bold wot-text-text-main">
|
||||
响应式背景色
|
||||
</text>
|
||||
</view>
|
||||
<view class="grid grid-cols-1 gap-3 lg:grid-cols-4 md:grid-cols-3 sm:grid-cols-2">
|
||||
<view
|
||||
v-for="n in 8"
|
||||
:key="n"
|
||||
class="rounded bg-purple-100 p-3 text-center text-3 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300"
|
||||
>
|
||||
项目 {{ n }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="mt-4 rounded p-2 text-2.5 leading-relaxed font-mono wot-text-text-secondary" @click="copyCode('grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4')">
|
||||
grid-cols-1 sm:grid-cols-2\n
|
||||
md:grid-cols-3 lg:grid-cols-4
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 实用工具类 -->
|
||||
<demo-block title="实用工具类" transparent>
|
||||
<view class="space-y-4">
|
||||
<!-- 显示隐藏 -->
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-4 font-bold wot-text-text-main">
|
||||
显示/隐藏
|
||||
</view>
|
||||
<view class="mb-3 flex flex-wrap gap-2">
|
||||
<view class="rounded bg-green-100 px-3 py-2 text-3 text-green-800">
|
||||
显示 (block)
|
||||
</view>
|
||||
<view class="rounded bg-blue-100 px-3 py-2 text-3 text-blue-800">
|
||||
内联 (inline)
|
||||
</view>
|
||||
<view class="rounded bg-purple-100 px-3 py-2 text-3 text-purple-800">
|
||||
弹性 (flex)
|
||||
</view>
|
||||
</view>
|
||||
<view class="rounded p-2 text-2.5 font-mono wot-text-text-secondary" @click="copyCode('block inline flex hidden')">
|
||||
block inline flex hidden
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 定位 -->
|
||||
<view class="rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<view class="mb-3 text-4 font-bold wot-text-text-main">
|
||||
定位系统
|
||||
</view>
|
||||
<view class="relative h-24 rounded-2 bg-gray-100">
|
||||
<view class="absolute left-2 top-2 rounded bg-red-500 px-2 py-1 text-2.5 text-white">
|
||||
top-2 left-2
|
||||
</view>
|
||||
<view class="absolute right-2 top-2 rounded bg-blue-500 px-2 py-1 text-2.5 text-white">
|
||||
top-2 right-2
|
||||
</view>
|
||||
<view class="absolute bottom-2 left-2 rounded bg-green-500 px-2 py-1 text-2.5 text-white">
|
||||
bottom-2 left-2
|
||||
</view>
|
||||
<view class="absolute bottom-2 right-2 rounded bg-purple-500 px-2 py-1 text-2.5 text-white wot-bg-warning">
|
||||
bottom-2 right-2
|
||||
</view>
|
||||
</view>
|
||||
<view class="mt-3 rounded p-2 text-2.5 font-mono wot-text-text-secondary" @click="copyCode('absolute relative top-2 left-2 right-2 bottom-2')">
|
||||
absolute relative top-2 left-2 right-2 bottom-2
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</demo-block>
|
||||
|
||||
<!-- 相关链接 -->
|
||||
<demo-block title="相关链接" transparent>
|
||||
<wd-cell-group border custom-class="rounded-2! overflow-hidden">
|
||||
<wd-cell title="📚 UnoCSS 官方文档" value="unocss.dev" is-link @click="handleNavigate('https://unocss.dev/')" />
|
||||
<wd-cell title="🐙 GitHub 仓库" value="unocss/unocss" is-link @click="handleNavigate('https://github.com/unocss/unocss')" />
|
||||
<wd-cell title="🎮 在线演练场" value="在线体验" is-link @click="handleNavigate('https://unocss.dev/play/')" />
|
||||
<wd-cell title="📖 Tailwind CSS" value="参考文档" is-link @click="handleNavigate('https://tailwindcss.com/docs')" />
|
||||
<wd-cell title="💡 最佳实践" value="使用指南" is-link @click="handleNavigate('https://unocss.dev/guide/')" />
|
||||
</wd-cell-group>
|
||||
</demo-block>
|
||||
</view>
|
||||
</template>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
definePage({
|
||||
name: 'root',
|
||||
style: {
|
||||
navigationBarTitleText: 'uni-ku/root',
|
||||
},
|
||||
})
|
||||
|
||||
const show = ref<boolean>(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<page-meta :page-style="`overflow:${show ? 'hidden' : 'visible'};`" />
|
||||
<view class="min-h-200vh">
|
||||
<demo-block title="锁定滚动" transparent>
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="锁定滚动" is-link @click="show = !show" />
|
||||
</wd-cell-group>
|
||||
</demo-block>
|
||||
|
||||
<wd-popup
|
||||
v-model="show"
|
||||
lock-scroll
|
||||
position="bottom"
|
||||
closable
|
||||
:safe-area-inset-bottom="true"
|
||||
custom-style="height: 200px;"
|
||||
@close="show = false"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
Reference in New Issue
Block a user