first commit

This commit is contained in:
2026-04-14 15:06:26 +08:00
commit 9ee0c6c597
582 changed files with 61051 additions and 0 deletions

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

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

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

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

View File

@@ -0,0 +1,94 @@
<template>
<!-- 搜索框入口 -->
<view @click="visible = true">
<wd-search :placeholder="placeholder" hide-cancel disabled />
</view>
<!-- 搜索弹窗 -->
<wd-popup v-model="visible" position="top" @close="visible = false">
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
<view class="yd-search-form-item">
<view class="yd-search-form-label">
部门名称
</view>
<wd-input
v-model="formData.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>

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

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

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

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

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

View File

@@ -0,0 +1,144 @@
<template>
<!-- 搜索框入口 -->
<view @click="visible = true">
<wd-search :placeholder="placeholder" hide-cancel disabled />
</view>
<!-- 搜索弹窗 -->
<wd-popup v-model="visible" position="top" @close="visible = false">
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
<view class="yd-search-form-item">
<view class="yd-search-form-label">
字典类型
</view>
<wd-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>

View File

@@ -0,0 +1,154 @@
<template>
<view>
<!-- 搜索组件 -->
<TypeSearchForm @search="handleQuery" @reset="handleReset" />
<!-- 字典类型列表 -->
<view class="p-24rpx">
<view
v-for="item in list"
:key="item.id"
class="mb-24rpx overflow-hidden rounded-12rpx bg-white shadow-sm"
@click="handleDetail(item)"
>
<view class="p-24rpx">
<view class="mb-16rpx flex items-center justify-between">
<view class="text-32rpx text-[#333] font-semibold">
{{ item.name }}
</view>
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="item.status" />
</view>
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
<text class="mr-8rpx shrink-0 text-[#999]">字典类型</text>
<text class="min-w-0 flex-1 truncate">{{ item.type }}</text>
</view>
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
<text class="mr-8rpx text-[#999]">备注</text>
<text class="min-w-0 flex-1 truncate">{{ item.remark || '-' }}</text>
</view>
<view class="mb-12rpx flex items-center text-28rpx text-[#666]">
<text class="mr-8rpx text-[#999]">创建时间</text>
<text>{{ formatDateTime(item.createTime) }}</text>
</view>
<!-- 查看数据按钮 -->
<view class="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>

View File

@@ -0,0 +1,169 @@
<template>
<!-- 搜索框入口 -->
<view @click="visible = true">
<wd-search :placeholder="placeholder" hide-cancel disabled />
</view>
<!-- 搜索弹窗 -->
<wd-popup v-model="visible" position="top" @close="visible = false">
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
<view class="yd-search-form-item">
<view class="yd-search-form-label">
字典名称
</view>
<wd-input
v-model="formData.name"
placeholder="请输入字典名称"
clearable
/>
</view>
<view class="yd-search-form-item">
<view class="yd-search-form-label">
字典类型
</view>
<wd-input
v-model="formData.type"
placeholder="请输入字典类型"
clearable
/>
</view>
<view class="yd-search-form-item">
<view class="yd-search-form-label">
状态
</view>
<wd-radio-group v-model="formData.status" shape="button">
<wd-radio :value="-1">
全部
</wd-radio>
<wd-radio
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
:key="dict.value"
:value="dict.value"
>
{{ dict.label }}
</wd-radio>
</wd-radio-group>
</view>
<view class="yd-search-form-item">
<view class="yd-search-form-label">
创建时间
</view>
<view class="yd-search-form-date-range-container">
<view class="flex-1" @click="visibleCreateTime[0] = true">
<view class="yd-search-form-date-range-picker">
{{ formatDate(formData.createTime?.[0]) || '开始日期' }}
</view>
</view>
-
<view class="flex-1" @click="visibleCreateTime[1] = true">
<view class="yd-search-form-date-range-picker">
{{ formatDate(formData.createTime?.[1]) || '结束日期' }}
</view>
</view>
</view>
<wd-datetime-picker-view v-if="visibleCreateTime[0]" v-model="tempCreateTime[0]" type="date" />
<view v-if="visibleCreateTime[0]" class="yd-search-form-date-range-actions">
<wd-button size="small" plain @click="visibleCreateTime[0] = false">
取消
</wd-button>
<wd-button size="small" type="primary" @click="handleCreateTime0Confirm">
确定
</wd-button>
</view>
<wd-datetime-picker-view v-if="visibleCreateTime[1]" v-model="tempCreateTime[1]" type="date" />
<view v-if="visibleCreateTime[1]" class="yd-search-form-date-range-actions">
<wd-button size="small" plain @click="visibleCreateTime[1] = false">
取消
</wd-button>
<wd-button size="small" type="primary" @click="handleCreateTime1Confirm">
确定
</wd-button>
</view>
</view>
<view class="yd-search-form-actions">
<wd-button class="flex-1" plain @click="handleReset">
重置
</wd-button>
<wd-button class="flex-1" type="primary" @click="handleSearch">
搜索
</wd-button>
</view>
</view>
</wd-popup>
</template>
<script lang="ts" setup>
import { computed, reactive, ref } from 'vue'
import { getDictLabel, getIntDictOptions } from '@/hooks/useDict'
import { getNavbarHeight } from '@/utils'
import { DICT_TYPE } from '@/utils/constants'
import { formatDate, formatDateRange } from '@/utils/date'
const emit = defineEmits<{
search: [data: Record<string, any>]
reset: []
}>()
const visible = ref(false)
const formData = reactive({
name: undefined as string | undefined,
type: undefined as string | undefined,
status: -1,
createTime: [undefined, undefined] as [number | undefined, number | undefined],
})
/** 搜索条件 placeholder 拼接 */
const placeholder = computed(() => {
const conditions: string[] = []
if (formData.name) {
conditions.push(`名称:${formData.name}`)
}
if (formData.type) {
conditions.push(`类型:${formData.type}`)
}
if (formData.status !== -1) {
conditions.push(`状态:${getDictLabel(DICT_TYPE.COMMON_STATUS, formData.status)}`)
}
if (formData.createTime?.[0] && formData.createTime?.[1]) {
conditions.push(`时间:${formatDate(formData.createTime[0])}~${formatDate(formData.createTime[1])}`)
}
return conditions.length > 0 ? conditions.join(' | ') : '搜索字典类型'
})
// 时间范围选择器状态
const visibleCreateTime = ref<[boolean, boolean]>([false, false])
const tempCreateTime = ref<[number, number]>([Date.now(), Date.now()])
/** 创建时间[0]确认 */
function handleCreateTime0Confirm() {
formData.createTime = [tempCreateTime.value[0], formData.createTime?.[1]]
visibleCreateTime.value[0] = false
}
/** 创建时间[1]确认 */
function handleCreateTime1Confirm() {
formData.createTime = [formData.createTime?.[0], tempCreateTime.value[1]]
visibleCreateTime.value[1] = false
}
/** 搜索 */
function handleSearch() {
visible.value = false
emit('search', {
name: formData.name || undefined,
type: formData.type || undefined,
status: formData.status === -1 ? undefined : formData.status,
createTime: formatDateRange(formData.createTime),
})
}
/** 重置 */
function handleReset() {
formData.name = undefined
formData.type = undefined
formData.status = -1
formData.createTime = [undefined, undefined]
visible.value = false
emit('reset')
}
</script>

View File

@@ -0,0 +1,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>

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

View File

@@ -0,0 +1,59 @@
<template>
<view class="yd-page-container">
<!-- 顶部导航栏 -->
<wd-navbar
title="字典管理"
left-arrow placeholder safe-area-inset-top fixed
@click-left="handleBack"
/>
<!-- Tab 切换 -->
<view class="bg-white">
<wd-tabs v-model="tabIndex" shrink @change="handleTabChange">
<wd-tab title="字典类型" />
<wd-tab title="字典数据" />
</wd-tabs>
</view>
<!-- 列表内容 -->
<TypeList v-show="tabType === 'type'" @select="handleTypeSelect" />
<DataList v-show="tabType === 'data'" :dict-type="selectedDictType" />
</view>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue'
import { navigateBackPlus } from '@/utils'
import DataList from './components/data-list.vue'
import TypeList from './components/type-list.vue'
definePage({
style: {
navigationBarTitleText: '',
navigationStyle: 'custom',
},
})
const tabTypes: string[] = ['type', 'data']
const tabIndex = ref(0)
const tabType = computed<string>(() => tabTypes[tabIndex.value])
const selectedDictType = ref<string>() // 选中的字典类型
/** Tab 切换 */
function handleTabChange({ index }: { index: number }) {
tabIndex.value = index
}
/** 选择字典类型 */
function handleTypeSelect(dictType: string) {
selectedDictType.value = dictType
tabIndex.value = 1 // 切换到字典数据 tab
}
/** 返回上一页 */
function handleBack() {
navigateBackPlus()
}
</script>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,128 @@
<template>
<view class="yd-page-container">
<!-- 顶部导航栏 -->
<wd-navbar
title="字典类型详情"
left-arrow placeholder safe-area-inset-top fixed
@click-left="handleBack"
/>
<!-- 详情内容 -->
<view>
<wd-cell-group border>
<wd-cell title="字典编号" :value="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>

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

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

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

View File

@@ -0,0 +1,144 @@
<template>
<!-- 搜索框入口 -->
<view @click="visible = true">
<wd-search :placeholder="placeholder" hide-cancel disabled />
</view>
<!-- 搜索弹窗 -->
<wd-popup v-model="visible" position="top" @close="visible = false">
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
<view class="yd-search-form-item">
<view class="yd-search-form-label">
用户名称
</view>
<wd-input
v-model="formData.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>

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View File

@@ -0,0 +1,94 @@
<template>
<!-- 搜索框入口 -->
<view @click="visible = true">
<wd-search :placeholder="placeholder" hide-cancel disabled />
</view>
<!-- 搜索弹窗 -->
<wd-popup v-model="visible" position="top" @close="visible = false">
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
<view class="yd-search-form-item">
<view class="yd-search-form-label">
公告标题
</view>
<wd-input
v-model="formData.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>

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

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

View File

@@ -0,0 +1,163 @@
<template>
<view class="yd-page-container">
<!-- 顶部导航栏 -->
<wd-navbar
title="通知公告管理"
left-arrow placeholder safe-area-inset-top fixed
@click-left="handleBack"
/>
<!-- 搜索组件 -->
<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>

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

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

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

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

View File

@@ -0,0 +1,52 @@
<template>
<view class="yd-page-container">
<!-- 顶部导航栏 -->
<wd-navbar
title="站内信管理"
left-arrow placeholder safe-area-inset-top fixed
@click-left="handleBack"
/>
<!-- Tab 切换 -->
<view class="bg-white">
<wd-tabs v-model="tabIndex" shrink @change="handleTabChange">
<wd-tab title="站内信模板" />
<wd-tab title="站内信消息" />
</wd-tabs>
</view>
<!-- 列表内容 -->
<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>

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

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

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

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

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

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

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

View File

@@ -0,0 +1,94 @@
<template>
<!-- 搜索框入口 -->
<view @click="visible = true">
<wd-search :placeholder="placeholder" hide-cancel disabled />
</view>
<!-- 搜索弹窗 -->
<wd-popup v-model="visible" position="top" @close="visible = false">
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
<view class="yd-search-form-item">
<view class="yd-search-form-label">
应用名
</view>
<wd-input
v-model="formData.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>

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

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

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

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

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

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

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

View File

@@ -0,0 +1,128 @@
<template>
<view class="yd-page-container">
<!-- 顶部导航栏 -->
<wd-navbar
title="岗位详情"
left-arrow placeholder safe-area-inset-top fixed
@click-left="handleBack"
/>
<!-- 详情内容 -->
<view>
<wd-cell-group border>
<wd-cell title="岗位名称" :value="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>

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View File

@@ -0,0 +1,153 @@
<template>
<!-- 搜索框入口 -->
<view @click="visible = true">
<wd-search :placeholder="placeholder" hide-cancel disabled />
</view>
<!-- 搜索弹窗 -->
<wd-popup v-model="visible" position="top" @close="visible = false">
<view class="yd-search-form-container" :style="{ paddingTop: `${getNavbarHeight()}px` }">
<view class="yd-search-form-item">
<view class="yd-search-form-label">
套餐名称
</view>
<wd-input
v-model="formData.name"
placeholder="请输入套餐名称"
clearable
/>
</view>
<view class="yd-search-form-item">
<view class="yd-search-form-label">
状态
</view>
<wd-radio-group v-model="formData.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>

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

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

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

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

View 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