first commit
This commit is contained in:
63
src/pages-system/area/components/area-tree-item.vue
Normal file
63
src/pages-system/area/components/area-tree-item.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<view class="mb-16rpx overflow-hidden rounded-12rpx bg-white shadow-sm">
|
||||
<view
|
||||
class="flex items-center justify-between p-24rpx"
|
||||
@click="handleToggle"
|
||||
>
|
||||
<view class="flex items-center">
|
||||
<!-- 展开/收起图标 -->
|
||||
<view class="mr-16rpx w-40rpx">
|
||||
<wd-icon
|
||||
v-if="hasChildren"
|
||||
:name="expanded ? 'arrow-down' : 'arrow-right'"
|
||||
size="16px"
|
||||
color="#999"
|
||||
/>
|
||||
</view>
|
||||
<!-- 地区信息 -->
|
||||
<view class="text-28rpx text-[#333]">
|
||||
{{ item.name }}
|
||||
</view>
|
||||
</view>
|
||||
<!-- 编码 -->
|
||||
<view class="text-24rpx text-[#999]">
|
||||
编码:{{ item.id }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 子节点 -->
|
||||
<view v-if="expanded && hasChildren" class="border-t border-[#f5f5f5] pl-56rpx">
|
||||
<AreaTreeItem
|
||||
v-for="child in item.children"
|
||||
:key="child.id"
|
||||
:item="child"
|
||||
:level="level + 1"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Area } from '@/api/system/area'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
item: Area
|
||||
level?: number
|
||||
}>(), {
|
||||
level: 0,
|
||||
})
|
||||
|
||||
const expanded = ref(false)
|
||||
const hasChildren = computed(() => props.item.children && props.item.children.length > 0)
|
||||
|
||||
/** 切换展开/收起 */
|
||||
function handleToggle() {
|
||||
if (hasChildren.value) {
|
||||
expanded.value = !expanded.value
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
74
src/pages-system/area/components/ip-query-form.vue
Normal file
74
src/pages-system/area/components/ip-query-form.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<wd-popup v-model="visible" position="bottom" closable safe-area-inset-bottom>
|
||||
<view class="p-32rpx">
|
||||
<view class="mb-24rpx text-32rpx font-semibold">
|
||||
IP 查询
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="ipAddress"
|
||||
label="IP 地址"
|
||||
label-width="160rpx"
|
||||
placeholder="请输入 IP 地址"
|
||||
clearable
|
||||
/>
|
||||
<wd-input
|
||||
v-model="ipResult"
|
||||
label="地址"
|
||||
label-width="160rpx"
|
||||
placeholder="展示查询 IP 结果"
|
||||
readonly
|
||||
class="mt-24rpx"
|
||||
/>
|
||||
<wd-button type="primary" block class="mt-32rpx" @click="handleQueryIp">
|
||||
查询
|
||||
</wd-button>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { getAreaByIp } from '@/api/system/area'
|
||||
import { isIp } from '@/utils/validator'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const ipAddress = ref('')
|
||||
const ipResult = ref('')
|
||||
|
||||
watch(() => props.modelValue, (val) => {
|
||||
visible.value = val
|
||||
if (val) {
|
||||
ipAddress.value = ''
|
||||
ipResult.value = ''
|
||||
}
|
||||
})
|
||||
|
||||
watch(visible, (val) => {
|
||||
emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
/** 查询 IP */
|
||||
async function handleQueryIp() {
|
||||
if (!ipAddress.value) {
|
||||
uni.showToast({ title: '请输入 IP 地址', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!isIp(ipAddress.value)) {
|
||||
uni.showToast({ title: '请输入正确的 IP 地址', icon: 'none' })
|
||||
return
|
||||
}
|
||||
ipResult.value = await getAreaByIp(ipAddress.value)
|
||||
uni.showToast({ title: '查询成功', icon: 'success' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
91
src/pages-system/area/index.vue
Normal file
91
src/pages-system/area/index.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="地区管理"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 地区树列表 -->
|
||||
<view class="p-24rpx">
|
||||
<!-- 加载中 -->
|
||||
<view v-if="loading" class="py-100rpx text-center">
|
||||
<wd-loading />
|
||||
</view>
|
||||
<!-- 地区树 -->
|
||||
<view v-else-if="areaList.length > 0">
|
||||
<AreaTreeItem
|
||||
v-for="item in areaList"
|
||||
:key="item.id"
|
||||
:item="item"
|
||||
/>
|
||||
</view>
|
||||
<!-- 空状态 -->
|
||||
<view v-else class="py-100rpx text-center">
|
||||
<wd-status-tip image="content" tip="暂无地区数据" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 搜索按钮 -->
|
||||
<wd-fab
|
||||
position="right-bottom"
|
||||
type="primary"
|
||||
:expandable="false"
|
||||
icon="search"
|
||||
@click="handleOpenIpQuery"
|
||||
/>
|
||||
|
||||
<!-- IP 查询弹窗 -->
|
||||
<IpQueryForm v-model="showIpQuery" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Area } from '@/api/system/area'
|
||||
import { ref } from 'vue'
|
||||
import { getAreaTree } from '@/api/system/area'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import AreaTreeItem from './components/area-tree-item.vue'
|
||||
import IpQueryForm from './components/ip-query-form.vue'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const areaList = ref<Area[]>([])
|
||||
|
||||
const showIpQuery = ref(false) // 是否显示 IP 查询弹窗
|
||||
|
||||
/** 获取地区树 */
|
||||
async function getList() {
|
||||
loading.value = true
|
||||
try {
|
||||
areaList.value = await getAreaTree()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 打开 IP 查询弹窗 */
|
||||
function handleOpenIpQuery() {
|
||||
showIpQuery.value = true
|
||||
}
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus()
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
90
src/pages-system/dept/components/breadcrumb.vue
Normal file
90
src/pages-system/dept/components/breadcrumb.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<view v-if="breadcrumbList.length > 0" class="bg-white px-24rpx py-16rpx">
|
||||
<scroll-view scroll-x class="whitespace-nowrap">
|
||||
<view class="inline-flex items-center">
|
||||
<view
|
||||
class="flex items-center text-28rpx"
|
||||
:class="breadcrumbList.length > 0 ? 'text-[#1890ff]' : 'text-[#333]'"
|
||||
@click="handleClick(-1)"
|
||||
>
|
||||
<text>全部部门</text>
|
||||
</view>
|
||||
<template v-for="(item, index) in breadcrumbList" :key="item.id">
|
||||
<wd-icon name="arrow-right" size="12px" color="#999" custom-class="mx-8rpx" />
|
||||
<view
|
||||
class="flex items-center text-28rpx"
|
||||
:class="index < breadcrumbList.length - 1 ? 'text-[#1890ff]' : 'text-[#333]'"
|
||||
@click="handleClick(index)"
|
||||
>
|
||||
<text>{{ item.name }}</text>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
interface BreadcrumbItem {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: number]
|
||||
}>()
|
||||
|
||||
const breadcrumbList = ref<BreadcrumbItem[]>([])
|
||||
|
||||
/** 监听外部值变化 */
|
||||
watch(() => props.modelValue, (val) => {
|
||||
if (val === 0) {
|
||||
breadcrumbList.value = []
|
||||
}
|
||||
})
|
||||
|
||||
/** 点击面包屑 */
|
||||
function handleClick(index: number) {
|
||||
if (index === -1) {
|
||||
// 点击"全部部门"
|
||||
breadcrumbList.value = []
|
||||
emit('update:modelValue', 0)
|
||||
} else if (index < breadcrumbList.value.length - 1) {
|
||||
// 点击中间层级
|
||||
const item = breadcrumbList.value[index]
|
||||
breadcrumbList.value = breadcrumbList.value.slice(0, index + 1)
|
||||
emit('update:modelValue', item.id)
|
||||
}
|
||||
}
|
||||
|
||||
/** 进入子层级 */
|
||||
function enter(item: BreadcrumbItem) {
|
||||
breadcrumbList.value.push(item)
|
||||
emit('update:modelValue', item.id)
|
||||
}
|
||||
|
||||
/** 返回上一层级 */
|
||||
function back(): boolean {
|
||||
if (breadcrumbList.value.length === 0) {
|
||||
return false
|
||||
}
|
||||
breadcrumbList.value.pop()
|
||||
const lastItem = breadcrumbList.value[breadcrumbList.value.length - 1]
|
||||
emit('update:modelValue', lastItem?.id ?? 0)
|
||||
return true
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
function reset() {
|
||||
breadcrumbList.value = []
|
||||
emit('update:modelValue', 0)
|
||||
}
|
||||
|
||||
defineExpose({ enter, back, reset })
|
||||
</script>
|
||||
94
src/pages-system/dept/components/search-form.vue
Normal file
94
src/pages-system/dept/components/search-form.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<!-- 搜索框入口 -->
|
||||
<view @click="visible = true">
|
||||
<wd-search :placeholder="placeholder" hide-cancel disabled />
|
||||
</view>
|
||||
|
||||
<!-- 搜索弹窗 -->
|
||||
<wd-popup v-model="visible" position="top" @close="visible = false">
|
||||
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
部门名称
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.name"
|
||||
placeholder="请输入部门名称"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
状态
|
||||
</view>
|
||||
<wd-radio-group v-model="formData.status" shape="button">
|
||||
<wd-radio :value="-1">
|
||||
全部
|
||||
</wd-radio>
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
<view class="yd-search-form-actions">
|
||||
<wd-button class="flex-1" plain @click="handleReset">
|
||||
重置
|
||||
</wd-button>
|
||||
<wd-button class="flex-1" type="primary" @click="handleSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { getDictLabel, getIntDictOptions } from '@/hooks/useDict'
|
||||
import { getNavbarHeight } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
|
||||
const emit = defineEmits<{
|
||||
search: [data: Record<string, any>]
|
||||
reset: []
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const formData = reactive({
|
||||
name: undefined as string | undefined,
|
||||
status: -1, // -1 表示全部
|
||||
})
|
||||
|
||||
/** 搜索条件 placeholder 拼接 */
|
||||
const placeholder = computed(() => {
|
||||
const conditions: string[] = []
|
||||
if (formData.name) {
|
||||
conditions.push(`名称:${formData.name}`)
|
||||
}
|
||||
if (formData.status !== -1) {
|
||||
conditions.push(`状态:${getDictLabel(DICT_TYPE.COMMON_STATUS, formData.status)}`)
|
||||
}
|
||||
return conditions.length > 0 ? conditions.join(' | ') : '搜索部门'
|
||||
})
|
||||
|
||||
/** 搜索 */
|
||||
function handleSearch() {
|
||||
visible.value = false
|
||||
emit('search', {
|
||||
...formData,
|
||||
status: formData.status === -1 ? undefined : formData.status,
|
||||
})
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
function handleReset() {
|
||||
formData.name = undefined
|
||||
formData.status = -1
|
||||
visible.value = false
|
||||
emit('reset')
|
||||
}
|
||||
</script>
|
||||
149
src/pages-system/dept/detail/index.vue
Normal file
149
src/pages-system/dept/detail/index.vue
Normal file
@@ -0,0 +1,149 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="部门详情"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 详情内容 -->
|
||||
<view>
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="部门名称" :value="formData?.name" />
|
||||
<wd-cell title="上级部门" :value="getParentName() || '-'" />
|
||||
<wd-cell title="负责人" :value="getLeaderName() || '-'" />
|
||||
<wd-cell title="联系电话" :value="formData?.phone || '-'" />
|
||||
<wd-cell title="邮箱" :value="formData?.email || '-'" />
|
||||
<wd-cell title="显示顺序" :value="formData?.sort" />
|
||||
<wd-cell title="状态">
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="formData?.status" />
|
||||
</wd-cell>
|
||||
<wd-cell title="创建时间" :value="formatDateTime(formData?.createTime) || '-'" />
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<view class="yd-detail-footer-actions">
|
||||
<wd-button class="flex-1" type="warning" @click="handleEdit">
|
||||
编辑
|
||||
</wd-button>
|
||||
<wd-button class="flex-1" type="error" :loading="deleting" @click="handleDelete">
|
||||
删除
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Dept } from '@/api/system/dept'
|
||||
import type { User } from '@/api/system/user'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { deleteDept, getDept, getSimpleDeptList } from '@/api/system/dept'
|
||||
import { getSimpleUserList } from '@/api/system/user'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
|
||||
const props = defineProps<{
|
||||
id: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const formData = ref<Dept>()
|
||||
const deleting = ref(false)
|
||||
const deptList = ref<Dept[]>([])
|
||||
const userList = ref<User[]>([])
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-system/dept/index')
|
||||
}
|
||||
|
||||
/** 获取上级部门名称 */
|
||||
function getParentName(): string {
|
||||
if (!formData.value?.parentId || formData.value.parentId === 0) {
|
||||
return '顶级部门'
|
||||
}
|
||||
const parent = deptList.value.find(d => d.id === formData.value?.parentId)
|
||||
return parent?.name || '未知'
|
||||
}
|
||||
|
||||
/** 获取负责人名称 */
|
||||
function getLeaderName(): string {
|
||||
if (!formData.value?.leaderUserId) {
|
||||
return '未设置'
|
||||
}
|
||||
const user = userList.value.find(u => u.id === formData.value?.leaderUserId)
|
||||
return user?.nickname || '未知'
|
||||
}
|
||||
|
||||
/** 加载部门详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
toast.loading('加载中...')
|
||||
try {
|
||||
formData.value = await getDept(props.id)
|
||||
} finally {
|
||||
toast.close()
|
||||
}
|
||||
}
|
||||
|
||||
/** 编辑部门 */
|
||||
function handleEdit() {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/dept/form/index?id=${props.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除部门 */
|
||||
function handleDelete() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要删除该部门吗?',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) {
|
||||
return
|
||||
}
|
||||
deleting.value = true
|
||||
try {
|
||||
await deleteDept(props.id)
|
||||
toast.success('删除成功')
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
// 获取部门列表
|
||||
deptList.value = await getSimpleDeptList()
|
||||
// 获取用户列表
|
||||
userList.value = await getSimpleUserList()
|
||||
// 获取详情
|
||||
await getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
185
src/pages-system/dept/form/components/dept-picker.vue
Normal file
185
src/pages-system/dept/form/components/dept-picker.vue
Normal file
@@ -0,0 +1,185 @@
|
||||
<template>
|
||||
<wd-col-picker
|
||||
v-model="selectedValue"
|
||||
:label="label"
|
||||
label-width="180rpx"
|
||||
:columns="deptColumns"
|
||||
value-key="id"
|
||||
label-key="name"
|
||||
:column-change="handleColumnChange"
|
||||
:display-format="displayFormat"
|
||||
@confirm="handleConfirm"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Dept } from '@/api/system/dept'
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import { getSimpleDeptList } from '@/api/system/dept'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue?: number
|
||||
label?: string
|
||||
showRoot?: boolean // 是否显示顶级部门节点
|
||||
}>(), {
|
||||
label: '上级部门',
|
||||
showRoot: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: number | undefined): void
|
||||
}>()
|
||||
|
||||
const deptList = ref<Dept[]>([])
|
||||
const deptColumns = ref<any[]>([])
|
||||
const selectedValue = ref<number[]>([])
|
||||
|
||||
/** 监听外部值变化,回显选中值 */
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
// 0 或 undefined 都视为顶级部门(如果允许显示顶级)
|
||||
if (val && val !== 0 && deptList.value.length > 0) {
|
||||
const path = findDeptPath(val)
|
||||
selectedValue.value = path
|
||||
// 构建列数据以支持回显
|
||||
buildColumnsForPath(path)
|
||||
} else {
|
||||
if (props.showRoot) {
|
||||
// 顶级部门或未选择,重置
|
||||
selectedValue.value = [0]
|
||||
} else {
|
||||
selectedValue.value = []
|
||||
}
|
||||
// 重新构建第一列,确保正确
|
||||
if (deptList.value.length > 0) {
|
||||
initFirstColumn()
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
/** 初始化第一列 */
|
||||
function initFirstColumn() {
|
||||
const topDepts = deptList.value.filter(item => item.parentId === 0)
|
||||
if (props.showRoot) {
|
||||
deptColumns.value = [
|
||||
[
|
||||
{ id: 0, name: '顶级部门' },
|
||||
...topDepts,
|
||||
],
|
||||
]
|
||||
} else {
|
||||
deptColumns.value = [topDepts]
|
||||
}
|
||||
}
|
||||
|
||||
/** 构建带"选择当前"选项的子列表 */
|
||||
function buildChildrenWithCurrent(parentId: number) {
|
||||
const children = deptList.value.filter(item => item.parentId === parentId)
|
||||
// 添加"选择当前"选项,使用父节点 ID 的负数作为标识
|
||||
return [
|
||||
{ id: -parentId, name: '✓ 选择当前' },
|
||||
...children,
|
||||
]
|
||||
}
|
||||
|
||||
/** 加载部门列表 */
|
||||
async function loadDeptList() {
|
||||
deptList.value = await getSimpleDeptList()
|
||||
|
||||
// 初始化第一列
|
||||
initFirstColumn()
|
||||
|
||||
// 如果有初始值,回显
|
||||
if (props.modelValue && props.modelValue !== 0) {
|
||||
const path = findDeptPath(props.modelValue)
|
||||
selectedValue.value = path
|
||||
buildColumnsForPath(path)
|
||||
}
|
||||
}
|
||||
|
||||
/** 查找部门路径 */
|
||||
function findDeptPath(targetId: number): number[] {
|
||||
const path: number[] = []
|
||||
const findPath = (parentId: number, id: number): boolean => {
|
||||
const items = deptList.value.filter(d => d.parentId === parentId)
|
||||
for (const item of items) {
|
||||
if (item.id === id) {
|
||||
path.push(item.id)
|
||||
return true
|
||||
}
|
||||
if (findPath(item.id, id)) {
|
||||
path.unshift(item.id)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
findPath(0, targetId)
|
||||
return path
|
||||
}
|
||||
|
||||
/** 根据路径构建列数据 */
|
||||
function buildColumnsForPath(path: number[]) {
|
||||
if (path.length === 0) {
|
||||
return
|
||||
}
|
||||
// 第一列已经有了,从第二列开始构建
|
||||
const columns = [deptColumns.value[0]]
|
||||
for (let i = 0; i < path.length - 1; i++) {
|
||||
const parentId = path[i]
|
||||
const children = deptList.value.filter(item => item.parentId === parentId)
|
||||
if (children.length > 0) {
|
||||
columns.push(children)
|
||||
}
|
||||
}
|
||||
deptColumns.value = columns
|
||||
}
|
||||
|
||||
/** 列变化 */
|
||||
function handleColumnChange({ selectedItem, resolve, finish }: any) {
|
||||
// 选择顶级部门或"选择当前",结束
|
||||
if (selectedItem.id === 0 || selectedItem.id < 0) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
const children = deptList.value.filter(item => item.parentId === selectedItem.id)
|
||||
if (children.length > 0) {
|
||||
resolve(buildChildrenWithCurrent(selectedItem.id))
|
||||
} else {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
/** 格式化显示 */
|
||||
function displayFormat(selectedItems: any[]) {
|
||||
// 过滤掉"选择当前"选项
|
||||
return selectedItems
|
||||
.filter(item => item.id >= 0)
|
||||
.map(item => item.name)
|
||||
.join('/')
|
||||
}
|
||||
|
||||
/** 确认选择 */
|
||||
function handleConfirm({ value }: { value: number[] }) {
|
||||
if (value && value.length > 0) {
|
||||
const lastValue = value[value.length - 1]
|
||||
// 如果选择的是"选择当前"(负数 ID),取其绝对值作为实际选中的部门 ID
|
||||
if (lastValue < 0) {
|
||||
emit('update:modelValue', Math.abs(lastValue))
|
||||
} else {
|
||||
emit('update:modelValue', lastValue)
|
||||
}
|
||||
} else {
|
||||
// 如果允许 root,默认顶级 0;否则 undefined
|
||||
emit('update:modelValue', props.showRoot ? 0 : undefined)
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
loadDeptList()
|
||||
})
|
||||
</script>
|
||||
167
src/pages-system/dept/form/index.vue
Normal file
167
src/pages-system/dept/form/index.vue
Normal file
@@ -0,0 +1,167 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
:title="getTitle"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 表单区域 -->
|
||||
<view>
|
||||
<wd-form ref="formRef" :model="formData" :rules="formRules">
|
||||
<wd-cell-group border>
|
||||
<DeptPicker
|
||||
v-model="formData.parentId"
|
||||
label="上级部门"
|
||||
:show-root="true"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="formData.name"
|
||||
label="部门名称"
|
||||
label-width="180rpx"
|
||||
prop="name"
|
||||
clearable
|
||||
placeholder="请输入部门名称"
|
||||
/>
|
||||
<wd-cell title="显示顺序" title-width="180rpx" prop="sort" center>
|
||||
<wd-input-number
|
||||
v-model="formData.sort"
|
||||
:min="0"
|
||||
/>
|
||||
</wd-cell>
|
||||
<UserPicker
|
||||
v-model="formData.leaderUserId"
|
||||
type="radio"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="formData.phone"
|
||||
label="联系电话"
|
||||
label-width="180rpx"
|
||||
prop="phone"
|
||||
clearable
|
||||
placeholder="请输入联系电话"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="formData.email"
|
||||
label="邮箱"
|
||||
label-width="180rpx"
|
||||
prop="email"
|
||||
clearable
|
||||
placeholder="请输入邮箱"
|
||||
/>
|
||||
<wd-cell title="状态" title-width="180rpx" prop="status" center>
|
||||
<wd-switch
|
||||
v-model="formData.status"
|
||||
:active-value="CommonStatusEnum.ENABLE"
|
||||
:inactive-value="CommonStatusEnum.DISABLE"
|
||||
/>
|
||||
</wd-cell>
|
||||
</wd-cell-group>
|
||||
</wd-form>
|
||||
</view>
|
||||
|
||||
<!-- 底部保存按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<wd-button
|
||||
type="primary"
|
||||
block
|
||||
:loading="formLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
保存
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance } from 'wot-design-uni/components/wd-form/types'
|
||||
import type { Dept } from '@/api/system/dept'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { createDept, getDept, updateDept } from '@/api/system/dept'
|
||||
import UserPicker from '@/components/system-select/user-picker.vue'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { CommonStatusEnum } from '@/utils/constants'
|
||||
import DeptPicker from './components/dept-picker.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
parentId?: number
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const getTitle = computed(() => props.id ? '编辑部门' : '新增部门')
|
||||
const formLoading = ref(false)
|
||||
const formData = ref<Dept>({
|
||||
id: undefined,
|
||||
name: '',
|
||||
parentId: props.parentId || 0,
|
||||
sort: 0,
|
||||
status: CommonStatusEnum.ENABLE,
|
||||
leaderUserId: undefined,
|
||||
phone: '',
|
||||
email: '',
|
||||
})
|
||||
const formRules = {
|
||||
parentId: [{ required: true, message: '上级部门不能为空' }],
|
||||
name: [{ required: true, message: '部门名称不能为空' }],
|
||||
sort: [{ required: true, message: '显示顺序不能为空' }],
|
||||
status: [{ required: true, message: '状态不能为空' }],
|
||||
}
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-system/dept/index')
|
||||
}
|
||||
|
||||
/** 加载部门详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
formData.value = await getDept(props.id)
|
||||
}
|
||||
|
||||
/** 提交表单 */
|
||||
async function handleSubmit() {
|
||||
const { valid } = await formRef.value.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
|
||||
formLoading.value = true
|
||||
try {
|
||||
if (props.id) {
|
||||
await updateDept(formData.value)
|
||||
toast.success('修改成功')
|
||||
} else {
|
||||
await createDept(formData.value)
|
||||
toast.success('新增成功')
|
||||
}
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
// 获取详情
|
||||
await getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
174
src/pages-system/dept/index.vue
Normal file
174
src/pages-system/dept/index.vue
Normal file
@@ -0,0 +1,174 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="部门管理"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 搜索组件 -->
|
||||
<SearchForm @search="handleQuery" @reset="handleReset" />
|
||||
|
||||
<!-- 面包屑导航 -->
|
||||
<Breadcrumb ref="breadcrumbRef" v-model="currentParentId" />
|
||||
|
||||
<!-- 部门列表 -->
|
||||
<view class="p-24rpx">
|
||||
<view
|
||||
v-for="item in currentList"
|
||||
:key="item.id"
|
||||
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
|
||||
>
|
||||
<!-- 主内容区域:点击进入详情 -->
|
||||
<view class="p-24rpx" @click="handleDetail(item)">
|
||||
<!-- 第一行:名称、状态标签 -->
|
||||
<view class="flex items-center justify-between">
|
||||
<view class="flex items-center">
|
||||
<view class="mr-16rpx h-48rpx w-48rpx flex items-center justify-center rounded-8rpx bg-[#1890ff]">
|
||||
<wd-icon name="folder" size="20px" color="#fff" />
|
||||
</view>
|
||||
<view class="text-32rpx text-[#333] font-semibold">
|
||||
{{ item.name }}
|
||||
</view>
|
||||
</view>
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="item.status" />
|
||||
</view>
|
||||
<!-- 第二行:负责人、子部门入口 -->
|
||||
<view class="mt-12rpx flex items-center justify-between pl-64rpx">
|
||||
<view class="text-24rpx text-[#999]">
|
||||
负责人:{{ getLeaderName(item.leaderUserId) }}
|
||||
</view>
|
||||
<view
|
||||
v-if="item.children && item.children.length > 0"
|
||||
class="flex items-center"
|
||||
@click.stop="handleEnterChildren(item)"
|
||||
>
|
||||
<text class="text-24rpx text-[#1890ff]">子部门 ({{ item.children.length }})</text>
|
||||
<wd-icon name="arrow-right" size="12px" color="#1890ff" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view v-if="!loading && currentList.length === 0" class="py-100rpx text-center">
|
||||
<wd-status-tip image="content" tip="暂无部门数据" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 新增按钮 -->
|
||||
<wd-fab
|
||||
position="right-bottom"
|
||||
type="primary"
|
||||
:expandable="false"
|
||||
@click="handleAdd"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Dept } from '@/api/system/dept'
|
||||
import type { User } from '@/api/system/user'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { getDeptList } from '@/api/system/dept'
|
||||
import { getSimpleUserList } from '@/api/system/user'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { findChildren, handleTree } from '@/utils/tree'
|
||||
import Breadcrumb from './components/breadcrumb.vue'
|
||||
import SearchForm from './components/search-form.vue'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const list = ref<Dept[]>([]) // 完整部门列表(树形结构)
|
||||
const userList = ref<User[]>([]) // 用户列表
|
||||
|
||||
const currentParentId = ref(0) // 当前层级的父节点编号
|
||||
const currentList = computed(() => {
|
||||
if (currentParentId.value === 0) {
|
||||
return list.value.filter(item => item.parentId === 0)
|
||||
}
|
||||
return findChildren(list.value, currentParentId.value)
|
||||
}) // 当前层级的部门列表
|
||||
const breadcrumbRef = ref<InstanceType<typeof Breadcrumb>>()
|
||||
|
||||
const queryParams = ref<Record<string, any>>({})
|
||||
|
||||
/** 返回上一页或上一层级 */
|
||||
function handleBack() {
|
||||
if (!breadcrumbRef.value?.back()) {
|
||||
navigateBackPlus()
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取负责人名称 */
|
||||
function getLeaderName(leaderUserId?: number): string {
|
||||
if (!leaderUserId) {
|
||||
return '未设置'
|
||||
}
|
||||
const user = userList.value.find(u => u.id === leaderUserId)
|
||||
return user?.nickname || '未知'
|
||||
}
|
||||
|
||||
/** 进入子部门层级 */
|
||||
function handleEnterChildren(item: Dept) {
|
||||
breadcrumbRef.value?.enter({ id: item.id!, name: item.name })
|
||||
}
|
||||
|
||||
/** 查询部门列表 */
|
||||
async function getList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await getDeptList(queryParams.value)
|
||||
list.value = handleTree(data)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery(data?: Record<string, any>) {
|
||||
queryParams.value = { ...data }
|
||||
// 重置面包屑
|
||||
currentParentId.value = 0
|
||||
breadcrumbRef.value?.reset()
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function handleReset() {
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 新增部门 */
|
||||
function handleAdd() {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/dept/form/index?parentId=${currentParentId.value}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 查看详情 */
|
||||
function handleDetail(item: Dept) {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/dept/detail/index?id=${item.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
// 获取用户列表
|
||||
userList.value = await getSimpleUserList()
|
||||
// 获取部门列表
|
||||
await getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
185
src/pages-system/dict/components/data-list.vue
Normal file
185
src/pages-system/dict/components/data-list.vue
Normal file
@@ -0,0 +1,185 @@
|
||||
<template>
|
||||
<view>
|
||||
<!-- 搜索组件 -->
|
||||
<DataSearchForm :dict-type="dictType" @search="handleQuery" @reset="handleReset" />
|
||||
|
||||
<!-- 字典数据列表 -->
|
||||
<view class="p-24rpx">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
|
||||
@click="handleDetail(item)"
|
||||
>
|
||||
<view class="p-24rpx">
|
||||
<view class="mb-16rpx flex items-center justify-between">
|
||||
<view class="text-32rpx text-[#333] font-semibold">
|
||||
{{ item.label }}
|
||||
</view>
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="item.status" />
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">字典键值:</text>
|
||||
<text class="min-w-0 flex-1 truncate">{{ item.value }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">字典排序:</text>
|
||||
<text>{{ item.sort }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">颜色类型:</text>
|
||||
<wd-tag v-if="item.colorType" :type="getTagType(item.colorType)">
|
||||
{{ item.colorType }}
|
||||
</wd-tag>
|
||||
<text v-else>-</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">CSS Class:</text>
|
||||
<view v-if="item.cssClass" class="rounded-4rpx px-8rpx py-2rpx text-24rpx text-white" :style="{ backgroundColor: item.cssClass }">
|
||||
{{ item.cssClass }}
|
||||
</view>
|
||||
<text v-else>-</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">创建时间:</text>
|
||||
<text>{{ formatDateTime(item.createTime) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="loadMoreState !== 'loading' && list.length === 0" class="py-100rpx text-center">
|
||||
<wd-status-tip image="content" tip="暂无字典数据" />
|
||||
</view>
|
||||
<wd-loadmore
|
||||
v-if="list.length > 0"
|
||||
:state="loadMoreState"
|
||||
@reload="loadMore"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 新增按钮 -->
|
||||
<wd-fab
|
||||
v-if="hasAccessByCodes(['system:dict:create']) && dictType"
|
||||
position="right-bottom"
|
||||
type="primary"
|
||||
:expandable="false"
|
||||
@click="handleAdd"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { TagType } from 'wot-design-uni/components/wd-tag/types'
|
||||
import type { DictData } from '@/api/system/dict/data'
|
||||
import type { LoadMoreState } from '@/http/types'
|
||||
import { ref, watch } from 'vue'
|
||||
import { getDictDataPage } from '@/api/system/dict/data'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import DataSearchForm from './data-search-form.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
dictType?: string
|
||||
}>()
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const total = ref(0)
|
||||
const list = ref<DictData[]>([])
|
||||
const loadMoreState = ref<LoadMoreState>('loading')
|
||||
const queryParams = ref({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
|
||||
/** 颜色类型 => wd-tag 的 type 映射,和 src/components/dict-tag/dict-tag.vue 是一致的 */
|
||||
const COLOR_TYPE_MAP: Record<string, TagType> = {
|
||||
default: 'default',
|
||||
primary: 'primary',
|
||||
success: 'success',
|
||||
info: 'default', // wd-tag 无 info,映射为 default
|
||||
warning: 'warning',
|
||||
danger: 'danger',
|
||||
}
|
||||
|
||||
/** 获取标签类型 */
|
||||
function getTagType(colorType: string): TagType {
|
||||
return COLOR_TYPE_MAP[colorType] || 'default'
|
||||
}
|
||||
|
||||
/** 查询列表 */
|
||||
async function getList() {
|
||||
loadMoreState.value = 'loading'
|
||||
try {
|
||||
const data = await getDictDataPage(queryParams.value)
|
||||
list.value = [...list.value, ...data.list]
|
||||
total.value = data.total
|
||||
loadMoreState.value = list.value.length >= total.value ? 'finished' : 'loading'
|
||||
} catch {
|
||||
queryParams.value.pageNo = queryParams.value.pageNo > 1 ? queryParams.value.pageNo - 1 : 1
|
||||
loadMoreState.value = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery(data?: Record<string, any>) {
|
||||
queryParams.value = {
|
||||
...data,
|
||||
pageNo: 1,
|
||||
pageSize: queryParams.value.pageSize,
|
||||
}
|
||||
list.value = []
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function handleReset() {
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 加载更多 */
|
||||
function loadMore() {
|
||||
if (loadMoreState.value === 'finished') {
|
||||
return
|
||||
}
|
||||
queryParams.value.pageNo++
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 新增 */
|
||||
function handleAdd() {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/dict/data/form/index?dictType=${props.dictType}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 查看详情 */
|
||||
function handleDetail(item: DictData) {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/dict/data/detail/index?id=${item.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 监听 dictType 变化,重新查询 */
|
||||
watch(
|
||||
() => props.dictType,
|
||||
() => {
|
||||
if (props.dictType) {
|
||||
queryParams.value.pageNo = 1
|
||||
list.value = []
|
||||
getList()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
/** 触底加载更多 */
|
||||
onReachBottom(() => {
|
||||
loadMore()
|
||||
})
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
144
src/pages-system/dict/components/data-search-form.vue
Normal file
144
src/pages-system/dict/components/data-search-form.vue
Normal file
@@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<!-- 搜索框入口 -->
|
||||
<view @click="visible = true">
|
||||
<wd-search :placeholder="placeholder" hide-cancel disabled />
|
||||
</view>
|
||||
|
||||
<!-- 搜索弹窗 -->
|
||||
<wd-popup v-model="visible" position="top" @close="visible = false">
|
||||
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
字典类型
|
||||
</view>
|
||||
<wd-picker
|
||||
v-model="formData.dictType"
|
||||
:columns="dictTypeOptions"
|
||||
label-key="label"
|
||||
value-key="value"
|
||||
placeholder="请选择字典类型"
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
字典标签
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.label"
|
||||
placeholder="请输入字典标签"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
状态
|
||||
</view>
|
||||
<wd-radio-group v-model="formData.status" shape="button">
|
||||
<wd-radio :value="-1">
|
||||
全部
|
||||
</wd-radio>
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
<view class="yd-search-form-actions">
|
||||
<wd-button class="flex-1" plain @click="handleReset">
|
||||
重置
|
||||
</wd-button>
|
||||
<wd-button class="flex-1" type="primary" @click="handleSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { getSimpleDictTypeList } from '@/api/system/dict/type'
|
||||
import { getDictLabel, getIntDictOptions } from '@/hooks/useDict'
|
||||
import { getNavbarHeight } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
|
||||
const props = defineProps<{
|
||||
dictType?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
search: [data: Record<string, any>]
|
||||
reset: []
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const formData = reactive({
|
||||
dictType: undefined as string | undefined,
|
||||
label: undefined as string | undefined,
|
||||
status: -1,
|
||||
})
|
||||
|
||||
/** 字典类型选项 */
|
||||
const dictTypeOptions = ref<{ label: string, value: string }[]>([])
|
||||
|
||||
/** 加载字典类型列表 */
|
||||
async function loadDictTypeList() {
|
||||
const list = await getSimpleDictTypeList()
|
||||
dictTypeOptions.value = list.map(item => ({
|
||||
label: item.name,
|
||||
value: item.type,
|
||||
}))
|
||||
}
|
||||
|
||||
/** 搜索条件 placeholder 拼接 */
|
||||
const placeholder = computed(() => {
|
||||
const conditions: string[] = []
|
||||
if (formData.dictType) {
|
||||
const dictTypeItem = dictTypeOptions.value.find(item => item.value === formData.dictType)
|
||||
conditions.push(`类型:${dictTypeItem?.label || formData.dictType}`)
|
||||
}
|
||||
if (formData.label) {
|
||||
conditions.push(`标签:${formData.label}`)
|
||||
}
|
||||
if (formData.status !== -1) {
|
||||
conditions.push(`状态:${getDictLabel(DICT_TYPE.COMMON_STATUS, formData.status)}`)
|
||||
}
|
||||
return conditions.length > 0 ? conditions.join(' | ') : '搜索字典数据'
|
||||
})
|
||||
|
||||
/** 搜索 */
|
||||
function handleSearch() {
|
||||
visible.value = false
|
||||
emit('search', {
|
||||
dictType: formData.dictType || undefined,
|
||||
label: formData.label || undefined,
|
||||
status: formData.status === -1 ? undefined : formData.status,
|
||||
})
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
function handleReset() {
|
||||
formData.dictType = undefined
|
||||
formData.label = undefined
|
||||
formData.status = -1
|
||||
visible.value = false
|
||||
emit('reset')
|
||||
}
|
||||
|
||||
/** 监听外部 dictType 变化 */
|
||||
watch(
|
||||
() => props.dictType,
|
||||
(val) => {
|
||||
formData.dictType = val
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
loadDictTypeList()
|
||||
})
|
||||
</script>
|
||||
154
src/pages-system/dict/components/type-list.vue
Normal file
154
src/pages-system/dict/components/type-list.vue
Normal file
@@ -0,0 +1,154 @@
|
||||
<template>
|
||||
<view>
|
||||
<!-- 搜索组件 -->
|
||||
<TypeSearchForm @search="handleQuery" @reset="handleReset" />
|
||||
|
||||
<!-- 字典类型列表 -->
|
||||
<view class="p-24rpx">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
|
||||
@click="handleDetail(item)"
|
||||
>
|
||||
<view class="p-24rpx">
|
||||
<view class="mb-16rpx flex items-center justify-between">
|
||||
<view class="text-32rpx text-[#333] font-semibold">
|
||||
{{ item.name }}
|
||||
</view>
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="item.status" />
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">字典类型:</text>
|
||||
<text class="min-w-0 flex-1 truncate">{{ item.type }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">备注:</text>
|
||||
<text class="min-w-0 flex-1 truncate">{{ item.remark || '-' }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">创建时间:</text>
|
||||
<text>{{ formatDateTime(item.createTime) }}</text>
|
||||
</view>
|
||||
<!-- 查看数据按钮 -->
|
||||
<view class="flex justify-end -mt-8">
|
||||
<wd-button size="small" type="info" @click.stop="handleSelectType(item)">
|
||||
字典数据
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="loadMoreState !== 'loading' && list.length === 0" class="py-100rpx text-center">
|
||||
<wd-status-tip image="content" tip="暂无字典类型数据" />
|
||||
</view>
|
||||
<wd-loadmore
|
||||
v-if="list.length > 0"
|
||||
:state="loadMoreState"
|
||||
@reload="loadMore"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 新增按钮 -->
|
||||
<wd-fab
|
||||
v-if="hasAccessByCodes(['system:dict:create'])"
|
||||
position="right-bottom"
|
||||
type="primary"
|
||||
:expandable="false"
|
||||
@click="handleAdd"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { DictType } from '@/api/system/dict/type'
|
||||
import type { LoadMoreState } from '@/http/types'
|
||||
import { ref } from 'vue'
|
||||
import { getDictTypePage } from '@/api/system/dict/type'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import TypeSearchForm from './type-search-form.vue'
|
||||
|
||||
const emit = defineEmits<{
|
||||
select: [dictType: string]
|
||||
}>()
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const total = ref(0)
|
||||
const list = ref<DictType[]>([])
|
||||
const loadMoreState = ref<LoadMoreState>('loading')
|
||||
const queryParams = ref({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
|
||||
/** 查询列表 */
|
||||
async function getList() {
|
||||
loadMoreState.value = 'loading'
|
||||
try {
|
||||
const data = await getDictTypePage(queryParams.value)
|
||||
list.value = [...list.value, ...data.list]
|
||||
total.value = data.total
|
||||
loadMoreState.value = list.value.length >= total.value ? 'finished' : 'loading'
|
||||
} catch {
|
||||
queryParams.value.pageNo = queryParams.value.pageNo > 1 ? queryParams.value.pageNo - 1 : 1
|
||||
loadMoreState.value = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery(data?: Record<string, any>) {
|
||||
queryParams.value = {
|
||||
...data,
|
||||
pageNo: 1,
|
||||
pageSize: queryParams.value.pageSize,
|
||||
}
|
||||
list.value = []
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function handleReset() {
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 加载更多 */
|
||||
function loadMore() {
|
||||
if (loadMoreState.value === 'finished') {
|
||||
return
|
||||
}
|
||||
queryParams.value.pageNo++
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 新增 */
|
||||
function handleAdd() {
|
||||
uni.navigateTo({
|
||||
url: '/pages-system/dict/type/form/index',
|
||||
})
|
||||
}
|
||||
|
||||
/** 查看详情 */
|
||||
function handleDetail(item: DictType) {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/dict/type/detail/index?id=${item.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 选择字典类型,查看数据 */
|
||||
function handleSelectType(item: DictType) {
|
||||
emit('select', item.type)
|
||||
}
|
||||
|
||||
/** 触底加载更多 */
|
||||
onReachBottom(() => {
|
||||
loadMore()
|
||||
})
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
169
src/pages-system/dict/components/type-search-form.vue
Normal file
169
src/pages-system/dict/components/type-search-form.vue
Normal file
@@ -0,0 +1,169 @@
|
||||
<template>
|
||||
<!-- 搜索框入口 -->
|
||||
<view @click="visible = true">
|
||||
<wd-search :placeholder="placeholder" hide-cancel disabled />
|
||||
</view>
|
||||
|
||||
<!-- 搜索弹窗 -->
|
||||
<wd-popup v-model="visible" position="top" @close="visible = false">
|
||||
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
字典名称
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.name"
|
||||
placeholder="请输入字典名称"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
字典类型
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.type"
|
||||
placeholder="请输入字典类型"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
状态
|
||||
</view>
|
||||
<wd-radio-group v-model="formData.status" shape="button">
|
||||
<wd-radio :value="-1">
|
||||
全部
|
||||
</wd-radio>
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
创建时间
|
||||
</view>
|
||||
<view class="yd-search-form-date-range-container">
|
||||
<view class="flex-1" @click="visibleCreateTime[0] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.createTime?.[0]) || '开始日期' }}
|
||||
</view>
|
||||
</view>
|
||||
-
|
||||
<view class="flex-1" @click="visibleCreateTime[1] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.createTime?.[1]) || '结束日期' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleCreateTime[0]" v-model="tempCreateTime[0]" type="date" />
|
||||
<view v-if="visibleCreateTime[0]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleCreateTime[0] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleCreateTime0Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleCreateTime[1]" v-model="tempCreateTime[1]" type="date" />
|
||||
<view v-if="visibleCreateTime[1]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleCreateTime[1] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleCreateTime1Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="yd-search-form-actions">
|
||||
<wd-button class="flex-1" plain @click="handleReset">
|
||||
重置
|
||||
</wd-button>
|
||||
<wd-button class="flex-1" type="primary" @click="handleSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { getDictLabel, getIntDictOptions } from '@/hooks/useDict'
|
||||
import { getNavbarHeight } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDate, formatDateRange } from '@/utils/date'
|
||||
|
||||
const emit = defineEmits<{
|
||||
search: [data: Record<string, any>]
|
||||
reset: []
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const formData = reactive({
|
||||
name: undefined as string | undefined,
|
||||
type: undefined as string | undefined,
|
||||
status: -1,
|
||||
createTime: [undefined, undefined] as [number | undefined, number | undefined],
|
||||
})
|
||||
|
||||
/** 搜索条件 placeholder 拼接 */
|
||||
const placeholder = computed(() => {
|
||||
const conditions: string[] = []
|
||||
if (formData.name) {
|
||||
conditions.push(`名称:${formData.name}`)
|
||||
}
|
||||
if (formData.type) {
|
||||
conditions.push(`类型:${formData.type}`)
|
||||
}
|
||||
if (formData.status !== -1) {
|
||||
conditions.push(`状态:${getDictLabel(DICT_TYPE.COMMON_STATUS, formData.status)}`)
|
||||
}
|
||||
if (formData.createTime?.[0] && formData.createTime?.[1]) {
|
||||
conditions.push(`时间:${formatDate(formData.createTime[0])}~${formatDate(formData.createTime[1])}`)
|
||||
}
|
||||
return conditions.length > 0 ? conditions.join(' | ') : '搜索字典类型'
|
||||
})
|
||||
|
||||
// 时间范围选择器状态
|
||||
const visibleCreateTime = ref<[boolean, boolean]>([false, false])
|
||||
const tempCreateTime = ref<[number, number]>([Date.now(), Date.now()])
|
||||
|
||||
/** 创建时间[0]确认 */
|
||||
function handleCreateTime0Confirm() {
|
||||
formData.createTime = [tempCreateTime.value[0], formData.createTime?.[1]]
|
||||
visibleCreateTime.value[0] = false
|
||||
}
|
||||
|
||||
/** 创建时间[1]确认 */
|
||||
function handleCreateTime1Confirm() {
|
||||
formData.createTime = [formData.createTime?.[0], tempCreateTime.value[1]]
|
||||
visibleCreateTime.value[1] = false
|
||||
}
|
||||
|
||||
/** 搜索 */
|
||||
function handleSearch() {
|
||||
visible.value = false
|
||||
emit('search', {
|
||||
name: formData.name || undefined,
|
||||
type: formData.type || undefined,
|
||||
status: formData.status === -1 ? undefined : formData.status,
|
||||
createTime: formatDateRange(formData.createTime),
|
||||
})
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
function handleReset() {
|
||||
formData.name = undefined
|
||||
formData.type = undefined
|
||||
formData.status = -1
|
||||
formData.createTime = [undefined, undefined]
|
||||
visible.value = false
|
||||
emit('reset')
|
||||
}
|
||||
</script>
|
||||
160
src/pages-system/dict/data/detail/index.vue
Normal file
160
src/pages-system/dict/data/detail/index.vue
Normal file
@@ -0,0 +1,160 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="字典数据详情"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 详情内容 -->
|
||||
<view>
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="字典编码" :value="formData?.id" />
|
||||
<wd-cell title="字典类型" :value="formData?.dictType" />
|
||||
<wd-cell title="字典标签" :value="formData?.label" />
|
||||
<wd-cell title="字典键值" :value="formData?.value" />
|
||||
<wd-cell title="字典排序" :value="formData?.sort" />
|
||||
<wd-cell title="状态">
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="formData?.status" />
|
||||
</wd-cell>
|
||||
<wd-cell title="颜色类型">
|
||||
<wd-tag v-if="formData?.colorType" :type="getTagType(formData.colorType)">
|
||||
{{ formData.colorType }}
|
||||
</wd-tag>
|
||||
<text v-else>-</text>
|
||||
</wd-cell>
|
||||
<wd-cell title="CSS Class">
|
||||
<view v-if="formData?.cssClass" class="rounded-4rpx px-8rpx py-2rpx text-24rpx text-white" :style="{ backgroundColor: formData.cssClass }">
|
||||
{{ formData.cssClass }}
|
||||
</view>
|
||||
<text v-else>-</text>
|
||||
</wd-cell>
|
||||
<wd-cell title="备注" :value="formData?.remark ?? '-'" />
|
||||
<wd-cell title="创建时间" :value="formatDateTime(formData?.createTime)" />
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<view class="yd-detail-footer-actions">
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['system:dict:update'])"
|
||||
class="flex-1" type="warning" @click="handleEdit"
|
||||
>
|
||||
编辑
|
||||
</wd-button>
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['system:dict:delete'])"
|
||||
class="flex-1" type="error" :loading="deleting" @click="handleDelete"
|
||||
>
|
||||
删除
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { TagType } from 'wot-design-uni/components/wd-tag/types'
|
||||
import type { DictData } from '@/api/system/dict/data'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { deleteDictData, getDictData } from '@/api/system/dict/data'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const toast = useToast()
|
||||
const formData = ref<DictData>()
|
||||
const deleting = ref(false)
|
||||
|
||||
/** 颜色类型 => wd-tag 的 type 映射 */
|
||||
const COLOR_TYPE_MAP: Record<string, TagType> = {
|
||||
default: 'default',
|
||||
primary: 'primary',
|
||||
success: 'success',
|
||||
info: 'default',
|
||||
warning: 'warning',
|
||||
danger: 'danger',
|
||||
error: 'danger',
|
||||
processing: 'primary',
|
||||
}
|
||||
|
||||
/** 获取标签类型 */
|
||||
function getTagType(colorType: string): TagType {
|
||||
return COLOR_TYPE_MAP[colorType] || 'default'
|
||||
}
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-system/dict/index')
|
||||
}
|
||||
|
||||
/** 加载详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
toast.loading('加载中...')
|
||||
formData.value = await getDictData(props.id)
|
||||
} finally {
|
||||
toast.close()
|
||||
}
|
||||
}
|
||||
|
||||
/** 编辑 */
|
||||
function handleEdit() {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/dict/data/form/index?id=${props.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除 */
|
||||
function handleDelete() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要删除该字典数据吗?',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) {
|
||||
return
|
||||
}
|
||||
deleting.value = true
|
||||
try {
|
||||
await deleteDictData(props.id)
|
||||
toast.success('删除成功')
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
206
src/pages-system/dict/data/form/index.vue
Normal file
206
src/pages-system/dict/data/form/index.vue
Normal file
@@ -0,0 +1,206 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
:title="getTitle"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 表单区域 -->
|
||||
<view>
|
||||
<wd-form ref="formRef" :model="formData" :rules="formRules">
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="字典类型" title-width="200rpx" prop="dictType" center>
|
||||
<wd-picker
|
||||
v-model="formData.dictType"
|
||||
:columns="dictTypeOptions"
|
||||
label-key="label"
|
||||
value-key="value"
|
||||
:disabled="!!formData.id"
|
||||
placeholder="请选择字典类型"
|
||||
/>
|
||||
</wd-cell>
|
||||
<wd-input
|
||||
v-model="formData.label"
|
||||
label="数据标签"
|
||||
label-width="200rpx"
|
||||
prop="label"
|
||||
clearable
|
||||
placeholder="请输入数据标签"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="formData.value"
|
||||
label="数据键值"
|
||||
label-width="200rpx"
|
||||
prop="value"
|
||||
clearable
|
||||
placeholder="请输入数据键值"
|
||||
/>
|
||||
<wd-input
|
||||
v-model.number="formData.sort"
|
||||
label="显示排序"
|
||||
label-width="200rpx"
|
||||
prop="sort"
|
||||
type="number"
|
||||
clearable
|
||||
placeholder="请输入显示排序"
|
||||
/>
|
||||
<wd-cell title="状态" title-width="200rpx" prop="status" center>
|
||||
<wd-radio-group v-model="formData.status" shape="button">
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-cell>
|
||||
<wd-cell title="颜色类型" title-width="200rpx" prop="colorType" center>
|
||||
<wd-picker
|
||||
v-model="formData.colorType"
|
||||
:columns="getStrDictOptions(DICT_TYPE.SYSTEM_DICT_COLOR_TYPE)"
|
||||
label-key="label"
|
||||
value-key="value"
|
||||
placeholder="请选择颜色类型"
|
||||
/>
|
||||
</wd-cell>
|
||||
<wd-input
|
||||
v-model="formData.cssClass"
|
||||
label="CSS Class"
|
||||
label-width="200rpx"
|
||||
prop="cssClass"
|
||||
clearable
|
||||
placeholder="请输入 CSS Class,如 #108ee9"
|
||||
/>
|
||||
<wd-textarea
|
||||
v-model="formData.remark"
|
||||
label="备注"
|
||||
label-width="200rpx"
|
||||
prop="remark"
|
||||
clearable
|
||||
placeholder="请输入备注"
|
||||
/>
|
||||
</wd-cell-group>
|
||||
</wd-form>
|
||||
</view>
|
||||
|
||||
<!-- 底部保存按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<wd-button
|
||||
type="primary"
|
||||
block
|
||||
:loading="formLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
保存
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance } from 'wot-design-uni/components/wd-form/types'
|
||||
import type { DictData } from '@/api/system/dict/data'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { createDictData, getDictData, updateDictData } from '@/api/system/dict/data'
|
||||
import { getSimpleDictTypeList } from '@/api/system/dict/type'
|
||||
import { getIntDictOptions, getStrDictOptions } from '@/hooks/useDict'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { CommonStatusEnum, DICT_TYPE } from '@/utils/constants'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
dictType?: string | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const getTitle = computed(() => props.id ? '编辑字典数据' : '新增字典数据')
|
||||
const formLoading = ref(false)
|
||||
const formData = ref<DictData>({
|
||||
id: undefined,
|
||||
dictType: props.dictType || '',
|
||||
label: '',
|
||||
value: '',
|
||||
sort: 0,
|
||||
status: CommonStatusEnum.ENABLE,
|
||||
colorType: '',
|
||||
cssClass: '',
|
||||
remark: '',
|
||||
})
|
||||
const formRules = {
|
||||
dictType: [{ required: true, message: '字典类型不能为空' }],
|
||||
label: [{ required: true, message: '数据标签不能为空' }],
|
||||
value: [{ required: true, message: '数据键值不能为空' }],
|
||||
sort: [{ required: true, message: '显示排序不能为空' }],
|
||||
status: [{ required: true, message: '状态不能为空' }],
|
||||
}
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/** 字典类型选项 */
|
||||
const dictTypeOptions = ref<{ label: string, value: string }[]>([])
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-system/dict/index')
|
||||
}
|
||||
|
||||
/** 加载字典类型列表 */
|
||||
async function loadDictTypeList() {
|
||||
const list = await getSimpleDictTypeList()
|
||||
dictTypeOptions.value = list.map(item => ({
|
||||
label: item.name,
|
||||
value: item.type,
|
||||
}))
|
||||
}
|
||||
|
||||
/** 加载详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
formData.value = await getDictData(props.id)
|
||||
}
|
||||
|
||||
/** 提交表单 */
|
||||
async function handleSubmit() {
|
||||
const { valid } = await formRef.value.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
|
||||
formLoading.value = true
|
||||
try {
|
||||
if (props.id) {
|
||||
await updateDictData(formData.value)
|
||||
toast.success('修改成功')
|
||||
} else {
|
||||
await createDictData(formData.value)
|
||||
toast.success('新增成功')
|
||||
}
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
await loadDictTypeList()
|
||||
await getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
59
src/pages-system/dict/index.vue
Normal file
59
src/pages-system/dict/index.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="字典管理"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- Tab 切换 -->
|
||||
<view class="bg-white">
|
||||
<wd-tabs v-model="tabIndex" shrink @change="handleTabChange">
|
||||
<wd-tab title="字典类型" />
|
||||
<wd-tab title="字典数据" />
|
||||
</wd-tabs>
|
||||
</view>
|
||||
<!-- 列表内容 -->
|
||||
<TypeList v-show="tabType === 'type'" @select="handleTypeSelect" />
|
||||
<DataList v-show="tabType === 'data'" :dict-type="selectedDictType" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import DataList from './components/data-list.vue'
|
||||
import TypeList from './components/type-list.vue'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const tabTypes: string[] = ['type', 'data']
|
||||
const tabIndex = ref(0)
|
||||
const tabType = computed<string>(() => tabTypes[tabIndex.value])
|
||||
const selectedDictType = ref<string>() // 选中的字典类型
|
||||
|
||||
/** Tab 切换 */
|
||||
function handleTabChange({ index }: { index: number }) {
|
||||
tabIndex.value = index
|
||||
}
|
||||
|
||||
/** 选择字典类型 */
|
||||
function handleTypeSelect(dictType: string) {
|
||||
selectedDictType.value = dictType
|
||||
tabIndex.value = 1 // 切换到字典数据 tab
|
||||
}
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
128
src/pages-system/dict/type/detail/index.vue
Normal file
128
src/pages-system/dict/type/detail/index.vue
Normal file
@@ -0,0 +1,128 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="字典类型详情"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 详情内容 -->
|
||||
<view>
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="字典编号" :value="formData?.id" />
|
||||
<wd-cell title="字典名称" :value="formData?.name" />
|
||||
<wd-cell title="字典类型" :value="formData?.type" />
|
||||
<wd-cell title="状态">
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="formData?.status" />
|
||||
</wd-cell>
|
||||
<wd-cell title="备注" :value="formData?.remark ?? '-'" />
|
||||
<wd-cell title="创建时间" :value="formatDateTime(formData?.createTime)" />
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<view class="yd-detail-footer-actions">
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['system:dict:update'])"
|
||||
class="flex-1" type="warning" @click="handleEdit"
|
||||
>
|
||||
编辑
|
||||
</wd-button>
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['system:dict:delete'])"
|
||||
class="flex-1" type="error" :loading="deleting" @click="handleDelete"
|
||||
>
|
||||
删除
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { DictType } from '@/api/system/dict/type'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { deleteDictType, getDictType } from '@/api/system/dict/type'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const toast = useToast()
|
||||
const formData = ref<DictType>()
|
||||
const deleting = ref(false)
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-system/dict/index')
|
||||
}
|
||||
|
||||
/** 加载详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
toast.loading('加载中...')
|
||||
formData.value = await getDictType(props.id)
|
||||
} finally {
|
||||
toast.close()
|
||||
}
|
||||
}
|
||||
|
||||
/** 编辑 */
|
||||
function handleEdit() {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/dict/type/form/index?id=${props.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除 */
|
||||
function handleDelete() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要删除该字典类型吗?',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) {
|
||||
return
|
||||
}
|
||||
deleting.value = true
|
||||
try {
|
||||
await deleteDictType(props.id)
|
||||
toast.success('删除成功')
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
150
src/pages-system/dict/type/form/index.vue
Normal file
150
src/pages-system/dict/type/form/index.vue
Normal file
@@ -0,0 +1,150 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
:title="getTitle"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 表单区域 -->
|
||||
<view>
|
||||
<wd-form ref="formRef" :model="formData" :rules="formRules">
|
||||
<wd-cell-group border>
|
||||
<wd-input
|
||||
v-model="formData.name"
|
||||
label="字典名称"
|
||||
label-width="200rpx"
|
||||
prop="name"
|
||||
clearable
|
||||
placeholder="请输入字典名称"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="formData.type"
|
||||
label="字典类型"
|
||||
label-width="200rpx"
|
||||
prop="type"
|
||||
clearable
|
||||
:disabled="!!formData.id"
|
||||
placeholder="请输入字典类型"
|
||||
/>
|
||||
<wd-cell title="状态" title-width="200rpx" prop="status" center>
|
||||
<wd-radio-group v-model="formData.status" shape="button">
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-cell>
|
||||
<wd-textarea
|
||||
v-model="formData.remark"
|
||||
label="备注"
|
||||
label-width="200rpx"
|
||||
prop="remark"
|
||||
clearable
|
||||
placeholder="请输入备注"
|
||||
/>
|
||||
</wd-cell-group>
|
||||
</wd-form>
|
||||
</view>
|
||||
|
||||
<!-- 底部保存按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<wd-button
|
||||
type="primary"
|
||||
block
|
||||
:loading="formLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
保存
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance } from 'wot-design-uni/components/wd-form/types'
|
||||
import type { DictType } from '@/api/system/dict/type'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { createDictType, getDictType, updateDictType } from '@/api/system/dict/type'
|
||||
import { getIntDictOptions } from '@/hooks/useDict'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { CommonStatusEnum, DICT_TYPE } from '@/utils/constants'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const getTitle = computed(() => props.id ? '编辑字典类型' : '新增字典类型')
|
||||
const formLoading = ref(false)
|
||||
const formData = ref<DictType>({
|
||||
id: undefined,
|
||||
name: '',
|
||||
type: '',
|
||||
status: CommonStatusEnum.ENABLE,
|
||||
remark: '',
|
||||
})
|
||||
const formRules = {
|
||||
name: [{ required: true, message: '字典名称不能为空' }],
|
||||
type: [{ required: true, message: '字典类型不能为空' }],
|
||||
status: [{ required: true, message: '状态不能为空' }],
|
||||
}
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-system/dict/index')
|
||||
}
|
||||
|
||||
/** 加载详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
formData.value = await getDictType(props.id)
|
||||
}
|
||||
|
||||
/** 提交表单 */
|
||||
async function handleSubmit() {
|
||||
const { valid } = await formRef.value.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
|
||||
formLoading.value = true
|
||||
try {
|
||||
if (props.id) {
|
||||
await updateDictType(formData.value)
|
||||
toast.success('修改成功')
|
||||
} else {
|
||||
await createDictType(formData.value)
|
||||
toast.success('新增成功')
|
||||
}
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
78
src/pages-system/login-log/detail/index.vue
Normal file
78
src/pages-system/login-log/detail/index.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="登录日志详情"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 详情内容 -->
|
||||
<view>
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="日志编号" :value="formData?.id" />
|
||||
<wd-cell title="登录类型">
|
||||
<dict-tag :type="DICT_TYPE.SYSTEM_LOGIN_TYPE" :value="formData?.logType" />
|
||||
</wd-cell>
|
||||
<wd-cell title="用户名称" :value="formData?.username || '-'" />
|
||||
<wd-cell title="登录地址" :value="formData?.userIp || '-'" />
|
||||
<wd-cell title="浏览器" :value="formData?.userAgent || '-'" />
|
||||
<wd-cell title="登录结果">
|
||||
<dict-tag :type="DICT_TYPE.SYSTEM_LOGIN_RESULT" :value="formData?.result" />
|
||||
</wd-cell>
|
||||
<wd-cell title="登录时间" :value="formatDateTime(formData?.createTime) || '-'" />
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { LoginLog } from '@/api/system/login-log'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
|
||||
import { getLoginLog } from '@/api/system/login-log'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const formData = ref<LoginLog>()
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-system/login-log/index')
|
||||
}
|
||||
|
||||
/** 加载登录日志详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
toast.loading('加载中...')
|
||||
formData.value = await getLoginLog(props.id)
|
||||
} finally {
|
||||
toast.close()
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
147
src/pages-system/login-log/index.vue
Normal file
147
src/pages-system/login-log/index.vue
Normal file
@@ -0,0 +1,147 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="登录日志"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 搜索组件 -->
|
||||
<SearchForm @search="handleQuery" @reset="handleReset" />
|
||||
|
||||
<!-- 登录日志列表 -->
|
||||
<view class="p-24rpx">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
|
||||
@click="handleDetail(item)"
|
||||
>
|
||||
<view class="p-24rpx">
|
||||
<view class="mb-16rpx flex items-center justify-between gap-16rpx">
|
||||
<view class="min-w-0 flex-1 truncate text-32rpx text-[#333] font-semibold">
|
||||
{{ item.username || '-' }}
|
||||
</view>
|
||||
<view class="flex shrink-0 items-center gap-12rpx">
|
||||
<dict-tag :type="DICT_TYPE.SYSTEM_LOGIN_TYPE" :value="item.logType" />
|
||||
<dict-tag :type="DICT_TYPE.SYSTEM_LOGIN_RESULT" :value="item.result" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">登录地址:</text>
|
||||
<text class="line-clamp-1">{{ item.userIp }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">登录时间:</text>
|
||||
<text>{{ formatDateTime(item.createTime) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="loadMoreState !== 'loading' && list.length === 0" class="py-100rpx text-center">
|
||||
<wd-status-tip image="content" tip="暂无登录日志数据" />
|
||||
</view>
|
||||
<wd-loadmore
|
||||
v-if="list.length > 0"
|
||||
:state="loadMoreState"
|
||||
@reload="loadMore"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { LoginLog } from '@/api/system/login-log'
|
||||
import type { LoadMoreState } from '@/http/types'
|
||||
import { onReachBottom } from '@dcloudio/uni-app'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
import { getLoginLogPage } from '@/api/system/login-log'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
|
||||
import SearchForm from './modules/search-form.vue'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const total = ref(0)
|
||||
const list = ref<LoginLog[]>([])
|
||||
const loadMoreState = ref<LoadMoreState>('loading')
|
||||
|
||||
const queryParams = ref({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus()
|
||||
}
|
||||
|
||||
/** 查询登录日志列表 */
|
||||
async function getList() {
|
||||
loadMoreState.value = 'loading'
|
||||
try {
|
||||
const data = await getLoginLogPage(queryParams.value)
|
||||
list.value = [...list.value, ...data.list]
|
||||
total.value = data.total
|
||||
loadMoreState.value = list.value.length >= total.value ? 'finished' : 'loading'
|
||||
} catch {
|
||||
queryParams.value.pageNo = queryParams.value.pageNo > 1 ? queryParams.value.pageNo - 1 : 1
|
||||
loadMoreState.value = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery(data?: Record<string, any>) {
|
||||
queryParams.value = {
|
||||
...data,
|
||||
pageNo: 1,
|
||||
pageSize: queryParams.value.pageSize,
|
||||
}
|
||||
list.value = []
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function handleReset() {
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 加载更多 */
|
||||
function loadMore() {
|
||||
if (loadMoreState.value === 'finished') {
|
||||
return
|
||||
}
|
||||
queryParams.value.pageNo++
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 查看详情 */
|
||||
function handleDetail(item: LoginLog) {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/login-log/detail/index?id=${item.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 触底加载更多 */
|
||||
onReachBottom(() => {
|
||||
loadMore()
|
||||
})
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
144
src/pages-system/login-log/modules/search-form.vue
Normal file
144
src/pages-system/login-log/modules/search-form.vue
Normal file
@@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<!-- 搜索框入口 -->
|
||||
<view @click="visible = true">
|
||||
<wd-search :placeholder="placeholder" hide-cancel disabled />
|
||||
</view>
|
||||
|
||||
<!-- 搜索弹窗 -->
|
||||
<wd-popup v-model="visible" position="top" @close="visible = false">
|
||||
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
用户名称
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.username"
|
||||
placeholder="请输入用户名称"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
登录地址
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.userIp"
|
||||
placeholder="请输入登录地址"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
登录时间
|
||||
</view>
|
||||
<view class="yd-search-form-date-range-container">
|
||||
<view class="flex-1" @click="visibleCreateTime[0] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.createTime?.[0]) || '开始日期' }}
|
||||
</view>
|
||||
</view>
|
||||
-
|
||||
<view class="flex-1" @click="visibleCreateTime[1] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.createTime?.[1]) || '结束日期' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleCreateTime[0]" v-model="tempCreateTime[0]" type="date" />
|
||||
<view v-if="visibleCreateTime[0]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleCreateTime[0] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleCreateTime0Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleCreateTime[1]" v-model="tempCreateTime[1]" type="date" />
|
||||
<view v-if="visibleCreateTime[1]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleCreateTime[1] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleCreateTime1Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="yd-search-form-actions">
|
||||
<wd-button class="flex-1" plain @click="handleReset">
|
||||
重置
|
||||
</wd-button>
|
||||
<wd-button class="flex-1" type="primary" @click="handleSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { getNavbarHeight } from '@/utils'
|
||||
import { formatDate, formatDateRange } from '@/utils/date'
|
||||
|
||||
const emit = defineEmits<{
|
||||
search: [data: Record<string, any>]
|
||||
reset: []
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const formData = reactive({
|
||||
username: undefined as string | undefined,
|
||||
userIp: undefined as string | undefined,
|
||||
createTime: [undefined, undefined] as [number | undefined, number | undefined],
|
||||
})
|
||||
|
||||
/** 搜索条件 placeholder 拼接 */
|
||||
const placeholder = computed(() => {
|
||||
const conditions: string[] = []
|
||||
if (formData.username) {
|
||||
conditions.push(`用户名称:${formData.username}`)
|
||||
}
|
||||
if (formData.userIp) {
|
||||
conditions.push(`登录地址:${formData.userIp}`)
|
||||
}
|
||||
if (formData.createTime?.[0] && formData.createTime?.[1]) {
|
||||
conditions.push(`${formatDate(formData.createTime[0])}~${formatDate(formData.createTime[1])}`)
|
||||
}
|
||||
return conditions.length > 0 ? conditions.join(' | ') : '搜索登录日志'
|
||||
})
|
||||
|
||||
// 时间范围选择器状态
|
||||
const visibleCreateTime = ref<[boolean, boolean]>([false, false])
|
||||
const tempCreateTime = ref<[number, number]>([Date.now(), Date.now()])
|
||||
|
||||
/** 登录时间[0]确认 */
|
||||
function handleCreateTime0Confirm() {
|
||||
formData.createTime = [tempCreateTime.value[0], formData.createTime?.[1]]
|
||||
visibleCreateTime.value[0] = false
|
||||
}
|
||||
|
||||
/** 登录时间[1]确认 */
|
||||
function handleCreateTime1Confirm() {
|
||||
formData.createTime = [formData.createTime?.[0], tempCreateTime.value[1]]
|
||||
visibleCreateTime.value[1] = false
|
||||
}
|
||||
|
||||
/** 搜索 */
|
||||
function handleSearch() {
|
||||
visible.value = false
|
||||
emit('search', {
|
||||
username: formData.username,
|
||||
userIp: formData.userIp,
|
||||
createTime: formatDateRange(formData.createTime),
|
||||
})
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
function handleReset() {
|
||||
formData.username = undefined
|
||||
formData.userIp = undefined
|
||||
formData.createTime = [undefined, undefined]
|
||||
visible.value = false
|
||||
emit('reset')
|
||||
}
|
||||
</script>
|
||||
132
src/pages-system/mail/account/detail/index.vue
Normal file
132
src/pages-system/mail/account/detail/index.vue
Normal file
@@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="邮箱账号详情"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 详情内容 -->
|
||||
<view>
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="编号" :value="formData?.id" />
|
||||
<wd-cell title="邮箱" :value="formData?.mail" />
|
||||
<wd-cell title="用户名" :value="formData?.username" />
|
||||
<wd-cell title="SMTP 服务器域名" :value="formData?.host" />
|
||||
<wd-cell title="SMTP 服务器端口" :value="formData?.port" />
|
||||
<wd-cell title="是否开启 SSL">
|
||||
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="formData?.sslEnable" />
|
||||
</wd-cell>
|
||||
<wd-cell title="是否开启 STARTTLS">
|
||||
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="formData?.starttlsEnable" />
|
||||
</wd-cell>
|
||||
<wd-cell title="创建时间" :value="formatDateTime(formData?.createTime)" />
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<view class="yd-detail-footer-actions">
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['system:mail-account:update'])"
|
||||
class="flex-1" type="warning" @click="handleEdit"
|
||||
>
|
||||
编辑
|
||||
</wd-button>
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['system:mail-account:delete'])"
|
||||
class="flex-1" type="error" :loading="deleting" @click="handleDelete"
|
||||
>
|
||||
删除
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { MailAccount } from '@/api/system/mail/account'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { deleteMailAccount, getMailAccount } from '@/api/system/mail/account'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const toast = useToast()
|
||||
const formData = ref<MailAccount>()
|
||||
const deleting = ref(false)
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-system/mail/index')
|
||||
}
|
||||
|
||||
/** 加载详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
toast.loading('加载中...')
|
||||
formData.value = await getMailAccount(props.id)
|
||||
} finally {
|
||||
toast.close()
|
||||
}
|
||||
}
|
||||
|
||||
/** 编辑 */
|
||||
function handleEdit() {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/mail/account/form/index?id=${props.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除 */
|
||||
function handleDelete() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要删除该邮箱账号吗?',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) {
|
||||
return
|
||||
}
|
||||
deleting.value = true
|
||||
try {
|
||||
await deleteMailAccount(props.id)
|
||||
toast.success('删除成功')
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
180
src/pages-system/mail/account/form/index.vue
Normal file
180
src/pages-system/mail/account/form/index.vue
Normal file
@@ -0,0 +1,180 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
:title="getTitle"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 表单区域 -->
|
||||
<view>
|
||||
<wd-form ref="formRef" :model="formData" :rules="formRules">
|
||||
<wd-cell-group border>
|
||||
<wd-input
|
||||
v-model="formData.mail"
|
||||
label="邮箱"
|
||||
label-width="220rpx"
|
||||
prop="mail"
|
||||
clearable
|
||||
placeholder="请输入邮箱"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="formData.username"
|
||||
label="用户名"
|
||||
label-width="220rpx"
|
||||
prop="username"
|
||||
clearable
|
||||
placeholder="请输入用户名"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="formData.password"
|
||||
label="密码"
|
||||
label-width="220rpx"
|
||||
prop="password"
|
||||
clearable
|
||||
placeholder="请输入密码"
|
||||
show-password
|
||||
/>
|
||||
<wd-input
|
||||
v-model="formData.host"
|
||||
label="SMTP 服务器域名"
|
||||
label-width="220rpx"
|
||||
prop="host"
|
||||
clearable
|
||||
placeholder="请输入 SMTP 服务器域名"
|
||||
/>
|
||||
<wd-cell title="SMTP 服务器端口" title-width="220rpx" prop="port" center>
|
||||
<wd-input-number v-model="formData.port" :min="0" :max="65535" />
|
||||
</wd-cell>
|
||||
<wd-cell title="是否开启 SSL" title-width="220rpx" prop="sslEnable" center>
|
||||
<wd-radio-group v-model="formData.sslEnable" shape="button">
|
||||
<wd-radio
|
||||
v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
|
||||
:key="String(dict.value)"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-cell>
|
||||
<wd-cell title="是否开启 STARTTLS" title-width="220rpx" prop="starttlsEnable" center>
|
||||
<wd-radio-group v-model="formData.starttlsEnable" shape="button">
|
||||
<wd-radio
|
||||
v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
|
||||
:key="String(dict.value)"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-cell>
|
||||
</wd-cell-group>
|
||||
</wd-form>
|
||||
</view>
|
||||
|
||||
<!-- 底部保存按钮 -->
|
||||
<!-- TODO @芋艿: -->
|
||||
<view class="yd-detail-footer">
|
||||
<wd-button
|
||||
type="primary"
|
||||
block
|
||||
:loading="formLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
保存
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance } from 'wot-design-uni/components/wd-form/types'
|
||||
import type { MailAccount } from '@/api/system/mail/account'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { createMailAccount, getMailAccount, updateMailAccount } from '@/api/system/mail/account'
|
||||
import { getBoolDictOptions } from '@/hooks/useDict'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const getTitle = computed(() => props.id ? '编辑邮箱账号' : '新增邮箱账号')
|
||||
const formLoading = ref(false)
|
||||
const formData = ref<MailAccount>({
|
||||
id: undefined,
|
||||
mail: '',
|
||||
username: '',
|
||||
password: '',
|
||||
host: '',
|
||||
port: 25,
|
||||
sslEnable: true,
|
||||
starttlsEnable: false,
|
||||
})
|
||||
const formRules = {
|
||||
mail: [{ required: true, message: '邮箱不能为空' }],
|
||||
username: [{ required: true, message: '用户名不能为空' }],
|
||||
password: [{ required: true, message: '密码不能为空' }],
|
||||
host: [{ required: true, message: 'SMTP 服务器域名不能为空' }],
|
||||
port: [{ required: true, message: 'SMTP 服务器端口不能为空' }],
|
||||
sslEnable: [{ required: true, message: '是否开启 SSL 不能为空' }],
|
||||
starttlsEnable: [{ required: true, message: '是否开启 STARTTLS 不能为空' }],
|
||||
}
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-system/mail/index')
|
||||
}
|
||||
|
||||
/** 加载详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
formData.value = await getMailAccount(props.id)
|
||||
}
|
||||
|
||||
/** 提交表单 */
|
||||
async function handleSubmit() {
|
||||
const { valid } = await formRef.value.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
|
||||
formLoading.value = true
|
||||
try {
|
||||
if (props.id) {
|
||||
await updateMailAccount(formData.value)
|
||||
toast.success('修改成功')
|
||||
} else {
|
||||
await createMailAccount(formData.value)
|
||||
toast.success('新增成功')
|
||||
}
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
133
src/pages-system/mail/components/account-list.vue
Normal file
133
src/pages-system/mail/components/account-list.vue
Normal file
@@ -0,0 +1,133 @@
|
||||
<template>
|
||||
<view>
|
||||
<!-- 搜索组件 -->
|
||||
<AccountSearchForm @search="handleQuery" @reset="handleReset" />
|
||||
|
||||
<!-- 账号列表 -->
|
||||
<view class="p-24rpx">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
|
||||
@click="handleDetail(item)"
|
||||
>
|
||||
<view class="p-24rpx">
|
||||
<view class="mb-16rpx flex items-center justify-between">
|
||||
<view class="text-32rpx text-[#333] font-semibold">
|
||||
{{ item.mail }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">用户名:</text>
|
||||
<text class="min-w-0 flex-1 truncate">{{ item.username || '-' }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">创建时间:</text>
|
||||
<text>{{ formatDateTime(item.createTime) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="loadMoreState !== 'loading' && list.length === 0" class="py-100rpx text-center">
|
||||
<wd-status-tip image="content" tip="暂无邮箱账号数据" />
|
||||
</view>
|
||||
<wd-loadmore
|
||||
v-if="list.length > 0"
|
||||
:state="loadMoreState"
|
||||
@reload="loadMore"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 新增按钮 -->
|
||||
<wd-fab
|
||||
v-if="hasAccessByCodes(['system:mail-account:create'])"
|
||||
position="right-bottom"
|
||||
type="primary"
|
||||
:expandable="false"
|
||||
@click="handleAdd"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { MailAccount } from '@/api/system/mail/account'
|
||||
import type { LoadMoreState } from '@/http/types'
|
||||
import { ref } from 'vue'
|
||||
import { getMailAccountPage } from '@/api/system/mail/account'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import AccountSearchForm from './account-search-form.vue'
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const total = ref(0)
|
||||
const list = ref<MailAccount[]>([])
|
||||
const loadMoreState = ref<LoadMoreState>('loading')
|
||||
const queryParams = ref({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
|
||||
/** 查询列表 */
|
||||
async function getList() {
|
||||
loadMoreState.value = 'loading'
|
||||
try {
|
||||
const data = await getMailAccountPage(queryParams.value)
|
||||
list.value = [...list.value, ...data.list]
|
||||
total.value = data.total
|
||||
loadMoreState.value = list.value.length >= total.value ? 'finished' : 'loading'
|
||||
} catch {
|
||||
queryParams.value.pageNo = queryParams.value.pageNo > 1 ? queryParams.value.pageNo - 1 : 1
|
||||
loadMoreState.value = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery(data?: Record<string, any>) {
|
||||
queryParams.value = {
|
||||
...data,
|
||||
pageNo: 1,
|
||||
pageSize: queryParams.value.pageSize,
|
||||
}
|
||||
list.value = []
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function handleReset() {
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 加载更多 */
|
||||
function loadMore() {
|
||||
if (loadMoreState.value === 'finished') {
|
||||
return
|
||||
}
|
||||
queryParams.value.pageNo++
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 新增 */
|
||||
function handleAdd() {
|
||||
uni.navigateTo({
|
||||
url: '/pages-system/mail/account/form/index',
|
||||
})
|
||||
}
|
||||
|
||||
/** 查看详情 */
|
||||
function handleDetail(item: MailAccount) {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/mail/account/detail/index?id=${item.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 触底加载更多 */
|
||||
onReachBottom(() => {
|
||||
loadMore()
|
||||
})
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
85
src/pages-system/mail/components/account-search-form.vue
Normal file
85
src/pages-system/mail/components/account-search-form.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<!-- 搜索框入口 -->
|
||||
<view @click="visible = true">
|
||||
<wd-search :placeholder="placeholder" hide-cancel disabled />
|
||||
</view>
|
||||
|
||||
<!-- 搜索弹窗 -->
|
||||
<wd-popup v-model="visible" position="top" @close="visible = false">
|
||||
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
邮箱
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.mail"
|
||||
placeholder="请输入邮箱"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
用户名
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.username"
|
||||
placeholder="请输入用户名"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-actions">
|
||||
<wd-button class="flex-1" plain @click="handleReset">
|
||||
重置
|
||||
</wd-button>
|
||||
<wd-button class="flex-1" type="primary" @click="handleSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { getNavbarHeight } from '@/utils'
|
||||
|
||||
const emit = defineEmits<{
|
||||
search: [data: Record<string, any>]
|
||||
reset: []
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const formData = reactive({
|
||||
mail: undefined as string | undefined,
|
||||
username: undefined as string | undefined,
|
||||
})
|
||||
|
||||
/** 搜索条件 placeholder 拼接 */
|
||||
const placeholder = computed(() => {
|
||||
const conditions: string[] = []
|
||||
if (formData.mail) {
|
||||
conditions.push(`邮箱:${formData.mail}`)
|
||||
}
|
||||
if (formData.username) {
|
||||
conditions.push(`用户名:${formData.username}`)
|
||||
}
|
||||
return conditions.length > 0 ? conditions.join(' | ') : '搜索邮箱账号'
|
||||
})
|
||||
|
||||
/** 搜索 */
|
||||
function handleSearch() {
|
||||
visible.value = false
|
||||
emit('search', {
|
||||
mail: formData.mail || undefined,
|
||||
username: formData.username || undefined,
|
||||
})
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
function handleReset() {
|
||||
formData.mail = undefined
|
||||
formData.username = undefined
|
||||
visible.value = false
|
||||
emit('reset')
|
||||
}
|
||||
</script>
|
||||
129
src/pages-system/mail/components/log-list.vue
Normal file
129
src/pages-system/mail/components/log-list.vue
Normal file
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<view>
|
||||
<!-- 搜索组件 -->
|
||||
<LogSearchForm @search="handleQuery" @reset="handleReset" />
|
||||
|
||||
<!-- 日志列表 -->
|
||||
<view class="p-24rpx">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
|
||||
@click="handleDetail(item)"
|
||||
>
|
||||
<view class="p-24rpx">
|
||||
<view class="mb-16rpx flex items-center justify-between">
|
||||
<view class="text-32rpx text-[#333] font-semibold">
|
||||
{{ item.templateTitle }}
|
||||
</view>
|
||||
<dict-tag :type="DICT_TYPE.SYSTEM_MAIL_SEND_STATUS" :value="item.sendStatus" />
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">发送邮箱:</text>
|
||||
<text class="min-w-0 flex-1 truncate">{{ item.fromMail }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">收件人:</text>
|
||||
<text class="min-w-0 flex-1 truncate">{{ formatMails(item.toMails) }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">发送时间:</text>
|
||||
<text>{{ formatDateTime(item.sendTime) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="loadMoreState !== 'loading' && list.length === 0" class="py-100rpx text-center">
|
||||
<wd-status-tip image="content" tip="暂无邮件日志数据" />
|
||||
</view>
|
||||
<wd-loadmore
|
||||
v-if="list.length > 0"
|
||||
:state="loadMoreState"
|
||||
@reload="loadMore"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { MailLog } from '@/api/system/mail/log'
|
||||
import type { LoadMoreState } from '@/http/types'
|
||||
import { ref } from 'vue'
|
||||
import { getMailLogPage } from '@/api/system/mail/log'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import LogSearchForm from './log-search-form.vue'
|
||||
|
||||
const total = ref(0)
|
||||
const list = ref<MailLog[]>([])
|
||||
const loadMoreState = ref<LoadMoreState>('loading')
|
||||
const queryParams = ref({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
|
||||
/** 格式化邮件列表 */
|
||||
function formatMails(mails?: string[]) {
|
||||
if (!mails || mails.length === 0) {
|
||||
return '-'
|
||||
}
|
||||
return mails.join('、')
|
||||
}
|
||||
|
||||
/** 查询列表 */
|
||||
async function getList() {
|
||||
loadMoreState.value = 'loading'
|
||||
try {
|
||||
const data = await getMailLogPage(queryParams.value)
|
||||
list.value = [...list.value, ...data.list]
|
||||
total.value = data.total
|
||||
loadMoreState.value = list.value.length >= total.value ? 'finished' : 'loading'
|
||||
} catch {
|
||||
queryParams.value.pageNo = queryParams.value.pageNo > 1 ? queryParams.value.pageNo - 1 : 1
|
||||
loadMoreState.value = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery(data?: Record<string, any>) {
|
||||
queryParams.value = {
|
||||
...data,
|
||||
pageNo: 1,
|
||||
pageSize: queryParams.value.pageSize,
|
||||
}
|
||||
list.value = []
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function handleReset() {
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 加载更多 */
|
||||
function loadMore() {
|
||||
if (loadMoreState.value === 'finished') {
|
||||
return
|
||||
}
|
||||
queryParams.value.pageNo++
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 查看详情 */
|
||||
function handleDetail(item: MailLog) {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/mail/log/detail/index?id=${item.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 触底加载更多 */
|
||||
onReachBottom(() => {
|
||||
loadMore()
|
||||
})
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
236
src/pages-system/mail/components/log-search-form.vue
Normal file
236
src/pages-system/mail/components/log-search-form.vue
Normal file
@@ -0,0 +1,236 @@
|
||||
<template>
|
||||
<!-- 搜索框入口 -->
|
||||
<view @click="visible = true">
|
||||
<wd-search :placeholder="placeholder" hide-cancel disabled />
|
||||
</view>
|
||||
|
||||
<!-- 搜索弹窗 -->
|
||||
<wd-popup v-model="visible" position="top" @close="visible = false">
|
||||
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
用户编号
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.userId"
|
||||
placeholder="请输入用户编号"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
用户类型
|
||||
</view>
|
||||
<wd-radio-group v-model="formData.userType" shape="button">
|
||||
<wd-radio :value="-1">
|
||||
全部
|
||||
</wd-radio>
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.USER_TYPE)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
发送状态
|
||||
</view>
|
||||
<wd-radio-group v-model="formData.sendStatus" shape="button">
|
||||
<wd-radio :value="-1">
|
||||
全部
|
||||
</wd-radio>
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_MAIL_SEND_STATUS)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
邮箱账号
|
||||
</view>
|
||||
<wd-picker
|
||||
v-model="formData.accountId"
|
||||
:columns="accountOptions"
|
||||
placeholder="请选择邮箱账号"
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
模板编号
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.templateId"
|
||||
placeholder="请输入模板编号"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
发送时间
|
||||
</view>
|
||||
<view class="yd-search-form-date-range-container">
|
||||
<view class="flex-1" @click="visibleSendTime[0] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.sendTime?.[0]) || '开始日期' }}
|
||||
</view>
|
||||
</view>
|
||||
-
|
||||
<view class="flex-1" @click="visibleSendTime[1] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.sendTime?.[1]) || '结束日期' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleSendTime[0]" v-model="tempSendTime[0]" type="date" />
|
||||
<view v-if="visibleSendTime[0]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleSendTime[0] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleSendTime0Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleSendTime[1]" v-model="tempSendTime[1]" type="date" />
|
||||
<view v-if="visibleSendTime[1]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleSendTime[1] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleSendTime1Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="yd-search-form-actions">
|
||||
<wd-button class="flex-1" plain @click="handleReset">
|
||||
重置
|
||||
</wd-button>
|
||||
<wd-button class="flex-1" type="primary" @click="handleSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { getSimpleMailAccountList } from '@/api/system/mail/account'
|
||||
import { getDictLabel, getIntDictOptions } from '@/hooks/useDict'
|
||||
import { getNavbarHeight } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDate, formatDateRange } from '@/utils/date'
|
||||
|
||||
const emit = defineEmits<{
|
||||
search: [data: Record<string, any>]
|
||||
reset: []
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const formData = reactive({
|
||||
sendTime: [undefined, undefined] as [number | undefined, number | undefined],
|
||||
userId: undefined as number | undefined,
|
||||
userType: -1,
|
||||
sendStatus: -1,
|
||||
accountId: undefined as number | undefined,
|
||||
templateId: undefined as number | undefined,
|
||||
})
|
||||
|
||||
/** 邮箱账号列表 */
|
||||
const accountList = ref<{ id?: number, mail: string }[]>([])
|
||||
|
||||
/** 邮箱账号选项 */
|
||||
const accountOptions = computed(() => {
|
||||
return accountList.value.map(item => ({
|
||||
value: item.id,
|
||||
label: item.mail,
|
||||
}))
|
||||
})
|
||||
|
||||
/** 获取邮箱账号名称 */
|
||||
function getAccountMail(accountId?: number) {
|
||||
return accountList.value.find(item => item.id === accountId)?.mail
|
||||
}
|
||||
|
||||
/** 搜索条件 placeholder 拼接 */
|
||||
const placeholder = computed(() => {
|
||||
const conditions: string[] = []
|
||||
if (formData.sendTime?.[0] && formData.sendTime?.[1]) {
|
||||
conditions.push(`时间:${formatDate(formData.sendTime[0])}~${formatDate(formData.sendTime[1])}`)
|
||||
}
|
||||
if (formData.userId) {
|
||||
conditions.push(`用户:${formData.userId}`)
|
||||
}
|
||||
if (formData.userType !== -1) {
|
||||
conditions.push(`类型:${getDictLabel(DICT_TYPE.USER_TYPE, formData.userType)}`)
|
||||
}
|
||||
if (formData.sendStatus !== -1) {
|
||||
conditions.push(`发送:${getDictLabel(DICT_TYPE.SYSTEM_MAIL_SEND_STATUS, formData.sendStatus)}`)
|
||||
}
|
||||
if (formData.accountId) {
|
||||
conditions.push(`账号:${getAccountMail(formData.accountId) || formData.accountId}`)
|
||||
}
|
||||
if (formData.templateId) {
|
||||
conditions.push(`模板:${formData.templateId}`)
|
||||
}
|
||||
return conditions.length > 0 ? conditions.join(' | ') : '搜索邮件日志'
|
||||
})
|
||||
|
||||
// 时间范围选择器状态
|
||||
const visibleSendTime = ref<[boolean, boolean]>([false, false])
|
||||
const tempSendTime = ref<[number, number]>([Date.now(), Date.now()])
|
||||
|
||||
/** 发送时间[0]确认 */
|
||||
function handleSendTime0Confirm() {
|
||||
formData.sendTime = [tempSendTime.value[0], formData.sendTime?.[1]]
|
||||
visibleSendTime.value[0] = false
|
||||
}
|
||||
|
||||
/** 发送时间[1]确认 */
|
||||
function handleSendTime1Confirm() {
|
||||
formData.sendTime = [formData.sendTime?.[0], tempSendTime.value[1]]
|
||||
visibleSendTime.value[1] = false
|
||||
}
|
||||
|
||||
/** 搜索 */
|
||||
function handleSearch() {
|
||||
visible.value = false
|
||||
const dateRange = formatDateRange(formData.sendTime)
|
||||
emit('search', {
|
||||
beginTime: dateRange?.[0],
|
||||
endTime: dateRange?.[1],
|
||||
userId: formData.userId,
|
||||
userType: formData.userType === -1 ? undefined : formData.userType,
|
||||
sendStatus: formData.sendStatus === -1 ? undefined : formData.sendStatus,
|
||||
accountId: formData.accountId || undefined,
|
||||
templateId: formData.templateId || undefined,
|
||||
})
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
function handleReset() {
|
||||
formData.sendTime = [undefined, undefined]
|
||||
formData.userId = undefined
|
||||
formData.userType = -1
|
||||
formData.sendStatus = -1
|
||||
formData.accountId = undefined
|
||||
formData.templateId = undefined
|
||||
visible.value = false
|
||||
emit('reset')
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
try {
|
||||
accountList.value = await getSimpleMailAccountList()
|
||||
} catch {
|
||||
accountList.value = []
|
||||
}
|
||||
})
|
||||
</script>
|
||||
139
src/pages-system/mail/components/template-list.vue
Normal file
139
src/pages-system/mail/components/template-list.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<view>
|
||||
<!-- 搜索组件 -->
|
||||
<TemplateSearchForm @search="handleQuery" @reset="handleReset" />
|
||||
|
||||
<!-- 模板列表 -->
|
||||
<view class="p-24rpx">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
|
||||
@click="handleDetail(item)"
|
||||
>
|
||||
<view class="p-24rpx">
|
||||
<view class="mb-16rpx flex items-center justify-between">
|
||||
<view class="text-32rpx text-[#333] font-semibold">
|
||||
{{ item.name }}
|
||||
</view>
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="item.status" />
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">模板编码:</text>
|
||||
<text class="min-w-0 flex-1 truncate">{{ item.code }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">标题:</text>
|
||||
<text class="min-w-0 flex-1 truncate">{{ item.title }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">创建时间:</text>
|
||||
<text>{{ formatDateTime(item.createTime) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="loadMoreState !== 'loading' && list.length === 0" class="py-100rpx text-center">
|
||||
<wd-status-tip image="content" tip="暂无邮件模板数据" />
|
||||
</view>
|
||||
<wd-loadmore
|
||||
v-if="list.length > 0"
|
||||
:state="loadMoreState"
|
||||
@reload="loadMore"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 新增按钮 -->
|
||||
<wd-fab
|
||||
v-if="hasAccessByCodes(['system:mail-template:create'])"
|
||||
position="right-bottom"
|
||||
type="primary"
|
||||
:expandable="false"
|
||||
@click="handleAdd"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { MailTemplate } from '@/api/system/mail/template'
|
||||
import type { LoadMoreState } from '@/http/types'
|
||||
import { ref } from 'vue'
|
||||
import { getMailTemplatePage } from '@/api/system/mail/template'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import TemplateSearchForm from './template-search-form.vue'
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const total = ref(0)
|
||||
const list = ref<MailTemplate[]>([])
|
||||
const loadMoreState = ref<LoadMoreState>('loading')
|
||||
const queryParams = ref({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
|
||||
/** 查询列表 */
|
||||
async function getList() {
|
||||
loadMoreState.value = 'loading'
|
||||
try {
|
||||
const data = await getMailTemplatePage(queryParams.value)
|
||||
list.value = [...list.value, ...data.list]
|
||||
total.value = data.total
|
||||
loadMoreState.value = list.value.length >= total.value ? 'finished' : 'loading'
|
||||
} catch {
|
||||
queryParams.value.pageNo = queryParams.value.pageNo > 1 ? queryParams.value.pageNo - 1 : 1
|
||||
loadMoreState.value = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery(data?: Record<string, any>) {
|
||||
queryParams.value = {
|
||||
...data,
|
||||
pageNo: 1,
|
||||
pageSize: queryParams.value.pageSize,
|
||||
}
|
||||
list.value = []
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function handleReset() {
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 加载更多 */
|
||||
function loadMore() {
|
||||
if (loadMoreState.value === 'finished') {
|
||||
return
|
||||
}
|
||||
queryParams.value.pageNo++
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 新增 */
|
||||
function handleAdd() {
|
||||
uni.navigateTo({
|
||||
url: '/pages-system/mail/template/form/index',
|
||||
})
|
||||
}
|
||||
|
||||
/** 查看详情 */
|
||||
function handleDetail(item: MailTemplate) {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/mail/template/detail/index?id=${item.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 触底加载更多 */
|
||||
onReachBottom(() => {
|
||||
loadMore()
|
||||
})
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
213
src/pages-system/mail/components/template-search-form.vue
Normal file
213
src/pages-system/mail/components/template-search-form.vue
Normal file
@@ -0,0 +1,213 @@
|
||||
<template>
|
||||
<!-- 搜索框入口 -->
|
||||
<view @click="visible = true">
|
||||
<wd-search :placeholder="placeholder" hide-cancel disabled />
|
||||
</view>
|
||||
|
||||
<!-- 搜索弹窗 -->
|
||||
<wd-popup v-model="visible" position="top" @close="visible = false">
|
||||
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
模板编码
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.code"
|
||||
placeholder="请输入模板编码"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
模板名称
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.name"
|
||||
placeholder="请输入模板名称"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
开启状态
|
||||
</view>
|
||||
<wd-radio-group v-model="formData.status" shape="button">
|
||||
<wd-radio :value="-1">
|
||||
全部
|
||||
</wd-radio>
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
邮箱账号
|
||||
</view>
|
||||
<wd-picker
|
||||
v-model="formData.accountId"
|
||||
:columns="accountOptions"
|
||||
placeholder="请选择邮箱账号"
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
创建时间
|
||||
</view>
|
||||
<view class="yd-search-form-date-range-container">
|
||||
<view class="flex-1" @click="visibleCreateTime[0] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.createTime?.[0]) || '开始日期' }}
|
||||
</view>
|
||||
</view>
|
||||
-
|
||||
<view class="flex-1" @click="visibleCreateTime[1] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.createTime?.[1]) || '结束日期' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleCreateTime[0]" v-model="tempCreateTime[0]" type="date" />
|
||||
<view v-if="visibleCreateTime[0]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleCreateTime[0] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleCreateTime0Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleCreateTime[1]" v-model="tempCreateTime[1]" type="date" />
|
||||
<view v-if="visibleCreateTime[1]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleCreateTime[1] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleCreateTime1Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="yd-search-form-actions">
|
||||
<wd-button class="flex-1" plain @click="handleReset">
|
||||
重置
|
||||
</wd-button>
|
||||
<wd-button class="flex-1" type="primary" @click="handleSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { getSimpleMailAccountList } from '@/api/system/mail/account'
|
||||
import { getDictLabel, getIntDictOptions } from '@/hooks/useDict'
|
||||
import { getNavbarHeight } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDate, formatDateRange } from '@/utils/date'
|
||||
|
||||
const emit = defineEmits<{
|
||||
search: [data: Record<string, any>]
|
||||
reset: []
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const formData = reactive({
|
||||
status: -1,
|
||||
code: undefined as string | undefined,
|
||||
name: undefined as string | undefined,
|
||||
accountId: undefined as number | undefined,
|
||||
createTime: [undefined, undefined] as [number | undefined, number | undefined],
|
||||
})
|
||||
|
||||
/** 邮箱账号列表 */
|
||||
const accountList = ref<{ id?: number, mail: string }[]>([])
|
||||
|
||||
/** 邮箱账号选项 */
|
||||
const accountOptions = computed(() => {
|
||||
return accountList.value.map(item => ({
|
||||
value: item.id,
|
||||
label: item.mail,
|
||||
}))
|
||||
})
|
||||
|
||||
/** 获取邮箱账号名称 */
|
||||
function getAccountMail(accountId?: number) {
|
||||
return accountList.value.find(item => item.id === accountId)?.mail
|
||||
}
|
||||
|
||||
/** 搜索条件 placeholder 拼接 */
|
||||
const placeholder = computed(() => {
|
||||
const conditions: string[] = []
|
||||
if (formData.status !== -1) {
|
||||
conditions.push(`状态:${getDictLabel(DICT_TYPE.COMMON_STATUS, formData.status)}`)
|
||||
}
|
||||
if (formData.code) {
|
||||
conditions.push(`编码:${formData.code}`)
|
||||
}
|
||||
if (formData.name) {
|
||||
conditions.push(`名称:${formData.name}`)
|
||||
}
|
||||
if (formData.accountId) {
|
||||
conditions.push(`账号:${getAccountMail(formData.accountId) || formData.accountId}`)
|
||||
}
|
||||
if (formData.createTime?.[0] && formData.createTime?.[1]) {
|
||||
conditions.push(`时间:${formatDate(formData.createTime[0])}~${formatDate(formData.createTime[1])}`)
|
||||
}
|
||||
return conditions.length > 0 ? conditions.join(' | ') : '搜索邮件模板'
|
||||
})
|
||||
|
||||
// 时间范围选择器状态
|
||||
const visibleCreateTime = ref<[boolean, boolean]>([false, false])
|
||||
const tempCreateTime = ref<[number, number]>([Date.now(), Date.now()])
|
||||
|
||||
/** 创建时间[0]确认 */
|
||||
function handleCreateTime0Confirm() {
|
||||
formData.createTime = [tempCreateTime.value[0], formData.createTime?.[1]]
|
||||
visibleCreateTime.value[0] = false
|
||||
}
|
||||
|
||||
/** 创建时间[1]确认 */
|
||||
function handleCreateTime1Confirm() {
|
||||
formData.createTime = [formData.createTime?.[0], tempCreateTime.value[1]]
|
||||
visibleCreateTime.value[1] = false
|
||||
}
|
||||
|
||||
/** 搜索 */
|
||||
function handleSearch() {
|
||||
visible.value = false
|
||||
const dateRange = formatDateRange(formData.createTime)
|
||||
emit('search', {
|
||||
status: formData.status === -1 ? undefined : formData.status,
|
||||
code: formData.code || undefined,
|
||||
name: formData.name || undefined,
|
||||
accountId: formData.accountId || undefined,
|
||||
beginTime: dateRange?.[0],
|
||||
endTime: dateRange?.[1],
|
||||
})
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
function handleReset() {
|
||||
formData.status = -1
|
||||
formData.code = undefined
|
||||
formData.name = undefined
|
||||
formData.accountId = undefined
|
||||
formData.createTime = [undefined, undefined]
|
||||
visible.value = false
|
||||
emit('reset')
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
try {
|
||||
accountList.value = await getSimpleMailAccountList()
|
||||
} catch {
|
||||
accountList.value = []
|
||||
}
|
||||
})
|
||||
</script>
|
||||
55
src/pages-system/mail/index.vue
Normal file
55
src/pages-system/mail/index.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="邮件管理"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- Tab 切换 -->
|
||||
<view class="bg-white">
|
||||
<wd-tabs v-model="tabIndex" shrink @change="handleTabChange">
|
||||
<wd-tab title="邮箱账号" />
|
||||
<wd-tab title="邮件模板" />
|
||||
<wd-tab title="邮件日志" />
|
||||
</wd-tabs>
|
||||
</view>
|
||||
<!-- 列表内容 -->
|
||||
<AccountList v-show="tabType === 'account'" />
|
||||
<TemplateList v-show="tabType === 'template'" />
|
||||
<LogList v-show="tabType === 'log'" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import AccountList from './components/account-list.vue'
|
||||
import LogList from './components/log-list.vue'
|
||||
import TemplateList from './components/template-list.vue'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const tabTypes: string[] = ['account', 'template', 'log']
|
||||
const tabIndex = ref(0)
|
||||
const tabType = computed<string>(() => tabTypes[tabIndex.value])
|
||||
|
||||
/** Tab 切换 */
|
||||
function handleTabChange({ index }: { index: number }) {
|
||||
tabIndex.value = index
|
||||
}
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
98
src/pages-system/mail/log/detail/index.vue
Normal file
98
src/pages-system/mail/log/detail/index.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="邮件日志详情"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 详情内容 -->
|
||||
<view>
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="日志编号" :value="formData?.id" />
|
||||
<wd-cell title="发送邮箱" :value="formData?.fromMail" />
|
||||
<wd-cell title="接收信息" :value="formatReceiveInfo(formData)" />
|
||||
<wd-cell title="模板编号" :value="formData?.templateId" />
|
||||
<wd-cell title="模板编码" :value="formData?.templateCode" />
|
||||
<wd-cell title="邮件标题" :value="formData?.templateTitle" />
|
||||
<wd-cell title="邮件内容" :value="formData?.templateContent" />
|
||||
<wd-cell title="发送状态">
|
||||
<dict-tag :type="DICT_TYPE.SYSTEM_MAIL_SEND_STATUS" :value="formData?.sendStatus" />
|
||||
</wd-cell>
|
||||
<wd-cell title="发送时间" :value="formatDateTime(formData?.sendTime)" />
|
||||
<wd-cell title="发送消息编号" :value="formData?.sendMessageId ?? '-'" />
|
||||
<wd-cell title="发送异常" :value="formData?.sendException ?? '-'" />
|
||||
<wd-cell title="创建时间" :value="formatDateTime(formData?.createTime)" />
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { MailLog } from '@/api/system/mail/log'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { getMailLog } from '@/api/system/mail/log'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const formData = ref<MailLog>()
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-system/mail/index')
|
||||
}
|
||||
|
||||
/** 格式化接收信息 */
|
||||
function formatReceiveInfo(data?: MailLog) {
|
||||
if (!data) {
|
||||
return '-'
|
||||
}
|
||||
const lines: string[] = []
|
||||
if (data.toMails && data.toMails.length > 0) {
|
||||
lines.push(`收件:${data.toMails.join('、')}`)
|
||||
}
|
||||
if (data.ccMails && data.ccMails.length > 0) {
|
||||
lines.push(`抄送:${data.ccMails.join('、')}`)
|
||||
}
|
||||
if (data.bccMails && data.bccMails.length > 0) {
|
||||
lines.push(`密送:${data.bccMails.join('、')}`)
|
||||
}
|
||||
return lines.length > 0 ? lines.join(';') : '-'
|
||||
}
|
||||
|
||||
/** 加载详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
toast.loading('加载中...')
|
||||
formData.value = await getMailLog(Number(props.id))
|
||||
} finally {
|
||||
toast.close()
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
184
src/pages-system/mail/template/detail/components/send-form.vue
Normal file
184
src/pages-system/mail/template/detail/components/send-form.vue
Normal file
@@ -0,0 +1,184 @@
|
||||
<template>
|
||||
<wd-popup v-model="visible" position="bottom" closable custom-style="border-radius: 16rpx 16rpx 0 0;">
|
||||
<view class="p-24rpx">
|
||||
<view class="mb-24rpx text-32rpx text-[#333] font-semibold">
|
||||
发送测试邮件
|
||||
</view>
|
||||
<wd-form ref="sendFormRef" :model="sendFormData" :rules="sendFormRules">
|
||||
<wd-cell-group border>
|
||||
<wd-textarea
|
||||
v-model="sendFormData.content"
|
||||
label="模板内容"
|
||||
label-width="180rpx"
|
||||
disabled
|
||||
:rows="3"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="sendFormData.toMails"
|
||||
label="收件邮箱"
|
||||
label-width="180rpx"
|
||||
prop="toMails"
|
||||
clearable
|
||||
placeholder="多个邮箱用逗号分隔"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="sendFormData.ccMails"
|
||||
label="抄送邮箱"
|
||||
label-width="180rpx"
|
||||
clearable
|
||||
placeholder="多个邮箱用逗号分隔"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="sendFormData.bccMails"
|
||||
label="密送邮箱"
|
||||
label-width="180rpx"
|
||||
clearable
|
||||
placeholder="多个邮箱用逗号分隔"
|
||||
/>
|
||||
<template v-for="param in template?.params" :key="param">
|
||||
<wd-input
|
||||
v-model="sendFormData.templateParams[param]"
|
||||
:label="`参数 ${param}`"
|
||||
label-width="180rpx"
|
||||
:prop="`templateParams.${param}`"
|
||||
clearable
|
||||
:placeholder="`请输入参数 ${param}`"
|
||||
/>
|
||||
</template>
|
||||
</wd-cell-group>
|
||||
</wd-form>
|
||||
<view class="mt-24rpx">
|
||||
<wd-button type="primary" block :loading="sendLoading" @click="handleSendSubmit">
|
||||
发送
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { MailTemplate } from '@/api/system/mail/template'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { sendMail } from '@/api/system/mail/template'
|
||||
import { isEmail } from '@/utils/validator'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
template?: MailTemplate
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
'success': []
|
||||
}>()
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
const visible = computed({
|
||||
get() {
|
||||
return props.modelValue
|
||||
},
|
||||
set(value: boolean) {
|
||||
emit('update:modelValue', value)
|
||||
},
|
||||
})
|
||||
|
||||
const sendLoading = ref(false)
|
||||
const sendFormRef = ref<any>()
|
||||
const sendFormData = ref({
|
||||
content: '',
|
||||
toMails: '',
|
||||
ccMails: '',
|
||||
bccMails: '',
|
||||
templateParams: {} as Record<string, string>,
|
||||
})
|
||||
|
||||
/** 发送表单校验规则 */
|
||||
const sendFormRules = computed(() => {
|
||||
const rules: Record<string, any> = {
|
||||
toMails: [{ required: true, message: '收件邮箱不能为空' }],
|
||||
}
|
||||
if (props.template?.params) {
|
||||
props.template.params.forEach((param) => {
|
||||
rules[`templateParams.${param}`] = [{ required: true, message: `参数 ${param} 不能为空` }]
|
||||
})
|
||||
}
|
||||
return rules
|
||||
})
|
||||
|
||||
/** 格式化邮箱列表 */
|
||||
function normalizeMailList(text: string) {
|
||||
const list = text
|
||||
.split(/[,,;;\s]+/)
|
||||
.map(s => s.trim())
|
||||
.filter(Boolean)
|
||||
const invalid = list.find(item => !isEmail(item))
|
||||
if (invalid) {
|
||||
toast.warning(`邮箱格式不正确:${invalid}`)
|
||||
return null
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
/** 初始化发送表单 */
|
||||
function initSendForm() {
|
||||
sendFormData.value = {
|
||||
content: props.template?.content || '',
|
||||
toMails: '',
|
||||
ccMails: '',
|
||||
bccMails: '',
|
||||
templateParams: {},
|
||||
}
|
||||
if (props.template?.params) {
|
||||
props.template.params.forEach((param) => {
|
||||
sendFormData.value.templateParams[param] = ''
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
if (val) {
|
||||
initSendForm()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
/** 提交发送 */
|
||||
async function handleSendSubmit() {
|
||||
const { valid } = await sendFormRef.value.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
const toMails = normalizeMailList(sendFormData.value.toMails)
|
||||
if (!toMails || toMails.length === 0) {
|
||||
return
|
||||
}
|
||||
const ccMails = normalizeMailList(sendFormData.value.ccMails)
|
||||
if (ccMails === null) {
|
||||
return
|
||||
}
|
||||
const bccMails = normalizeMailList(sendFormData.value.bccMails)
|
||||
if (bccMails === null) {
|
||||
return
|
||||
}
|
||||
|
||||
sendLoading.value = true
|
||||
try {
|
||||
await sendMail({
|
||||
templateCode: props.template?.code || '',
|
||||
templateParams: sendFormData.value.templateParams,
|
||||
toMails,
|
||||
ccMails: ccMails.length > 0 ? ccMails : undefined,
|
||||
bccMails: bccMails.length > 0 ? bccMails : undefined,
|
||||
})
|
||||
toast.success('邮件发送成功')
|
||||
emit('success')
|
||||
visible.value = false
|
||||
} finally {
|
||||
sendLoading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
170
src/pages-system/mail/template/detail/index.vue
Normal file
170
src/pages-system/mail/template/detail/index.vue
Normal file
@@ -0,0 +1,170 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="邮件模板详情"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 详情内容 -->
|
||||
<view>
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="模板编号" :value="formData?.id" />
|
||||
<wd-cell title="模板名称" :value="formData?.name" />
|
||||
<wd-cell title="模板编码" :value="formData?.code" />
|
||||
<wd-cell title="邮箱账号" :value="getAccountMail(formData?.accountId) || formData?.accountId" />
|
||||
<wd-cell title="发送人名称" :value="formData?.nickname" />
|
||||
<wd-cell title="模板标题" :value="formData?.title" />
|
||||
<wd-cell title="开启状态">
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="formData?.status" />
|
||||
</wd-cell>
|
||||
<wd-cell title="模板内容" :value="formData?.content" />
|
||||
<wd-cell title="备注" :value="formData?.remark ?? '-'" />
|
||||
<wd-cell title="创建时间" :value="formatDateTime(formData?.createTime)" />
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
|
||||
<!-- 发送测试邮件弹窗 -->
|
||||
<SendForm v-model="sendVisible" :template="formData" />
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<view class="yd-detail-footer-actions">
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['system:mail-template:send-mail'])"
|
||||
class="flex-1" type="primary" @click="handleSendTest"
|
||||
>
|
||||
测试
|
||||
</wd-button>
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['system:mail-template:update'])"
|
||||
class="flex-1" type="warning" @click="handleEdit"
|
||||
>
|
||||
编辑
|
||||
</wd-button>
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['system:mail-template:delete'])"
|
||||
class="flex-1" type="error" :loading="deleting" @click="handleDelete"
|
||||
>
|
||||
删除
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { MailAccount } from '@/api/system/mail/account'
|
||||
import type { MailTemplate } from '@/api/system/mail/template'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { getSimpleMailAccountList } from '@/api/system/mail/account'
|
||||
import { deleteMailTemplate, getMailTemplate } from '@/api/system/mail/template'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import SendForm from './components/send-form.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const toast = useToast()
|
||||
const formData = ref<MailTemplate>()
|
||||
const deleting = ref(false)
|
||||
|
||||
// 发送测试邮件相关
|
||||
const sendVisible = ref(false)
|
||||
|
||||
/** 邮箱账号列表 */
|
||||
const accountList = ref<MailAccount[]>([])
|
||||
|
||||
/** 获取邮箱账号名称 */
|
||||
function getAccountMail(accountId?: number) {
|
||||
return accountList.value.find((item: MailAccount) => item.id === accountId)?.mail
|
||||
}
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-system/mail/index')
|
||||
}
|
||||
|
||||
/** 加载邮箱账号列表 */
|
||||
async function loadAccountList() {
|
||||
try {
|
||||
accountList.value = await getSimpleMailAccountList()
|
||||
} catch {
|
||||
accountList.value = []
|
||||
}
|
||||
}
|
||||
|
||||
/** 加载详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
toast.loading('加载中...')
|
||||
formData.value = await getMailTemplate(props.id)
|
||||
} finally {
|
||||
toast.close()
|
||||
}
|
||||
}
|
||||
|
||||
/** 编辑 */
|
||||
function handleEdit() {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/mail/template/form/index?id=${props.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除 */
|
||||
function handleDelete() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要删除该邮件模板吗?',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) {
|
||||
return
|
||||
}
|
||||
deleting.value = true
|
||||
try {
|
||||
await deleteMailTemplate(props.id)
|
||||
toast.success('删除成功')
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 打开发送测试邮件弹窗 */
|
||||
function handleSendTest() {
|
||||
sendVisible.value = true
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
await loadAccountList()
|
||||
await getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
199
src/pages-system/mail/template/form/index.vue
Normal file
199
src/pages-system/mail/template/form/index.vue
Normal file
@@ -0,0 +1,199 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
:title="getTitle"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 表单区域 -->
|
||||
<view>
|
||||
<wd-form ref="formRef" :model="formData" :rules="formRules">
|
||||
<wd-cell-group border>
|
||||
<wd-input
|
||||
v-model="formData.name"
|
||||
label="模板名称"
|
||||
label-width="200rpx"
|
||||
prop="name"
|
||||
clearable
|
||||
placeholder="请输入模板名称"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="formData.code"
|
||||
label="模板编码"
|
||||
label-width="200rpx"
|
||||
prop="code"
|
||||
clearable
|
||||
placeholder="请输入模板编码"
|
||||
/>
|
||||
<wd-cell title="邮箱账号" title-width="200rpx" prop="accountId" center>
|
||||
<wd-picker
|
||||
v-model="formData.accountId"
|
||||
:columns="accountList"
|
||||
label-key="mail"
|
||||
value-key="id"
|
||||
placeholder="请选择邮箱账号"
|
||||
/>
|
||||
</wd-cell>
|
||||
<wd-input
|
||||
v-model="formData.nickname"
|
||||
label="发送人名称"
|
||||
label-width="200rpx"
|
||||
clearable
|
||||
placeholder="请输入发送人名称"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="formData.title"
|
||||
label="模板标题"
|
||||
label-width="200rpx"
|
||||
prop="title"
|
||||
clearable
|
||||
placeholder="请输入模板标题"
|
||||
/>
|
||||
<wd-textarea
|
||||
v-model="formData.content"
|
||||
label="模板内容"
|
||||
label-width="200rpx"
|
||||
prop="content"
|
||||
clearable
|
||||
placeholder="请输入模板内容"
|
||||
:rows="4"
|
||||
/>
|
||||
<wd-cell title="开启状态" title-width="200rpx" prop="status" center>
|
||||
<wd-radio-group v-model="formData.status" shape="button">
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-cell>
|
||||
<wd-textarea
|
||||
v-model="formData.remark"
|
||||
label="备注"
|
||||
label-width="200rpx"
|
||||
clearable
|
||||
placeholder="请输入备注"
|
||||
/>
|
||||
</wd-cell-group>
|
||||
</wd-form>
|
||||
</view>
|
||||
|
||||
<!-- 底部保存按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<wd-button
|
||||
type="primary"
|
||||
block
|
||||
:loading="formLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
保存
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance } from 'wot-design-uni/components/wd-form/types'
|
||||
import type { MailAccount } from '@/api/system/mail/account'
|
||||
import type { MailTemplate } from '@/api/system/mail/template'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { getSimpleMailAccountList } from '@/api/system/mail/account'
|
||||
import { createMailTemplate, getMailTemplate, updateMailTemplate } from '@/api/system/mail/template'
|
||||
import { getIntDictOptions } from '@/hooks/useDict'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { CommonStatusEnum, DICT_TYPE } from '@/utils/constants'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const getTitle = computed(() => props.id ? '编辑邮件模板' : '新增邮件模板')
|
||||
const formLoading = ref(false)
|
||||
const formData = ref<MailTemplate>({
|
||||
id: undefined,
|
||||
name: '',
|
||||
code: '',
|
||||
accountId: undefined,
|
||||
nickname: '',
|
||||
title: '',
|
||||
content: '',
|
||||
status: CommonStatusEnum.ENABLE,
|
||||
remark: '',
|
||||
})
|
||||
const formRules = {
|
||||
name: [{ required: true, message: '模板名称不能为空' }],
|
||||
code: [{ required: true, message: '模板编码不能为空' }],
|
||||
accountId: [{ required: true, message: '邮箱账号不能为空' }],
|
||||
title: [{ required: true, message: '模板标题不能为空' }],
|
||||
content: [{ required: true, message: '模板内容不能为空' }],
|
||||
status: [{ required: true, message: '开启状态不能为空' }],
|
||||
}
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/** 邮箱账号列表 */
|
||||
const accountList = ref<MailAccount[]>([])
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-system/mail/index')
|
||||
}
|
||||
|
||||
/** 加载邮箱账号列表 */
|
||||
async function loadAccountList() {
|
||||
accountList.value = await getSimpleMailAccountList()
|
||||
}
|
||||
|
||||
/** 加载详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
formData.value = await getMailTemplate(props.id)
|
||||
}
|
||||
|
||||
/** 提交表单 */
|
||||
async function handleSubmit() {
|
||||
const { valid } = await formRef.value.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
|
||||
formLoading.value = true
|
||||
try {
|
||||
if (props.id) {
|
||||
await updateMailTemplate(formData.value)
|
||||
toast.success('修改成功')
|
||||
} else {
|
||||
await createMailTemplate(formData.value)
|
||||
toast.success('新增成功')
|
||||
}
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
await loadAccountList()
|
||||
await getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
106
src/pages-system/menu/components/breadcrumb.vue
Normal file
106
src/pages-system/menu/components/breadcrumb.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<view class="bg-white px-24rpx py-16rpx">
|
||||
<scroll-view scroll-x class="whitespace-nowrap">
|
||||
<view class="inline-flex items-center text-28rpx">
|
||||
<template v-for="(item, index) in breadcrumbItems" :key="item.id">
|
||||
<text v-if="index > 0" class="mx-8rpx text-[#999]">/</text>
|
||||
<text
|
||||
:class="index === breadcrumbItems.length - 1 ? 'text-[#333]' : 'text-[#1890ff]'"
|
||||
@click="handleClick(index)"
|
||||
>
|
||||
{{ item.name }}
|
||||
</text>
|
||||
</template>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
export interface BreadcrumbNode {
|
||||
id: number
|
||||
name: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue?: number // 当前父节点编号
|
||||
rootName?: string // 根目录名称
|
||||
}>(), {
|
||||
modelValue: 0,
|
||||
rootName: '根目录',
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: number]
|
||||
'back': [] // 返回上一层级事件
|
||||
}>()
|
||||
|
||||
const breadcrumbs = ref<BreadcrumbNode[]>([]) // 面包屑路径(不包含根目录)
|
||||
|
||||
const breadcrumbItems = computed(() => [
|
||||
{ id: 0, name: props.rootName },
|
||||
...breadcrumbs.value,
|
||||
]) // 面包屑显示数据(包含根目录)
|
||||
|
||||
const currentParentId = computed({
|
||||
get: () => props.modelValue,
|
||||
set: val => emit('update:modelValue', val),
|
||||
}) // 当前父节点编号
|
||||
|
||||
/** 面包屑点击 */
|
||||
function handleClick(index: number) {
|
||||
if (index === breadcrumbItems.value.length - 1)
|
||||
return // 点击当前层级不处理
|
||||
if (index === 0) {
|
||||
breadcrumbs.value = []
|
||||
currentParentId.value = 0
|
||||
} else {
|
||||
breadcrumbs.value = breadcrumbs.value.slice(0, index)
|
||||
currentParentId.value = breadcrumbs.value[index - 1].id
|
||||
}
|
||||
}
|
||||
|
||||
/** 进入子层级 */
|
||||
function enter(node: BreadcrumbNode) {
|
||||
breadcrumbs.value.push({ id: node.id, name: node.name })
|
||||
currentParentId.value = node.id
|
||||
}
|
||||
|
||||
/** 返回上一层级,返回 true 表示还有上层,false 表示已在根目录 */
|
||||
function back(): boolean {
|
||||
if (breadcrumbs.value.length > 0) {
|
||||
breadcrumbs.value.pop()
|
||||
currentParentId.value = breadcrumbs.value.length > 0
|
||||
? breadcrumbs.value[breadcrumbs.value.length - 1].id
|
||||
: 0
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/** 重置面包屑 */
|
||||
function reset() {
|
||||
breadcrumbs.value = []
|
||||
currentParentId.value = 0
|
||||
}
|
||||
|
||||
/** 监听外部 modelValue 变化,重置面包屑(用于外部重置场景) */
|
||||
watch(() => props.modelValue, (val) => {
|
||||
if (val === 0 && breadcrumbs.value.length > 0) {
|
||||
breadcrumbs.value = []
|
||||
}
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
enter,
|
||||
back,
|
||||
reset,
|
||||
breadcrumbs,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
87
src/pages-system/menu/components/search-form.vue
Normal file
87
src/pages-system/menu/components/search-form.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<!-- 搜索框入口 -->
|
||||
<view @click="visible = true">
|
||||
<wd-search :placeholder="placeholder" hide-cancel disabled />
|
||||
</view>
|
||||
|
||||
<!-- 搜索弹窗 -->
|
||||
<wd-popup v-model="visible" position="top" @close="visible = false">
|
||||
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
菜单名称
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.name"
|
||||
placeholder="请输入菜单名称"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
菜单状态
|
||||
</view>
|
||||
<wd-radio-group v-model="formData.status" shape="button">
|
||||
<wd-radio v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" :key="dict.value" :value="dict.value">
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
<view class="yd-search-form-actions">
|
||||
<wd-button class="flex-1" plain @click="handleReset">
|
||||
重置
|
||||
</wd-button>
|
||||
<wd-button class="flex-1" type="primary" @click="handleSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { getIntDictOptions } from '@/hooks/useDict'
|
||||
import { getNavbarHeight } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
|
||||
const emit = defineEmits<{
|
||||
search: [data: Record<string, any>]
|
||||
reset: []
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const formData = reactive({
|
||||
name: undefined as string | undefined,
|
||||
status: undefined as number | undefined,
|
||||
})
|
||||
|
||||
/** 搜索条件 placeholder 拼接 */
|
||||
const placeholder = computed(() => {
|
||||
const conditions: string[] = []
|
||||
if (formData.name) {
|
||||
conditions.push(`名称:${formData.name}`)
|
||||
}
|
||||
if (formData.status !== undefined) {
|
||||
const dict = getIntDictOptions(DICT_TYPE.COMMON_STATUS).find(d => d.value === formData.status)
|
||||
if (dict) {
|
||||
conditions.push(`状态:${dict.label}`)
|
||||
}
|
||||
}
|
||||
return conditions.length > 0 ? conditions.join(' | ') : '搜索菜单'
|
||||
})
|
||||
|
||||
/** 搜索 */
|
||||
function handleSearch() {
|
||||
visible.value = false
|
||||
emit('search', { ...formData })
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
function handleReset() {
|
||||
formData.name = undefined
|
||||
formData.status = undefined
|
||||
visible.value = false
|
||||
emit('reset')
|
||||
}
|
||||
</script>
|
||||
152
src/pages-system/menu/detail/index.vue
Normal file
152
src/pages-system/menu/detail/index.vue
Normal file
@@ -0,0 +1,152 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="菜单详情"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 详情内容 -->
|
||||
<view>
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="菜单名称" :value="formData?.name || '-'" />
|
||||
<wd-cell title="菜单类型">
|
||||
<dict-tag :type="DICT_TYPE.SYSTEM_MENU_TYPE" :value="formData?.type" />
|
||||
</wd-cell>
|
||||
<wd-cell title="上级菜单" :value="parentMenuName" />
|
||||
<wd-cell title="显示排序" :value="formData?.sort" />
|
||||
<wd-cell title="路由地址" :value="formData?.path || '-'" />
|
||||
<wd-cell v-if="formData?.type === SystemMenuTypeEnum.MENU" title="组件路径" :value="formData?.component || '-'" />
|
||||
<wd-cell v-if="formData?.type === SystemMenuTypeEnum.MENU" title="组件名称" :value="formData?.componentName || '-'" />
|
||||
<wd-cell v-if="formData?.type !== SystemMenuTypeEnum.DIR" title="权限标识" :value="formData?.permission || '-'" />
|
||||
<wd-cell title="菜单状态">
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="formData?.status" />
|
||||
</wd-cell>
|
||||
<wd-cell v-if="formData?.type !== SystemMenuTypeEnum.BUTTON" title="显示状态">
|
||||
<wd-tag v-if="formData?.visible" type="success" plain>
|
||||
显示
|
||||
</wd-tag>
|
||||
<wd-tag v-else type="warning" plain>
|
||||
隐藏
|
||||
</wd-tag>
|
||||
</wd-cell>
|
||||
<wd-cell v-if="formData?.type === SystemMenuTypeEnum.MENU" title="缓存状态">
|
||||
<wd-tag v-if="formData?.keepAlive" type="success" plain>
|
||||
缓存
|
||||
</wd-tag>
|
||||
<wd-tag v-else type="default" plain>
|
||||
不缓存
|
||||
</wd-tag>
|
||||
</wd-cell>
|
||||
<wd-cell title="创建时间" :value="formatDateTime(formData?.createTime)" />
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<view class="yd-detail-footer-actions">
|
||||
<wd-button class="flex-1" type="warning" @click="handleEdit">
|
||||
编辑
|
||||
</wd-button>
|
||||
<wd-button class="flex-1" type="error" :loading="deleting" @click="handleDelete">
|
||||
删除
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Menu } from '@/api/system/menu'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { deleteMenu, getMenu, getSimpleMenuList } from '@/api/system/menu'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE, SystemMenuTypeEnum } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
|
||||
const props = defineProps<{
|
||||
id: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const formData = ref<Menu>()
|
||||
const deleting = ref(false)
|
||||
const parentMenuName = ref('-')
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-system/menu/index')
|
||||
}
|
||||
|
||||
/** 加载菜单详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
toast.loading('加载中...')
|
||||
try {
|
||||
formData.value = await getMenu(props.id)
|
||||
// 获取上级菜单名称
|
||||
if (formData.value?.parentId === 0) {
|
||||
parentMenuName.value = '主类目'
|
||||
} else if (formData.value?.parentId) {
|
||||
// TODO @芋艿:后续这里可以优化,由后端返回 menuName;
|
||||
const menuList = await getSimpleMenuList()
|
||||
const parent = menuList.find(item => item.id === formData.value?.parentId)
|
||||
parentMenuName.value = parent?.name || '-'
|
||||
}
|
||||
} finally {
|
||||
toast.close()
|
||||
}
|
||||
}
|
||||
|
||||
/** 编辑菜单 */
|
||||
function handleEdit() {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/menu/form/index?id=${props.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除菜单 */
|
||||
function handleDelete() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要删除该菜单吗?',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) {
|
||||
return
|
||||
}
|
||||
deleting.value = true
|
||||
try {
|
||||
await deleteMenu(props.id)
|
||||
toast.success('删除成功')
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
165
src/pages-system/menu/form/components/menu-picker.vue
Normal file
165
src/pages-system/menu/form/components/menu-picker.vue
Normal file
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<wd-col-picker
|
||||
v-model="selectedValue"
|
||||
label="上级菜单"
|
||||
label-width="180rpx"
|
||||
:columns="menuColumns"
|
||||
value-key="id"
|
||||
label-key="name"
|
||||
:column-change="handleColumnChange"
|
||||
:display-format="displayFormat"
|
||||
@confirm="handleConfirm"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Menu } from '@/api/system/menu'
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import { getSimpleMenuList } from '@/api/system/menu'
|
||||
import { SystemMenuTypeEnum } from '@/utils/constants'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue?: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: number): void
|
||||
}>()
|
||||
|
||||
const menuList = ref<Menu[]>([])
|
||||
const menuColumns = ref<any[]>([])
|
||||
const selectedValue = ref<number[]>([])
|
||||
|
||||
/** 监听外部值变化,回显选中值 */
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
if (val !== undefined && val !== 0 && menuList.value.length > 0) {
|
||||
const path = findMenuPath(val)
|
||||
selectedValue.value = path
|
||||
buildColumnsForPath(path)
|
||||
} else {
|
||||
selectedValue.value = [0]
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
/** 加载菜单列表 */
|
||||
async function loadMenuList() {
|
||||
const list = await getSimpleMenuList()
|
||||
// 只保留目录和菜单
|
||||
menuList.value = list.filter(item => item.type !== SystemMenuTypeEnum.BUTTON)
|
||||
// 构建第一列数据(主类目 + 顶级菜单)
|
||||
const topMenus = menuList.value.filter(item => item.parentId === 0)
|
||||
menuColumns.value = [[
|
||||
{ id: 0, name: '主类目' },
|
||||
...topMenus,
|
||||
]]
|
||||
// 如果有初始值,回显
|
||||
if (props.modelValue !== undefined && props.modelValue !== 0) {
|
||||
const path = findMenuPath(props.modelValue)
|
||||
selectedValue.value = path
|
||||
buildColumnsForPath(path)
|
||||
} else {
|
||||
selectedValue.value = [0]
|
||||
}
|
||||
}
|
||||
|
||||
/** 查找菜单路径 */
|
||||
function findMenuPath(targetId: number): number[] {
|
||||
if (targetId === 0) {
|
||||
return [0]
|
||||
}
|
||||
const path: number[] = []
|
||||
const findPath = (parentId: number, id: number): boolean => {
|
||||
const items = menuList.value.filter(m => m.parentId === parentId)
|
||||
for (const item of items) {
|
||||
if (item.id === id) {
|
||||
path.push(item.id!)
|
||||
return true
|
||||
}
|
||||
if (findPath(item.id!, id)) {
|
||||
path.unshift(item.id!)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
findPath(0, targetId)
|
||||
return path.length > 0 ? path : [0]
|
||||
}
|
||||
|
||||
/** 根据路径构建列数据 */
|
||||
function buildColumnsForPath(path: number[]) {
|
||||
if (path.length === 0 || (path.length === 1 && path[0] === 0)) {
|
||||
return
|
||||
}
|
||||
// 第一列已经有了,从第二列开始构建
|
||||
const columns = [menuColumns.value[0]]
|
||||
for (let i = 0; i < path.length; i++) {
|
||||
const parentId = path[i]
|
||||
if (parentId === 0) {
|
||||
continue
|
||||
}
|
||||
const children = menuList.value.filter(item => item.parentId === parentId)
|
||||
if (children.length > 0) {
|
||||
columns.push(children)
|
||||
}
|
||||
}
|
||||
menuColumns.value = columns
|
||||
}
|
||||
|
||||
/** 构建带"选择当前"选项的子列表 */
|
||||
function buildChildrenWithCurrent(parentId: number) {
|
||||
const children = menuList.value.filter(item => item.parentId === parentId)
|
||||
// 添加"选择当前"选项,使用父节点 ID 的负数作为标识
|
||||
return [
|
||||
{ id: -parentId, name: '✓ 选择当前' },
|
||||
...children,
|
||||
]
|
||||
}
|
||||
|
||||
/** 列变化 */
|
||||
function handleColumnChange({ selectedItem, resolve, finish }: any) {
|
||||
// 选择主类目或"选择当前",结束
|
||||
if (selectedItem.id === 0 || selectedItem.id < 0) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
const children = menuList.value.filter(item => item.parentId === selectedItem.id)
|
||||
if (children.length > 0) {
|
||||
resolve(buildChildrenWithCurrent(selectedItem.id))
|
||||
} else {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
/** 格式化显示 */
|
||||
function displayFormat(selectedItems: any[]) {
|
||||
// 过滤掉"选择当前"选项
|
||||
return selectedItems
|
||||
.filter(item => item.id >= 0)
|
||||
.map(item => item.name)
|
||||
.join(' / ')
|
||||
}
|
||||
|
||||
/** 确认选择 */
|
||||
function handleConfirm({ value }: { value: number[] }) {
|
||||
if (value && value.length > 0) {
|
||||
const lastValue = value[value.length - 1]
|
||||
// 如果选择的是"选择当前"(负数 ID),取其绝对值作为实际选中的菜单 ID
|
||||
if (lastValue < 0) {
|
||||
emit('update:modelValue', Math.abs(lastValue))
|
||||
} else {
|
||||
emit('update:modelValue', lastValue)
|
||||
}
|
||||
} else {
|
||||
emit('update:modelValue', 0)
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
loadMenuList()
|
||||
})
|
||||
</script>
|
||||
250
src/pages-system/menu/form/index.vue
Normal file
250
src/pages-system/menu/form/index.vue
Normal file
@@ -0,0 +1,250 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
:title="getTitle"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 表单区域 -->
|
||||
<view>
|
||||
<wd-form ref="formRef" :model="formData" :rules="formRules">
|
||||
<wd-cell-group border>
|
||||
<MenuPicker v-model="formData.parentId" />
|
||||
<wd-cell title="菜单类型" title-width="180rpx" prop="type">
|
||||
<wd-radio-group v-model="formData.type" shape="button" @change="handleTypeChange">
|
||||
<wd-radio v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_MENU_TYPE)" :key="dict.value" :value="dict.value">
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-cell>
|
||||
<wd-input
|
||||
v-model="formData.name"
|
||||
label="菜单名称"
|
||||
label-width="180rpx"
|
||||
prop="name"
|
||||
clearable
|
||||
placeholder="请输入菜单名称"
|
||||
/>
|
||||
<wd-input
|
||||
v-if="formData.type !== SystemMenuTypeEnum.BUTTON"
|
||||
v-model="formData.icon"
|
||||
label="菜单图标"
|
||||
label-width="180rpx"
|
||||
clearable
|
||||
placeholder="请输入菜单图标"
|
||||
/>
|
||||
<wd-input
|
||||
v-if="formData.type !== SystemMenuTypeEnum.BUTTON"
|
||||
v-model="formData.path"
|
||||
label="路由地址"
|
||||
label-width="180rpx"
|
||||
prop="path"
|
||||
clearable
|
||||
placeholder="请输入路由地址"
|
||||
/>
|
||||
<wd-input
|
||||
v-if="formData.type === SystemMenuTypeEnum.MENU"
|
||||
v-model="formData.component"
|
||||
label="组件路径"
|
||||
label-width="180rpx"
|
||||
clearable
|
||||
placeholder="例如:system/user/index"
|
||||
/>
|
||||
<wd-input
|
||||
v-if="formData.type === SystemMenuTypeEnum.MENU"
|
||||
v-model="formData.componentName"
|
||||
label="组件名称"
|
||||
label-width="180rpx"
|
||||
clearable
|
||||
placeholder="例如:SystemUser"
|
||||
/>
|
||||
<wd-input
|
||||
v-if="formData.type !== SystemMenuTypeEnum.DIR"
|
||||
v-model="formData.permission"
|
||||
label="权限标识"
|
||||
label-width="180rpx"
|
||||
clearable
|
||||
placeholder="请输入权限标识"
|
||||
/>
|
||||
<wd-cell title="显示排序" title-width="180rpx" prop="sort" center>
|
||||
<wd-input-number
|
||||
v-model="formData.sort"
|
||||
:min="0"
|
||||
/>
|
||||
</wd-cell>
|
||||
<wd-cell title="菜单状态" title-width="180rpx" prop="status" center>
|
||||
<wd-switch
|
||||
v-model="formData.status"
|
||||
:active-value="CommonStatusEnum.ENABLE"
|
||||
:inactive-value="CommonStatusEnum.DISABLE"
|
||||
/>
|
||||
</wd-cell>
|
||||
<wd-cell v-if="formData.type !== SystemMenuTypeEnum.BUTTON" title="显示状态" title-width="180rpx" center>
|
||||
<wd-switch
|
||||
v-model="formData.visible"
|
||||
:active-value="true"
|
||||
:inactive-value="false"
|
||||
/>
|
||||
</wd-cell>
|
||||
<wd-cell v-if="formData.type !== SystemMenuTypeEnum.BUTTON" title="总是显示" title-width="180rpx" center>
|
||||
<wd-switch
|
||||
v-model="formData.alwaysShow"
|
||||
:active-value="true"
|
||||
:inactive-value="false"
|
||||
/>
|
||||
</wd-cell>
|
||||
<wd-cell v-if="formData.type === SystemMenuTypeEnum.MENU" title="缓存状态" title-width="180rpx" center>
|
||||
<wd-switch
|
||||
v-model="formData.keepAlive"
|
||||
:active-value="true"
|
||||
:inactive-value="false"
|
||||
/>
|
||||
</wd-cell>
|
||||
</wd-cell-group>
|
||||
</wd-form>
|
||||
</view>
|
||||
|
||||
<!-- 底部保存按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<wd-button
|
||||
type="primary"
|
||||
block
|
||||
:loading="formLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
保存
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance } from 'wot-design-uni/components/wd-form/types'
|
||||
import type { Menu } from '@/api/system/menu'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { createMenu, getMenu, updateMenu } from '@/api/system/menu'
|
||||
import { getIntDictOptions } from '@/hooks/useDict'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { CommonStatusEnum, DICT_TYPE, SystemMenuTypeEnum } from '@/utils/constants'
|
||||
import MenuPicker from './components/menu-picker.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
parentId?: number
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const getTitle = computed(() => props.id ? '编辑菜单' : '新增菜单')
|
||||
const formLoading = ref(false)
|
||||
const formData = ref<Menu>({
|
||||
id: undefined,
|
||||
name: '',
|
||||
permission: '',
|
||||
type: SystemMenuTypeEnum.DIR,
|
||||
sort: 0,
|
||||
parentId: 0,
|
||||
path: '',
|
||||
icon: '',
|
||||
component: '',
|
||||
componentName: '',
|
||||
status: CommonStatusEnum.ENABLE,
|
||||
visible: true,
|
||||
keepAlive: true,
|
||||
alwaysShow: true,
|
||||
})
|
||||
const formRules = {
|
||||
name: [{ required: true, message: '菜单名称不能为空' }],
|
||||
type: [{ required: true, message: '菜单类型不能为空' }],
|
||||
sort: [{ required: true, message: '显示排序不能为空' }],
|
||||
status: [{ required: true, message: '状态不能为空' }],
|
||||
}
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-system/menu/index')
|
||||
}
|
||||
|
||||
/** 菜单类型变更: */
|
||||
function handleTypeChange() {
|
||||
// 切换类型时,清空不需要的字段
|
||||
if (formData.value.type === SystemMenuTypeEnum.BUTTON) {
|
||||
formData.value.path = ''
|
||||
formData.value.component = ''
|
||||
formData.value.componentName = ''
|
||||
formData.value.icon = ''
|
||||
} else if (formData.value.type === SystemMenuTypeEnum.DIR) {
|
||||
formData.value.component = ''
|
||||
formData.value.componentName = ''
|
||||
formData.value.permission = ''
|
||||
}
|
||||
}
|
||||
|
||||
/** 加载菜单详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
// 新增时,设置默认的上级菜单
|
||||
if (props.parentId) {
|
||||
formData.value.parentId = props.parentId
|
||||
}
|
||||
return
|
||||
}
|
||||
formData.value = await getMenu(props.id)
|
||||
}
|
||||
|
||||
/** 提交表单 */
|
||||
async function handleSubmit() {
|
||||
const { valid } = await formRef.value.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
// 路由地址校验
|
||||
if (formData.value.type !== SystemMenuTypeEnum.BUTTON) {
|
||||
const path = formData.value.path
|
||||
const isExternal = /^(?:https?:|mailto:|tel:)/.test(path)
|
||||
if (!isExternal) {
|
||||
if (formData.value.parentId === 0 && path.charAt(0) !== '/') {
|
||||
toast.error('路径必须以 / 开头')
|
||||
return
|
||||
} else if (formData.value.parentId !== 0 && path.charAt(0) === '/') {
|
||||
toast.error('路径不能以 / 开头')
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
formLoading.value = true
|
||||
try {
|
||||
if (props.id) {
|
||||
await updateMenu(formData.value)
|
||||
toast.success('修改成功')
|
||||
} else {
|
||||
await createMenu(formData.value)
|
||||
toast.success('新增成功')
|
||||
}
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
201
src/pages-system/menu/index.vue
Normal file
201
src/pages-system/menu/index.vue
Normal file
@@ -0,0 +1,201 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="菜单管理"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 搜索组件 -->
|
||||
<SearchForm @search="handleQuery" @reset="handleReset" />
|
||||
|
||||
<!-- 面包屑导航 -->
|
||||
<Breadcrumb ref="breadcrumbRef" v-model="currentParentId" />
|
||||
|
||||
<!-- 菜单列表 -->
|
||||
<view class="p-24rpx">
|
||||
<view
|
||||
v-for="item in currentList"
|
||||
:key="item.id"
|
||||
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
|
||||
>
|
||||
<!-- 主内容区域:点击进入详情 -->
|
||||
<view class="p-24rpx" @click="handleDetail(item)">
|
||||
<!-- 第一行:图标、名称、状态标签 -->
|
||||
<view class="flex items-center justify-between">
|
||||
<view class="flex items-center">
|
||||
<view class="mr-16rpx h-48rpx w-48rpx flex items-center justify-center rounded-8rpx" :class="getTypeIconBg(item.type)">
|
||||
<wd-icon :name="getTypeIcon(item.type)" size="20px" color="#fff" />
|
||||
</view>
|
||||
<view class="text-32rpx text-[#333] font-semibold">
|
||||
{{ item.name }}
|
||||
</view>
|
||||
</view>
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="item.status" />
|
||||
</view>
|
||||
<!-- 第二行:类型描述、子菜单入口 -->
|
||||
<view class="mt-12rpx flex items-center justify-between pl-64rpx">
|
||||
<view class="text-24rpx text-[#999]">
|
||||
{{ getTypeDesc(item) }}
|
||||
</view>
|
||||
<view
|
||||
v-if="item.children && item.children.length > 0"
|
||||
class="flex items-center"
|
||||
@click.stop="handleEnterChildren(item)"
|
||||
>
|
||||
<text class="text-24rpx text-[#1890ff]">子菜单 ({{ item.children.length }})</text>
|
||||
<wd-icon name="arrow-right" size="12px" color="#1890ff" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view v-if="!loading && currentList.length === 0" class="py-100rpx text-center">
|
||||
<wd-status-tip image="content" tip="暂无菜单数据" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 新增按钮 -->
|
||||
<wd-fab
|
||||
position="right-bottom"
|
||||
type="primary"
|
||||
:expandable="false"
|
||||
@click="handleAdd"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Menu } from '@/api/system/menu'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { getMenuList } from '@/api/system/menu'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE, SystemMenuTypeEnum } from '@/utils/constants'
|
||||
import { findChildren, handleTree } from '@/utils/tree'
|
||||
import Breadcrumb from './components/breadcrumb.vue'
|
||||
import SearchForm from './components/search-form.vue'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const list = ref<Menu[]>([]) // 完整菜单列表(树形结构)
|
||||
|
||||
const currentParentId = ref(0) // 当前层级的父节点编号
|
||||
const currentList = computed(() => {
|
||||
if (currentParentId.value === 0) {
|
||||
return list.value.filter(item => item.parentId === 0)
|
||||
}
|
||||
return findChildren(list.value, currentParentId.value)
|
||||
}) // 当前层级的菜单列表
|
||||
const breadcrumbRef = ref<InstanceType<typeof Breadcrumb>>()
|
||||
|
||||
const queryParams = ref<Record<string, any>>({})
|
||||
|
||||
/** 返回上一页或上一层级 */
|
||||
function handleBack() {
|
||||
if (!breadcrumbRef.value?.back()) {
|
||||
navigateBackPlus()
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取菜单类型图标 */
|
||||
function getTypeIcon(type: number): string {
|
||||
switch (type) {
|
||||
case SystemMenuTypeEnum.DIR:
|
||||
return 'folder'
|
||||
case SystemMenuTypeEnum.MENU:
|
||||
return 'read'
|
||||
case SystemMenuTypeEnum.BUTTON:
|
||||
return 'tips'
|
||||
default:
|
||||
return 'folder'
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取菜单类型图标背景色 */
|
||||
function getTypeIconBg(type: number): string {
|
||||
switch (type) {
|
||||
case SystemMenuTypeEnum.DIR:
|
||||
return 'bg-[#1890ff]'
|
||||
case SystemMenuTypeEnum.MENU:
|
||||
return 'bg-[#52c41a]'
|
||||
case SystemMenuTypeEnum.BUTTON:
|
||||
return 'bg-[#faad14]'
|
||||
default:
|
||||
return 'bg-[#1890ff]'
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取菜单类型描述(根据类型展示不同信息) */
|
||||
function getTypeDesc(item: Menu): string {
|
||||
switch (item.type) {
|
||||
case SystemMenuTypeEnum.DIR:
|
||||
return `路由:${item.path}`
|
||||
case SystemMenuTypeEnum.MENU:
|
||||
return `路由:${item.path}`
|
||||
case SystemMenuTypeEnum.BUTTON:
|
||||
return `权限:${item.permission}`
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
/** 进入子菜单层级 */
|
||||
function handleEnterChildren(item: Menu) {
|
||||
breadcrumbRef.value?.enter({ id: item.id!, name: item.name })
|
||||
}
|
||||
|
||||
/** 查询菜单列表 */
|
||||
async function getList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await getMenuList(queryParams.value)
|
||||
list.value = handleTree(data)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery(data?: Record<string, any>) {
|
||||
queryParams.value = { ...data }
|
||||
// 重置面包屑
|
||||
currentParentId.value = 0
|
||||
breadcrumbRef.value?.reset()
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function handleReset() {
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 新增菜单 */
|
||||
function handleAdd() {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/menu/form/index?parentId=${currentParentId.value}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 查看详情 */
|
||||
function handleDetail(item: Menu) {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/menu/detail/index?id=${item.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
94
src/pages-system/notice/components/search-form.vue
Normal file
94
src/pages-system/notice/components/search-form.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<!-- 搜索框入口 -->
|
||||
<view @click="visible = true">
|
||||
<wd-search :placeholder="placeholder" hide-cancel disabled />
|
||||
</view>
|
||||
|
||||
<!-- 搜索弹窗 -->
|
||||
<wd-popup v-model="visible" position="top" @close="visible = false">
|
||||
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
公告标题
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.title"
|
||||
placeholder="请输入公告标题"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
公告状态
|
||||
</view>
|
||||
<wd-radio-group v-model="formData.status" shape="button">
|
||||
<wd-radio :value="-1">
|
||||
全部
|
||||
</wd-radio>
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
<view class="yd-search-form-actions">
|
||||
<wd-button class="flex-1" plain @click="handleReset">
|
||||
重置
|
||||
</wd-button>
|
||||
<wd-button class="flex-1" type="primary" @click="handleSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { getDictLabel, getIntDictOptions } from '@/hooks/useDict'
|
||||
import { getNavbarHeight } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
|
||||
const emit = defineEmits<{
|
||||
search: [data: Record<string, any>]
|
||||
reset: []
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const formData = reactive({
|
||||
title: undefined as string | undefined,
|
||||
status: -1, // -1 表示全部
|
||||
})
|
||||
|
||||
/** 搜索条件 placeholder 拼接 */
|
||||
const placeholder = computed(() => {
|
||||
const conditions: string[] = []
|
||||
if (formData.title) {
|
||||
conditions.push(`公告标题:${formData.title}`)
|
||||
}
|
||||
if (formData.status !== -1) {
|
||||
conditions.push(`公告状态:${getDictLabel(DICT_TYPE.COMMON_STATUS, formData.status)}`)
|
||||
}
|
||||
return conditions.length > 0 ? conditions.join(' | ') : '搜索通知公告'
|
||||
})
|
||||
|
||||
/** 搜索 */
|
||||
function handleSearch() {
|
||||
visible.value = false
|
||||
emit('search', {
|
||||
...formData,
|
||||
status: formData.status === -1 ? undefined : formData.status,
|
||||
})
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
function handleReset() {
|
||||
formData.title = undefined
|
||||
formData.status = -1
|
||||
visible.value = false
|
||||
emit('reset')
|
||||
}
|
||||
</script>
|
||||
130
src/pages-system/notice/detail/index.vue
Normal file
130
src/pages-system/notice/detail/index.vue
Normal file
@@ -0,0 +1,130 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="通知公告详情"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 详情内容 -->
|
||||
<view>
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="公告编号" :value="formData?.id" />
|
||||
<wd-cell title="公告标题" :value="formData?.title" />
|
||||
<wd-cell title="公告内容" :value="formData?.content" />
|
||||
<wd-cell title="公告类型">
|
||||
<dict-tag :type="DICT_TYPE.SYSTEM_NOTICE_TYPE" :value="formData?.type" />
|
||||
</wd-cell>
|
||||
<wd-cell title="公告状态">
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="formData?.status" />
|
||||
</wd-cell>
|
||||
<wd-cell title="创建时间" :value="formatDateTime(formData?.createTime)" />
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<view class="yd-detail-footer-actions">
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['system:notice:update'])"
|
||||
class="flex-1" type="warning" @click="handleEdit"
|
||||
>
|
||||
编辑
|
||||
</wd-button>
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['system:notice:delete'])"
|
||||
class="flex-1" type="error" :loading="deleting" @click="handleDelete"
|
||||
>
|
||||
删除
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Notice } from '@/api/system/notice'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { deleteNotice, getNotice } from '@/api/system/notice'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const toast = useToast()
|
||||
const formData = ref<Notice>()
|
||||
const deleting = ref(false)
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-system/notice/index')
|
||||
}
|
||||
|
||||
/** 加载通知公告详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
toast.loading('加载中...')
|
||||
formData.value = await getNotice(props.id)
|
||||
} finally {
|
||||
toast.close()
|
||||
}
|
||||
}
|
||||
|
||||
/** 编辑通知公告 */
|
||||
function handleEdit() {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/notice/form/index?id=${props.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除通知公告 */
|
||||
function handleDelete() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要删除该通知公告吗?',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) {
|
||||
return
|
||||
}
|
||||
deleting.value = true
|
||||
try {
|
||||
await deleteNotice(props.id)
|
||||
toast.success('删除成功')
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
153
src/pages-system/notice/form/index.vue
Normal file
153
src/pages-system/notice/form/index.vue
Normal file
@@ -0,0 +1,153 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
:title="getTitle"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 表单区域 -->
|
||||
<view>
|
||||
<wd-form ref="formRef" :model="formData" :rules="formRules">
|
||||
<wd-cell-group border>
|
||||
<wd-input
|
||||
v-model="formData.title"
|
||||
label="公告标题"
|
||||
label-width="180rpx"
|
||||
prop="title"
|
||||
clearable
|
||||
placeholder="请输入公告标题"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="formData.content"
|
||||
label="公告内容"
|
||||
label-width="180rpx"
|
||||
prop="content"
|
||||
clearable
|
||||
placeholder="请输入公告内容"
|
||||
/>
|
||||
<wd-cell title="公告类型" title-width="180rpx" prop="type" center>
|
||||
<wd-radio-group v-model="formData.type" shape="button">
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_NOTICE_TYPE)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-cell>
|
||||
<wd-cell title="公告状态" title-width="180rpx" prop="status" center>
|
||||
<wd-radio-group v-model="formData.status" shape="button">
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-cell>
|
||||
</wd-cell-group>
|
||||
</wd-form>
|
||||
</view>
|
||||
|
||||
<!-- 底部保存按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<wd-button
|
||||
type="primary"
|
||||
block
|
||||
:loading="formLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
保存
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance } from 'wot-design-uni/components/wd-form/types'
|
||||
import type { Notice } from '@/api/system/notice'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { createNotice, getNotice, updateNotice } from '@/api/system/notice'
|
||||
import { getIntDictOptions } from '@/hooks/useDict'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { CommonStatusEnum, DICT_TYPE } from '@/utils/constants'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const getTitle = computed(() => props.id ? '编辑通知公告' : '新增通知公告')
|
||||
const formLoading = ref(false)
|
||||
const formData = ref<Notice>({
|
||||
id: undefined,
|
||||
title: '',
|
||||
content: '',
|
||||
type: undefined,
|
||||
status: CommonStatusEnum.ENABLE,
|
||||
})
|
||||
const formRules = {
|
||||
title: [{ required: true, message: '公告标题不能为空' }],
|
||||
content: [{ required: true, message: '公告内容不能为空' }],
|
||||
type: [{ required: true, message: '公告类型不能为空' }],
|
||||
status: [{ required: true, message: '公告状态不能为空' }],
|
||||
}
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-system/notice/index')
|
||||
}
|
||||
|
||||
/** 加载通知公告详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
formData.value = await getNotice(props.id)
|
||||
}
|
||||
|
||||
/** 提交表单 */
|
||||
async function handleSubmit() {
|
||||
const { valid } = await formRef.value.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
|
||||
formLoading.value = true
|
||||
try {
|
||||
if (props.id) {
|
||||
await updateNotice(formData.value)
|
||||
toast.success('修改成功')
|
||||
} else {
|
||||
await createNotice(formData.value)
|
||||
toast.success('新增成功')
|
||||
}
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
163
src/pages-system/notice/index.vue
Normal file
163
src/pages-system/notice/index.vue
Normal file
@@ -0,0 +1,163 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="通知公告管理"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 搜索组件 -->
|
||||
<SearchForm @search="handleQuery" @reset="handleReset" />
|
||||
|
||||
<!-- 通知公告列表 -->
|
||||
<view class="p-24rpx">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
|
||||
@click="handleDetail(item)"
|
||||
>
|
||||
<view class="p-24rpx">
|
||||
<view class="mb-16rpx flex items-center justify-between">
|
||||
<view class="text-32rpx text-[#333] font-semibold">
|
||||
{{ item.title }}
|
||||
</view>
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="item.status" />
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">公告内容:</text>
|
||||
<text class="min-w-0 flex-1 truncate">{{ item.content }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">公告类型:</text>
|
||||
<dict-tag :type="DICT_TYPE.SYSTEM_NOTICE_TYPE" :value="item.type" />
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">创建时间:</text>
|
||||
<text class="line-clamp-1">{{ formatDateTime(item.createTime) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="loadMoreState !== 'loading' && list.length === 0" class="py-100rpx text-center">
|
||||
<wd-status-tip image="content" tip="暂无通知公告数据" />
|
||||
</view>
|
||||
<wd-loadmore
|
||||
v-if="list.length > 0"
|
||||
:state="loadMoreState"
|
||||
@reload="loadMore"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 新增按钮 -->
|
||||
<wd-fab
|
||||
v-if="hasAccessByCodes(['system:notice:create'])"
|
||||
position="right-bottom"
|
||||
type="primary"
|
||||
:expandable="false"
|
||||
@click="handleAdd"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Notice } from '@/api/system/notice'
|
||||
import type { LoadMoreState } from '@/http/types'
|
||||
import { onReachBottom } from '@dcloudio/uni-app'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { getNoticePage } from '@/api/system/notice'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import SearchForm from './components/search-form.vue'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const total = ref(0)
|
||||
const list = ref<Notice[]>([])
|
||||
const loadMoreState = ref<LoadMoreState>('loading')
|
||||
const queryParams = ref({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus()
|
||||
}
|
||||
|
||||
/** 查询通知公告列表 */
|
||||
async function getList() {
|
||||
loadMoreState.value = 'loading'
|
||||
try {
|
||||
const data = await getNoticePage(queryParams.value)
|
||||
list.value = [...list.value, ...data.list]
|
||||
total.value = data.total
|
||||
loadMoreState.value = list.value.length >= total.value ? 'finished' : 'loading'
|
||||
} catch {
|
||||
queryParams.value.pageNo = queryParams.value.pageNo > 1 ? queryParams.value.pageNo - 1 : 1
|
||||
loadMoreState.value = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery(data?: Record<string, any>) {
|
||||
queryParams.value = {
|
||||
...data,
|
||||
pageNo: 1,
|
||||
pageSize: queryParams.value.pageSize,
|
||||
}
|
||||
list.value = []
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function handleReset() {
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 加载更多 */
|
||||
function loadMore() {
|
||||
if (loadMoreState.value === 'finished') {
|
||||
return
|
||||
}
|
||||
queryParams.value.pageNo++
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 新增通知公告 */
|
||||
function handleAdd() {
|
||||
uni.navigateTo({
|
||||
url: '/pages-system/notice/form/index',
|
||||
})
|
||||
}
|
||||
|
||||
/** 查看详情 */
|
||||
function handleDetail(item: Notice) {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/notice/detail/index?id=${item.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 触底加载更多 */
|
||||
onReachBottom(() => {
|
||||
loadMore()
|
||||
})
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
133
src/pages-system/notify/components/message-list.vue
Normal file
133
src/pages-system/notify/components/message-list.vue
Normal file
@@ -0,0 +1,133 @@
|
||||
<template>
|
||||
<view>
|
||||
<!-- 搜索组件 -->
|
||||
<MessageSearchForm @search="handleQuery" @reset="handleReset" />
|
||||
|
||||
<!-- 消息列表 -->
|
||||
<view class="p-24rpx">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
|
||||
@click="handleDetail(item)"
|
||||
>
|
||||
<view class="p-24rpx">
|
||||
<view class="mb-16rpx flex items-center justify-between">
|
||||
<view class="text-32rpx text-[#333] font-semibold">
|
||||
{{ item.templateNickname }}
|
||||
</view>
|
||||
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="item.readStatus" />
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">用户类型:</text>
|
||||
<dict-tag :type="DICT_TYPE.USER_TYPE" :value="item.userType" />
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">用户编号:</text>
|
||||
<text class="min-w-0 flex-1 truncate">{{ item.userId }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">模板编码:</text>
|
||||
<text class="min-w-0 flex-1 truncate">{{ item.templateCode }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">模版类型:</text>
|
||||
<dict-tag :type="DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE" :value="item.templateType" />
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">模版内容:</text>
|
||||
<text class="min-w-0 flex-1 truncate">{{ item.templateContent }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">创建时间:</text>
|
||||
<text>{{ formatDateTime(item.createTime) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="loadMoreState !== 'loading' && list.length === 0" class="py-100rpx text-center">
|
||||
<wd-status-tip image="content" tip="暂无站内信消息数据" />
|
||||
</view>
|
||||
<wd-loadmore
|
||||
v-if="list.length > 0"
|
||||
:state="loadMoreState"
|
||||
@reload="loadMore"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { NotifyMessage } from '@/api/system/notify/message'
|
||||
import type { LoadMoreState } from '@/http/types'
|
||||
import { ref } from 'vue'
|
||||
import { getNotifyMessagePage } from '@/api/system/notify/message'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import MessageSearchForm from './message-search-form.vue'
|
||||
|
||||
const total = ref(0)
|
||||
const list = ref<NotifyMessage[]>([])
|
||||
const loadMoreState = ref<LoadMoreState>('loading')
|
||||
const queryParams = ref({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
|
||||
/** 查询列表 */
|
||||
async function getList() {
|
||||
loadMoreState.value = 'loading'
|
||||
try {
|
||||
const data = await getNotifyMessagePage(queryParams.value)
|
||||
list.value = [...list.value, ...data.list]
|
||||
total.value = data.total
|
||||
loadMoreState.value = list.value.length >= total.value ? 'finished' : 'loading'
|
||||
} catch {
|
||||
queryParams.value.pageNo = queryParams.value.pageNo > 1 ? queryParams.value.pageNo - 1 : 1
|
||||
loadMoreState.value = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery(data?: Record<string, any>) {
|
||||
queryParams.value = {
|
||||
...data,
|
||||
pageNo: 1,
|
||||
pageSize: queryParams.value.pageSize,
|
||||
}
|
||||
list.value = []
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function handleReset() {
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 加载更多 */
|
||||
function loadMore() {
|
||||
if (loadMoreState.value === 'finished') {
|
||||
return
|
||||
}
|
||||
queryParams.value.pageNo++
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 查看详情 */
|
||||
function handleDetail(item: NotifyMessage) {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/notify/message/detail/index?id=${item.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 触底加载更多 */
|
||||
onReachBottom(() => {
|
||||
loadMore()
|
||||
})
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
192
src/pages-system/notify/components/message-search-form.vue
Normal file
192
src/pages-system/notify/components/message-search-form.vue
Normal file
@@ -0,0 +1,192 @@
|
||||
<template>
|
||||
<!-- 搜索框入口 -->
|
||||
<view @click="visible = true">
|
||||
<wd-search :placeholder="placeholder" hide-cancel disabled />
|
||||
</view>
|
||||
|
||||
<!-- 搜索弹窗 -->
|
||||
<wd-popup v-model="visible" position="top" @close="visible = false">
|
||||
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
用户编号
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.userId"
|
||||
placeholder="请输入用户编号"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
用户类型
|
||||
</view>
|
||||
<wd-radio-group v-model="formData.userType" shape="button">
|
||||
<wd-radio :value="-1">
|
||||
全部
|
||||
</wd-radio>
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.USER_TYPE)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
模板编码
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.templateCode"
|
||||
placeholder="请输入模板编码"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
模版类型
|
||||
</view>
|
||||
<wd-radio-group v-model="formData.templateType" shape="button">
|
||||
<wd-radio :value="-1">
|
||||
全部
|
||||
</wd-radio>
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
创建时间
|
||||
</view>
|
||||
<view class="yd-search-form-date-range-container">
|
||||
<view class="flex-1" @click="visibleCreateTime[0] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.createTime?.[0]) || '开始日期' }}
|
||||
</view>
|
||||
</view>
|
||||
-
|
||||
<view class="flex-1" @click="visibleCreateTime[1] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.createTime?.[1]) || '结束日期' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleCreateTime[0]" v-model="tempCreateTime[0]" type="date" />
|
||||
<view v-if="visibleCreateTime[0]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleCreateTime[0] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleCreateTime0Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleCreateTime[1]" v-model="tempCreateTime[1]" type="date" />
|
||||
<view v-if="visibleCreateTime[1]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleCreateTime[1] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleCreateTime1Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="yd-search-form-actions">
|
||||
<wd-button class="flex-1" plain @click="handleReset">
|
||||
重置
|
||||
</wd-button>
|
||||
<wd-button class="flex-1" type="primary" @click="handleSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { getDictLabel, getIntDictOptions } from '@/hooks/useDict'
|
||||
import { getNavbarHeight } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDate, formatDateRange } from '@/utils/date'
|
||||
|
||||
const emit = defineEmits<{
|
||||
search: [data: Record<string, any>]
|
||||
reset: []
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const formData = reactive({
|
||||
userId: undefined as string | undefined,
|
||||
userType: -1,
|
||||
templateCode: undefined as string | undefined,
|
||||
templateType: -1,
|
||||
createTime: [undefined, undefined] as [number | undefined, number | undefined],
|
||||
})
|
||||
|
||||
/** 搜索条件 placeholder 拼接 */
|
||||
const placeholder = computed(() => {
|
||||
const conditions: string[] = []
|
||||
if (formData.userId) {
|
||||
conditions.push(`用户:${formData.userId}`)
|
||||
}
|
||||
if (formData.userType !== -1) {
|
||||
conditions.push(`类型:${getDictLabel(DICT_TYPE.USER_TYPE, formData.userType)}`)
|
||||
}
|
||||
if (formData.templateCode) {
|
||||
conditions.push(`编码:${formData.templateCode}`)
|
||||
}
|
||||
if (formData.templateType !== -1) {
|
||||
conditions.push(`模版:${getDictLabel(DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE, formData.templateType)}`)
|
||||
}
|
||||
if (formData.createTime?.[0] && formData.createTime?.[1]) {
|
||||
conditions.push(`时间:${formatDate(formData.createTime[0])}~${formatDate(formData.createTime[1])}`)
|
||||
}
|
||||
return conditions.length > 0 ? conditions.join(' | ') : '搜索站内信消息'
|
||||
})
|
||||
|
||||
// 时间范围选择器状态
|
||||
const visibleCreateTime = ref<[boolean, boolean]>([false, false])
|
||||
const tempCreateTime = ref<[number, number]>([Date.now(), Date.now()])
|
||||
|
||||
/** 创建时间[0]确认 */
|
||||
function handleCreateTime0Confirm() {
|
||||
formData.createTime = [tempCreateTime.value[0], formData.createTime?.[1]]
|
||||
visibleCreateTime.value[0] = false
|
||||
}
|
||||
|
||||
/** 创建时间[1]确认 */
|
||||
function handleCreateTime1Confirm() {
|
||||
formData.createTime = [formData.createTime?.[0], tempCreateTime.value[1]]
|
||||
visibleCreateTime.value[1] = false
|
||||
}
|
||||
|
||||
/** 搜索 */
|
||||
function handleSearch() {
|
||||
visible.value = false
|
||||
emit('search', {
|
||||
userId: formData.userId || undefined,
|
||||
userType: formData.userType === -1 ? undefined : formData.userType,
|
||||
templateCode: formData.templateCode || undefined,
|
||||
templateType: formData.templateType === -1 ? undefined : formData.templateType,
|
||||
createTime: formatDateRange(formData.createTime),
|
||||
})
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
function handleReset() {
|
||||
formData.userId = undefined
|
||||
formData.userType = -1
|
||||
formData.templateCode = undefined
|
||||
formData.templateType = -1
|
||||
formData.createTime = [undefined, undefined]
|
||||
visible.value = false
|
||||
emit('reset')
|
||||
}
|
||||
</script>
|
||||
147
src/pages-system/notify/components/template-list.vue
Normal file
147
src/pages-system/notify/components/template-list.vue
Normal file
@@ -0,0 +1,147 @@
|
||||
<template>
|
||||
<view>
|
||||
<!-- 搜索组件 -->
|
||||
<TemplateSearchForm @search="handleQuery" @reset="handleReset" />
|
||||
|
||||
<!-- 模板列表 -->
|
||||
<view class="p-24rpx">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
|
||||
@click="handleDetail(item)"
|
||||
>
|
||||
<view class="p-24rpx">
|
||||
<view class="mb-16rpx flex items-center justify-between">
|
||||
<view class="text-32rpx text-[#333] font-semibold">
|
||||
{{ item.name }}
|
||||
</view>
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="item.status" />
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">模板编码:</text>
|
||||
<text class="min-w-0 flex-1 truncate">{{ item.code }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">发送人名称:</text>
|
||||
<text class="min-w-0 flex-1 truncate">{{ item.nickname }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">模板类型:</text>
|
||||
<dict-tag :type="DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE" :value="item.type" />
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">模板内容:</text>
|
||||
<text class="min-w-0 flex-1 truncate">{{ item.content }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">创建时间:</text>
|
||||
<text>{{ formatDateTime(item.createTime) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="loadMoreState !== 'loading' && list.length === 0" class="py-100rpx text-center">
|
||||
<wd-status-tip image="content" tip="暂无站内信模板数据" />
|
||||
</view>
|
||||
<wd-loadmore
|
||||
v-if="list.length > 0"
|
||||
:state="loadMoreState"
|
||||
@reload="loadMore"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 新增按钮 -->
|
||||
<wd-fab
|
||||
v-if="hasAccessByCodes(['system:notify-template:create'])"
|
||||
position="right-bottom"
|
||||
type="primary"
|
||||
:expandable="false"
|
||||
@click="handleAdd"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { NotifyTemplate } from '@/api/system/notify/template'
|
||||
import type { LoadMoreState } from '@/http/types'
|
||||
import { ref } from 'vue'
|
||||
import { getNotifyTemplatePage } from '@/api/system/notify/template'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import TemplateSearchForm from './template-search-form.vue'
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const total = ref(0)
|
||||
const list = ref<NotifyTemplate[]>([])
|
||||
const loadMoreState = ref<LoadMoreState>('loading')
|
||||
const queryParams = ref({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
|
||||
/** 查询列表 */
|
||||
async function getList() {
|
||||
loadMoreState.value = 'loading'
|
||||
try {
|
||||
const data = await getNotifyTemplatePage(queryParams.value)
|
||||
list.value = [...list.value, ...data.list]
|
||||
total.value = data.total
|
||||
loadMoreState.value = list.value.length >= total.value ? 'finished' : 'loading'
|
||||
} catch {
|
||||
queryParams.value.pageNo = queryParams.value.pageNo > 1 ? queryParams.value.pageNo - 1 : 1
|
||||
loadMoreState.value = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery(data?: Record<string, any>) {
|
||||
queryParams.value = {
|
||||
...data,
|
||||
pageNo: 1,
|
||||
pageSize: queryParams.value.pageSize,
|
||||
}
|
||||
list.value = []
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function handleReset() {
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 加载更多 */
|
||||
function loadMore() {
|
||||
if (loadMoreState.value === 'finished') {
|
||||
return
|
||||
}
|
||||
queryParams.value.pageNo++
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 新增 */
|
||||
function handleAdd() {
|
||||
uni.navigateTo({
|
||||
url: '/pages-system/notify/template/form/index',
|
||||
})
|
||||
}
|
||||
|
||||
/** 查看详情 */
|
||||
function handleDetail(item: NotifyTemplate) {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/notify/template/detail/index?id=${item.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 触底加载更多 */
|
||||
onReachBottom(() => {
|
||||
loadMore()
|
||||
})
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
193
src/pages-system/notify/components/template-search-form.vue
Normal file
193
src/pages-system/notify/components/template-search-form.vue
Normal file
@@ -0,0 +1,193 @@
|
||||
<template>
|
||||
<!-- 搜索框入口 -->
|
||||
<view @click="visible = true">
|
||||
<wd-search :placeholder="placeholder" hide-cancel disabled />
|
||||
</view>
|
||||
|
||||
<!-- 搜索弹窗 -->
|
||||
<wd-popup v-model="visible" position="top" @close="visible = false">
|
||||
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
模板名称
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.name"
|
||||
placeholder="请输入模板名称"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
模板编码
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.code"
|
||||
placeholder="请输入模板编码"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
状态
|
||||
</view>
|
||||
<wd-radio-group v-model="formData.status" shape="button">
|
||||
<wd-radio :value="-1">
|
||||
全部
|
||||
</wd-radio>
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
模板类型
|
||||
</view>
|
||||
<wd-radio-group v-model="formData.type" shape="button">
|
||||
<wd-radio :value="-1">
|
||||
全部
|
||||
</wd-radio>
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
<!-- 创建时间 -->
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
创建时间
|
||||
</view>
|
||||
<view class="yd-search-form-date-range-container">
|
||||
<view class="flex-1" @click="visibleCreateTime[0] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.createTime?.[0]) || '开始日期' }}
|
||||
</view>
|
||||
</view>
|
||||
-
|
||||
<view class="flex-1" @click="visibleCreateTime[1] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.createTime?.[1]) || '结束日期' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleCreateTime[0]" v-model="tempCreateTime[0]" type="date" />
|
||||
<view v-if="visibleCreateTime[0]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleCreateTime[0] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleCreateTime0Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleCreateTime[1]" v-model="tempCreateTime[1]" type="date" />
|
||||
<view v-if="visibleCreateTime[1]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleCreateTime[1] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleCreateTime1Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="yd-search-form-actions">
|
||||
<wd-button class="flex-1" plain @click="handleReset">
|
||||
重置
|
||||
</wd-button>
|
||||
<wd-button class="flex-1" type="primary" @click="handleSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { getDictLabel, getIntDictOptions } from '@/hooks/useDict'
|
||||
import { getNavbarHeight } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDate, formatDateRange } from '@/utils/date'
|
||||
|
||||
const emit = defineEmits<{
|
||||
search: [data: Record<string, any>]
|
||||
reset: []
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const formData = reactive({
|
||||
name: undefined as string | undefined,
|
||||
code: undefined as string | undefined,
|
||||
status: -1,
|
||||
type: -1,
|
||||
createTime: [undefined, undefined] as [number | undefined, number | undefined],
|
||||
})
|
||||
|
||||
/** 搜索条件 placeholder 拼接 */
|
||||
const placeholder = computed(() => {
|
||||
const conditions: string[] = []
|
||||
if (formData.name) {
|
||||
conditions.push(`名称:${formData.name}`)
|
||||
}
|
||||
if (formData.code) {
|
||||
conditions.push(`编码:${formData.code}`)
|
||||
}
|
||||
if (formData.status !== -1) {
|
||||
conditions.push(`状态:${getDictLabel(DICT_TYPE.COMMON_STATUS, formData.status)}`)
|
||||
}
|
||||
if (formData.type !== -1) {
|
||||
conditions.push(`类型:${getDictLabel(DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE, formData.type)}`)
|
||||
}
|
||||
if (formData.createTime?.[0] && formData.createTime?.[1]) {
|
||||
conditions.push(`时间:${formatDate(formData.createTime[0])}~${formatDate(formData.createTime[1])}`)
|
||||
}
|
||||
return conditions.length > 0 ? conditions.join(' | ') : '搜索站内信模板'
|
||||
})
|
||||
|
||||
// 时间范围选择器状态
|
||||
const visibleCreateTime = ref<[boolean, boolean]>([false, false])
|
||||
const tempCreateTime = ref<[number, number]>([Date.now(), Date.now()])
|
||||
|
||||
/** 创建时间[0]确认 */
|
||||
function handleCreateTime0Confirm() {
|
||||
formData.createTime = [tempCreateTime.value[0], formData.createTime?.[1]]
|
||||
visibleCreateTime.value[0] = false
|
||||
}
|
||||
|
||||
/** 创建时间[1]确认 */
|
||||
function handleCreateTime1Confirm() {
|
||||
formData.createTime = [formData.createTime?.[0], tempCreateTime.value[1]]
|
||||
visibleCreateTime.value[1] = false
|
||||
}
|
||||
|
||||
/** 搜索 */
|
||||
function handleSearch() {
|
||||
visible.value = false
|
||||
emit('search', {
|
||||
name: formData.name || undefined,
|
||||
code: formData.code || undefined,
|
||||
status: formData.status === -1 ? undefined : formData.status,
|
||||
type: formData.type === -1 ? undefined : formData.type,
|
||||
createTime: formatDateRange(formData.createTime),
|
||||
})
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
function handleReset() {
|
||||
formData.name = undefined
|
||||
formData.code = undefined
|
||||
formData.status = -1
|
||||
formData.type = -1
|
||||
formData.createTime = [undefined, undefined]
|
||||
visible.value = false
|
||||
emit('reset')
|
||||
}
|
||||
</script>
|
||||
52
src/pages-system/notify/index.vue
Normal file
52
src/pages-system/notify/index.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="站内信管理"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- Tab 切换 -->
|
||||
<view class="bg-white">
|
||||
<wd-tabs v-model="tabIndex" shrink @change="handleTabChange">
|
||||
<wd-tab title="站内信模板" />
|
||||
<wd-tab title="站内信消息" />
|
||||
</wd-tabs>
|
||||
</view>
|
||||
<!-- 列表内容 -->
|
||||
<TemplateList v-show="tabType === 'template'" />
|
||||
<MessageList v-show="tabType === 'message'" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import MessageList from './components/message-list.vue'
|
||||
import TemplateList from './components/template-list.vue'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const tabTypes: string[] = ['template', 'message']
|
||||
const tabIndex = ref(0)
|
||||
const tabType = computed<string>(() => tabTypes[tabIndex.value])
|
||||
|
||||
/** Tab 切换 */
|
||||
function handleTabChange({ index }: { index: number }) {
|
||||
tabIndex.value = index
|
||||
}
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
96
src/pages-system/notify/message/detail/index.vue
Normal file
96
src/pages-system/notify/message/detail/index.vue
Normal file
@@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="站内信详情"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 详情内容 -->
|
||||
<view>
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="编号" :value="formData?.id" />
|
||||
<wd-cell title="用户类型">
|
||||
<dict-tag :type="DICT_TYPE.USER_TYPE" :value="formData?.userType" />
|
||||
</wd-cell>
|
||||
<wd-cell title="用户编号" :value="formData?.userId" />
|
||||
<wd-cell title="模版编号" :value="formData?.templateId" />
|
||||
<wd-cell title="模板编码" :value="formData?.templateCode" />
|
||||
<wd-cell title="发送人名称" :value="formData?.templateNickname" />
|
||||
<wd-cell title="模版内容" :value="formData?.templateContent" />
|
||||
<wd-cell title="模版参数" :value="formatTemplateParams(formData?.templateParams)" />
|
||||
<wd-cell title="模版类型">
|
||||
<dict-tag :type="DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE" :value="formData?.templateType" />
|
||||
</wd-cell>
|
||||
<wd-cell title="是否已读">
|
||||
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="formData?.readStatus" />
|
||||
</wd-cell>
|
||||
<wd-cell title="阅读时间" :value="formatDateTime(formData?.readTime) || '-'" />
|
||||
<wd-cell title="创建时间" :value="formatDateTime(formData?.createTime)" />
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { NotifyMessage } from '@/api/system/notify/message'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { getNotifyMessage } from '@/api/system/notify/message'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const formData = ref<NotifyMessage>()
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-system/notify/index')
|
||||
}
|
||||
|
||||
/** 格式化模版参数 */
|
||||
function formatTemplateParams(params: any) {
|
||||
if (!params) {
|
||||
return '-'
|
||||
}
|
||||
try {
|
||||
return typeof params === 'string' ? params : JSON.stringify(params)
|
||||
} catch {
|
||||
return '-'
|
||||
}
|
||||
}
|
||||
|
||||
/** 加载详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
toast.loading('加载中...')
|
||||
formData.value = await getNotifyMessage(Number(props.id))
|
||||
} finally {
|
||||
toast.close()
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
190
src/pages-system/notify/template/detail/components/send-form.vue
Normal file
190
src/pages-system/notify/template/detail/components/send-form.vue
Normal file
@@ -0,0 +1,190 @@
|
||||
<template>
|
||||
<!-- TODO @芋艿:【优化】底部操作的样式 -->
|
||||
<wd-popup v-model="visible" position="bottom" closable custom-style="border-radius: 16rpx 16rpx 0 0;">
|
||||
<view class="p-24rpx">
|
||||
<view class="mb-24rpx text-32rpx text-[#333] font-semibold">
|
||||
发送测试站内信
|
||||
</view>
|
||||
<wd-form ref="sendFormRef" :model="sendFormData" :rules="sendFormRules">
|
||||
<wd-cell-group border>
|
||||
<wd-textarea
|
||||
v-model="sendFormData.content"
|
||||
label="模板内容"
|
||||
label-width="180rpx"
|
||||
disabled
|
||||
:rows="3"
|
||||
/>
|
||||
<!-- 用户类型 -->
|
||||
<wd-cell title="用户类型" title-width="180rpx" prop="userType" center>
|
||||
<wd-radio-group v-model="sendFormData.userType" shape="button">
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.USER_TYPE)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-cell>
|
||||
<!-- 会员用户:输入用户编号 -->
|
||||
<wd-input
|
||||
v-if="sendFormData.userType === UserTypeEnum.MEMBER"
|
||||
v-model="sendFormData.userId"
|
||||
label="接收人 ID"
|
||||
label-width="180rpx"
|
||||
prop="userId"
|
||||
clearable
|
||||
placeholder="请输入用户编号"
|
||||
/>
|
||||
<!-- 管理员用户:选择用户 -->
|
||||
<wd-cell
|
||||
v-if="sendFormData.userType === UserTypeEnum.ADMIN"
|
||||
title="接收人"
|
||||
title-width="180rpx"
|
||||
prop="userId"
|
||||
center
|
||||
>
|
||||
<wd-picker
|
||||
v-model="sendFormData.userId"
|
||||
:columns="userOptions"
|
||||
placeholder="请选择接收人"
|
||||
/>
|
||||
</wd-cell>
|
||||
<!-- 动态参数 -->
|
||||
<template v-for="param in template?.params" :key="param">
|
||||
<wd-input
|
||||
v-model="sendFormData.templateParams[param]"
|
||||
:label="`参数 ${param}`"
|
||||
label-width="180rpx"
|
||||
:prop="`templateParams.${param}`"
|
||||
clearable
|
||||
:placeholder="`请输入参数 ${param}`"
|
||||
/>
|
||||
</template>
|
||||
</wd-cell-group>
|
||||
</wd-form>
|
||||
<view class="mt-24rpx">
|
||||
<wd-button type="primary" block :loading="sendLoading" @click="handleSendSubmit">
|
||||
发送
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { NotifyTemplate } from '@/api/system/notify/template'
|
||||
import type { User } from '@/api/system/user'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { sendNotify } from '@/api/system/notify/template'
|
||||
import { getSimpleUserList } from '@/api/system/user'
|
||||
import { getIntDictOptions } from '@/hooks/useDict'
|
||||
import { DICT_TYPE, UserTypeEnum } from '@/utils/constants'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
template?: NotifyTemplate
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
'success': []
|
||||
}>()
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
const visible = computed({
|
||||
get() {
|
||||
return props.modelValue
|
||||
},
|
||||
set(value: boolean) {
|
||||
emit('update:modelValue', value)
|
||||
},
|
||||
})
|
||||
|
||||
const sendLoading = ref(false)
|
||||
const sendFormRef = ref<any>()
|
||||
const sendFormData = ref({
|
||||
content: '',
|
||||
userType: UserTypeEnum.MEMBER,
|
||||
userId: undefined as number | string | undefined,
|
||||
templateParams: {} as Record<string, string>,
|
||||
})
|
||||
|
||||
/** 用户列表 */
|
||||
const userList = ref<User[]>([])
|
||||
const userOptions = computed(() => {
|
||||
return userList.value.map(item => ({
|
||||
value: item.id,
|
||||
label: item.nickname,
|
||||
}))
|
||||
})
|
||||
|
||||
/** 发送表单校验规则 */
|
||||
const sendFormRules = computed(() => {
|
||||
const rules: Record<string, any> = {
|
||||
userType: [{ required: true, message: '用户类型不能为空' }],
|
||||
userId: [{ required: true, message: '接收人不能为空' }],
|
||||
}
|
||||
if (props.template?.params) {
|
||||
props.template.params.forEach((param) => {
|
||||
rules[`templateParams.${param}`] = [{ required: true, message: `参数 ${param} 不能为空` }]
|
||||
})
|
||||
}
|
||||
return rules
|
||||
})
|
||||
|
||||
/** 加载用户列表 */
|
||||
async function loadUserList() {
|
||||
userList.value = await getSimpleUserList()
|
||||
}
|
||||
|
||||
/** 初始化发送表单 */
|
||||
function initSendForm() {
|
||||
sendFormData.value = {
|
||||
content: props.template?.content || '',
|
||||
userType: UserTypeEnum.MEMBER,
|
||||
userId: undefined,
|
||||
templateParams: {},
|
||||
}
|
||||
if (props.template?.params) {
|
||||
props.template.params.forEach((param) => {
|
||||
sendFormData.value.templateParams[param] = ''
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
if (val) {
|
||||
initSendForm()
|
||||
loadUserList()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
/** 提交发送 */
|
||||
async function handleSendSubmit() {
|
||||
const { valid } = await sendFormRef.value.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
|
||||
sendLoading.value = true
|
||||
try {
|
||||
await sendNotify({
|
||||
userId: Number(sendFormData.value.userId),
|
||||
userType: sendFormData.value.userType,
|
||||
templateCode: props.template?.code || '',
|
||||
templateParams: sendFormData.value.templateParams,
|
||||
})
|
||||
toast.success('站内信发送成功')
|
||||
emit('success')
|
||||
visible.value = false
|
||||
} finally {
|
||||
sendLoading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
151
src/pages-system/notify/template/detail/index.vue
Normal file
151
src/pages-system/notify/template/detail/index.vue
Normal file
@@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="站内信模板详情"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 详情内容 -->
|
||||
<view>
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="模板编号" :value="formData?.id" />
|
||||
<wd-cell title="模板名称" :value="formData?.name" />
|
||||
<wd-cell title="模板编码" :value="formData?.code" />
|
||||
<wd-cell title="发送人名称" :value="formData?.nickname" />
|
||||
<wd-cell title="模板类型">
|
||||
<dict-tag :type="DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE" :value="formData?.type" />
|
||||
</wd-cell>
|
||||
<wd-cell title="状态">
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="formData?.status" />
|
||||
</wd-cell>
|
||||
<wd-cell title="模板内容" :value="formData?.content" />
|
||||
<wd-cell title="备注" :value="formData?.remark ?? '-'" />
|
||||
<wd-cell title="创建时间" :value="formatDateTime(formData?.createTime)" />
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
|
||||
<!-- 发送测试站内信弹窗 -->
|
||||
<SendForm v-model="sendVisible" :template="formData" />
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<view class="yd-detail-footer-actions">
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['system:notify-template:send-notify'])"
|
||||
class="flex-1" type="primary" @click="handleSendTest"
|
||||
>
|
||||
测试
|
||||
</wd-button>
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['system:notify-template:update'])"
|
||||
class="flex-1" type="warning" @click="handleEdit"
|
||||
>
|
||||
编辑
|
||||
</wd-button>
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['system:notify-template:delete'])"
|
||||
class="flex-1" type="error" :loading="deleting" @click="handleDelete"
|
||||
>
|
||||
删除
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { NotifyTemplate } from '@/api/system/notify/template'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { deleteNotifyTemplate, getNotifyTemplate } from '@/api/system/notify/template'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import SendForm from './components/send-form.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const toast = useToast()
|
||||
const formData = ref<NotifyTemplate>()
|
||||
const deleting = ref(false)
|
||||
|
||||
// 发送测试站内信相关
|
||||
const sendVisible = ref(false)
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-system/notify/index')
|
||||
}
|
||||
|
||||
/** 加载详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
toast.loading('加载中...')
|
||||
formData.value = await getNotifyTemplate(props.id)
|
||||
} finally {
|
||||
toast.close()
|
||||
}
|
||||
}
|
||||
|
||||
/** 编辑 */
|
||||
function handleEdit() {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/notify/template/form/index?id=${props.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除 */
|
||||
function handleDelete() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要删除该站内信模板吗?',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) {
|
||||
return
|
||||
}
|
||||
deleting.value = true
|
||||
try {
|
||||
await deleteNotifyTemplate(props.id)
|
||||
toast.success('删除成功')
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 打开发送测试站内信弹窗 */
|
||||
function handleSendTest() {
|
||||
sendVisible.value = true
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
187
src/pages-system/notify/template/form/index.vue
Normal file
187
src/pages-system/notify/template/form/index.vue
Normal file
@@ -0,0 +1,187 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
:title="getTitle"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 表单区域 -->
|
||||
<view>
|
||||
<wd-form ref="formRef" :model="formData" :rules="formRules">
|
||||
<wd-cell-group border>
|
||||
<wd-input
|
||||
v-model="formData.name"
|
||||
label="模板名称"
|
||||
label-width="200rpx"
|
||||
prop="name"
|
||||
clearable
|
||||
placeholder="请输入模板名称"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="formData.code"
|
||||
label="模板编码"
|
||||
label-width="200rpx"
|
||||
prop="code"
|
||||
clearable
|
||||
placeholder="请输入模板编码"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="formData.nickname"
|
||||
label="发送人名称"
|
||||
label-width="200rpx"
|
||||
prop="nickname"
|
||||
clearable
|
||||
placeholder="请输入发送人名称"
|
||||
/>
|
||||
<wd-cell title="模板类型" title-width="200rpx" prop="type" center>
|
||||
<wd-picker
|
||||
v-model="formData.type"
|
||||
:columns="templateTypeOptions"
|
||||
placeholder="请选择模板类型"
|
||||
/>
|
||||
</wd-cell>
|
||||
<wd-cell title="状态" title-width="200rpx" prop="status" center>
|
||||
<wd-radio-group v-model="formData.status" shape="button">
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-cell>
|
||||
<wd-textarea
|
||||
v-model="formData.content"
|
||||
label="模板内容"
|
||||
label-width="200rpx"
|
||||
prop="content"
|
||||
clearable
|
||||
placeholder="请输入模板内容"
|
||||
:rows="4"
|
||||
/>
|
||||
<wd-textarea
|
||||
v-model="formData.remark"
|
||||
label="备注"
|
||||
label-width="200rpx"
|
||||
prop="remark"
|
||||
clearable
|
||||
placeholder="请输入备注"
|
||||
/>
|
||||
</wd-cell-group>
|
||||
</wd-form>
|
||||
</view>
|
||||
|
||||
<!-- 底部保存按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<wd-button
|
||||
type="primary"
|
||||
block
|
||||
:loading="formLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
保存
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance } from 'wot-design-uni/components/wd-form/types'
|
||||
import type { NotifyTemplate } from '@/api/system/notify/template'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { createNotifyTemplate, getNotifyTemplate, updateNotifyTemplate } from '@/api/system/notify/template'
|
||||
import { getIntDictOptions } from '@/hooks/useDict'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { CommonStatusEnum, DICT_TYPE } from '@/utils/constants'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const getTitle = computed(() => props.id ? '编辑站内信模板' : '新增站内信模板')
|
||||
const formLoading = ref(false)
|
||||
const formData = ref<NotifyTemplate>({
|
||||
id: undefined,
|
||||
name: '',
|
||||
code: '',
|
||||
nickname: '',
|
||||
content: '',
|
||||
type: undefined,
|
||||
status: CommonStatusEnum.ENABLE,
|
||||
remark: '',
|
||||
})
|
||||
const formRules = {
|
||||
name: [{ required: true, message: '模板名称不能为空' }],
|
||||
code: [{ required: true, message: '模板编码不能为空' }],
|
||||
nickname: [{ required: true, message: '发送人名称不能为空' }],
|
||||
type: [{ required: true, message: '模板类型不能为空' }],
|
||||
status: [{ required: true, message: '状态不能为空' }],
|
||||
content: [{ required: true, message: '模板内容不能为空' }],
|
||||
}
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/** 模板类型选项 */
|
||||
const templateTypeOptions = computed(() => {
|
||||
return getIntDictOptions(DICT_TYPE.SYSTEM_NOTIFY_TEMPLATE_TYPE).map(item => ({
|
||||
value: item.value,
|
||||
label: item.label,
|
||||
}))
|
||||
})
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-system/notify/index')
|
||||
}
|
||||
|
||||
/** 加载详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
formData.value = await getNotifyTemplate(props.id)
|
||||
}
|
||||
|
||||
/** 提交表单 */
|
||||
async function handleSubmit() {
|
||||
const { valid } = await formRef.value.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
|
||||
formLoading.value = true
|
||||
try {
|
||||
if (props.id) {
|
||||
await updateNotifyTemplate(formData.value)
|
||||
toast.success('修改成功')
|
||||
} else {
|
||||
await createNotifyTemplate(formData.value)
|
||||
toast.success('新增成功')
|
||||
}
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
133
src/pages-system/oauth2/client/detail/index.vue
Normal file
133
src/pages-system/oauth2/client/detail/index.vue
Normal file
@@ -0,0 +1,133 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="应用详情"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 详情内容 -->
|
||||
<view>
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="客户端编号" :value="formData?.clientId" />
|
||||
<wd-cell title="客户端密钥" :value="formData?.secret" />
|
||||
<wd-cell title="应用名" :value="formData?.name" />
|
||||
<wd-cell title="应用描述" :value="formData?.description ?? '-'" />
|
||||
<wd-cell title="状态">
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="formData?.status" />
|
||||
</wd-cell>
|
||||
<wd-cell title="访问令牌有效期" :value="`${formData?.accessTokenValiditySeconds} 秒`" />
|
||||
<wd-cell title="刷新令牌有效期" :value="`${formData?.refreshTokenValiditySeconds} 秒`" />
|
||||
<wd-cell title="授权类型" :value="formData?.authorizedGrantTypes?.join(', ') || '-'" />
|
||||
<wd-cell title="授权范围" :value="formData?.scopes?.join(', ') || '-'" />
|
||||
<wd-cell title="可重定向 URI" :value="formData?.redirectUris?.join(', ') || '-'" />
|
||||
<wd-cell title="创建时间" :value="formatDateTime(formData?.createTime)" />
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<view class="yd-detail-footer-actions">
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['system:oauth2-client:update'])"
|
||||
class="flex-1" type="warning" @click="handleEdit"
|
||||
>
|
||||
编辑
|
||||
</wd-button>
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['system:oauth2-client:delete'])"
|
||||
class="flex-1" type="error" :loading="deleting" @click="handleDelete"
|
||||
>
|
||||
删除
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { OAuth2Client } from '@/api/system/oauth2/client'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { deleteOAuth2Client, getOAuth2Client } from '@/api/system/oauth2/client'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const toast = useToast()
|
||||
const formData = ref<OAuth2Client>()
|
||||
const deleting = ref(false)
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-system/oauth2/index')
|
||||
}
|
||||
|
||||
/** 加载应用详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
toast.loading('加载中...')
|
||||
formData.value = await getOAuth2Client(props.id)
|
||||
} finally {
|
||||
toast.close()
|
||||
}
|
||||
}
|
||||
|
||||
/** 编辑应用 */
|
||||
function handleEdit() {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/oauth2/client/form/index?id=${props.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除应用 */
|
||||
function handleDelete() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要删除该应用吗?',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) {
|
||||
return
|
||||
}
|
||||
deleting.value = true
|
||||
try {
|
||||
await deleteOAuth2Client(props.id)
|
||||
toast.success('删除成功')
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
188
src/pages-system/oauth2/client/form/index.vue
Normal file
188
src/pages-system/oauth2/client/form/index.vue
Normal file
@@ -0,0 +1,188 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
:title="getTitle"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 表单区域 -->
|
||||
<view>
|
||||
<wd-form ref="formRef" :model="formData" :rules="formRules">
|
||||
<wd-cell-group border>
|
||||
<wd-input
|
||||
v-model="formData.clientId"
|
||||
label="客户端编号"
|
||||
label-width="220rpx"
|
||||
prop="clientId"
|
||||
clearable
|
||||
placeholder="请输入客户端编号"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="formData.secret"
|
||||
label="客户端密钥"
|
||||
label-width="220rpx"
|
||||
prop="secret"
|
||||
clearable
|
||||
placeholder="请输入客户端密钥"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="formData.name"
|
||||
label="应用名"
|
||||
label-width="220rpx"
|
||||
prop="name"
|
||||
clearable
|
||||
placeholder="请输入应用名"
|
||||
/>
|
||||
<wd-textarea
|
||||
v-model="formData.description"
|
||||
label="应用描述"
|
||||
label-width="220rpx"
|
||||
prop="description"
|
||||
clearable
|
||||
placeholder="请输入应用描述"
|
||||
/>
|
||||
<wd-cell title="状态" title-width="220rpx" prop="status" center>
|
||||
<wd-radio-group v-model="formData.status" shape="button">
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-cell>
|
||||
<wd-input
|
||||
v-model="formData.accessTokenValiditySeconds"
|
||||
label="访问令牌有效期"
|
||||
label-width="220rpx"
|
||||
prop="accessTokenValiditySeconds"
|
||||
type="number"
|
||||
clearable
|
||||
placeholder="请输入访问令牌有效期(秒)"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="formData.refreshTokenValiditySeconds"
|
||||
label="刷新令牌有效期"
|
||||
label-width="220rpx"
|
||||
prop="refreshTokenValiditySeconds"
|
||||
type="number"
|
||||
clearable
|
||||
placeholder="请输入刷新令牌有效期(秒)"
|
||||
/>
|
||||
</wd-cell-group>
|
||||
</wd-form>
|
||||
</view>
|
||||
|
||||
<!-- 底部保存按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<wd-button
|
||||
type="primary"
|
||||
block
|
||||
:loading="formLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
保存
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance } from 'wot-design-uni/components/wd-form/types'
|
||||
import type { OAuth2Client } from '@/api/system/oauth2/client'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { createOAuth2Client, getOAuth2Client, updateOAuth2Client } from '@/api/system/oauth2/client'
|
||||
import { getIntDictOptions } from '@/hooks/useDict'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { CommonStatusEnum, DICT_TYPE } from '@/utils/constants'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const getTitle = computed(() => props.id ? '编辑应用' : '新增应用')
|
||||
const formLoading = ref(false)
|
||||
const formData = ref<OAuth2Client>({
|
||||
id: undefined,
|
||||
clientId: '',
|
||||
secret: '',
|
||||
name: '',
|
||||
logo: '',
|
||||
description: '',
|
||||
status: CommonStatusEnum.ENABLE,
|
||||
accessTokenValiditySeconds: 1800,
|
||||
refreshTokenValiditySeconds: 43200,
|
||||
redirectUris: [],
|
||||
autoApprove: false,
|
||||
authorizedGrantTypes: [],
|
||||
scopes: [],
|
||||
authorities: [],
|
||||
resourceIds: [],
|
||||
additionalInformation: '',
|
||||
})
|
||||
const formRules = {
|
||||
clientId: [{ required: true, message: '客户端编号不能为空' }],
|
||||
secret: [{ required: true, message: '客户端密钥不能为空' }],
|
||||
name: [{ required: true, message: '应用名不能为空' }],
|
||||
accessTokenValiditySeconds: [{ required: true, message: '访问令牌有效期不能为空' }],
|
||||
refreshTokenValiditySeconds: [{ required: true, message: '刷新令牌有效期不能为空' }],
|
||||
}
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-system/oauth2/index')
|
||||
}
|
||||
|
||||
/** 加载应用详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
formData.value = await getOAuth2Client(props.id)
|
||||
}
|
||||
|
||||
/** 提交表单 */
|
||||
async function handleSubmit() {
|
||||
const { valid } = await formRef.value.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
|
||||
formLoading.value = true
|
||||
try {
|
||||
if (props.id) {
|
||||
await updateOAuth2Client(formData.value)
|
||||
toast.success('修改成功')
|
||||
} else {
|
||||
await createOAuth2Client(formData.value)
|
||||
toast.success('新增成功')
|
||||
}
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
143
src/pages-system/oauth2/components/client-list.vue
Normal file
143
src/pages-system/oauth2/components/client-list.vue
Normal file
@@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<view>
|
||||
<!-- 搜索组件 -->
|
||||
<ClientSearchForm @search="handleQuery" @reset="handleReset" />
|
||||
|
||||
<!-- 应用列表 -->
|
||||
<view class="p-24rpx">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
|
||||
@click="handleDetail(item)"
|
||||
>
|
||||
<view class="p-24rpx">
|
||||
<view class="mb-16rpx flex items-center justify-between">
|
||||
<view class="min-w-0 flex-1 truncate text-32rpx text-[#333] font-semibold">
|
||||
{{ item.name }}
|
||||
</view>
|
||||
<dict-tag class="ml-16rpx shrink-0" :type="DICT_TYPE.COMMON_STATUS" :value="item.status" />
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">客户端编号:</text>
|
||||
<text class="min-w-0 flex-1 truncate">{{ item.clientId }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">访问令牌有效期:</text>
|
||||
<text>{{ item.accessTokenValiditySeconds }} 秒</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">刷新令牌有效期:</text>
|
||||
<text>{{ item.refreshTokenValiditySeconds }} 秒</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">创建时间:</text>
|
||||
<text>{{ formatDateTime(item.createTime) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="loadMoreState !== 'loading' && list.length === 0" class="py-100rpx text-center">
|
||||
<wd-status-tip image="content" tip="暂无应用数据" />
|
||||
</view>
|
||||
<wd-loadmore
|
||||
v-if="list.length > 0"
|
||||
:state="loadMoreState"
|
||||
@reload="loadMore"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 新增按钮 -->
|
||||
<wd-fab
|
||||
v-if="hasAccessByCodes(['system:oauth2-client:create'])"
|
||||
position="right-bottom"
|
||||
type="primary"
|
||||
:expandable="false"
|
||||
@click="handleAdd"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { OAuth2Client } from '@/api/system/oauth2/client'
|
||||
import type { LoadMoreState } from '@/http/types'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { getOAuth2ClientPage } from '@/api/system/oauth2/client'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import ClientSearchForm from './client-search-form.vue'
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const total = ref(0)
|
||||
const list = ref<OAuth2Client[]>([])
|
||||
const loadMoreState = ref<LoadMoreState>('loading')
|
||||
const queryParams = ref({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
|
||||
/** 查询应用列表 */
|
||||
async function getList() {
|
||||
loadMoreState.value = 'loading'
|
||||
try {
|
||||
const data = await getOAuth2ClientPage(queryParams.value)
|
||||
list.value = [...list.value, ...data.list]
|
||||
total.value = data.total
|
||||
loadMoreState.value = list.value.length >= total.value ? 'finished' : 'loading'
|
||||
} catch {
|
||||
queryParams.value.pageNo = queryParams.value.pageNo > 1 ? queryParams.value.pageNo - 1 : 1
|
||||
loadMoreState.value = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery(data?: Record<string, any>) {
|
||||
queryParams.value = {
|
||||
...data,
|
||||
pageNo: 1,
|
||||
pageSize: queryParams.value.pageSize,
|
||||
}
|
||||
list.value = []
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function handleReset() {
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 加载更多 */
|
||||
function loadMore() {
|
||||
if (loadMoreState.value === 'finished') {
|
||||
return
|
||||
}
|
||||
queryParams.value.pageNo++
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 新增应用 */
|
||||
function handleAdd() {
|
||||
uni.navigateTo({
|
||||
url: '/pages-system/oauth2/client/form/index',
|
||||
})
|
||||
}
|
||||
|
||||
/** 查看详情 */
|
||||
function handleDetail(item: OAuth2Client) {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/oauth2/client/detail/index?id=${item.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 触底加载更多 */
|
||||
onReachBottom(() => {
|
||||
loadMore()
|
||||
})
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
94
src/pages-system/oauth2/components/client-search-form.vue
Normal file
94
src/pages-system/oauth2/components/client-search-form.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<!-- 搜索框入口 -->
|
||||
<view @click="visible = true">
|
||||
<wd-search :placeholder="placeholder" hide-cancel disabled />
|
||||
</view>
|
||||
|
||||
<!-- 搜索弹窗 -->
|
||||
<wd-popup v-model="visible" position="top" @close="visible = false">
|
||||
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
应用名
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.name"
|
||||
placeholder="请输入应用名"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
状态
|
||||
</view>
|
||||
<wd-radio-group v-model="formData.status" shape="button">
|
||||
<wd-radio :value="-1">
|
||||
全部
|
||||
</wd-radio>
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
<view class="yd-search-form-actions">
|
||||
<wd-button class="flex-1" plain @click="handleReset">
|
||||
重置
|
||||
</wd-button>
|
||||
<wd-button class="flex-1" type="primary" @click="handleSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { getDictLabel, getIntDictOptions } from '@/hooks/useDict'
|
||||
import { getNavbarHeight } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
|
||||
const emit = defineEmits<{
|
||||
search: [data: Record<string, any>]
|
||||
reset: []
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const formData = reactive({
|
||||
name: undefined as string | undefined,
|
||||
status: -1,
|
||||
})
|
||||
|
||||
/** 搜索条件 placeholder 拼接 */
|
||||
const placeholder = computed(() => {
|
||||
const conditions: string[] = []
|
||||
if (formData.name) {
|
||||
conditions.push(`应用名:${formData.name}`)
|
||||
}
|
||||
if (formData.status !== -1) {
|
||||
conditions.push(`状态:${getDictLabel(DICT_TYPE.COMMON_STATUS, formData.status)}`)
|
||||
}
|
||||
return conditions.length > 0 ? conditions.join(' | ') : '搜索应用'
|
||||
})
|
||||
|
||||
/** 搜索 */
|
||||
function handleSearch() {
|
||||
visible.value = false
|
||||
emit('search', {
|
||||
name: formData.name || undefined,
|
||||
status: formData.status === -1 ? undefined : formData.status,
|
||||
})
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
function handleReset() {
|
||||
formData.name = undefined
|
||||
formData.status = -1
|
||||
visible.value = false
|
||||
emit('reset')
|
||||
}
|
||||
</script>
|
||||
151
src/pages-system/oauth2/components/token-list.vue
Normal file
151
src/pages-system/oauth2/components/token-list.vue
Normal file
@@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<view>
|
||||
<!-- 搜索组件 -->
|
||||
<TokenSearchForm @search="handleQuery" @reset="handleReset" />
|
||||
|
||||
<!-- 令牌列表 -->
|
||||
<view class="p-24rpx">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.accessToken"
|
||||
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
|
||||
>
|
||||
<view class="p-24rpx">
|
||||
<view class="mb-16rpx flex items-center justify-between">
|
||||
<view class="text-28rpx text-[#333] font-semibold">
|
||||
用户编号: {{ item.userId }}
|
||||
</view>
|
||||
<dict-tag :type="DICT_TYPE.USER_TYPE" :value="item.userType" />
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">访问令牌:</text>
|
||||
<text class="min-w-0 flex-1 truncate">{{ item.accessToken }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">刷新令牌:</text>
|
||||
<text class="min-w-0 flex-1 truncate">{{ item.refreshToken }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">客户端编号:</text>
|
||||
<text>{{ item.clientId }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">过期时间:</text>
|
||||
<text>{{ formatDateTime(item.expiresTime) }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">创建时间:</text>
|
||||
<text>{{ formatDateTime(item.createTime) }}</text>
|
||||
</view>
|
||||
<!-- 删除按钮 -->
|
||||
<view
|
||||
v-if="hasAccessByCodes(['system:oauth2-token:delete'])"
|
||||
class="flex justify-end -mt-8"
|
||||
>
|
||||
<wd-button size="small" type="error" @click="handleDelete(item)">
|
||||
强退
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="loadMoreState !== 'loading' && list.length === 0" class="py-100rpx text-center">
|
||||
<wd-status-tip image="content" tip="暂无令牌数据" />
|
||||
</view>
|
||||
<wd-loadmore
|
||||
v-if="list.length > 0"
|
||||
:state="loadMoreState"
|
||||
@reload="loadMore"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { OAuth2Token } from '@/api/system/oauth2/token'
|
||||
import type { LoadMoreState } from '@/http/types'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { deleteOAuth2Token, getOAuth2TokenPage } from '@/api/system/oauth2/token'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import TokenSearchForm from './token-search-form.vue'
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const toast = useToast()
|
||||
const total = ref(0)
|
||||
const list = ref<OAuth2Token[]>([])
|
||||
const loadMoreState = ref<LoadMoreState>('loading')
|
||||
const queryParams = ref({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
|
||||
/** 查询令牌列表 */
|
||||
async function getList() {
|
||||
loadMoreState.value = 'loading'
|
||||
try {
|
||||
const data = await getOAuth2TokenPage(queryParams.value)
|
||||
list.value = [...list.value, ...data.list]
|
||||
total.value = data.total
|
||||
loadMoreState.value = list.value.length >= total.value ? 'finished' : 'loading'
|
||||
} catch {
|
||||
queryParams.value.pageNo = queryParams.value.pageNo > 1 ? queryParams.value.pageNo - 1 : 1
|
||||
loadMoreState.value = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery(data?: Record<string, any>) {
|
||||
queryParams.value = {
|
||||
...data,
|
||||
pageNo: 1,
|
||||
pageSize: queryParams.value.pageSize,
|
||||
}
|
||||
list.value = []
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function handleReset() {
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 加载更多 */
|
||||
function loadMore() {
|
||||
if (loadMoreState.value === 'finished') {
|
||||
return
|
||||
}
|
||||
queryParams.value.pageNo++
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 删除令牌 */
|
||||
function handleDelete(item: OAuth2Token) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要删除该令牌吗?',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) {
|
||||
return
|
||||
}
|
||||
await deleteOAuth2Token(item.accessToken)
|
||||
toast.success('删除成功')
|
||||
// 刷新列表
|
||||
handleQuery()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 触底加载更多 */
|
||||
onReachBottom(() => {
|
||||
loadMore()
|
||||
})
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
110
src/pages-system/oauth2/components/token-search-form.vue
Normal file
110
src/pages-system/oauth2/components/token-search-form.vue
Normal file
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<!-- 搜索框入口 -->
|
||||
<view @click="visible = true">
|
||||
<wd-search :placeholder="placeholder" hide-cancel disabled />
|
||||
</view>
|
||||
|
||||
<!-- 搜索弹窗 -->
|
||||
<wd-popup v-model="visible" position="top" @close="visible = false">
|
||||
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
用户编号
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.userId"
|
||||
placeholder="请输入用户编号"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
用户类型
|
||||
</view>
|
||||
<wd-radio-group v-model="formData.userType" shape="button">
|
||||
<wd-radio :value="-1">
|
||||
全部
|
||||
</wd-radio>
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.USER_TYPE)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
客户端编号
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.clientId"
|
||||
placeholder="请输入客户端编号"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-actions">
|
||||
<wd-button class="flex-1" plain @click="handleReset">
|
||||
重置
|
||||
</wd-button>
|
||||
<wd-button class="flex-1" type="primary" @click="handleSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { getDictLabel, getIntDictOptions } from '@/hooks/useDict'
|
||||
import { getNavbarHeight } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
|
||||
const emit = defineEmits<{
|
||||
search: [data: Record<string, any>]
|
||||
reset: []
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const formData = reactive({
|
||||
userId: undefined as string | undefined,
|
||||
userType: -1,
|
||||
clientId: undefined as string | undefined,
|
||||
})
|
||||
|
||||
/** 搜索条件 placeholder 拼接 */
|
||||
const placeholder = computed(() => {
|
||||
const conditions: string[] = []
|
||||
if (formData.userId) {
|
||||
conditions.push(`用户编号:${formData.userId}`)
|
||||
}
|
||||
if (formData.userType !== -1) {
|
||||
conditions.push(`类型:${getDictLabel(DICT_TYPE.USER_TYPE, formData.userType)}`)
|
||||
}
|
||||
if (formData.clientId) {
|
||||
conditions.push(`应用:${formData.clientId}`)
|
||||
}
|
||||
return conditions.length > 0 ? conditions.join(' | ') : '搜索令牌'
|
||||
})
|
||||
|
||||
/** 搜索 */
|
||||
function handleSearch() {
|
||||
visible.value = false
|
||||
emit('search', {
|
||||
userId: formData.userId || undefined,
|
||||
userType: formData.userType === -1 ? undefined : formData.userType,
|
||||
clientId: formData.clientId || undefined,
|
||||
})
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
function handleReset() {
|
||||
formData.userId = undefined
|
||||
formData.userType = -1
|
||||
formData.clientId = undefined
|
||||
visible.value = false
|
||||
emit('reset')
|
||||
}
|
||||
</script>
|
||||
53
src/pages-system/oauth2/index.vue
Normal file
53
src/pages-system/oauth2/index.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="OAuth2.0 管理"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- Tab 切换 -->
|
||||
<view class="bg-white">
|
||||
<wd-tabs v-model="tabIndex" shrink @change="handleTabChange">
|
||||
<wd-tab title="应用管理" />
|
||||
<wd-tab title="令牌管理" />
|
||||
</wd-tabs>
|
||||
</view>
|
||||
|
||||
<!-- 列表内容 -->
|
||||
<ClientList v-show="tabType === 'client'" />
|
||||
<TokenList v-show="tabType === 'token'" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import ClientList from './components/client-list.vue'
|
||||
import TokenList from './components/token-list.vue'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const tabTypes: string[] = ['client', 'token']
|
||||
const tabIndex = ref(0)
|
||||
const tabType = computed<string>(() => tabTypes[tabIndex.value])
|
||||
|
||||
/** Tab 切换 */
|
||||
function handleTabChange({ index }: { index: number }) {
|
||||
tabIndex.value = index
|
||||
}
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
89
src/pages-system/operate-log/detail/index.vue
Normal file
89
src/pages-system/operate-log/detail/index.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="操作日志详情"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 详情内容 -->
|
||||
<view>
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="日志编号" :value="formData?.id" />
|
||||
<wd-cell v-if="formData?.traceId" title="链路追踪" :value="formData.traceId" />
|
||||
<wd-cell title="操作人编号" :value="formData?.userId ?? '-'" />
|
||||
<wd-cell title="操作人类型">
|
||||
<dict-tag :type="DICT_TYPE.USER_TYPE" :value="formData?.userType" />
|
||||
</wd-cell>
|
||||
<wd-cell title="操作人名字" :value="formData?.userName" />
|
||||
<wd-cell title="操作人 IP" :value="formData?.userIp" />
|
||||
<wd-cell title="操作人 UA" :value="formData?.userAgent" />
|
||||
<wd-cell title="操作模块" :value="formData?.type" />
|
||||
<wd-cell title="操作名" :value="formData?.subType" />
|
||||
<wd-cell title="操作内容" :value="formData?.action" />
|
||||
<wd-cell v-if="formData?.extra" title="操作拓展参数" :value="formData.extra" />
|
||||
<wd-cell title="请求 URL">
|
||||
<template #value>
|
||||
<text v-if="formData?.requestMethod && formData?.requestUrl">
|
||||
{{ formData.requestMethod }} {{ formData.requestUrl }}
|
||||
</text>
|
||||
<text v-else>-</text>
|
||||
</template>
|
||||
</wd-cell>
|
||||
<wd-cell title="操作时间" :value="formatDateTime(formData?.createTime)" />
|
||||
<wd-cell title="业务编号" :value="formData?.bizId ?? '-'" />
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { OperateLog } from '@/api/system/operate-log'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { getOperateLog } from '@/api/system/operate-log'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const formData = ref<OperateLog>()
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-system/operate-log/index')
|
||||
}
|
||||
|
||||
/** 加载操作日志详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
toast.loading('加载中...')
|
||||
formData.value = await getOperateLog(props.id)
|
||||
} finally {
|
||||
toast.close()
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
154
src/pages-system/operate-log/index.vue
Normal file
154
src/pages-system/operate-log/index.vue
Normal file
@@ -0,0 +1,154 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="操作日志管理"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 搜索组件 -->
|
||||
<SearchForm @search="handleQuery" @reset="handleReset" />
|
||||
|
||||
<!-- 操作日志列表 -->
|
||||
<view class="p-24rpx">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
|
||||
@click="handleDetail(item)"
|
||||
>
|
||||
<view class="p-24rpx">
|
||||
<view class="mb-16rpx flex items-center justify-between">
|
||||
<view class="text-32rpx text-[#333] font-semibold">
|
||||
{{ item.type }} / {{ item.subType }}
|
||||
</view>
|
||||
<dict-tag :type="DICT_TYPE.USER_TYPE" :value="item.userType" />
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">操作人:</text>
|
||||
<text class="line-clamp-1">{{ item.userName }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">操作内容:</text>
|
||||
<text class="min-w-0 flex-1 truncate">{{ item.action }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">操作时间:</text>
|
||||
<text class="line-clamp-1">{{ formatDateTime(item.createTime) }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">业务编号:</text>
|
||||
<text class="line-clamp-1">{{ item.bizId }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">操作 IP:</text>
|
||||
<text class="line-clamp-1">{{ item.userIp }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="loadMoreState !== 'loading' && list.length === 0" class="py-100rpx text-center">
|
||||
<wd-status-tip image="content" tip="暂无操作日志数据" />
|
||||
</view>
|
||||
<wd-loadmore
|
||||
v-if="list.length > 0"
|
||||
:state="loadMoreState"
|
||||
@reload="loadMore"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { OperateLog } from '@/api/system/operate-log'
|
||||
import type { LoadMoreState } from '@/http/types'
|
||||
import { onReachBottom } from '@dcloudio/uni-app'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { getOperateLogPage } from '@/api/system/operate-log'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import SearchForm from './modules/search-form.vue'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const total = ref(0)
|
||||
const list = ref<OperateLog[]>([])
|
||||
const loadMoreState = ref<LoadMoreState>('loading') // 加载更多状态
|
||||
|
||||
const queryParams = ref({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus()
|
||||
}
|
||||
|
||||
/** 查询操作日志列表 */
|
||||
async function getList() {
|
||||
loadMoreState.value = 'loading'
|
||||
try {
|
||||
const data = await getOperateLogPage(queryParams.value)
|
||||
list.value = [...list.value, ...data.list]
|
||||
total.value = data.total
|
||||
loadMoreState.value = list.value.length >= total.value ? 'finished' : 'loading'
|
||||
} catch {
|
||||
queryParams.value.pageNo = queryParams.value.pageNo > 1 ? queryParams.value.pageNo - 1 : 1
|
||||
loadMoreState.value = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery(data?: Record<string, any>) {
|
||||
queryParams.value = {
|
||||
...data,
|
||||
pageNo: 1,
|
||||
pageSize: queryParams.value.pageSize,
|
||||
}
|
||||
list.value = []
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function handleReset() {
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 加载更多 */
|
||||
function loadMore() {
|
||||
if (loadMoreState.value === 'finished') {
|
||||
return
|
||||
}
|
||||
queryParams.value.pageNo++
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 查看详情 */
|
||||
function handleDetail(item: OperateLog) {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/operate-log/detail/index?id=${item.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 触底加载更多 */
|
||||
onReachBottom(() => {
|
||||
loadMore()
|
||||
})
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
192
src/pages-system/operate-log/modules/search-form.vue
Normal file
192
src/pages-system/operate-log/modules/search-form.vue
Normal file
@@ -0,0 +1,192 @@
|
||||
<template>
|
||||
<!-- 搜索框入口 -->
|
||||
<view @click="visible = true">
|
||||
<wd-search :placeholder="placeholder" hide-cancel disabled />
|
||||
</view>
|
||||
|
||||
<!-- 搜索弹窗 -->
|
||||
<wd-popup v-model="visible" position="top" @close="visible = false">
|
||||
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
操作人
|
||||
</view>
|
||||
<UserPicker
|
||||
ref="userPickerRef"
|
||||
v-model="formData.userId"
|
||||
type="radio"
|
||||
placeholder="请选择操作人员"
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
操作模块
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.type"
|
||||
placeholder="请输入操作模块"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
操作名
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.subType"
|
||||
placeholder="请输入操作名"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
操作内容
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.action"
|
||||
placeholder="请输入操作内容"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
操作时间
|
||||
</view>
|
||||
<view class="yd-search-form-date-range-container">
|
||||
<view class="flex-1" @click="visibleCreateTime[0] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.createTime?.[0]) || '开始日期' }}
|
||||
</view>
|
||||
</view>
|
||||
-
|
||||
<view class="flex-1" @click="visibleCreateTime[1] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.createTime?.[1]) || '结束日期' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleCreateTime[0]" v-model="tempCreateTime[0]" type="date" />
|
||||
<view v-if="visibleCreateTime[0]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleCreateTime[0] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleCreateTime0Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleCreateTime[1]" v-model="tempCreateTime[1]" type="date" />
|
||||
<view v-if="visibleCreateTime[1]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleCreateTime[1] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleCreateTime1Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
业务编号
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.bizId"
|
||||
placeholder="请输入业务编号"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-actions">
|
||||
<wd-button class="flex-1" plain @click="handleReset">
|
||||
重置
|
||||
</wd-button>
|
||||
<wd-button class="flex-1" type="primary" @click="handleSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import UserPicker from '@/components/system-select/user-picker.vue'
|
||||
import { getNavbarHeight } from '@/utils'
|
||||
import { formatDate, formatDateRange } from '@/utils/date'
|
||||
|
||||
const emit = defineEmits<{
|
||||
search: [data: Record<string, any>]
|
||||
reset: []
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const userPickerRef = ref<InstanceType<typeof UserPicker>>()
|
||||
const formData = reactive({
|
||||
userId: undefined as number | undefined,
|
||||
type: undefined as string | undefined,
|
||||
subType: undefined as string | undefined,
|
||||
action: undefined as string | undefined,
|
||||
createTime: [undefined, undefined] as [number | undefined, number | undefined],
|
||||
bizId: undefined as number | undefined,
|
||||
})
|
||||
|
||||
/** 搜索条件 placeholder 拼接 */
|
||||
const placeholder = computed(() => {
|
||||
const conditions: string[] = []
|
||||
if (formData.userId !== undefined) {
|
||||
const nickname = userPickerRef.value?.getUserNickname(formData.userId)
|
||||
conditions.push(`操作人:${nickname || formData.userId}`)
|
||||
}
|
||||
if (formData.type) {
|
||||
conditions.push(`操作模块:${formData.type}`)
|
||||
}
|
||||
if (formData.subType) {
|
||||
conditions.push(`操作名:${formData.subType}`)
|
||||
}
|
||||
if (formData.action) {
|
||||
conditions.push(`操作内容:${formData.action}`)
|
||||
}
|
||||
if (formData.createTime?.[0] && formData.createTime?.[1]) {
|
||||
conditions.push(`操作时间:${formatDate(formData.createTime[0])}~${formatDate(formData.createTime[1])}`)
|
||||
}
|
||||
if (formData.bizId !== undefined) {
|
||||
conditions.push(`业务编号:${formData.bizId}`)
|
||||
}
|
||||
return conditions.length > 0 ? conditions.join(' | ') : '搜索操作日志'
|
||||
})
|
||||
|
||||
// 时间范围选择器状态
|
||||
const visibleCreateTime = ref<[boolean, boolean]>([false, false])
|
||||
const tempCreateTime = ref<[number, number]>([Date.now(), Date.now()])
|
||||
|
||||
/** 操作时间[0]确认 */
|
||||
function handleCreateTime0Confirm() {
|
||||
formData.createTime = [tempCreateTime.value[0], formData.createTime?.[1]]
|
||||
visibleCreateTime.value[0] = false
|
||||
}
|
||||
|
||||
/** 操作时间[1]确认 */
|
||||
function handleCreateTime1Confirm() {
|
||||
formData.createTime = [formData.createTime?.[0], tempCreateTime.value[1]]
|
||||
visibleCreateTime.value[1] = false
|
||||
}
|
||||
|
||||
/** 搜索 */
|
||||
function handleSearch() {
|
||||
visible.value = false
|
||||
emit('search', {
|
||||
...formData,
|
||||
createTime: formatDateRange(formData.createTime),
|
||||
})
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
function handleReset() {
|
||||
formData.userId = undefined
|
||||
formData.type = undefined
|
||||
formData.subType = undefined
|
||||
formData.action = undefined
|
||||
formData.createTime = [undefined, undefined]
|
||||
formData.bizId = undefined
|
||||
visible.value = false
|
||||
emit('reset')
|
||||
}
|
||||
</script>
|
||||
106
src/pages-system/post/components/search-form.vue
Normal file
106
src/pages-system/post/components/search-form.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<!-- 搜索框入口 -->
|
||||
<view @click="visible = true">
|
||||
<wd-search :placeholder="placeholder" hide-cancel disabled />
|
||||
</view>
|
||||
|
||||
<!-- 搜索弹窗 -->
|
||||
<wd-popup v-model="visible" position="top" @close="visible = false">
|
||||
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
岗位名称
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.name"
|
||||
placeholder="请输入岗位名称"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
岗位编码
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.code"
|
||||
placeholder="请输入岗位编码"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
状态
|
||||
</view>
|
||||
<wd-radio-group v-model="formData.status" shape="button">
|
||||
<wd-radio :value="-1">
|
||||
全部
|
||||
</wd-radio>
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
<view class="yd-search-form-actions">
|
||||
<wd-button class="flex-1" plain @click="handleReset">
|
||||
重置
|
||||
</wd-button>
|
||||
<wd-button class="flex-1" type="primary" @click="handleSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { getDictLabel, getIntDictOptions } from '@/hooks/useDict'
|
||||
import { getNavbarHeight } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
|
||||
const emit = defineEmits<{
|
||||
search: [data: Record<string, any>]
|
||||
reset: []
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const formData = reactive({
|
||||
name: undefined as string | undefined,
|
||||
code: undefined as string | undefined,
|
||||
status: -1, // -1 表示全部
|
||||
})
|
||||
|
||||
/** 搜索条件 placeholder 拼接 */
|
||||
const placeholder = computed(() => {
|
||||
const conditions: string[] = []
|
||||
if (formData.name) {
|
||||
conditions.push(`名称:${formData.name}`)
|
||||
}
|
||||
if (formData.code) {
|
||||
conditions.push(`编码:${formData.code}`)
|
||||
}
|
||||
if (formData.status !== -1) {
|
||||
conditions.push(`状态:${getDictLabel(DICT_TYPE.COMMON_STATUS, formData.status)}`)
|
||||
}
|
||||
return conditions.length > 0 ? conditions.join(' | ') : '搜索岗位'
|
||||
})
|
||||
|
||||
/** 搜索 */
|
||||
function handleSearch() {
|
||||
visible.value = false
|
||||
emit('search', { ...formData })
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
function handleReset() {
|
||||
formData.name = undefined
|
||||
formData.code = undefined
|
||||
formData.status = -1
|
||||
visible.value = false
|
||||
emit('reset')
|
||||
}
|
||||
</script>
|
||||
128
src/pages-system/post/detail/index.vue
Normal file
128
src/pages-system/post/detail/index.vue
Normal file
@@ -0,0 +1,128 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="岗位详情"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 详情内容 -->
|
||||
<view>
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="岗位名称" :value="formData?.name" />
|
||||
<wd-cell title="岗位编码" :value="formData?.code" />
|
||||
<wd-cell title="显示顺序" :value="formData?.sort" />
|
||||
<wd-cell title="状态">
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="formData?.status" />
|
||||
</wd-cell>
|
||||
<wd-cell title="备注" :value="formData?.remark || '-'" />
|
||||
<wd-cell title="创建时间" :value="formatDateTime(formData?.createTime)" />
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<view class="yd-detail-footer-actions">
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['system:post:update'])"
|
||||
class="flex-1" type="warning" @click="handleEdit"
|
||||
>
|
||||
编辑
|
||||
</wd-button>
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['system:post:delete'])"
|
||||
class="flex-1" type="error" :loading="deleting" @click="handleDelete"
|
||||
>
|
||||
删除
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Post } from '@/api/system/post'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { deletePost, getPost } from '@/api/system/post'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const toast = useToast()
|
||||
const formData = ref<Post>()
|
||||
const deleting = ref(false)
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-system/post/index')
|
||||
}
|
||||
|
||||
/** 加载岗位详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
toast.loading('加载中...')
|
||||
formData.value = await getPost(props.id)
|
||||
} finally {
|
||||
toast.close()
|
||||
}
|
||||
}
|
||||
|
||||
/** 编辑岗位 */
|
||||
function handleEdit() {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/post/form/index?id=${props.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除岗位 */
|
||||
function handleDelete() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要删除该岗位吗?',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) {
|
||||
return
|
||||
}
|
||||
deleting.value = true
|
||||
try {
|
||||
await deletePost(props.id)
|
||||
toast.success('删除成功')
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
50
src/pages-system/post/form/components/post-picker.vue
Normal file
50
src/pages-system/post/form/components/post-picker.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<wd-select-picker
|
||||
v-model="selectedIds"
|
||||
label="岗位"
|
||||
label-width="180rpx"
|
||||
:columns="postList"
|
||||
value-key="id"
|
||||
label-key="name"
|
||||
type="checkbox"
|
||||
filterable
|
||||
@confirm="handleConfirm"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Post } from '@/api/system/post'
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import { getSimplePostList } from '@/api/system/post'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue?: number[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: number[]): void
|
||||
}>()
|
||||
|
||||
const postList = ref<Post[]>([])
|
||||
const selectedIds = ref<number[]>([])
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
selectedIds.value = val || []
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
async function loadPostList() {
|
||||
postList.value = await getSimplePostList()
|
||||
}
|
||||
|
||||
function handleConfirm({ value }: { value: number[] }) {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadPostList()
|
||||
})
|
||||
</script>
|
||||
153
src/pages-system/post/form/index.vue
Normal file
153
src/pages-system/post/form/index.vue
Normal file
@@ -0,0 +1,153 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
:title="getTitle"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 表单区域 -->
|
||||
<view>
|
||||
<wd-form ref="formRef" :model="formData" :rules="formRules">
|
||||
<wd-cell-group border>
|
||||
<wd-input
|
||||
v-model="formData.name"
|
||||
label="岗位名称"
|
||||
label-width="180rpx"
|
||||
prop="name"
|
||||
clearable
|
||||
placeholder="请输入岗位名称"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="formData.code"
|
||||
label="岗位编码"
|
||||
label-width="180rpx"
|
||||
prop="code"
|
||||
clearable
|
||||
placeholder="请输入岗位编码"
|
||||
/>
|
||||
<wd-cell title="显示顺序" title-width="180rpx" prop="sort" center>
|
||||
<wd-input-number
|
||||
v-model="formData.sort"
|
||||
:min="0"
|
||||
/>
|
||||
</wd-cell>
|
||||
<wd-cell title="状态" title-width="180rpx" prop="status" center>
|
||||
<wd-switch
|
||||
v-model="formData.status"
|
||||
:active-value="CommonStatusEnum.ENABLE"
|
||||
:inactive-value="CommonStatusEnum.DISABLE"
|
||||
/>
|
||||
</wd-cell>
|
||||
<wd-textarea
|
||||
v-model="formData.remark"
|
||||
label="备注"
|
||||
label-width="180rpx"
|
||||
placeholder="请输入备注"
|
||||
:maxlength="200"
|
||||
show-word-limit
|
||||
clearable
|
||||
/>
|
||||
</wd-cell-group>
|
||||
</wd-form>
|
||||
</view>
|
||||
|
||||
<!-- 底部保存按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<wd-button
|
||||
type="primary"
|
||||
block
|
||||
:loading="formLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
保存
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance } from 'wot-design-uni/components/wd-form/types'
|
||||
import type { Post } from '@/api/system/post'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { createPost, getPost, updatePost } from '@/api/system/post'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { CommonStatusEnum } from '@/utils/constants'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const getTitle = computed(() => props.id ? '编辑岗位' : '新增岗位')
|
||||
const formLoading = ref(false)
|
||||
const formData = ref<Post>({
|
||||
id: undefined,
|
||||
name: '',
|
||||
code: '',
|
||||
sort: 0,
|
||||
status: CommonStatusEnum.ENABLE,
|
||||
remark: '',
|
||||
})
|
||||
const formRules = {
|
||||
name: [{ required: true, message: '岗位名称不能为空' }],
|
||||
code: [{ required: true, message: '岗位编码不能为空' }],
|
||||
sort: [{ required: true, message: '显示顺序不能为空' }],
|
||||
status: [{ required: true, message: '状态不能为空' }],
|
||||
}
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-system/post/index')
|
||||
}
|
||||
|
||||
/** 加载岗位详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
formData.value = await getPost(props.id)
|
||||
}
|
||||
|
||||
/** 提交表单 */
|
||||
async function handleSubmit() {
|
||||
const { valid } = await formRef.value.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
|
||||
formLoading.value = true
|
||||
try {
|
||||
if (props.id) {
|
||||
await updatePost(formData.value)
|
||||
toast.success('修改成功')
|
||||
} else {
|
||||
await createPost(formData.value)
|
||||
toast.success('新增成功')
|
||||
}
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
162
src/pages-system/post/index.vue
Normal file
162
src/pages-system/post/index.vue
Normal file
@@ -0,0 +1,162 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="岗位管理"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 搜索组件 -->
|
||||
<SearchForm @search="handleQuery" @reset="handleReset" />
|
||||
|
||||
<!-- 岗位列表 -->
|
||||
<view class="p-24rpx">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
|
||||
@click="handleDetail(item)"
|
||||
>
|
||||
<view class="p-24rpx">
|
||||
<view class="mb-16rpx flex items-center justify-between">
|
||||
<view class="text-32rpx text-[#333] font-semibold">
|
||||
{{ item.name }}
|
||||
</view>
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="item.status" />
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">岗位编码:</text>
|
||||
<text>{{ item.code }}</text>
|
||||
</view>
|
||||
<view v-if="item.remark" class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">备注:</text>
|
||||
<text class="line-clamp-1">{{ item.remark }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="loadMoreState !== 'loading' && list.length === 0" class="py-100rpx text-center">
|
||||
<wd-status-tip image="content" tip="暂无岗位数据" />
|
||||
</view>
|
||||
<wd-loadmore
|
||||
v-if="list.length > 0"
|
||||
:state="loadMoreState"
|
||||
@reload="loadMore"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 新增按钮 -->
|
||||
<wd-fab
|
||||
v-if="hasAccessByCodes(['system:post:create'])"
|
||||
position="right-bottom"
|
||||
type="primary"
|
||||
:expandable="false"
|
||||
@click="handleAdd"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Post } from '@/api/system/post'
|
||||
import type { LoadMoreState } from '@/http/types'
|
||||
import { onReachBottom } from '@dcloudio/uni-app'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { getPostPage } from '@/api/system/post'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import SearchForm from './components/search-form.vue'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const total = ref(0)
|
||||
const list = ref<Post[]>([])
|
||||
const loadMoreState = ref<LoadMoreState>('loading')
|
||||
const queryParams = ref({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus()
|
||||
}
|
||||
|
||||
/** 查询岗位列表 */
|
||||
async function getList() {
|
||||
loadMoreState.value = 'loading'
|
||||
try {
|
||||
const params = { ...queryParams.value }
|
||||
if ((params as any).status === -1) {
|
||||
delete (params as any).status
|
||||
}
|
||||
const data = await getPostPage(params)
|
||||
list.value = [...list.value, ...data.list]
|
||||
total.value = data.total
|
||||
loadMoreState.value = list.value.length >= total.value ? 'finished' : 'loading'
|
||||
} catch {
|
||||
queryParams.value.pageNo = queryParams.value.pageNo > 1 ? queryParams.value.pageNo - 1 : 1
|
||||
loadMoreState.value = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery(data?: Record<string, any>) {
|
||||
queryParams.value = {
|
||||
...data,
|
||||
pageNo: 1,
|
||||
pageSize: queryParams.value.pageSize,
|
||||
}
|
||||
list.value = [] // 清空列表
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function handleReset() {
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 加载更多 */
|
||||
function loadMore() {
|
||||
if (loadMoreState.value === 'finished') {
|
||||
return
|
||||
}
|
||||
queryParams.value.pageNo++
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 新增岗位 */
|
||||
function handleAdd() {
|
||||
uni.navigateTo({
|
||||
url: '/pages-system/post/form/index',
|
||||
})
|
||||
}
|
||||
|
||||
/** 查看详情 */
|
||||
function handleDetail(item: Post) {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/post/detail/index?id=${item.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 触底加载更多 */
|
||||
onReachBottom(() => {
|
||||
loadMore()
|
||||
})
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
106
src/pages-system/role/components/search-form.vue
Normal file
106
src/pages-system/role/components/search-form.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<!-- 搜索框入口 -->
|
||||
<view @click="visible = true">
|
||||
<wd-search :placeholder="placeholder" hide-cancel disabled />
|
||||
</view>
|
||||
|
||||
<!-- 搜索弹窗 -->
|
||||
<wd-popup v-model="visible" position="top" @close="visible = false">
|
||||
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
角色名称
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.name"
|
||||
placeholder="请输入角色名称"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
角色标识
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.code"
|
||||
placeholder="请输入角色标识"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
状态
|
||||
</view>
|
||||
<wd-radio-group v-model="formData.status" shape="button">
|
||||
<wd-radio :value="-1">
|
||||
全部
|
||||
</wd-radio>
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
<view class="yd-search-form-actions">
|
||||
<wd-button class="flex-1" plain @click="handleReset">
|
||||
重置
|
||||
</wd-button>
|
||||
<wd-button class="flex-1" type="primary" @click="handleSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { getDictLabel, getIntDictOptions } from '@/hooks/useDict'
|
||||
import { getNavbarHeight } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
|
||||
const emit = defineEmits<{
|
||||
search: [data: Record<string, any>]
|
||||
reset: []
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const formData = reactive({
|
||||
name: undefined as string | undefined,
|
||||
code: undefined as string | undefined,
|
||||
status: -1, // -1 表示全部
|
||||
})
|
||||
|
||||
/** 搜索条件 placeholder 拼接 */
|
||||
const placeholder = computed(() => {
|
||||
const conditions: string[] = []
|
||||
if (formData.name) {
|
||||
conditions.push(`名称:${formData.name}`)
|
||||
}
|
||||
if (formData.code) {
|
||||
conditions.push(`标识:${formData.code}`)
|
||||
}
|
||||
if (formData.status !== -1) {
|
||||
conditions.push(`状态:${getDictLabel(DICT_TYPE.COMMON_STATUS, formData.status)}`)
|
||||
}
|
||||
return conditions.length > 0 ? conditions.join(' | ') : '搜索角色'
|
||||
})
|
||||
|
||||
/** 搜索 */
|
||||
function handleSearch() {
|
||||
visible.value = false
|
||||
emit('search', { ...formData })
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
function handleReset() {
|
||||
formData.name = undefined
|
||||
formData.code = undefined
|
||||
formData.status = -1
|
||||
visible.value = false
|
||||
emit('reset')
|
||||
}
|
||||
</script>
|
||||
129
src/pages-system/role/detail/index.vue
Normal file
129
src/pages-system/role/detail/index.vue
Normal file
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="角色详情"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 详情内容 -->
|
||||
<view>
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="角色名称" :value="formData?.name || '-'" />
|
||||
<wd-cell title="角色标识" :value="formData?.code || '-'" />
|
||||
<wd-cell title="显示顺序" :value="formData?.sort" />
|
||||
<wd-cell title="状态">
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="formData?.status" />
|
||||
</wd-cell>
|
||||
<wd-cell title="备注" :value="formData?.remark || '-'" />
|
||||
<wd-cell title="创建时间" :value="formatDateTime(formData?.createTime)" />
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<view class="yd-detail-footer-actions">
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['system:role:update'])"
|
||||
class="flex-1" type="warning" @click="handleEdit"
|
||||
>
|
||||
编辑
|
||||
</wd-button>
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['system:role:delete'])"
|
||||
class="flex-1" type="error" :loading="deleting" @click="handleDelete"
|
||||
>
|
||||
删除
|
||||
</wd-button>
|
||||
</view>
|
||||
<!-- TODO @芋艿:1)数据权限;2)菜单权限 -->
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Role } from '@/api/system/role'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { deleteRole, getRole } from '@/api/system/role'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const toast = useToast()
|
||||
const formData = ref<Role>()
|
||||
const deleting = ref(false)
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-system/role/index')
|
||||
}
|
||||
|
||||
/** 加载角色详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
toast.loading('加载中...')
|
||||
formData.value = await getRole(props.id)
|
||||
} finally {
|
||||
toast.close()
|
||||
}
|
||||
}
|
||||
|
||||
/** 编辑角色 */
|
||||
function handleEdit() {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/role/form/index?id=${props.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除角色 */
|
||||
function handleDelete() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要删除该角色吗?',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) {
|
||||
return
|
||||
}
|
||||
deleting.value = true
|
||||
try {
|
||||
await deleteRole(props.id)
|
||||
toast.success('删除成功')
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
153
src/pages-system/role/form/index.vue
Normal file
153
src/pages-system/role/form/index.vue
Normal file
@@ -0,0 +1,153 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
:title="getTitle"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 表单区域 -->
|
||||
<view>
|
||||
<wd-form ref="formRef" :model="formData" :rules="formRules">
|
||||
<wd-cell-group border>
|
||||
<wd-input
|
||||
v-model="formData.name"
|
||||
label="角色名称"
|
||||
label-width="180rpx"
|
||||
prop="name"
|
||||
clearable
|
||||
placeholder="请输入角色名称"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="formData.code"
|
||||
label="角色标识"
|
||||
label-width="180rpx"
|
||||
prop="code"
|
||||
clearable
|
||||
placeholder="请输入角色标识"
|
||||
/>
|
||||
<wd-cell title="显示顺序" title-width="180rpx" prop="sort" center>
|
||||
<wd-input-number
|
||||
v-model="formData.sort"
|
||||
:min="0"
|
||||
/>
|
||||
</wd-cell>
|
||||
<wd-cell title="状态" title-width="180rpx" prop="status" center>
|
||||
<wd-switch
|
||||
v-model="formData.status"
|
||||
:active-value="CommonStatusEnum.ENABLE"
|
||||
:inactive-value="CommonStatusEnum.DISABLE"
|
||||
/>
|
||||
</wd-cell>
|
||||
<wd-textarea
|
||||
v-model="formData.remark"
|
||||
label="备注"
|
||||
label-width="180rpx"
|
||||
placeholder="请输入备注"
|
||||
:maxlength="200"
|
||||
show-word-limit
|
||||
clearable
|
||||
/>
|
||||
</wd-cell-group>
|
||||
</wd-form>
|
||||
</view>
|
||||
|
||||
<!-- 底部保存按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<wd-button
|
||||
type="primary"
|
||||
block
|
||||
:loading="formLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
保存
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance } from 'wot-design-uni/components/wd-form/types'
|
||||
import type { Role } from '@/api/system/role'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { createRole, getRole, updateRole } from '@/api/system/role'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { CommonStatusEnum } from '@/utils/constants'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const getTitle = computed(() => props.id ? '编辑角色' : '新增角色')
|
||||
const formLoading = ref(false)
|
||||
const formData = ref<Role>({
|
||||
id: undefined,
|
||||
name: '',
|
||||
code: '',
|
||||
sort: 0,
|
||||
status: CommonStatusEnum.ENABLE,
|
||||
remark: '',
|
||||
})
|
||||
const formRules = {
|
||||
name: [{ required: true, message: '角色名称不能为空' }],
|
||||
code: [{ required: true, message: '角色标识不能为空' }],
|
||||
sort: [{ required: true, message: '显示顺序不能为空' }],
|
||||
status: [{ required: true, message: '状态不能为空' }],
|
||||
}
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-system/role/index')
|
||||
}
|
||||
|
||||
/** 加载角色详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
formData.value = await getRole(props.id)
|
||||
}
|
||||
|
||||
/** 提交表单 */
|
||||
async function handleSubmit() {
|
||||
const { valid } = await formRef.value.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
|
||||
formLoading.value = true
|
||||
try {
|
||||
if (props.id) {
|
||||
await updateRole(formData.value)
|
||||
toast.success('修改成功')
|
||||
} else {
|
||||
await createRole(formData.value)
|
||||
toast.success('新增成功')
|
||||
}
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
162
src/pages-system/role/index.vue
Normal file
162
src/pages-system/role/index.vue
Normal file
@@ -0,0 +1,162 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="角色管理"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 搜索组件 -->
|
||||
<SearchForm @search="handleQuery" @reset="handleReset" />
|
||||
|
||||
<!-- 角色列表 -->
|
||||
<view class="p-24rpx">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
|
||||
@click="handleDetail(item)"
|
||||
>
|
||||
<view class="p-24rpx">
|
||||
<view class="mb-16rpx flex items-center justify-between">
|
||||
<view class="text-32rpx text-[#333] font-semibold">
|
||||
{{ item.name }}
|
||||
</view>
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="item.status" />
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">角色标识:</text>
|
||||
<text>{{ item.code }}</text>
|
||||
</view>
|
||||
<view v-if="item.remark" class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">备注:</text>
|
||||
<text class="line-clamp-1">{{ item.remark }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="loadMoreState !== 'loading' && list.length === 0" class="py-100rpx text-center">
|
||||
<wd-status-tip image="content" tip="暂无角色数据" />
|
||||
</view>
|
||||
<wd-loadmore
|
||||
v-if="list.length > 0"
|
||||
:state="loadMoreState"
|
||||
@reload="loadMore"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 新增按钮 -->
|
||||
<wd-fab
|
||||
v-if="hasAccessByCodes(['system:role:create'])"
|
||||
position="right-bottom"
|
||||
type="primary"
|
||||
:expandable="false"
|
||||
@click="handleAdd"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Role } from '@/api/system/role'
|
||||
import type { LoadMoreState } from '@/http/types'
|
||||
import { onReachBottom } from '@dcloudio/uni-app'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { getRolePage } from '@/api/system/role'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import SearchForm from './components/search-form.vue'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const total = ref(0)
|
||||
const list = ref<Role[]>([])
|
||||
const loadMoreState = ref<LoadMoreState>('loading')
|
||||
const queryParams = ref({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus()
|
||||
}
|
||||
|
||||
/** 查询角色列表 */
|
||||
async function getList() {
|
||||
loadMoreState.value = 'loading'
|
||||
try {
|
||||
const params = { ...queryParams.value }
|
||||
if ((params as any).status === -1) {
|
||||
delete (params as any).status
|
||||
}
|
||||
const data = await getRolePage(params)
|
||||
list.value = [...list.value, ...data.list]
|
||||
total.value = data.total
|
||||
loadMoreState.value = list.value.length >= total.value ? 'finished' : 'loading'
|
||||
} catch {
|
||||
queryParams.value.pageNo = queryParams.value.pageNo > 1 ? queryParams.value.pageNo - 1 : 1
|
||||
loadMoreState.value = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery(data?: Record<string, any>) {
|
||||
queryParams.value = {
|
||||
...data,
|
||||
pageNo: 1,
|
||||
pageSize: queryParams.value.pageSize,
|
||||
}
|
||||
list.value = [] // 清空列表
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function handleReset() {
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 加载更多 */
|
||||
function loadMore() {
|
||||
if (loadMoreState.value === 'finished') {
|
||||
return
|
||||
}
|
||||
queryParams.value.pageNo++
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 新增角色 */
|
||||
function handleAdd() {
|
||||
uni.navigateTo({
|
||||
url: '/pages-system/role/form/index',
|
||||
})
|
||||
}
|
||||
|
||||
/** 查看详情 */
|
||||
function handleDetail(item: Role) {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/role/detail/index?id=${item.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 触底加载更多 */
|
||||
onReachBottom(() => {
|
||||
loadMore()
|
||||
})
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
133
src/pages-system/sms/channel/detail/index.vue
Normal file
133
src/pages-system/sms/channel/detail/index.vue
Normal file
@@ -0,0 +1,133 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="短信渠道详情"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 详情内容 -->
|
||||
<view>
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="渠道编号" :value="formData?.id" />
|
||||
<wd-cell title="短信签名" :value="formData?.signature" />
|
||||
<wd-cell title="渠道编码">
|
||||
<dict-tag :type="DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE" :value="formData?.code" />
|
||||
</wd-cell>
|
||||
<wd-cell title="启用状态">
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="formData?.status" />
|
||||
</wd-cell>
|
||||
<wd-cell title="短信 API 账号" :value="formData?.apiKey" />
|
||||
<wd-cell title="短信 API 密钥" :value="formData?.apiSecret" />
|
||||
<wd-cell title="回调 URL" :value="formData?.callbackUrl" />
|
||||
<wd-cell title="备注" :value="formData?.remark ?? '-'" />
|
||||
<wd-cell title="创建时间" :value="formatDateTime(formData?.createTime)" />
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<view class="yd-detail-footer-actions">
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['system:sms-channel:update'])"
|
||||
class="flex-1" type="warning" @click="handleEdit"
|
||||
>
|
||||
编辑
|
||||
</wd-button>
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['system:sms-channel:delete'])"
|
||||
class="flex-1" type="error" :loading="deleting" @click="handleDelete"
|
||||
>
|
||||
删除
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { SmsChannel } from '@/api/system/sms/channel'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { deleteSmsChannel, getSmsChannel } from '@/api/system/sms/channel'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const toast = useToast()
|
||||
const formData = ref<SmsChannel>()
|
||||
const deleting = ref(false)
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-system/sms/index')
|
||||
}
|
||||
|
||||
/** 加载详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
toast.loading('加载中...')
|
||||
formData.value = await getSmsChannel(props.id)
|
||||
} finally {
|
||||
toast.close()
|
||||
}
|
||||
}
|
||||
|
||||
/** 编辑 */
|
||||
function handleEdit() {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/sms/channel/form/index?id=${props.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除 */
|
||||
function handleDelete() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要删除该短信渠道吗?',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) {
|
||||
return
|
||||
}
|
||||
deleting.value = true
|
||||
try {
|
||||
await deleteSmsChannel(props.id)
|
||||
toast.success('删除成功')
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
178
src/pages-system/sms/channel/form/index.vue
Normal file
178
src/pages-system/sms/channel/form/index.vue
Normal file
@@ -0,0 +1,178 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
:title="getTitle"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 表单区域 -->
|
||||
<view>
|
||||
<wd-form ref="formRef" :model="formData" :rules="formRules">
|
||||
<wd-cell-group border>
|
||||
<wd-input
|
||||
v-model="formData.signature"
|
||||
label="短信签名"
|
||||
label-width="200rpx"
|
||||
prop="signature"
|
||||
clearable
|
||||
placeholder="请输入短信签名"
|
||||
/>
|
||||
<wd-cell title="渠道编码" title-width="200rpx" prop="code" center>
|
||||
<wd-picker
|
||||
v-model="formData.code"
|
||||
:columns="getStrDictOptions(DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE)"
|
||||
label-key="label"
|
||||
value-key="value"
|
||||
placeholder="请选择渠道编码"
|
||||
/>
|
||||
</wd-cell>
|
||||
<wd-cell title="启用状态" title-width="200rpx" prop="status" center>
|
||||
<wd-radio-group v-model="formData.status" shape="button">
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-cell>
|
||||
<wd-input
|
||||
v-model="formData.apiKey"
|
||||
label="API 账号"
|
||||
label-width="200rpx"
|
||||
prop="apiKey"
|
||||
clearable
|
||||
placeholder="请输入短信 API 账号"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="formData.apiSecret"
|
||||
label="API 密钥"
|
||||
label-width="200rpx"
|
||||
prop="apiSecret"
|
||||
clearable
|
||||
placeholder="请输入短信 API 密钥"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="formData.callbackUrl"
|
||||
label="回调 URL"
|
||||
label-width="200rpx"
|
||||
prop="callbackUrl"
|
||||
clearable
|
||||
placeholder="请输入短信发送回调 URL"
|
||||
/>
|
||||
<wd-textarea
|
||||
v-model="formData.remark"
|
||||
label="备注"
|
||||
label-width="200rpx"
|
||||
prop="remark"
|
||||
clearable
|
||||
placeholder="请输入备注"
|
||||
/>
|
||||
</wd-cell-group>
|
||||
</wd-form>
|
||||
</view>
|
||||
|
||||
<!-- 底部保存按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<wd-button
|
||||
type="primary"
|
||||
block
|
||||
:loading="formLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
保存
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance } from 'wot-design-uni/components/wd-form/types'
|
||||
import type { SmsChannel } from '@/api/system/sms/channel'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { createSmsChannel, getSmsChannel, updateSmsChannel } from '@/api/system/sms/channel'
|
||||
import { getIntDictOptions, getStrDictOptions } from '@/hooks/useDict'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { CommonStatusEnum, DICT_TYPE } from '@/utils/constants'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const getTitle = computed(() => props.id ? '编辑短信渠道' : '新增短信渠道')
|
||||
const formLoading = ref(false)
|
||||
const formData = ref<SmsChannel>({
|
||||
id: undefined,
|
||||
signature: '',
|
||||
code: '',
|
||||
status: CommonStatusEnum.ENABLE,
|
||||
apiKey: '',
|
||||
apiSecret: '',
|
||||
callbackUrl: '',
|
||||
remark: '',
|
||||
})
|
||||
const formRules = {
|
||||
signature: [{ required: true, message: '短信签名不能为空' }],
|
||||
code: [{ required: true, message: '渠道编码不能为空' }],
|
||||
status: [{ required: true, message: '启用状态不能为空' }],
|
||||
apiKey: [{ required: true, message: 'API 账号不能为空' }],
|
||||
}
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-system/sms/index')
|
||||
}
|
||||
|
||||
/** 加载详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
formData.value = await getSmsChannel(props.id)
|
||||
}
|
||||
|
||||
/** 提交表单 */
|
||||
async function handleSubmit() {
|
||||
const { valid } = await formRef.value.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
|
||||
formLoading.value = true
|
||||
try {
|
||||
if (props.id) {
|
||||
await updateSmsChannel(formData.value)
|
||||
toast.success('修改成功')
|
||||
} else {
|
||||
await createSmsChannel(formData.value)
|
||||
toast.success('新增成功')
|
||||
}
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
139
src/pages-system/sms/components/channel-list.vue
Normal file
139
src/pages-system/sms/components/channel-list.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<view>
|
||||
<!-- 搜索组件 -->
|
||||
<ChannelSearchForm @search="handleQuery" @reset="handleReset" />
|
||||
|
||||
<!-- 渠道列表 -->
|
||||
<view class="p-24rpx">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
|
||||
@click="handleDetail(item)"
|
||||
>
|
||||
<view class="p-24rpx">
|
||||
<view class="mb-16rpx flex items-center justify-between">
|
||||
<view class="text-32rpx text-[#333] font-semibold">
|
||||
{{ item.signature }}
|
||||
</view>
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="item.status" />
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">渠道编码:</text>
|
||||
<dict-tag :type="DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE" :value="item.code" />
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">API 账号:</text>
|
||||
<text class="min-w-0 flex-1 truncate">{{ item.apiKey }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">创建时间:</text>
|
||||
<text>{{ formatDateTime(item.createTime) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="loadMoreState !== 'loading' && list.length === 0" class="py-100rpx text-center">
|
||||
<wd-status-tip image="content" tip="暂无短信渠道数据" />
|
||||
</view>
|
||||
<wd-loadmore
|
||||
v-if="list.length > 0"
|
||||
:state="loadMoreState"
|
||||
@reload="loadMore"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 新增按钮 -->
|
||||
<wd-fab
|
||||
v-if="hasAccessByCodes(['system:sms-channel:create'])"
|
||||
position="right-bottom"
|
||||
type="primary"
|
||||
:expandable="false"
|
||||
@click="handleAdd"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { SmsChannel } from '@/api/system/sms/channel'
|
||||
import type { LoadMoreState } from '@/http/types'
|
||||
import { ref } from 'vue'
|
||||
import { getSmsChannelPage } from '@/api/system/sms/channel'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import ChannelSearchForm from './channel-search-form.vue'
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const total = ref(0)
|
||||
const list = ref<SmsChannel[]>([])
|
||||
const loadMoreState = ref<LoadMoreState>('loading')
|
||||
const queryParams = ref({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
|
||||
/** 查询列表 */
|
||||
async function getList() {
|
||||
loadMoreState.value = 'loading'
|
||||
try {
|
||||
const data = await getSmsChannelPage(queryParams.value)
|
||||
list.value = [...list.value, ...data.list]
|
||||
total.value = data.total
|
||||
loadMoreState.value = list.value.length >= total.value ? 'finished' : 'loading'
|
||||
} catch {
|
||||
queryParams.value.pageNo = queryParams.value.pageNo > 1 ? queryParams.value.pageNo - 1 : 1
|
||||
loadMoreState.value = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery(data?: Record<string, any>) {
|
||||
queryParams.value = {
|
||||
...data,
|
||||
pageNo: 1,
|
||||
pageSize: queryParams.value.pageSize,
|
||||
}
|
||||
list.value = []
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function handleReset() {
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 加载更多 */
|
||||
function loadMore() {
|
||||
if (loadMoreState.value === 'finished') {
|
||||
return
|
||||
}
|
||||
queryParams.value.pageNo++
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 新增 */
|
||||
function handleAdd() {
|
||||
uni.navigateTo({
|
||||
url: '/pages-system/sms/channel/form/index',
|
||||
})
|
||||
}
|
||||
|
||||
/** 查看详情 */
|
||||
function handleDetail(item: SmsChannel) {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/sms/channel/detail/index?id=${item.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 触底加载更多 */
|
||||
onReachBottom(() => {
|
||||
loadMore()
|
||||
})
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
176
src/pages-system/sms/components/channel-search-form.vue
Normal file
176
src/pages-system/sms/components/channel-search-form.vue
Normal file
@@ -0,0 +1,176 @@
|
||||
<template>
|
||||
<!-- 搜索框入口 -->
|
||||
<view @click="visible = true">
|
||||
<wd-search :placeholder="placeholder" hide-cancel disabled />
|
||||
</view>
|
||||
|
||||
<!-- 搜索弹窗 -->
|
||||
<wd-popup v-model="visible" position="top" @close="visible = false">
|
||||
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
短信签名
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.signature"
|
||||
placeholder="请输入短信签名"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
渠道编码
|
||||
</view>
|
||||
<wd-radio-group v-model="formData.code" shape="button">
|
||||
<wd-radio value="">
|
||||
全部
|
||||
</wd-radio>
|
||||
<wd-radio
|
||||
v-for="dict in getStrDictOptions(DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
状态
|
||||
</view>
|
||||
<wd-radio-group v-model="formData.status" shape="button">
|
||||
<wd-radio :value="-1">
|
||||
全部
|
||||
</wd-radio>
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
创建时间
|
||||
</view>
|
||||
<view class="yd-search-form-date-range-container">
|
||||
<view class="flex-1" @click="visibleCreateTime[0] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.createTime?.[0]) || '开始日期' }}
|
||||
</view>
|
||||
</view>
|
||||
-
|
||||
<view class="flex-1" @click="visibleCreateTime[1] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.createTime?.[1]) || '结束日期' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleCreateTime[0]" v-model="tempCreateTime[0]" type="date" />
|
||||
<view v-if="visibleCreateTime[0]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleCreateTime[0] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleCreateTime0Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleCreateTime[1]" v-model="tempCreateTime[1]" type="date" />
|
||||
<view v-if="visibleCreateTime[1]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleCreateTime[1] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleCreateTime1Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="yd-search-form-actions">
|
||||
<wd-button class="flex-1" plain @click="handleReset">
|
||||
重置
|
||||
</wd-button>
|
||||
<wd-button class="flex-1" type="primary" @click="handleSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { getDictLabel, getIntDictOptions, getStrDictOptions } from '@/hooks/useDict'
|
||||
import { getNavbarHeight } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDate, formatDateRange } from '@/utils/date'
|
||||
|
||||
const emit = defineEmits<{
|
||||
search: [data: Record<string, any>]
|
||||
reset: []
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const formData = reactive({
|
||||
signature: undefined as string | undefined,
|
||||
code: '',
|
||||
status: -1,
|
||||
createTime: [undefined, undefined] as [number | undefined, number | undefined],
|
||||
})
|
||||
|
||||
/** 搜索条件 placeholder 拼接 */
|
||||
const placeholder = computed(() => {
|
||||
const conditions: string[] = []
|
||||
if (formData.signature) {
|
||||
conditions.push(`签名:${formData.signature}`)
|
||||
}
|
||||
if (formData.code) {
|
||||
conditions.push(`渠道:${getDictLabel(DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE, formData.code)}`)
|
||||
}
|
||||
if (formData.status !== -1) {
|
||||
conditions.push(`状态:${getDictLabel(DICT_TYPE.COMMON_STATUS, formData.status)}`)
|
||||
}
|
||||
if (formData.createTime?.[0] && formData.createTime?.[1]) {
|
||||
conditions.push(`时间:${formatDate(formData.createTime[0])}~${formatDate(formData.createTime[1])}`)
|
||||
}
|
||||
return conditions.length > 0 ? conditions.join(' | ') : '搜索短信渠道'
|
||||
})
|
||||
|
||||
// 时间范围选择器状态
|
||||
const visibleCreateTime = ref<[boolean, boolean]>([false, false])
|
||||
const tempCreateTime = ref<[number, number]>([Date.now(), Date.now()])
|
||||
|
||||
/** 创建时间[0]确认 */
|
||||
function handleCreateTime0Confirm() {
|
||||
formData.createTime = [tempCreateTime.value[0], formData.createTime?.[1]]
|
||||
visibleCreateTime.value[0] = false
|
||||
}
|
||||
|
||||
/** 创建时间[1]确认 */
|
||||
function handleCreateTime1Confirm() {
|
||||
formData.createTime = [formData.createTime?.[0], tempCreateTime.value[1]]
|
||||
visibleCreateTime.value[1] = false
|
||||
}
|
||||
|
||||
/** 搜索 */
|
||||
function handleSearch() {
|
||||
visible.value = false
|
||||
emit('search', {
|
||||
signature: formData.signature || undefined,
|
||||
code: formData.code || undefined,
|
||||
status: formData.status === -1 ? undefined : formData.status,
|
||||
createTime: formatDateRange(formData.createTime),
|
||||
})
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
function handleReset() {
|
||||
formData.signature = undefined
|
||||
formData.code = ''
|
||||
formData.status = -1
|
||||
formData.createTime = [undefined, undefined]
|
||||
visible.value = false
|
||||
emit('reset')
|
||||
}
|
||||
</script>
|
||||
129
src/pages-system/sms/components/log-list.vue
Normal file
129
src/pages-system/sms/components/log-list.vue
Normal file
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<view>
|
||||
<!-- 搜索组件 -->
|
||||
<LogSearchForm @search="handleQuery" @reset="handleReset" />
|
||||
|
||||
<!-- 日志列表 -->
|
||||
<view class="p-24rpx">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
|
||||
@click="handleDetail(item)"
|
||||
>
|
||||
<view class="p-24rpx">
|
||||
<view class="mb-16rpx flex items-center justify-between">
|
||||
<view class="text-32rpx text-[#333] font-semibold">
|
||||
{{ item.mobile }}
|
||||
</view>
|
||||
<dict-tag :type="DICT_TYPE.SYSTEM_SMS_SEND_STATUS" :value="item.sendStatus" />
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">短信渠道:</text>
|
||||
<dict-tag :type="DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE" :value="item.channelCode" />
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">短信类型:</text>
|
||||
<dict-tag :type="DICT_TYPE.SYSTEM_SMS_TEMPLATE_TYPE" :value="item.templateType" />
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">短信内容:</text>
|
||||
<text class="min-w-0 flex-1 truncate">{{ item.templateContent }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">发送时间:</text>
|
||||
<text>{{ formatDateTime(item.sendTime) || '-' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="loadMoreState !== 'loading' && list.length === 0" class="py-100rpx text-center">
|
||||
<wd-status-tip image="content" tip="暂无短信日志数据" />
|
||||
</view>
|
||||
<wd-loadmore
|
||||
v-if="list.length > 0"
|
||||
:state="loadMoreState"
|
||||
@reload="loadMore"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { SmsLog } from '@/api/system/sms/log'
|
||||
import type { LoadMoreState } from '@/http/types'
|
||||
import { ref } from 'vue'
|
||||
import { getSmsLogPage } from '@/api/system/sms/log'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import LogSearchForm from './log-search-form.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
active?: boolean
|
||||
}>()
|
||||
|
||||
const total = ref(0)
|
||||
const list = ref<SmsLog[]>([])
|
||||
const loadMoreState = ref<LoadMoreState>('loading')
|
||||
const queryParams = ref({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
|
||||
/** 查询列表 */
|
||||
async function getList() {
|
||||
loadMoreState.value = 'loading'
|
||||
try {
|
||||
const data = await getSmsLogPage(queryParams.value)
|
||||
list.value = [...list.value, ...data.list]
|
||||
total.value = data.total
|
||||
loadMoreState.value = list.value.length >= total.value ? 'finished' : 'loading'
|
||||
} catch {
|
||||
queryParams.value.pageNo = queryParams.value.pageNo > 1 ? queryParams.value.pageNo - 1 : 1
|
||||
loadMoreState.value = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery(data?: Record<string, any>) {
|
||||
queryParams.value = {
|
||||
...data,
|
||||
pageNo: 1,
|
||||
pageSize: queryParams.value.pageSize,
|
||||
}
|
||||
list.value = []
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function handleReset() {
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 加载更多 */
|
||||
function loadMore() {
|
||||
if (loadMoreState.value === 'finished') {
|
||||
return
|
||||
}
|
||||
queryParams.value.pageNo++
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 查看详情 */
|
||||
function handleDetail(item: SmsLog) {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/sms/log/detail/index?id=${item.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 触底加载更多 */
|
||||
onReachBottom(() => {
|
||||
loadMore()
|
||||
})
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
176
src/pages-system/sms/components/log-search-form.vue
Normal file
176
src/pages-system/sms/components/log-search-form.vue
Normal file
@@ -0,0 +1,176 @@
|
||||
<template>
|
||||
<!-- 搜索框入口 -->
|
||||
<view @click="visible = true">
|
||||
<wd-search :placeholder="placeholder" hide-cancel disabled />
|
||||
</view>
|
||||
|
||||
<!-- 搜索弹窗 -->
|
||||
<wd-popup v-model="visible" position="top" @close="visible = false">
|
||||
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
手机号
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.mobile"
|
||||
placeholder="请输入手机号"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
发送状态
|
||||
</view>
|
||||
<wd-radio-group v-model="formData.sendStatus" shape="button">
|
||||
<wd-radio :value="-1">
|
||||
全部
|
||||
</wd-radio>
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_SMS_SEND_STATUS)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
接收状态
|
||||
</view>
|
||||
<wd-radio-group v-model="formData.receiveStatus" shape="button">
|
||||
<wd-radio :value="-1">
|
||||
全部
|
||||
</wd-radio>
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_SMS_RECEIVE_STATUS)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
发送时间
|
||||
</view>
|
||||
<view class="yd-search-form-date-range-container">
|
||||
<view class="flex-1" @click="visibleSendTime[0] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.sendTime?.[0]) || '开始日期' }}
|
||||
</view>
|
||||
</view>
|
||||
-
|
||||
<view class="flex-1" @click="visibleSendTime[1] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.sendTime?.[1]) || '结束日期' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleSendTime[0]" v-model="tempSendTime[0]" type="date" />
|
||||
<view v-if="visibleSendTime[0]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleSendTime[0] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleSendTime0Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleSendTime[1]" v-model="tempSendTime[1]" type="date" />
|
||||
<view v-if="visibleSendTime[1]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleSendTime[1] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleSendTime1Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="yd-search-form-actions">
|
||||
<wd-button class="flex-1" plain @click="handleReset">
|
||||
重置
|
||||
</wd-button>
|
||||
<wd-button class="flex-1" type="primary" @click="handleSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { getDictLabel, getIntDictOptions } from '@/hooks/useDict'
|
||||
import { getNavbarHeight } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDate, formatDateRange } from '@/utils/date'
|
||||
|
||||
const emit = defineEmits<{
|
||||
search: [data: Record<string, any>]
|
||||
reset: []
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const formData = reactive({
|
||||
mobile: undefined as string | undefined,
|
||||
sendStatus: -1,
|
||||
receiveStatus: -1,
|
||||
sendTime: [undefined, undefined] as [number | undefined, number | undefined],
|
||||
})
|
||||
|
||||
/** 搜索条件 placeholder 拼接 */
|
||||
const placeholder = computed(() => {
|
||||
const conditions: string[] = []
|
||||
if (formData.mobile) {
|
||||
conditions.push(`手机号:${formData.mobile}`)
|
||||
}
|
||||
if (formData.sendStatus !== -1) {
|
||||
conditions.push(`发送:${getDictLabel(DICT_TYPE.SYSTEM_SMS_SEND_STATUS, formData.sendStatus)}`)
|
||||
}
|
||||
if (formData.receiveStatus !== -1) {
|
||||
conditions.push(`接收:${getDictLabel(DICT_TYPE.SYSTEM_SMS_RECEIVE_STATUS, formData.receiveStatus)}`)
|
||||
}
|
||||
if (formData.sendTime?.[0] && formData.sendTime?.[1]) {
|
||||
conditions.push(`时间:${formatDate(formData.sendTime[0])}~${formatDate(formData.sendTime[1])}`)
|
||||
}
|
||||
return conditions.length > 0 ? conditions.join(' | ') : '搜索短信日志'
|
||||
})
|
||||
|
||||
// 时间范围选择器状态
|
||||
const visibleSendTime = ref<[boolean, boolean]>([false, false])
|
||||
const tempSendTime = ref<[number, number]>([Date.now(), Date.now()])
|
||||
|
||||
/** 发送时间[0]确认 */
|
||||
function handleSendTime0Confirm() {
|
||||
formData.sendTime = [tempSendTime.value[0], formData.sendTime?.[1]]
|
||||
visibleSendTime.value[0] = false
|
||||
}
|
||||
|
||||
/** 发送时间[1]确认 */
|
||||
function handleSendTime1Confirm() {
|
||||
formData.sendTime = [formData.sendTime?.[0], tempSendTime.value[1]]
|
||||
visibleSendTime.value[1] = false
|
||||
}
|
||||
|
||||
/** 搜索 */
|
||||
function handleSearch() {
|
||||
visible.value = false
|
||||
emit('search', {
|
||||
mobile: formData.mobile || undefined,
|
||||
sendStatus: formData.sendStatus === -1 ? undefined : formData.sendStatus,
|
||||
receiveStatus: formData.receiveStatus === -1 ? undefined : formData.receiveStatus,
|
||||
sendTime: formatDateRange(formData.sendTime),
|
||||
})
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
function handleReset() {
|
||||
formData.mobile = undefined
|
||||
formData.sendStatus = -1
|
||||
formData.receiveStatus = -1
|
||||
formData.sendTime = [undefined, undefined]
|
||||
visible.value = false
|
||||
emit('reset')
|
||||
}
|
||||
</script>
|
||||
143
src/pages-system/sms/components/template-list.vue
Normal file
143
src/pages-system/sms/components/template-list.vue
Normal file
@@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<view>
|
||||
<!-- 搜索组件 -->
|
||||
<TemplateSearchForm @search="handleQuery" @reset="handleReset" />
|
||||
|
||||
<!-- 模板列表 -->
|
||||
<view class="p-24rpx">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
|
||||
@click="handleDetail(item)"
|
||||
>
|
||||
<view class="p-24rpx">
|
||||
<view class="mb-16rpx flex items-center justify-between">
|
||||
<view class="text-32rpx text-[#333] font-semibold">
|
||||
{{ item.name }}
|
||||
</view>
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="item.status" />
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">模板编码:</text>
|
||||
<text class="min-w-0 flex-1 truncate">{{ item.code }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">短信类型:</text>
|
||||
<dict-tag :type="DICT_TYPE.SYSTEM_SMS_TEMPLATE_TYPE" :value="item.type" />
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">模板内容:</text>
|
||||
<text class="min-w-0 flex-1 truncate">{{ item.content }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">创建时间:</text>
|
||||
<text>{{ formatDateTime(item.createTime) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="loadMoreState !== 'loading' && list.length === 0" class="py-100rpx text-center">
|
||||
<wd-status-tip image="content" tip="暂无短信模板数据" />
|
||||
</view>
|
||||
<wd-loadmore
|
||||
v-if="list.length > 0"
|
||||
:state="loadMoreState"
|
||||
@reload="loadMore"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 新增按钮 -->
|
||||
<wd-fab
|
||||
v-if="hasAccessByCodes(['system:sms-template:create'])"
|
||||
position="right-bottom"
|
||||
type="primary"
|
||||
:expandable="false"
|
||||
@click="handleAdd"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { SmsTemplate } from '@/api/system/sms/template'
|
||||
import type { LoadMoreState } from '@/http/types'
|
||||
import { ref } from 'vue'
|
||||
import { getSmsTemplatePage } from '@/api/system/sms/template'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import TemplateSearchForm from './template-search-form.vue'
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const total = ref(0)
|
||||
const list = ref<SmsTemplate[]>([])
|
||||
const loadMoreState = ref<LoadMoreState>('loading')
|
||||
const queryParams = ref({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
|
||||
/** 查询列表 */
|
||||
async function getList() {
|
||||
loadMoreState.value = 'loading'
|
||||
try {
|
||||
const data = await getSmsTemplatePage(queryParams.value)
|
||||
list.value = [...list.value, ...data.list]
|
||||
total.value = data.total
|
||||
loadMoreState.value = list.value.length >= total.value ? 'finished' : 'loading'
|
||||
} catch {
|
||||
queryParams.value.pageNo = queryParams.value.pageNo > 1 ? queryParams.value.pageNo - 1 : 1
|
||||
loadMoreState.value = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery(data?: Record<string, any>) {
|
||||
queryParams.value = {
|
||||
...data,
|
||||
pageNo: 1,
|
||||
pageSize: queryParams.value.pageSize,
|
||||
}
|
||||
list.value = []
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function handleReset() {
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 加载更多 */
|
||||
function loadMore() {
|
||||
if (loadMoreState.value === 'finished') {
|
||||
return
|
||||
}
|
||||
queryParams.value.pageNo++
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 新增 */
|
||||
function handleAdd() {
|
||||
uni.navigateTo({
|
||||
url: '/pages-system/sms/template/form/index',
|
||||
})
|
||||
}
|
||||
|
||||
/** 查看详情 */
|
||||
function handleDetail(item: SmsTemplate) {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/sms/template/detail/index?id=${item.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 触底加载更多 */
|
||||
onReachBottom(() => {
|
||||
loadMore()
|
||||
})
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
192
src/pages-system/sms/components/template-search-form.vue
Normal file
192
src/pages-system/sms/components/template-search-form.vue
Normal file
@@ -0,0 +1,192 @@
|
||||
<template>
|
||||
<!-- 搜索框入口 -->
|
||||
<view @click="visible = true">
|
||||
<wd-search :placeholder="placeholder" hide-cancel disabled />
|
||||
</view>
|
||||
|
||||
<!-- 搜索弹窗 -->
|
||||
<wd-popup v-model="visible" position="top" @close="visible = false">
|
||||
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
模板名称
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.name"
|
||||
placeholder="请输入模板名称"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
模板编码
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.code"
|
||||
placeholder="请输入模板编码"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
短信类型
|
||||
</view>
|
||||
<wd-radio-group v-model="formData.type" shape="button">
|
||||
<wd-radio :value="-1">
|
||||
全部
|
||||
</wd-radio>
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_SMS_TEMPLATE_TYPE)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
开启状态
|
||||
</view>
|
||||
<wd-radio-group v-model="formData.status" shape="button">
|
||||
<wd-radio :value="-1">
|
||||
全部
|
||||
</wd-radio>
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
创建时间
|
||||
</view>
|
||||
<view class="yd-search-form-date-range-container">
|
||||
<view class="flex-1" @click="visibleCreateTime[0] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.createTime?.[0]) || '开始日期' }}
|
||||
</view>
|
||||
</view>
|
||||
-
|
||||
<view class="flex-1" @click="visibleCreateTime[1] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.createTime?.[1]) || '结束日期' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleCreateTime[0]" v-model="tempCreateTime[0]" type="date" />
|
||||
<view v-if="visibleCreateTime[0]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleCreateTime[0] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleCreateTime0Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleCreateTime[1]" v-model="tempCreateTime[1]" type="date" />
|
||||
<view v-if="visibleCreateTime[1]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleCreateTime[1] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleCreateTime1Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="yd-search-form-actions">
|
||||
<wd-button class="flex-1" plain @click="handleReset">
|
||||
重置
|
||||
</wd-button>
|
||||
<wd-button class="flex-1" type="primary" @click="handleSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { getDictLabel, getIntDictOptions } from '@/hooks/useDict'
|
||||
import { getNavbarHeight } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDate, formatDateRange } from '@/utils/date'
|
||||
|
||||
const emit = defineEmits<{
|
||||
search: [data: Record<string, any>]
|
||||
reset: []
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const formData = reactive({
|
||||
name: undefined as string | undefined,
|
||||
code: undefined as string | undefined,
|
||||
type: -1,
|
||||
status: -1,
|
||||
createTime: [undefined, undefined] as [number | undefined, number | undefined],
|
||||
})
|
||||
|
||||
/** 搜索条件 placeholder 拼接 */
|
||||
const placeholder = computed(() => {
|
||||
const conditions: string[] = []
|
||||
if (formData.name) {
|
||||
conditions.push(`名称:${formData.name}`)
|
||||
}
|
||||
if (formData.code) {
|
||||
conditions.push(`编码:${formData.code}`)
|
||||
}
|
||||
if (formData.type !== -1) {
|
||||
conditions.push(`类型:${getDictLabel(DICT_TYPE.SYSTEM_SMS_TEMPLATE_TYPE, formData.type)}`)
|
||||
}
|
||||
if (formData.status !== -1) {
|
||||
conditions.push(`状态:${getDictLabel(DICT_TYPE.COMMON_STATUS, formData.status)}`)
|
||||
}
|
||||
if (formData.createTime?.[0] && formData.createTime?.[1]) {
|
||||
conditions.push(`时间:${formatDate(formData.createTime[0])}~${formatDate(formData.createTime[1])}`)
|
||||
}
|
||||
return conditions.length > 0 ? conditions.join(' | ') : '搜索短信模板'
|
||||
})
|
||||
|
||||
// 时间范围选择器状态
|
||||
const visibleCreateTime = ref<[boolean, boolean]>([false, false])
|
||||
const tempCreateTime = ref<[number, number]>([Date.now(), Date.now()])
|
||||
|
||||
/** 创建时间[0]确认 */
|
||||
function handleCreateTime0Confirm() {
|
||||
formData.createTime = [tempCreateTime.value[0], formData.createTime?.[1]]
|
||||
visibleCreateTime.value[0] = false
|
||||
}
|
||||
|
||||
/** 创建时间[1]确认 */
|
||||
function handleCreateTime1Confirm() {
|
||||
formData.createTime = [formData.createTime?.[0], tempCreateTime.value[1]]
|
||||
visibleCreateTime.value[1] = false
|
||||
}
|
||||
|
||||
/** 搜索 */
|
||||
function handleSearch() {
|
||||
visible.value = false
|
||||
emit('search', {
|
||||
name: formData.name || undefined,
|
||||
code: formData.code || undefined,
|
||||
type: formData.type === -1 ? undefined : formData.type,
|
||||
status: formData.status === -1 ? undefined : formData.status,
|
||||
createTime: formatDateRange(formData.createTime),
|
||||
})
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
function handleReset() {
|
||||
formData.name = undefined
|
||||
formData.code = undefined
|
||||
formData.type = -1
|
||||
formData.status = -1
|
||||
formData.createTime = [undefined, undefined]
|
||||
visible.value = false
|
||||
emit('reset')
|
||||
}
|
||||
</script>
|
||||
55
src/pages-system/sms/index.vue
Normal file
55
src/pages-system/sms/index.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="短信管理"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- Tab 切换 -->
|
||||
<view class="bg-white">
|
||||
<wd-tabs v-model="tabIndex" shrink @change="handleTabChange">
|
||||
<wd-tab title="短信渠道" />
|
||||
<wd-tab title="短信模板" />
|
||||
<wd-tab title="短信日志" />
|
||||
</wd-tabs>
|
||||
</view>
|
||||
<!-- 列表内容 -->
|
||||
<ChannelList v-show="tabType === 'channel'" />
|
||||
<TemplateList v-show="tabType === 'template'" />
|
||||
<LogList v-show="tabType === 'log'" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import ChannelList from './components/channel-list.vue'
|
||||
import LogList from './components/log-list.vue'
|
||||
import TemplateList from './components/template-list.vue'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const tabTypes: string[] = ['channel', 'template', 'log']
|
||||
const tabIndex = ref(0)
|
||||
const tabType = computed<string>(() => tabTypes[tabIndex.value])
|
||||
|
||||
/** Tab 切换 */
|
||||
function handleTabChange({ index }: { index: number }) {
|
||||
tabIndex.value = index
|
||||
}
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
91
src/pages-system/sms/log/detail/index.vue
Normal file
91
src/pages-system/sms/log/detail/index.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="短信日志详情"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 详情内容 -->
|
||||
<view>
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="日志编号" :value="formData?.id" />
|
||||
<wd-cell title="手机号" :value="formData?.mobile" />
|
||||
<wd-cell title="短信渠道">
|
||||
<dict-tag :type="DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE" :value="formData?.channelCode" />
|
||||
</wd-cell>
|
||||
<wd-cell title="模板编号" :value="formData?.templateId" />
|
||||
<wd-cell title="模板类型">
|
||||
<dict-tag :type="DICT_TYPE.SYSTEM_SMS_TEMPLATE_TYPE" :value="formData?.templateType" />
|
||||
</wd-cell>
|
||||
<wd-cell title="短信内容" :value="formData?.templateContent" />
|
||||
<wd-cell title="发送状态">
|
||||
<dict-tag :type="DICT_TYPE.SYSTEM_SMS_SEND_STATUS" :value="formData?.sendStatus" />
|
||||
</wd-cell>
|
||||
<wd-cell title="发送时间" :value="formatDateTime(formData?.sendTime)" />
|
||||
<wd-cell title="API 发送编码" :value="formData?.apiSendCode" />
|
||||
<wd-cell title="API 发送消息" :value="formData?.apiSendMsg" />
|
||||
<wd-cell title="接收状态">
|
||||
<dict-tag :type="DICT_TYPE.SYSTEM_SMS_RECEIVE_STATUS" :value="formData?.receiveStatus" />
|
||||
</wd-cell>
|
||||
<wd-cell title="接收时间" :value="formatDateTime(formData?.receiveTime) || '-'" />
|
||||
<wd-cell title="API 接收编码" :value="formData?.apiReceiveCode ?? '-'" />
|
||||
<wd-cell title="API 接收消息" :value="formData?.apiReceiveMsg ?? '-'" />
|
||||
<wd-cell title="API 请求 ID" :value="formData?.apiRequestId ?? '-'" />
|
||||
<wd-cell title="API 序列号" :value="formData?.apiSerialNo ?? '-'" />
|
||||
<wd-cell title="创建时间" :value="formatDateTime(formData?.createTime)" />
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { SmsLog } from '@/api/system/sms/log'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { getSmsLog } from '@/api/system/sms/log'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const formData = ref<SmsLog>()
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-system/sms/index')
|
||||
}
|
||||
|
||||
/** 加载详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
toast.loading('加载中...')
|
||||
formData.value = await getSmsLog(Number(props.id))
|
||||
} finally {
|
||||
toast.close()
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
138
src/pages-system/sms/template/detail/components/send-form.vue
Normal file
138
src/pages-system/sms/template/detail/components/send-form.vue
Normal file
@@ -0,0 +1,138 @@
|
||||
<template>
|
||||
<!-- TODO @芋艿:【优化】底部操作的样式 -->
|
||||
<wd-popup v-model="visible" position="bottom" closable custom-style="border-radius: 16rpx 16rpx 0 0;">
|
||||
<view class="p-24rpx">
|
||||
<view class="mb-24rpx text-32rpx text-[#333] font-semibold">
|
||||
发送测试短信
|
||||
</view>
|
||||
<wd-form ref="sendFormRef" :model="sendFormData" :rules="sendFormRules">
|
||||
<wd-cell-group border>
|
||||
<wd-textarea
|
||||
v-model="sendFormData.content"
|
||||
label="模板内容"
|
||||
label-width="180rpx"
|
||||
disabled
|
||||
:rows="3"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="sendFormData.mobile"
|
||||
label="手机号码"
|
||||
label-width="180rpx"
|
||||
prop="mobile"
|
||||
clearable
|
||||
placeholder="请输入手机号码"
|
||||
/>
|
||||
<template v-for="param in template?.params" :key="param">
|
||||
<wd-input
|
||||
v-model="sendFormData.templateParams[param]"
|
||||
:label="`参数 ${param}`"
|
||||
label-width="180rpx"
|
||||
:prop="`templateParams.${param}`"
|
||||
clearable
|
||||
:placeholder="`请输入参数 ${param}`"
|
||||
/>
|
||||
</template>
|
||||
</wd-cell-group>
|
||||
</wd-form>
|
||||
<view class="mt-24rpx">
|
||||
<wd-button type="primary" block :loading="sendLoading" @click="handleSendSubmit">
|
||||
发送
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { SmsTemplate } from '@/api/system/sms/template'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { sendSms } from '@/api/system/sms/template'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
template?: SmsTemplate
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
'success': []
|
||||
}>()
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
const visible = computed({
|
||||
get() {
|
||||
return props.modelValue
|
||||
},
|
||||
set(value: boolean) {
|
||||
emit('update:modelValue', value)
|
||||
},
|
||||
})
|
||||
|
||||
const sendLoading = ref(false)
|
||||
const sendFormRef = ref<any>()
|
||||
const sendFormData = ref({
|
||||
content: '',
|
||||
mobile: '',
|
||||
templateParams: {} as Record<string, string>,
|
||||
})
|
||||
|
||||
/** 发送表单校验规则 */
|
||||
const sendFormRules = computed(() => {
|
||||
const rules: Record<string, any> = {
|
||||
mobile: [{ required: true, message: '手机号码不能为空' }],
|
||||
}
|
||||
if (props.template?.params) {
|
||||
props.template.params.forEach((param) => {
|
||||
rules[`templateParams.${param}`] = [{ required: true, message: `参数 ${param} 不能为空` }]
|
||||
})
|
||||
}
|
||||
return rules
|
||||
})
|
||||
|
||||
/** 初始化发送表单 */
|
||||
function initSendForm() {
|
||||
sendFormData.value = {
|
||||
content: props.template?.content || '',
|
||||
mobile: '',
|
||||
templateParams: {},
|
||||
}
|
||||
if (props.template?.params) {
|
||||
props.template.params.forEach((param) => {
|
||||
sendFormData.value.templateParams[param] = ''
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
if (val) {
|
||||
initSendForm()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
/** 提交发送 */
|
||||
async function handleSendSubmit() {
|
||||
const { valid } = await sendFormRef.value.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
|
||||
sendLoading.value = true
|
||||
try {
|
||||
await sendSms({
|
||||
mobile: sendFormData.value.mobile,
|
||||
templateCode: props.template?.code || '',
|
||||
templateParams: sendFormData.value.templateParams,
|
||||
})
|
||||
toast.success('短信发送成功')
|
||||
emit('success')
|
||||
visible.value = false
|
||||
} finally {
|
||||
sendLoading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
154
src/pages-system/sms/template/detail/index.vue
Normal file
154
src/pages-system/sms/template/detail/index.vue
Normal file
@@ -0,0 +1,154 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="短信模板详情"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 详情内容 -->
|
||||
<view>
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="模板编号" :value="formData?.id" />
|
||||
<wd-cell title="模板名称" :value="formData?.name" />
|
||||
<wd-cell title="模板编码" :value="formData?.code" />
|
||||
<wd-cell title="短信类型">
|
||||
<dict-tag :type="DICT_TYPE.SYSTEM_SMS_TEMPLATE_TYPE" :value="formData?.type" />
|
||||
</wd-cell>
|
||||
<wd-cell title="开启状态">
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="formData?.status" />
|
||||
</wd-cell>
|
||||
<wd-cell title="模板内容" :value="formData?.content" />
|
||||
<wd-cell title="API 模板编号" :value="formData?.apiTemplateId" />
|
||||
<wd-cell title="短信渠道">
|
||||
<dict-tag :type="DICT_TYPE.SYSTEM_SMS_CHANNEL_CODE" :value="formData?.channelCode" />
|
||||
</wd-cell>
|
||||
<wd-cell title="备注" :value="formData?.remark ?? '-'" />
|
||||
<wd-cell title="创建时间" :value="formatDateTime(formData?.createTime) || '-'" />
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
|
||||
<!-- 发送测试短信弹窗 -->
|
||||
<SendForm v-model="sendVisible" :template="formData" />
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<view class="yd-detail-footer-actions">
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['system:sms-template:send-sms'])"
|
||||
class="flex-1" type="primary" @click="handleSendTest"
|
||||
>
|
||||
测试
|
||||
</wd-button>
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['system:sms-template:update'])"
|
||||
class="flex-1" type="warning" @click="handleEdit"
|
||||
>
|
||||
编辑
|
||||
</wd-button>
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['system:sms-template:delete'])"
|
||||
class="flex-1" type="error" :loading="deleting" @click="handleDelete"
|
||||
>
|
||||
删除
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { SmsTemplate } from '@/api/system/sms/template'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { deleteSmsTemplate, getSmsTemplate } from '@/api/system/sms/template'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import SendForm from './components/send-form.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const toast = useToast()
|
||||
const formData = ref<SmsTemplate>()
|
||||
const deleting = ref(false)
|
||||
|
||||
// 发送测试短信相关
|
||||
const sendVisible = ref(false)
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-system/sms/index')
|
||||
}
|
||||
|
||||
/** 加载详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
toast.loading('加载中...')
|
||||
formData.value = await getSmsTemplate(props.id)
|
||||
} finally {
|
||||
toast.close()
|
||||
}
|
||||
}
|
||||
|
||||
/** 编辑 */
|
||||
function handleEdit() {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/sms/template/form/index?id=${props.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除 */
|
||||
function handleDelete() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要删除该短信模板吗?',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) {
|
||||
return
|
||||
}
|
||||
deleting.value = true
|
||||
try {
|
||||
await deleteSmsTemplate(props.id)
|
||||
toast.success('删除成功')
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 打开发送测试短信弹窗 */
|
||||
function handleSendTest() {
|
||||
sendVisible.value = true
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
215
src/pages-system/sms/template/form/index.vue
Normal file
215
src/pages-system/sms/template/form/index.vue
Normal file
@@ -0,0 +1,215 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
:title="getTitle"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 表单区域 -->
|
||||
<view>
|
||||
<wd-form ref="formRef" :model="formData" :rules="formRules">
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="短信类型" title-width="200rpx" prop="type" center>
|
||||
<wd-picker
|
||||
v-model="formData.type"
|
||||
:columns="templateTypeOptions"
|
||||
placeholder="请选择短信类型"
|
||||
/>
|
||||
</wd-cell>
|
||||
<wd-input
|
||||
v-model="formData.name"
|
||||
label="模板名称"
|
||||
label-width="200rpx"
|
||||
prop="name"
|
||||
clearable
|
||||
placeholder="请输入模板名称"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="formData.code"
|
||||
label="模板编码"
|
||||
label-width="200rpx"
|
||||
prop="code"
|
||||
clearable
|
||||
placeholder="请输入模板编码"
|
||||
/>
|
||||
<wd-cell title="短信渠道" title-width="200rpx" prop="channelId" center>
|
||||
<wd-picker
|
||||
v-model="formData.channelId"
|
||||
:columns="channelOptions"
|
||||
placeholder="请选择短信渠道"
|
||||
/>
|
||||
</wd-cell>
|
||||
<wd-cell title="开启状态" title-width="200rpx" prop="status" center>
|
||||
<wd-radio-group v-model="formData.status" shape="button">
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-cell>
|
||||
<wd-textarea
|
||||
v-model="formData.content"
|
||||
label="模板内容"
|
||||
label-width="200rpx"
|
||||
prop="content"
|
||||
clearable
|
||||
placeholder="请输入模板内容"
|
||||
:rows="4"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="formData.apiTemplateId"
|
||||
label="API 模板编号"
|
||||
label-width="200rpx"
|
||||
prop="apiTemplateId"
|
||||
clearable
|
||||
placeholder="请输入短信 API 的模板编号"
|
||||
/>
|
||||
<wd-textarea
|
||||
v-model="formData.remark"
|
||||
label="备注"
|
||||
label-width="200rpx"
|
||||
prop="remark"
|
||||
clearable
|
||||
placeholder="请输入备注"
|
||||
/>
|
||||
</wd-cell-group>
|
||||
</wd-form>
|
||||
</view>
|
||||
|
||||
<!-- 底部保存按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<wd-button
|
||||
type="primary"
|
||||
block
|
||||
:loading="formLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
保存
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance } from 'wot-design-uni/components/wd-form/types'
|
||||
import type { SmsChannel } from '@/api/system/sms/channel'
|
||||
import type { SmsTemplate } from '@/api/system/sms/template'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { getSimpleSmsChannelList } from '@/api/system/sms/channel'
|
||||
import { createSmsTemplate, getSmsTemplate, updateSmsTemplate } from '@/api/system/sms/template'
|
||||
import { getIntDictOptions } from '@/hooks/useDict'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { CommonStatusEnum, DICT_TYPE } from '@/utils/constants'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const getTitle = computed(() => props.id ? '编辑短信模板' : '新增短信模板')
|
||||
const formLoading = ref(false)
|
||||
const formData = ref<SmsTemplate>({
|
||||
id: undefined,
|
||||
type: undefined,
|
||||
name: '',
|
||||
code: '',
|
||||
channelId: undefined,
|
||||
status: CommonStatusEnum.ENABLE,
|
||||
content: '',
|
||||
apiTemplateId: '',
|
||||
remark: '',
|
||||
})
|
||||
const formRules = {
|
||||
type: [{ required: true, message: '短信类型不能为空' }],
|
||||
name: [{ required: true, message: '模板名称不能为空' }],
|
||||
code: [{ required: true, message: '模板编码不能为空' }],
|
||||
channelId: [{ required: true, message: '短信渠道不能为空' }],
|
||||
status: [{ required: true, message: '开启状态不能为空' }],
|
||||
content: [{ required: true, message: '模板内容不能为空' }],
|
||||
apiTemplateId: [{ required: true, message: 'API 模板编号不能为空' }],
|
||||
}
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/** 短信渠道列表 */
|
||||
const channelList = ref<SmsChannel[]>([])
|
||||
|
||||
/** 短信类型选项 */
|
||||
const templateTypeOptions = computed(() => {
|
||||
return getIntDictOptions(DICT_TYPE.SYSTEM_SMS_TEMPLATE_TYPE).map(item => ({
|
||||
value: item.value,
|
||||
label: item.label,
|
||||
}))
|
||||
})
|
||||
|
||||
/** 短信渠道选项 */
|
||||
const channelOptions = computed(() => {
|
||||
return channelList.value.map(item => ({
|
||||
value: item.id,
|
||||
label: item.signature,
|
||||
}))
|
||||
})
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-system/sms/index')
|
||||
}
|
||||
|
||||
/** 加载短信渠道列表 */
|
||||
async function loadChannelList() {
|
||||
channelList.value = await getSimpleSmsChannelList()
|
||||
}
|
||||
|
||||
/** 加载详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
formData.value = await getSmsTemplate(props.id)
|
||||
}
|
||||
|
||||
/** 提交表单 */
|
||||
async function handleSubmit() {
|
||||
const { valid } = await formRef.value.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
|
||||
formLoading.value = true
|
||||
try {
|
||||
if (props.id) {
|
||||
await updateSmsTemplate(formData.value)
|
||||
toast.success('修改成功')
|
||||
} else {
|
||||
await createSmsTemplate(formData.value)
|
||||
toast.success('新增成功')
|
||||
}
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
await loadChannelList()
|
||||
await getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
136
src/pages-system/social/client/detail/index.vue
Normal file
136
src/pages-system/social/client/detail/index.vue
Normal file
@@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="三方应用详情"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 详情内容 -->
|
||||
<view>
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="编号" :value="formData?.id" />
|
||||
<wd-cell title="应用名" :value="formData?.name" />
|
||||
<wd-cell title="社交平台">
|
||||
<dict-tag :type="DICT_TYPE.SYSTEM_SOCIAL_TYPE" :value="formData?.socialType" />
|
||||
</wd-cell>
|
||||
<wd-cell title="用户类型">
|
||||
<dict-tag :type="DICT_TYPE.USER_TYPE" :value="formData?.userType" />
|
||||
</wd-cell>
|
||||
<wd-cell title="应用编号" :value="formData?.clientId" />
|
||||
<wd-cell title="应用密钥" :value="formData?.clientSecret" />
|
||||
<wd-cell title="agentId" :value="formData?.agentId ?? '-'" />
|
||||
<wd-cell title="publicKey" :value="formData?.publicKey ?? '-'" />
|
||||
<wd-cell title="状态">
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="formData?.status" />
|
||||
</wd-cell>
|
||||
<wd-cell title="创建时间" :value="formatDateTime(formData?.createTime)" />
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<view class="yd-detail-footer-actions">
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['system:social-client:update'])"
|
||||
class="flex-1" type="warning" @click="handleEdit"
|
||||
>
|
||||
编辑
|
||||
</wd-button>
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['system:social-client:delete'])"
|
||||
class="flex-1" type="error" :loading="deleting" @click="handleDelete"
|
||||
>
|
||||
删除
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { SocialClient } from '@/api/system/social/client'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { deleteSocialClient, getSocialClient } from '@/api/system/social/client'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const toast = useToast()
|
||||
const formData = ref<SocialClient>()
|
||||
const deleting = ref(false)
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-system/social/index')
|
||||
}
|
||||
|
||||
/** 加载三方应用详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
toast.loading('加载中...')
|
||||
formData.value = await getSocialClient(props.id)
|
||||
} finally {
|
||||
toast.close()
|
||||
}
|
||||
}
|
||||
|
||||
/** 编辑三方应用 */
|
||||
function handleEdit() {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/social/client/form/index?id=${props.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除三方应用 */
|
||||
function handleDelete() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要删除该三方应用吗?',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) {
|
||||
return
|
||||
}
|
||||
deleting.value = true
|
||||
try {
|
||||
await deleteSocialClient(props.id)
|
||||
toast.success('删除成功')
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
193
src/pages-system/social/client/form/index.vue
Normal file
193
src/pages-system/social/client/form/index.vue
Normal file
@@ -0,0 +1,193 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
:title="getTitle"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 表单区域 -->
|
||||
<view>
|
||||
<wd-form ref="formRef" :model="formData" :rules="formRules">
|
||||
<wd-cell-group border>
|
||||
<wd-input
|
||||
v-model="formData.name"
|
||||
label="应用名"
|
||||
label-width="200rpx"
|
||||
prop="name"
|
||||
clearable
|
||||
placeholder="请输入应用名"
|
||||
/>
|
||||
<wd-cell title="社交平台" title-width="200rpx" prop="socialType" center>
|
||||
<wd-picker
|
||||
v-model="formData.socialType"
|
||||
:columns="getIntDictOptions(DICT_TYPE.SYSTEM_SOCIAL_TYPE)"
|
||||
label-key="label"
|
||||
value-key="value"
|
||||
placeholder="请选择社交平台"
|
||||
/>
|
||||
</wd-cell>
|
||||
<wd-cell title="用户类型" title-width="200rpx" prop="userType" center>
|
||||
<wd-radio-group v-model="formData.userType" shape="button">
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.USER_TYPE)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-cell>
|
||||
<wd-input
|
||||
v-model="formData.clientId"
|
||||
label="应用编号"
|
||||
label-width="200rpx"
|
||||
prop="clientId"
|
||||
clearable
|
||||
placeholder="请输入应用编号,对应各平台的 appKey"
|
||||
/>
|
||||
<wd-input
|
||||
v-model="formData.clientSecret"
|
||||
label="应用密钥"
|
||||
label-width="200rpx"
|
||||
prop="clientSecret"
|
||||
clearable
|
||||
placeholder="请输入应用密钥,对应各平台的 appSecret"
|
||||
/>
|
||||
<wd-input
|
||||
v-show="formData.socialType === 30"
|
||||
v-model="formData.agentId"
|
||||
label="agentId"
|
||||
label-width="200rpx"
|
||||
prop="agentId"
|
||||
clearable
|
||||
placeholder="授权方的网页应用 ID,有则填"
|
||||
/>
|
||||
<wd-input
|
||||
v-show="formData.socialType === 40"
|
||||
v-model="formData.publicKey"
|
||||
label="publicKey"
|
||||
label-width="200rpx"
|
||||
prop="publicKey"
|
||||
clearable
|
||||
placeholder="请输入 publicKey 公钥"
|
||||
/>
|
||||
<wd-cell title="状态" title-width="200rpx" prop="status" center>
|
||||
<wd-radio-group v-model="formData.status" shape="button">
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-cell>
|
||||
</wd-cell-group>
|
||||
</wd-form>
|
||||
</view>
|
||||
|
||||
<!-- 底部保存按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<wd-button
|
||||
type="primary"
|
||||
block
|
||||
:loading="formLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
保存
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance } from 'wot-design-uni/components/wd-form/types'
|
||||
import type { SocialClient } from '@/api/system/social/client'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { createSocialClient, getSocialClient, updateSocialClient } from '@/api/system/social/client'
|
||||
import { getIntDictOptions } from '@/hooks/useDict'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { CommonStatusEnum, DICT_TYPE } from '@/utils/constants'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const getTitle = computed(() => props.id ? '编辑三方应用' : '新增三方应用')
|
||||
const formLoading = ref(false)
|
||||
const formData = ref<SocialClient>({
|
||||
id: undefined,
|
||||
name: '',
|
||||
socialType: undefined,
|
||||
userType: 1,
|
||||
clientId: '',
|
||||
clientSecret: '',
|
||||
agentId: '',
|
||||
publicKey: '',
|
||||
status: CommonStatusEnum.ENABLE,
|
||||
})
|
||||
const formRules = {
|
||||
name: [{ required: true, message: '应用名不能为空' }],
|
||||
socialType: [{ required: true, message: '社交平台不能为空' }],
|
||||
userType: [{ required: true, message: '用户类型不能为空' }],
|
||||
clientId: [{ required: true, message: '应用编号不能为空' }],
|
||||
clientSecret: [{ required: true, message: '应用密钥不能为空' }],
|
||||
}
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-system/social/index')
|
||||
}
|
||||
|
||||
/** 加载三方应用详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
formData.value = await getSocialClient(props.id)
|
||||
}
|
||||
|
||||
/** 提交表单 */
|
||||
async function handleSubmit() {
|
||||
const { valid } = await formRef.value.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
|
||||
formLoading.value = true
|
||||
try {
|
||||
if (props.id) {
|
||||
await updateSocialClient(formData.value)
|
||||
toast.success('修改成功')
|
||||
} else {
|
||||
await createSocialClient(formData.value)
|
||||
toast.success('新增成功')
|
||||
}
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
143
src/pages-system/social/components/client-list.vue
Normal file
143
src/pages-system/social/components/client-list.vue
Normal file
@@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<view>
|
||||
<!-- 搜索组件 -->
|
||||
<ClientSearchForm @search="handleQuery" @reset="handleReset" />
|
||||
|
||||
<!-- 三方应用列表 -->
|
||||
<view class="p-24rpx">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
|
||||
@click="handleDetail(item)"
|
||||
>
|
||||
<view class="p-24rpx">
|
||||
<view class="mb-16rpx flex items-center justify-between">
|
||||
<view class="text-32rpx text-[#333] font-semibold">
|
||||
{{ item.name }}
|
||||
</view>
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="item.status" />
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">三方平台:</text>
|
||||
<dict-tag :type="DICT_TYPE.SYSTEM_SOCIAL_TYPE" :value="item.socialType" />
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">用户类型:</text>
|
||||
<dict-tag :type="DICT_TYPE.USER_TYPE" :value="item.userType" />
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">应用编号:</text>
|
||||
<text class="min-w-0 flex-1 truncate">{{ item.clientId }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">创建时间:</text>
|
||||
<text>{{ formatDateTime(item.createTime) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="loadMoreState !== 'loading' && list.length === 0" class="py-100rpx text-center">
|
||||
<wd-status-tip image="content" tip="暂无三方应用数据" />
|
||||
</view>
|
||||
<wd-loadmore
|
||||
v-if="list.length > 0"
|
||||
:state="loadMoreState"
|
||||
@reload="loadMore"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 新增按钮 -->
|
||||
<wd-fab
|
||||
v-if="hasAccessByCodes(['system:social-client:create'])"
|
||||
position="right-bottom"
|
||||
type="primary"
|
||||
:expandable="false"
|
||||
@click="handleAdd"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { SocialClient } from '@/api/system/social/client'
|
||||
import type { LoadMoreState } from '@/http/types'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { getSocialClientPage } from '@/api/system/social/client'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import ClientSearchForm from './client-search-form.vue'
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const total = ref(0)
|
||||
const list = ref<SocialClient[]>([])
|
||||
const loadMoreState = ref<LoadMoreState>('loading')
|
||||
const queryParams = ref({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
|
||||
/** 查询三方应用列表 */
|
||||
async function getList() {
|
||||
loadMoreState.value = 'loading'
|
||||
try {
|
||||
const data = await getSocialClientPage(queryParams.value)
|
||||
list.value = [...list.value, ...data.list]
|
||||
total.value = data.total
|
||||
loadMoreState.value = list.value.length >= total.value ? 'finished' : 'loading'
|
||||
} catch {
|
||||
queryParams.value.pageNo = queryParams.value.pageNo > 1 ? queryParams.value.pageNo - 1 : 1
|
||||
loadMoreState.value = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery(data?: Record<string, any>) {
|
||||
queryParams.value = {
|
||||
...data,
|
||||
pageNo: 1,
|
||||
pageSize: queryParams.value.pageSize,
|
||||
}
|
||||
list.value = []
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function handleReset() {
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 加载更多 */
|
||||
function loadMore() {
|
||||
if (loadMoreState.value === 'finished') {
|
||||
return
|
||||
}
|
||||
queryParams.value.pageNo++
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 新增三方应用 */
|
||||
function handleAdd() {
|
||||
uni.navigateTo({
|
||||
url: '/pages-system/social/client/form/index',
|
||||
})
|
||||
}
|
||||
|
||||
/** 查看详情 */
|
||||
function handleDetail(item: SocialClient) {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/social/client/detail/index?id=${item.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 触底加载更多 */
|
||||
onReachBottom(() => {
|
||||
loadMore()
|
||||
})
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
140
src/pages-system/social/components/client-search-form.vue
Normal file
140
src/pages-system/social/components/client-search-form.vue
Normal file
@@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<!-- 搜索框入口 -->
|
||||
<view @click="visible = true">
|
||||
<wd-search :placeholder="placeholder" hide-cancel disabled />
|
||||
</view>
|
||||
|
||||
<!-- 搜索弹窗 -->
|
||||
<wd-popup v-model="visible" position="top" @close="visible = false">
|
||||
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
应用名
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.name"
|
||||
placeholder="请输入应用名"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
三方平台
|
||||
</view>
|
||||
<wd-radio-group v-model="formData.socialType" shape="button">
|
||||
<wd-radio :value="-1">
|
||||
全部
|
||||
</wd-radio>
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_SOCIAL_TYPE)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
用户类型
|
||||
</view>
|
||||
<wd-radio-group v-model="formData.userType" shape="button">
|
||||
<wd-radio :value="-1">
|
||||
全部
|
||||
</wd-radio>
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.USER_TYPE)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
状态
|
||||
</view>
|
||||
<wd-radio-group v-model="formData.status" shape="button">
|
||||
<wd-radio :value="-1">
|
||||
全部
|
||||
</wd-radio>
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
<view class="yd-search-form-actions">
|
||||
<wd-button class="flex-1" plain @click="handleReset">
|
||||
重置
|
||||
</wd-button>
|
||||
<wd-button class="flex-1" type="primary" @click="handleSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { getDictLabel, getIntDictOptions } from '@/hooks/useDict'
|
||||
import { getNavbarHeight } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
|
||||
const emit = defineEmits<{
|
||||
search: [data: Record<string, any>]
|
||||
reset: []
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const formData = reactive({
|
||||
name: undefined as string | undefined,
|
||||
socialType: -1,
|
||||
userType: -1,
|
||||
status: -1,
|
||||
})
|
||||
|
||||
/** 搜索条件 placeholder 拼接 */
|
||||
const placeholder = computed(() => {
|
||||
const conditions: string[] = []
|
||||
if (formData.name) {
|
||||
conditions.push(`应用名:${formData.name}`)
|
||||
}
|
||||
if (formData.socialType !== -1) {
|
||||
conditions.push(`平台:${getDictLabel(DICT_TYPE.SYSTEM_SOCIAL_TYPE, formData.socialType)}`)
|
||||
}
|
||||
if (formData.userType !== -1) {
|
||||
conditions.push(`类型:${getDictLabel(DICT_TYPE.USER_TYPE, formData.userType)}`)
|
||||
}
|
||||
if (formData.status !== -1) {
|
||||
conditions.push(`状态:${getDictLabel(DICT_TYPE.COMMON_STATUS, formData.status)}`)
|
||||
}
|
||||
return conditions.length > 0 ? conditions.join(' | ') : '搜索三方应用'
|
||||
})
|
||||
|
||||
/** 搜索 */
|
||||
function handleSearch() {
|
||||
visible.value = false
|
||||
emit('search', {
|
||||
name: formData.name || undefined,
|
||||
socialType: formData.socialType === -1 ? undefined : formData.socialType,
|
||||
userType: formData.userType === -1 ? undefined : formData.userType,
|
||||
status: formData.status === -1 ? undefined : formData.status,
|
||||
})
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
function handleReset() {
|
||||
formData.name = undefined
|
||||
formData.socialType = -1
|
||||
formData.userType = -1
|
||||
formData.status = -1
|
||||
visible.value = false
|
||||
emit('reset')
|
||||
}
|
||||
</script>
|
||||
121
src/pages-system/social/components/user-list.vue
Normal file
121
src/pages-system/social/components/user-list.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<view>
|
||||
<!-- 搜索组件 -->
|
||||
<UserSearchForm @search="handleQuery" @reset="handleReset" />
|
||||
|
||||
<!-- 三方用户列表 -->
|
||||
<view class="p-24rpx">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
|
||||
@click="handleDetail(item)"
|
||||
>
|
||||
<view class="p-24rpx">
|
||||
<view class="mb-16rpx flex items-center justify-between">
|
||||
<view class="text-32rpx text-[#333] font-semibold">
|
||||
{{ item.nickname || '-' }}
|
||||
</view>
|
||||
<dict-tag :type="DICT_TYPE.SYSTEM_SOCIAL_TYPE" :value="item.type" />
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">社交 openid:</text>
|
||||
<text class="min-w-0 flex-1 truncate">{{ item.openid }}</text>
|
||||
</view>
|
||||
<view v-if="item.avatar" class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">头像:</text>
|
||||
<wd-img :src="item.avatar" width="60rpx" height="60rpx" />
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">创建时间:</text>
|
||||
<text>{{ formatDateTime(item.createTime) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="loadMoreState !== 'loading' && list.length === 0" class="py-100rpx text-center">
|
||||
<wd-status-tip image="content" tip="暂无三方用户数据" />
|
||||
</view>
|
||||
<wd-loadmore
|
||||
v-if="list.length > 0"
|
||||
:state="loadMoreState"
|
||||
@reload="loadMore"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { SocialUser } from '@/api/system/social/user'
|
||||
import type { LoadMoreState } from '@/http/types'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { getSocialUserPage } from '@/api/system/social/user'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import UserSearchForm from './user-search-form.vue'
|
||||
|
||||
const total = ref(0)
|
||||
const list = ref<SocialUser[]>([])
|
||||
const loadMoreState = ref<LoadMoreState>('loading')
|
||||
const queryParams = ref({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
|
||||
/** 查询三方用户列表 */
|
||||
async function getList() {
|
||||
loadMoreState.value = 'loading'
|
||||
try {
|
||||
const data = await getSocialUserPage(queryParams.value)
|
||||
list.value = [...list.value, ...data.list]
|
||||
total.value = data.total
|
||||
loadMoreState.value = list.value.length >= total.value ? 'finished' : 'loading'
|
||||
} catch {
|
||||
queryParams.value.pageNo = queryParams.value.pageNo > 1 ? queryParams.value.pageNo - 1 : 1
|
||||
loadMoreState.value = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery(data?: Record<string, any>) {
|
||||
queryParams.value = {
|
||||
...data,
|
||||
pageNo: 1,
|
||||
pageSize: queryParams.value.pageSize,
|
||||
}
|
||||
list.value = []
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function handleReset() {
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 加载更多 */
|
||||
function loadMore() {
|
||||
if (loadMoreState.value === 'finished') {
|
||||
return
|
||||
}
|
||||
queryParams.value.pageNo++
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 查看详情 */
|
||||
function handleDetail(item: SocialUser) {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/social/user/detail/index?id=${item.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 触底加载更多 */
|
||||
onReachBottom(() => {
|
||||
loadMore()
|
||||
})
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
110
src/pages-system/social/components/user-search-form.vue
Normal file
110
src/pages-system/social/components/user-search-form.vue
Normal file
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<!-- 搜索框入口 -->
|
||||
<view @click="visible = true">
|
||||
<wd-search :placeholder="placeholder" hide-cancel disabled />
|
||||
</view>
|
||||
|
||||
<!-- 搜索弹窗 -->
|
||||
<wd-popup v-model="visible" position="top" @close="visible = false">
|
||||
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
三方平台
|
||||
</view>
|
||||
<wd-radio-group v-model="formData.type" shape="button">
|
||||
<wd-radio :value="-1">
|
||||
全部
|
||||
</wd-radio>
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_SOCIAL_TYPE)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
用户昵称
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.nickname"
|
||||
placeholder="请输入用户昵称"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
社交 openid
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.openid"
|
||||
placeholder="请输入社交 openid"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-actions">
|
||||
<wd-button class="flex-1" plain @click="handleReset">
|
||||
重置
|
||||
</wd-button>
|
||||
<wd-button class="flex-1" type="primary" @click="handleSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { getDictLabel, getIntDictOptions } from '@/hooks/useDict'
|
||||
import { getNavbarHeight } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
|
||||
const emit = defineEmits<{
|
||||
search: [data: Record<string, any>]
|
||||
reset: []
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const formData = reactive({
|
||||
type: -1,
|
||||
nickname: undefined as string | undefined,
|
||||
openid: undefined as string | undefined,
|
||||
})
|
||||
|
||||
/** 搜索条件 placeholder 拼接 */
|
||||
const placeholder = computed(() => {
|
||||
const conditions: string[] = []
|
||||
if (formData.type !== -1) {
|
||||
conditions.push(`平台:${getDictLabel(DICT_TYPE.SYSTEM_SOCIAL_TYPE, formData.type)}`)
|
||||
}
|
||||
if (formData.nickname) {
|
||||
conditions.push(`昵称:${formData.nickname}`)
|
||||
}
|
||||
if (formData.openid) {
|
||||
conditions.push(`openid:${formData.openid}`)
|
||||
}
|
||||
return conditions.length > 0 ? conditions.join(' | ') : '搜索三方用户'
|
||||
})
|
||||
|
||||
/** 搜索 */
|
||||
function handleSearch() {
|
||||
visible.value = false
|
||||
emit('search', {
|
||||
type: formData.type === -1 ? undefined : formData.type,
|
||||
nickname: formData.nickname || undefined,
|
||||
openid: formData.openid || undefined,
|
||||
})
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
function handleReset() {
|
||||
formData.type = -1
|
||||
formData.nickname = undefined
|
||||
formData.openid = undefined
|
||||
visible.value = false
|
||||
emit('reset')
|
||||
}
|
||||
</script>
|
||||
53
src/pages-system/social/index.vue
Normal file
53
src/pages-system/social/index.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="三方用户管理"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- Tab 切换 -->
|
||||
<view class="bg-white">
|
||||
<wd-tabs v-model="tabIndex" shrink @change="handleTabChange">
|
||||
<wd-tab title="三方应用" />
|
||||
<wd-tab title="三方用户" />
|
||||
</wd-tabs>
|
||||
</view>
|
||||
|
||||
<!-- 列表内容 -->
|
||||
<ClientList v-show="tabType === 'client'" />
|
||||
<UserList v-show="tabType === 'user'" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import ClientList from './components/client-list.vue'
|
||||
import UserList from './components/user-list.vue'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const tabTypes: string[] = ['client', 'user']
|
||||
const tabIndex = ref(0)
|
||||
const tabType = computed<string>(() => tabTypes[tabIndex.value])
|
||||
|
||||
/** Tab 切换 */
|
||||
function handleTabChange({ index }: { index: number }) {
|
||||
tabIndex.value = index
|
||||
}
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
111
src/pages-system/social/user/detail/index.vue
Normal file
111
src/pages-system/social/user/detail/index.vue
Normal file
@@ -0,0 +1,111 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="三方用户详情"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 详情内容 -->
|
||||
<view>
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="三方平台">
|
||||
<dict-tag :type="DICT_TYPE.SYSTEM_SOCIAL_TYPE" :value="formData?.type" />
|
||||
</wd-cell>
|
||||
<wd-cell title="用户昵称" :value="formData?.nickname ?? '-'" />
|
||||
<wd-cell v-if="formData?.avatar" title="用户头像">
|
||||
<wd-img :src="formData.avatar" width="120rpx" height="120rpx" />
|
||||
</wd-cell>
|
||||
<wd-cell title="社交 openid" is-link @click="handleCopyText(formData?.openid, '社交 openid')">
|
||||
<view class="max-w-400rpx truncate text-right">
|
||||
{{ formData?.openid }}
|
||||
</view>
|
||||
</wd-cell>
|
||||
<wd-cell title="社交 token" is-link @click="handleCopyText(formData?.token, '社交 token')">
|
||||
<view class="max-w-400rpx truncate text-right">
|
||||
{{ formData?.token }}
|
||||
</view>
|
||||
</wd-cell>
|
||||
<wd-cell title="原始 Token 数据" is-link @click="handleCopyText(formData?.rawTokenInfo, '原始 Token 数据')">
|
||||
<view class="max-w-400rpx truncate text-right">
|
||||
{{ formData?.rawTokenInfo }}
|
||||
</view>
|
||||
</wd-cell>
|
||||
<wd-cell title="原始 User 数据" is-link @click="handleCopyText(formData?.rawUserInfo, '原始 User 数据')">
|
||||
<view class="max-w-400rpx truncate text-right">
|
||||
{{ formData?.rawUserInfo }}
|
||||
</view>
|
||||
</wd-cell>
|
||||
<wd-cell title="最后一次的认证 code" :value="formData?.code ?? '-'" />
|
||||
<wd-cell title="最后一次的认证 state" :value="formData?.state ?? '-'" />
|
||||
<wd-cell title="创建时间" :value="formatDateTime(formData?.createTime)" />
|
||||
<wd-cell title="更新时间" :value="formatDateTime(formData?.updateTime)" />
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { SocialUser } from '@/api/system/social/user'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { getSocialUser } from '@/api/system/social/user'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const formData = ref<SocialUser>()
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-system/social/index')
|
||||
}
|
||||
|
||||
/** 复制文本并提示 */
|
||||
function handleCopyText(text?: string, title?: string) {
|
||||
if (!text || text === '-') {
|
||||
return
|
||||
}
|
||||
uni.setClipboardData({
|
||||
data: text,
|
||||
success: () => {
|
||||
uni.hideToast()
|
||||
toast.success(`${title || '内容'}已复制`)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 加载三方用户详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
toast.loading('加载中...')
|
||||
formData.value = await getSocialUser(props.id)
|
||||
} finally {
|
||||
toast.close()
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
139
src/pages-system/tenant/components/package-list.vue
Normal file
139
src/pages-system/tenant/components/package-list.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<view>
|
||||
<!-- 搜索组件 -->
|
||||
<PackageSearchForm @search="handleQuery" @reset="handleReset" />
|
||||
|
||||
<!-- 租户套餐列表 -->
|
||||
<view class="p-24rpx">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
|
||||
@click="handleDetail(item)"
|
||||
>
|
||||
<view class="p-24rpx">
|
||||
<view class="mb-16rpx flex items-center justify-between">
|
||||
<view class="text-32rpx text-[#333] font-semibold">
|
||||
{{ item.name }}
|
||||
</view>
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="item.status" />
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">套餐编号:</text>
|
||||
<text class="min-w-0 flex-1 truncate">{{ item.id || '-' }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">备注:</text>
|
||||
<text class="min-w-0 flex-1 truncate">{{ item.remark || '-' }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">创建时间:</text>
|
||||
<text>{{ formatDateTime(item.createTime) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="loadMoreState !== 'loading' && list.length === 0" class="py-100rpx text-center">
|
||||
<wd-status-tip image="content" tip="暂无租户套餐数据" />
|
||||
</view>
|
||||
<wd-loadmore
|
||||
v-if="list.length > 0"
|
||||
:state="loadMoreState"
|
||||
@reload="loadMore"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 新增按钮 -->
|
||||
<wd-fab
|
||||
v-if="hasAccessByCodes(['system:tenant-package:create'])"
|
||||
position="right-bottom"
|
||||
type="primary"
|
||||
:expandable="false"
|
||||
@click="handleAdd"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { TenantPackage } from '@/api/system/tenant/package'
|
||||
import type { LoadMoreState } from '@/http/types'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { getTenantPackagePage } from '@/api/system/tenant/package'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import PackageSearchForm from './package-search-form.vue'
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const total = ref(0)
|
||||
const list = ref<TenantPackage[]>([])
|
||||
const loadMoreState = ref<LoadMoreState>('loading')
|
||||
const queryParams = ref({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
|
||||
/** 查询租户套餐列表 */
|
||||
async function getList() {
|
||||
loadMoreState.value = 'loading'
|
||||
try {
|
||||
const data = await getTenantPackagePage(queryParams.value)
|
||||
list.value = [...list.value, ...data.list]
|
||||
total.value = data.total
|
||||
loadMoreState.value = list.value.length >= total.value ? 'finished' : 'loading'
|
||||
} catch {
|
||||
queryParams.value.pageNo = queryParams.value.pageNo > 1 ? queryParams.value.pageNo - 1 : 1
|
||||
loadMoreState.value = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery(data?: Record<string, any>) {
|
||||
queryParams.value = {
|
||||
...data,
|
||||
pageNo: 1,
|
||||
pageSize: queryParams.value.pageSize,
|
||||
}
|
||||
list.value = []
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function handleReset() {
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 加载更多 */
|
||||
function loadMore() {
|
||||
if (loadMoreState.value === 'finished') {
|
||||
return
|
||||
}
|
||||
queryParams.value.pageNo++
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 新增租户套餐 */
|
||||
function handleAdd() {
|
||||
uni.navigateTo({
|
||||
url: '/pages-system/tenant/package/form/index',
|
||||
})
|
||||
}
|
||||
|
||||
/** 查看详情 */
|
||||
function handleDetail(item: TenantPackage) {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/tenant/package/detail/index?id=${item.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 触底加载更多 */
|
||||
onReachBottom(() => {
|
||||
loadMore()
|
||||
})
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
153
src/pages-system/tenant/components/package-search-form.vue
Normal file
153
src/pages-system/tenant/components/package-search-form.vue
Normal file
@@ -0,0 +1,153 @@
|
||||
<template>
|
||||
<!-- 搜索框入口 -->
|
||||
<view @click="visible = true">
|
||||
<wd-search :placeholder="placeholder" hide-cancel disabled />
|
||||
</view>
|
||||
|
||||
<!-- 搜索弹窗 -->
|
||||
<wd-popup v-model="visible" position="top" @close="visible = false">
|
||||
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
套餐名称
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.name"
|
||||
placeholder="请输入套餐名称"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
状态
|
||||
</view>
|
||||
<wd-radio-group v-model="formData.status" shape="button">
|
||||
<wd-radio :value="-1">
|
||||
全部
|
||||
</wd-radio>
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
创建时间
|
||||
</view>
|
||||
<view class="yd-search-form-date-range-container">
|
||||
<view class="flex-1" @click="visibleCreateTime[0] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.createTime?.[0]) || '开始日期' }}
|
||||
</view>
|
||||
</view>
|
||||
-
|
||||
<view class="flex-1" @click="visibleCreateTime[1] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.createTime?.[1]) || '结束日期' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleCreateTime[0]" v-model="tempCreateTime[0]" type="date" />
|
||||
<view v-if="visibleCreateTime[0]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleCreateTime[0] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleCreateTime0Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleCreateTime[1]" v-model="tempCreateTime[1]" type="date" />
|
||||
<view v-if="visibleCreateTime[1]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleCreateTime[1] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleCreateTime1Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="yd-search-form-actions">
|
||||
<wd-button class="flex-1" plain @click="handleReset">
|
||||
重置
|
||||
</wd-button>
|
||||
<wd-button class="flex-1" type="primary" @click="handleSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { getDictLabel, getIntDictOptions } from '@/hooks/useDict'
|
||||
import { getNavbarHeight } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDate, formatDateRange } from '@/utils/date'
|
||||
|
||||
const emit = defineEmits<{
|
||||
search: [data: Record<string, any>]
|
||||
reset: []
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const formData = reactive({
|
||||
name: undefined as string | undefined,
|
||||
status: -1, // -1 表示全部
|
||||
createTime: [undefined, undefined] as [number | undefined, number | undefined],
|
||||
})
|
||||
|
||||
/** 搜索条件 placeholder 拼接 */
|
||||
const placeholder = computed(() => {
|
||||
const conditions: string[] = []
|
||||
if (formData.name) {
|
||||
conditions.push(`套餐名称:${formData.name}`)
|
||||
}
|
||||
if (formData.status !== -1) {
|
||||
conditions.push(`状态:${getDictLabel(DICT_TYPE.COMMON_STATUS, formData.status)}`)
|
||||
}
|
||||
if (formData.createTime?.[0] && formData.createTime?.[1]) {
|
||||
conditions.push(`时间:${formatDate(formData.createTime[0])}~${formatDate(formData.createTime[1])}`)
|
||||
}
|
||||
return conditions.length > 0 ? conditions.join(' | ') : '搜索租户套餐'
|
||||
})
|
||||
|
||||
// 时间范围选择器状态
|
||||
const visibleCreateTime = ref<[boolean, boolean]>([false, false])
|
||||
const tempCreateTime = ref<[number, number]>([Date.now(), Date.now()])
|
||||
|
||||
/** 创建时间[0]确认 */
|
||||
function handleCreateTime0Confirm() {
|
||||
formData.createTime = [tempCreateTime.value[0], formData.createTime?.[1]]
|
||||
visibleCreateTime.value[0] = false
|
||||
}
|
||||
|
||||
/** 创建时间[1]确认 */
|
||||
function handleCreateTime1Confirm() {
|
||||
formData.createTime = [formData.createTime?.[0], tempCreateTime.value[1]]
|
||||
visibleCreateTime.value[1] = false
|
||||
}
|
||||
|
||||
/** 搜索 */
|
||||
function handleSearch() {
|
||||
visible.value = false
|
||||
emit('search', {
|
||||
name: formData.name || undefined,
|
||||
status: formData.status === -1 ? undefined : formData.status,
|
||||
createTime: formatDateRange(formData.createTime),
|
||||
})
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
function handleReset() {
|
||||
formData.name = undefined
|
||||
formData.status = -1
|
||||
formData.createTime = [undefined, undefined]
|
||||
visible.value = false
|
||||
emit('reset')
|
||||
}
|
||||
</script>
|
||||
169
src/pages-system/tenant/components/tenant-list.vue
Normal file
169
src/pages-system/tenant/components/tenant-list.vue
Normal file
@@ -0,0 +1,169 @@
|
||||
<template>
|
||||
<view>
|
||||
<!-- 搜索组件 -->
|
||||
<TenantSearchForm @search="handleQuery" @reset="handleReset" />
|
||||
|
||||
<!-- 租户列表 -->
|
||||
<view class="p-24rpx">
|
||||
<view
|
||||
v-for="item in list"
|
||||
:key="item.id"
|
||||
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
|
||||
@click="handleDetail(item)"
|
||||
>
|
||||
<view class="p-24rpx">
|
||||
<view class="mb-16rpx flex items-center justify-between">
|
||||
<view class="text-32rpx text-[#333] font-semibold">
|
||||
{{ item.name }}
|
||||
</view>
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="item.status" />
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx shrink-0 text-[#999]">联系人:</text>
|
||||
<text class="min-w-0 flex-1 truncate">{{ item.contactName || '-' }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">租户套餐:</text>
|
||||
<text>{{ getPackageName(item.packageId) }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">联系手机:</text>
|
||||
<text>{{ item.contactMobile || '-' }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">账号额度:</text>
|
||||
<text>{{ item.accountCount }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">过期时间:</text>
|
||||
<text>{{ formatDateTime(item.expireTime) }}</text>
|
||||
</view>
|
||||
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
|
||||
<text class="mr-8rpx text-[#999]">创建时间:</text>
|
||||
<text>{{ formatDateTime(item.createTime) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view v-if="loadMoreState !== 'loading' && list.length === 0" class="py-100rpx text-center">
|
||||
<wd-status-tip image="content" tip="暂无租户数据" />
|
||||
</view>
|
||||
<wd-loadmore
|
||||
v-if="list.length > 0"
|
||||
:state="loadMoreState"
|
||||
@reload="loadMore"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 新增按钮 -->
|
||||
<wd-fab
|
||||
v-if="hasAccessByCodes(['system:tenant:create'])"
|
||||
position="right-bottom"
|
||||
type="primary"
|
||||
:expandable="false"
|
||||
@click="handleAdd"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { Tenant } from '@/api/system/tenant'
|
||||
import type { TenantPackage } from '@/api/system/tenant/package'
|
||||
import type { LoadMoreState } from '@/http/types'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { getTenantPage } from '@/api/system/tenant'
|
||||
import { getTenantPackageList } from '@/api/system/tenant/package'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
import TenantSearchForm from './tenant-search-form.vue'
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const total = ref(0)
|
||||
const list = ref<Tenant[]>([])
|
||||
const packageList = ref<TenantPackage[]>([])
|
||||
const loadMoreState = ref<LoadMoreState>('loading')
|
||||
const queryParams = ref({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
|
||||
/** 获取套餐名称 */
|
||||
function getPackageName(packageId?: number) {
|
||||
if (packageId === 0) {
|
||||
return '系统租户'
|
||||
}
|
||||
const pkg = packageList.value.find(item => item.id === packageId)
|
||||
return pkg?.name || '-'
|
||||
}
|
||||
|
||||
/** 加载租户套餐列表 */
|
||||
async function loadPackageList() {
|
||||
packageList.value = await getTenantPackageList()
|
||||
}
|
||||
|
||||
/** 查询租户列表 */
|
||||
async function getList() {
|
||||
loadMoreState.value = 'loading'
|
||||
try {
|
||||
const data = await getTenantPage(queryParams.value)
|
||||
list.value = [...list.value, ...data.list]
|
||||
total.value = data.total
|
||||
loadMoreState.value = list.value.length >= total.value ? 'finished' : 'loading'
|
||||
} catch {
|
||||
queryParams.value.pageNo = queryParams.value.pageNo > 1 ? queryParams.value.pageNo - 1 : 1
|
||||
loadMoreState.value = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery(data?: Record<string, any>) {
|
||||
queryParams.value = {
|
||||
...data,
|
||||
pageNo: 1,
|
||||
pageSize: queryParams.value.pageSize,
|
||||
}
|
||||
list.value = []
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function handleReset() {
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 加载更多 */
|
||||
function loadMore() {
|
||||
if (loadMoreState.value === 'finished') {
|
||||
return
|
||||
}
|
||||
queryParams.value.pageNo++
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 新增租户 */
|
||||
function handleAdd() {
|
||||
uni.navigateTo({
|
||||
url: '/pages-system/tenant/tenant/form/index',
|
||||
})
|
||||
}
|
||||
|
||||
/** 查看详情 */
|
||||
function handleDetail(item: Tenant) {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/tenant/tenant/detail/index?id=${item.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 触底加载更多 */
|
||||
onReachBottom(() => {
|
||||
loadMore()
|
||||
})
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
await loadPackageList()
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
185
src/pages-system/tenant/components/tenant-search-form.vue
Normal file
185
src/pages-system/tenant/components/tenant-search-form.vue
Normal file
@@ -0,0 +1,185 @@
|
||||
<template>
|
||||
<!-- 搜索框入口 -->
|
||||
<view @click="visible = true">
|
||||
<wd-search :placeholder="placeholder" hide-cancel disabled />
|
||||
</view>
|
||||
|
||||
<!-- 搜索弹窗 -->
|
||||
<wd-popup v-model="visible" position="top" @close="visible = false">
|
||||
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
租户名
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.name"
|
||||
placeholder="请输入租户名"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
联系人
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.contactName"
|
||||
placeholder="请输入联系人"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
联系手机
|
||||
</view>
|
||||
<wd-input
|
||||
v-model="formData.contactMobile"
|
||||
placeholder="请输入联系手机"
|
||||
clearable
|
||||
/>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
状态
|
||||
</view>
|
||||
<wd-radio-group v-model="formData.status" shape="button">
|
||||
<wd-radio :value="-1">
|
||||
全部
|
||||
</wd-radio>
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</view>
|
||||
<view class="yd-search-form-item">
|
||||
<view class="yd-search-form-label">
|
||||
创建时间
|
||||
</view>
|
||||
<view class="yd-search-form-date-range-container">
|
||||
<view class="flex-1" @click="visibleCreateTime[0] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.createTime?.[0]) || '开始日期' }}
|
||||
</view>
|
||||
</view>
|
||||
-
|
||||
<view class="flex-1" @click="visibleCreateTime[1] = true">
|
||||
<view class="yd-search-form-date-range-picker">
|
||||
{{ formatDate(formData.createTime?.[1]) || '结束日期' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleCreateTime[0]" v-model="tempCreateTime[0]" type="date" />
|
||||
<view v-if="visibleCreateTime[0]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleCreateTime[0] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleCreateTime0Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
<wd-datetime-picker-view v-if="visibleCreateTime[1]" v-model="tempCreateTime[1]" type="date" />
|
||||
<view v-if="visibleCreateTime[1]" class="yd-search-form-date-range-actions">
|
||||
<wd-button size="small" plain @click="visibleCreateTime[1] = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button size="small" type="primary" @click="handleCreateTime1Confirm">
|
||||
确定
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="yd-search-form-actions">
|
||||
<wd-button class="flex-1" plain @click="handleReset">
|
||||
重置
|
||||
</wd-button>
|
||||
<wd-button class="flex-1" type="primary" @click="handleSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { getDictLabel, getIntDictOptions } from '@/hooks/useDict'
|
||||
import { getNavbarHeight } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDate, formatDateRange } from '@/utils/date'
|
||||
|
||||
const emit = defineEmits<{
|
||||
search: [data: Record<string, any>]
|
||||
reset: []
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const formData = reactive({
|
||||
name: undefined as string | undefined,
|
||||
contactName: undefined as string | undefined,
|
||||
contactMobile: undefined as string | undefined,
|
||||
status: -1, // -1 表示全部
|
||||
createTime: [undefined, undefined] as [number | undefined, number | undefined],
|
||||
})
|
||||
|
||||
/** 搜索条件 placeholder 拼接 */
|
||||
const placeholder = computed(() => {
|
||||
const conditions: string[] = []
|
||||
if (formData.name) {
|
||||
conditions.push(`租户名:${formData.name}`)
|
||||
}
|
||||
if (formData.contactName) {
|
||||
conditions.push(`联系人:${formData.contactName}`)
|
||||
}
|
||||
if (formData.contactMobile) {
|
||||
conditions.push(`手机:${formData.contactMobile}`)
|
||||
}
|
||||
if (formData.status !== -1) {
|
||||
conditions.push(`状态:${getDictLabel(DICT_TYPE.COMMON_STATUS, formData.status)}`)
|
||||
}
|
||||
if (formData.createTime?.[0] && formData.createTime?.[1]) {
|
||||
conditions.push(`时间:${formatDate(formData.createTime[0])}~${formatDate(formData.createTime[1])}`)
|
||||
}
|
||||
return conditions.length > 0 ? conditions.join(' | ') : '搜索租户'
|
||||
})
|
||||
|
||||
// 时间范围选择器状态
|
||||
const visibleCreateTime = ref<[boolean, boolean]>([false, false])
|
||||
const tempCreateTime = ref<[number, number]>([Date.now(), Date.now()])
|
||||
|
||||
/** 创建时间[0]确认 */
|
||||
function handleCreateTime0Confirm() {
|
||||
formData.createTime = [tempCreateTime.value[0], formData.createTime?.[1]]
|
||||
visibleCreateTime.value[0] = false
|
||||
}
|
||||
|
||||
/** 创建时间[1]确认 */
|
||||
function handleCreateTime1Confirm() {
|
||||
formData.createTime = [formData.createTime?.[0], tempCreateTime.value[1]]
|
||||
visibleCreateTime.value[1] = false
|
||||
}
|
||||
|
||||
/** 搜索 */
|
||||
function handleSearch() {
|
||||
visible.value = false
|
||||
emit('search', {
|
||||
name: formData.name || undefined,
|
||||
contactName: formData.contactName || undefined,
|
||||
contactMobile: formData.contactMobile || undefined,
|
||||
status: formData.status === -1 ? undefined : formData.status,
|
||||
createTime: formatDateRange(formData.createTime),
|
||||
})
|
||||
}
|
||||
|
||||
/** 重置 */
|
||||
function handleReset() {
|
||||
formData.name = undefined
|
||||
formData.contactName = undefined
|
||||
formData.contactMobile = undefined
|
||||
formData.status = -1
|
||||
formData.createTime = [undefined, undefined]
|
||||
visible.value = false
|
||||
emit('reset')
|
||||
}
|
||||
</script>
|
||||
53
src/pages-system/tenant/index.vue
Normal file
53
src/pages-system/tenant/index.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="租户管理"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- Tab 切换 -->
|
||||
<view class="bg-white">
|
||||
<wd-tabs v-model="tabIndex" shrink @change="handleTabChange">
|
||||
<wd-tab title="租户列表" />
|
||||
<wd-tab title="租户套餐" />
|
||||
</wd-tabs>
|
||||
</view>
|
||||
|
||||
<!-- 列表内容 -->
|
||||
<TenantList v-show="tabType === 'tenant'" />
|
||||
<PackageList v-show="tabType === 'package'" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import PackageList from './components/package-list.vue'
|
||||
import TenantList from './components/tenant-list.vue'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const tabTypes: string[] = ['tenant', 'package']
|
||||
const tabIndex = ref(0)
|
||||
const tabType = computed<string>(() => tabTypes[tabIndex.value])
|
||||
|
||||
/** Tab 切换 */
|
||||
function handleTabChange({ index }: { index: number }) {
|
||||
tabIndex.value = index
|
||||
}
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
127
src/pages-system/tenant/package/detail/index.vue
Normal file
127
src/pages-system/tenant/package/detail/index.vue
Normal file
@@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
title="租户套餐详情"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 详情内容 -->
|
||||
<view>
|
||||
<wd-cell-group border>
|
||||
<wd-cell title="套餐编号" :value="formData?.id" />
|
||||
<wd-cell title="套餐名称" :value="formData?.name" />
|
||||
<wd-cell title="状态">
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="formData?.status" />
|
||||
</wd-cell>
|
||||
<wd-cell title="备注" :value="formData?.remark ?? '-'" />
|
||||
<wd-cell title="创建时间" :value="formatDateTime(formData?.createTime)" />
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<view class="yd-detail-footer-actions">
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['system:tenant-package:update'])"
|
||||
class="flex-1" type="warning" @click="handleEdit"
|
||||
>
|
||||
编辑
|
||||
</wd-button>
|
||||
<wd-button
|
||||
v-if="hasAccessByCodes(['system:tenant-package:delete'])"
|
||||
class="flex-1" type="error" :loading="deleting" @click="handleDelete"
|
||||
>
|
||||
删除
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { TenantPackage } from '@/api/system/tenant/package'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { deleteTenantPackage, getTenantPackage } from '@/api/system/tenant/package'
|
||||
import { useAccess } from '@/hooks/useAccess'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { DICT_TYPE } from '@/utils/constants'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const { hasAccessByCodes } = useAccess()
|
||||
const toast = useToast()
|
||||
const formData = ref<TenantPackage>()
|
||||
const deleting = ref(false)
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-system/tenant/index')
|
||||
}
|
||||
|
||||
/** 加载租户套餐详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
toast.loading('加载中...')
|
||||
formData.value = await getTenantPackage(props.id)
|
||||
} finally {
|
||||
toast.close()
|
||||
}
|
||||
}
|
||||
|
||||
/** 编辑租户套餐 */
|
||||
function handleEdit() {
|
||||
uni.navigateTo({
|
||||
url: `/pages-system/tenant/package/form/index?id=${props.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
/** 删除租户套餐 */
|
||||
function handleDelete() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要删除该租户套餐吗?',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) {
|
||||
return
|
||||
}
|
||||
deleting.value = true
|
||||
try {
|
||||
await deleteTenantPackage(props.id)
|
||||
toast.success('删除成功')
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
deleting.value = false
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
141
src/pages-system/tenant/package/form/index.vue
Normal file
141
src/pages-system/tenant/package/form/index.vue
Normal file
@@ -0,0 +1,141 @@
|
||||
<template>
|
||||
<view class="yd-page-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<wd-navbar
|
||||
:title="getTitle"
|
||||
left-arrow placeholder safe-area-inset-top fixed
|
||||
@click-left="handleBack"
|
||||
/>
|
||||
|
||||
<!-- 表单区域 -->
|
||||
<view>
|
||||
<wd-form ref="formRef" :model="formData" :rules="formRules">
|
||||
<wd-cell-group border>
|
||||
<wd-input
|
||||
v-model="formData.name"
|
||||
label="套餐名称"
|
||||
label-width="200rpx"
|
||||
prop="name"
|
||||
clearable
|
||||
placeholder="请输入套餐名称"
|
||||
/>
|
||||
<wd-cell title="状态" title-width="200rpx" prop="status" center>
|
||||
<wd-radio-group v-model="formData.status" shape="button">
|
||||
<wd-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:value="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-cell>
|
||||
<wd-textarea
|
||||
v-model="formData.remark"
|
||||
label="备注"
|
||||
label-width="200rpx"
|
||||
prop="remark"
|
||||
clearable
|
||||
placeholder="请输入备注"
|
||||
/>
|
||||
</wd-cell-group>
|
||||
</wd-form>
|
||||
</view>
|
||||
|
||||
<!-- 底部保存按钮 -->
|
||||
<view class="yd-detail-footer">
|
||||
<wd-button
|
||||
type="primary"
|
||||
block
|
||||
:loading="formLoading"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
保存
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance } from 'wot-design-uni/components/wd-form/types'
|
||||
import type { TenantPackage } from '@/api/system/tenant/package'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { createTenantPackage, getTenantPackage, updateTenantPackage } from '@/api/system/tenant/package'
|
||||
import { getIntDictOptions } from '@/hooks/useDict'
|
||||
import { navigateBackPlus } from '@/utils'
|
||||
import { CommonStatusEnum, DICT_TYPE } from '@/utils/constants'
|
||||
|
||||
const props = defineProps<{
|
||||
id?: number | any
|
||||
}>()
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const getTitle = computed(() => props.id ? '编辑租户套餐' : '新增租户套餐')
|
||||
const formLoading = ref(false)
|
||||
const formData = ref<TenantPackage>({
|
||||
id: undefined,
|
||||
name: '',
|
||||
status: CommonStatusEnum.ENABLE,
|
||||
remark: '',
|
||||
menuIds: [],
|
||||
})
|
||||
const formRules = {
|
||||
name: [{ required: true, message: '套餐名称不能为空' }],
|
||||
status: [{ required: true, message: '状态不能为空' }],
|
||||
}
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/** 返回上一页 */
|
||||
function handleBack() {
|
||||
navigateBackPlus('/pages-system/tenant/index')
|
||||
}
|
||||
|
||||
/** 加载租户套餐详情 */
|
||||
async function getDetail() {
|
||||
if (!props.id) {
|
||||
return
|
||||
}
|
||||
formData.value = await getTenantPackage(props.id)
|
||||
}
|
||||
|
||||
/** 提交表单 */
|
||||
async function handleSubmit() {
|
||||
const { valid } = await formRef.value.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
|
||||
formLoading.value = true
|
||||
try {
|
||||
if (props.id) {
|
||||
await updateTenantPackage(formData.value)
|
||||
toast.success('修改成功')
|
||||
} else {
|
||||
await createTenantPackage(formData.value)
|
||||
toast.success('新增成功')
|
||||
}
|
||||
setTimeout(() => {
|
||||
handleBack()
|
||||
}, 500)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
// TODO @芋艿:这里有个租户套餐的设置;只支持 pc 操作;
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user