- 删除根目录中重复的 NestJS 配置文件 - 删除 tsconfig.json, tsconfig.build.json, eslint.config.mjs, .prettierrc - 保留 wwjcloud-nest/ 目录中的完整配置 - 避免配置冲突,确保项目结构清晰
557 lines
13 KiB
Vue
557 lines
13 KiB
Vue
<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> |