Files
wwjcloud-nest-v1/admin-vben/apps/web-antd/src/views/site/group.vue
wanwu e7a1d6b4d6 🧹 清理重复配置文件
- 删除根目录中重复的 NestJS 配置文件
- 删除 tsconfig.json, tsconfig.build.json, eslint.config.mjs, .prettierrc
- 保留 wwjcloud-nest/ 目录中的完整配置
- 避免配置冲突,确保项目结构清晰
2025-10-14 23:56:20 +08:00

557 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="site-group-container">
<Card :bordered="false" class="search-card">
<Form
:model="searchForm"
layout="inline"
@finish="handleSearch"
class="search-form"
>
<FormItem name="keywords">
<Input
v-model:value="searchForm.keywords"
placeholder="请输入分组名称"
allow-clear
/>
</FormItem>
<FormItem>
<Button type="primary" html-type="submit" :loading="loading">
搜索
</Button>
<Button @click="handleReset" style="margin-left: 8px">
重置
</Button>
</FormItem>
</Form>
<div class="action-bar">
<Button type="primary" @click="handleAdd">
添加站点分组
</Button>
</div>
</Card>
<Card :bordered="false" class="table-card">
<Table
:columns="columns"
:data-source="tableData"
:loading="loading"
:pagination="pagination"
@change="handleTableChange"
row-key="group_id"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'app_name'">
<div v-if="record.app_list && record.app_list.length > 0" class="app-list">
<Tooltip
placement="top-start"
:title="getAppTooltipContent(record.app_list)"
>
<div class="app-icons">
<div
v-for="(app, index) in record.app_list.slice(0, 4)"
:key="index"
class="app-icon"
>
<Avatar
:src="app.icon"
:size="54"
shape="square"
/>
</div>
<div
v-if="record.app_list.length > 4"
class="app-more"
>
<span>...</span>
</div>
</div>
</Tooltip>
</div>
<div v-else class="empty-apps">
暂无应用
</div>
</template>
<template v-else-if="column.key === 'addon_name'">
<div v-if="record.addon_list && record.addon_list.length > 0" class="addon-list">
<Tooltip
placement="top-start"
:title="getAddonTooltipContent(record.addon_list)"
>
<div class="addon-icons">
<div
v-for="(addon, index) in record.addon_list.slice(0, 4)"
:key="index"
class="addon-icon"
>
<Avatar
:src="addon.icon"
:size="54"
shape="square"
/>
</div>
<div
v-if="record.addon_list.length > 4"
class="addon-more"
>
<span>...</span>
</div>
</div>
</Tooltip>
</div>
<div v-else class="empty-addons">
暂无插件
</div>
</template>
<template v-else-if="column.key === 'action'">
<Space>
<Button type="link" @click="handleEdit(record)">
编辑
</Button>
<Button type="link" @click="handleDelete(record)" danger>
删除
</Button>
</Space>
</template>
</template>
</Table>
</Card>
<!-- 添加/编辑分组对话框 -->
<Modal
v-model:open="groupModalVisible"
:title="isEdit ? '编辑站点分组' : '添加站点分组'"
@ok="handleSubmit"
:confirm-loading="submitLoading"
width="600px"
>
<Form
ref="groupFormRef"
:model="groupForm"
:rules="groupRules"
layout="vertical"
>
<FormItem label="分组名称" name="group_name" required>
<Input
v-model:value="groupForm.group_name"
placeholder="请输入分组名称"
maxlength="50"
show-count
/>
</FormItem>
<FormItem label="分组描述" name="group_desc">
<TextArea
v-model:value="groupForm.group_desc"
placeholder="请输入分组描述"
:rows="3"
maxlength="255"
show-count
/>
</FormItem>
<FormItem label="应用" name="app">
<Select
v-model:value="groupForm.app"
mode="multiple"
placeholder="请选择应用"
:options="appOptions"
:filter-option="filterAppOption"
show-search
/>
</FormItem>
<FormItem label="插件" name="addon">
<Select
v-model:value="groupForm.addon"
mode="multiple"
placeholder="请选择插件"
:options="addonOptions"
:filter-option="filterAddonOption"
show-search
/>
</FormItem>
</Form>
</Modal>
<!-- 删除确认对话框 -->
<Modal
v-model:open="deleteModalVisible"
title="确认删除"
@ok="confirmDelete"
:confirm-loading="deleteLoading"
>
<p>确定要删除该站点分组吗删除后无法恢复</p>
<p v-if="currentRecord?.site_count > 0" class="text-red-500">
注意该分组下还有 {{ currentRecord.site_count }} 个站点删除分组可能会影响这些站点
</p>
</Modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import {
Card,
Form,
FormItem,
Input,
Button,
Table,
Space,
Modal,
Avatar,
Tooltip,
Select,
TextArea
} from 'ant-design-vue'
import { useSiteApi } from '@/api/site'
// API
const siteApi = useSiteApi()
// 响应式数据
const loading = ref(false)
const submitLoading = ref(false)
const deleteLoading = ref(false)
const groupModalVisible = ref(false)
const deleteModalVisible = ref(false)
const isEdit = ref(false)
const currentRecord = ref<any>(null)
const groupFormRef = ref()
// 表单数据
const searchForm = reactive({
keywords: ''
})
const groupForm = reactive({
group_id: '',
group_name: '',
group_desc: '',
app: [],
addon: []
})
// 表格数据
const tableData = ref([])
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total: number) => `${total} 条记录`
})
// 选项数据
const appOptions = ref([])
const addonOptions = ref([])
// 表单验证规则
const groupRules = {
group_name: [
{ required: true, message: '请输入分组名称', trigger: 'blur' },
{ max: 50, message: '分组名称不能超过50个字符', trigger: 'blur' }
],
group_desc: [
{ max: 255, message: '分组描述不能超过255个字符', trigger: 'blur' }
]
}
// 表格列定义
const columns = [
{
title: '分组名称',
dataIndex: 'group_name',
key: 'group_name',
width: 150
},
{
title: '应用',
key: 'app_name',
width: 250
},
{
title: '插件',
key: 'addon_name',
width: 250
},
{
title: '创建时间',
dataIndex: 'create_time',
key: 'create_time',
width: 120
},
{
title: '操作',
key: 'action',
width: 120,
fixed: 'right'
}
]
// 方法
const loadGroupList = async () => {
loading.value = true
try {
const params = {
page: pagination.current,
limit: pagination.pageSize,
...searchForm
}
const response = await siteApi.getSiteGroupList(params)
tableData.value = response.data.list || []
pagination.total = response.data.total || 0
} catch (error) {
message.error('加载分组列表失败')
} finally {
loading.value = false
}
}
const handleSearch = () => {
pagination.current = 1
loadGroupList()
}
const handleReset = () => {
searchForm.keywords = ''
pagination.current = 1
loadGroupList()
}
const handleTableChange = (pag: any) => {
pagination.current = pag.current
pagination.pageSize = pag.pageSize
loadGroupList()
}
const handleAdd = () => {
isEdit.value = false
resetGroupForm()
groupModalVisible.value = true
}
const handleEdit = (record: any) => {
isEdit.value = true
currentRecord.value = record
Object.assign(groupForm, {
group_id: record.group_id,
group_name: record.group_name,
group_desc: record.group_desc,
app: record.app ? record.app.split(',').filter(Boolean) : [],
addon: record.addon ? record.addon.split(',').filter(Boolean) : []
})
groupModalVisible.value = true
}
const handleDelete = (record: any) => {
currentRecord.value = record
deleteModalVisible.value = true
}
const handleSubmit = async () => {
try {
await groupFormRef.value.validate()
submitLoading.value = true
const params = {
...groupForm,
app: groupForm.app.join(','),
addon: groupForm.addon.join(',')
}
if (isEdit.value) {
await siteApi.editSiteGroup(params)
message.success('编辑成功')
} else {
await siteApi.addSiteGroup(params)
message.success('添加成功')
}
groupModalVisible.value = false
loadGroupList()
} catch (error) {
if (error.errorFields) {
message.error('请检查表单填写')
} else {
message.error(isEdit.value ? '编辑失败' : '添加失败')
}
} finally {
submitLoading.value = false
}
}
const confirmDelete = async () => {
deleteLoading.value = true
try {
await siteApi.deleteSiteGroup(currentRecord.value.group_id)
message.success('删除成功')
deleteModalVisible.value = false
loadGroupList()
} catch (error) {
message.error('删除失败')
} finally {
deleteLoading.value = false
}
}
const resetGroupForm = () => {
Object.assign(groupForm, {
group_id: '',
group_name: '',
group_desc: '',
app: [],
addon: []
})
}
const getAppTooltipContent = (appList: any[]) => {
return (
<div class="app-tooltip">
{appList.map((app, index) => (
<div key={index} class="app-tooltip-item">
<img src={app.icon} class="app-tooltip-icon" />
<span>{app.title}</span>
</div>
))}
</div>
)
}
const getAddonTooltipContent = (addonList: any[]) => {
return (
<div class="addon-tooltip">
{addonList.map((addon, index) => (
<div key={index} class="addon-tooltip-item">
<img src={addon.icon} class="addon-tooltip-icon" />
<span>{addon.title}</span>
</div>
))}
</div>
)
}
const filterAppOption = (input: string, option: any) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
const filterAddonOption = (input: string, option: any) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}
const loadOptions = async () => {
try {
const [appResponse, addonResponse] = await Promise.all([
siteApi.getShowApp(),
siteApi.getShowMarketing()
])
appOptions.value = (appResponse.data || []).map((item: any) => ({
label: item.title,
value: item.key
}))
addonOptions.value = (addonResponse.data || []).map((item: any) => ({
label: item.title,
value: item.key
}))
} catch (error) {
console.error('加载选项数据失败', error)
}
}
// 初始化
onMounted(async () => {
await Promise.all([
loadGroupList(),
loadOptions()
])
})
</script>
<style scoped>
.site-group-container {
padding: 24px;
}
.search-card {
margin-bottom: 24px;
}
.search-form {
margin-bottom: 16px;
}
.action-bar {
display: flex;
justify-content: flex-end;
}
.table-card {
margin-top: 16px;
}
.app-list,
.addon-list {
display: flex;
align-items: center;
}
.app-icons,
.addon-icons {
display: flex;
align-items: center;
gap: 8px;
overflow: hidden;
}
.app-icon,
.addon-icon {
flex-shrink: 0;
}
.app-more,
.addon-more {
display: flex;
align-items: center;
height: 54px;
color: #999;
font-size: 12px;
}
.empty-apps,
.empty-addons {
color: #999;
font-size: 12px;
}
.app-tooltip,
.addon-tooltip {
display: flex;
flex-wrap: wrap;
gap: 8px;
max-width: 315px;
}
.app-tooltip-item,
.addon-tooltip-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.app-tooltip-icon,
.addon-tooltip-icon {
width: 54px;
height: 54px;
border-radius: 4px;
}
</style>