feat: 待审核变更支持手动搜索匹配已有站点

This commit is contained in:
mediabot-pt
2026-06-30 09:09:24 +08:00
parent 690d1ae926
commit 56e2615624
5 changed files with 110 additions and 1 deletions

View File

@@ -296,6 +296,28 @@ public class AdminController {
return ApiResponse.success(siteService.listChangesBySite(siteId));
}
@GetMapping("/sites/search")
public ApiResponse<List<com.par.core.dto.SiteDTO>> searchSites(
@RequestParam("q") String q, HttpServletRequest request) {
checkAdmin(request);
return ApiResponse.success(siteService.searchSites(q));
}
@PostMapping("/changes/{id}/link")
public ApiResponse<Void> linkChangeToSite(
@PathVariable Long id,
@RequestBody Map<String, String> payload,
HttpServletRequest request) {
checkAdmin(request);
String targetSiteId = payload.get("siteId");
if (targetSiteId == null || targetSiteId.isBlank()) {
return ApiResponse.error(400, "siteId is required");
}
siteService.linkChangeToSite(id, targetSiteId);
log.info("Change linked: changeId={}, siteId={}", id, targetSiteId);
return ApiResponse.success();
}
private void checkAdmin(HttpServletRequest request) {
TrustLevel level = (TrustLevel) request.getAttribute("trustLevel");
if (level == null || level != TrustLevel.ADMIN) {

View File

@@ -60,4 +60,10 @@ public interface SiteService {
/** 查看站点的变更记录 */
List<SiteChangeDTO> listChangesBySite(String siteId);
/** 搜索站点(按名称/URL 模糊匹配) */
List<SiteDTO> searchSites(String keyword);
/** 将变更关联到指定站点 */
void linkChangeToSite(Long changeId, String siteId);
}

View File

@@ -302,6 +302,29 @@ public class SiteServiceImpl implements SiteService {
.map(this::toChangeDTO).collect(Collectors.toList());
}
@Override
public List<SiteDTO> searchSites(String keyword) {
return siteMapper.selectList(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Site>()
.and(w -> w.like(Site::getNamesJson, keyword)
.or().like(Site::getUrlsJson, keyword)
.or().eq(Site::getSiteId, keyword))
.eq(Site::getDeleted, 0)
.orderByDesc(Site::getUpdatedAt)
.last("LIMIT 20")
).stream().map(this::toDTO).collect(Collectors.toList());
}
@Override
public void linkChangeToSite(Long changeId, String siteId) {
SiteChange entry = changeMapper.selectById(changeId);
if (entry == null) throw new IllegalArgumentException("变更记录不存在");
if (!"pending".equals(entry.getStatus())) throw new IllegalArgumentException("只能关联待审核的变更");
entry.setSiteId(siteId);
changeMapper.updateById(entry);
log.info("变更手动关联: changeId={} → siteId={}", changeId, siteId);
}
private SiteChangeDTO toChangeDTO(SiteChange entry) {
SiteChangeDTO dto = new SiteChangeDTO();
dto.setId(entry.getId());

View File

@@ -39,7 +39,9 @@ export default {
pendingChanges: () => api.get('/admin/sites/pending'),
approveChange: (id) => api.post(`/admin/changes/${id}/approve`),
rejectChange: (id) => api.post(`/admin/changes/${id}/reject`),
linkChange: (id, siteId) => api.post(`/admin/changes/${id}/link`, { siteId }),
changesBySite: (siteId) => api.get(`/admin/sites/${siteId}/changes`),
search: (q) => api.get(`/admin/sites/search`, { params: { q } }),
},
configs: {
list: () => api.get('/configs'),

View File

@@ -30,10 +30,11 @@
</template>
</el-table-column>
<el-table-column prop="createdAt" label="提交时间" width="160" />
<el-table-column label="操作" width="150">
<el-table-column label="操作" width="210">
<template #default="scope">
<el-button size="small" type="success" @click="handleApprove(scope.row)">通过</el-button>
<el-button size="small" type="danger" @click="handleReject(scope.row)">拒绝</el-button>
<el-button size="small" @click="handleMatch(scope.row)">匹配</el-button>
</template>
</el-table-column>
</el-table>
@@ -146,6 +147,27 @@
<el-button @click="historyVisible = false">关闭</el-button>
</template>
</el-dialog>
<!-- 手动匹配对话框 -->
<el-dialog v-model="matchVisible" title="关联到已有站点" width="500px" :close-on-click-modal="false">
<el-input v-model="matchQuery" placeholder="搜索站点名称或URL..." clearable @input="handleSearch" style="margin-bottom:12px" />
<el-table :data="matchResults" stripe size="small" v-loading="matchSearching" max-height="300" @row-click="handleLinkSite" highlight-current-row>
<el-table-column prop="siteId" label="站点ID" width="120" />
<el-table-column label="名称" min-width="150">
<template #default="scope">
<div v-for="(n, i) in scope.row.names" :key="i" class="text-xs">{{ n }}</div>
</template>
</el-table-column>
<el-table-column label="地址" min-width="150">
<template #default="scope">
<div v-for="(u, i) in scope.row.urls" :key="i" class="text-xs">{{ u }}</div>
</template>
</el-table-column>
</el-table>
<template #footer>
<el-button @click="matchVisible = false">取消</el-button>
</template>
</el-dialog>
</div>
</template>
@@ -170,6 +192,40 @@ const siteTypes = [
]
// 配置查看
const matchVisible = ref(false)
const matchQuery = ref('')
const matchResults = ref([])
const matchSearching = ref(false)
let matchTargetId = ref(null)
const handleMatch = (row) => {
matchTargetId.value = row.id
matchQuery.value = (row.names || [])[0] || ''
matchVisible.value = true
handleSearch()
}
const handleSearch = async () => {
if (!matchQuery.value.trim()) { matchResults.value = []; return }
matchSearching.value = true
try {
const res = await api.sites.search(matchQuery.value.trim())
matchResults.value = res.data || []
} catch { matchResults.value = [] }
finally { matchSearching.value = false }
}
const handleLinkSite = async (row) => {
try {
await api.sites.linkChange(matchTargetId.value, row.siteId)
ElMessage.success(`已关联到 ${row.siteId}`)
matchVisible.value = false
fetchAll()
} catch (e) {
ElMessage.error(e.response?.data?.message || '关联失败')
}
}
const historyVisible = ref(false)
const historyLoading = ref(false)
const historyList = ref([])