记得上下班打卡 | git大法好,push需谨慎

Commit 7cbf19f4 authored by wangyifan's avatar wangyifan

演出管理关联艺人

parent 087c6eb9
...@@ -48,10 +48,10 @@ CREATE TABLE `kylin_artist_performance` ( ...@@ -48,10 +48,10 @@ CREATE TABLE `kylin_artist_performance` (
`created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`mid`), PRIMARY KEY (`mid`),
UNIQUE KEY `uk_artist_performance` (`artist_id`,`performances_id`), UNIQUE KEY `uk_artist_performance` (`artist_id`,`performances_id`,`times_id`) USING BTREE COMMENT '艺人、演出、场次唯一索引',
KEY `idx_performances_id` (`performances_id`), KEY `idx_performances_id` (`performances_id`),
KEY `idx_artist_id` (`artist_id`) KEY `idx_artist_id` (`artist_id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='艺人-演出关联表'; ) ENGINE=InnoDB AUTO_INCREMENT=83 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='艺人-演出关联表';
-- 艺人操作记录表 -- 艺人操作记录表
......
package com.liquidnet.client.admin.web.controller.zhengzai.kylin; package com.liquidnet.client.admin.web.controller.zhengzai.kylin;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.github.pagehelper.PageInfo; import com.github.pagehelper.PageInfo;
import com.liquidnet.client.admin.common.annotation.Log; import com.liquidnet.client.admin.common.annotation.Log;
import com.liquidnet.client.admin.common.core.controller.BaseController; import com.liquidnet.client.admin.common.core.controller.BaseController;
import com.liquidnet.client.admin.common.core.domain.AjaxResult; import com.liquidnet.client.admin.common.core.domain.AjaxResult;
import com.liquidnet.client.admin.common.core.page.TableDataInfo; import com.liquidnet.client.admin.common.core.page.TableDataInfo;
import com.liquidnet.client.admin.common.enums.BusinessType; import com.liquidnet.client.admin.common.enums.BusinessType;
import com.liquidnet.service.kylin.dao.KylinArtistAssociationStatusDto;
import com.liquidnet.service.kylin.dao.KylinArtistDao; import com.liquidnet.service.kylin.dao.KylinArtistDao;
import com.liquidnet.service.kylin.dao.KylinArtistOperationLogDao; import com.liquidnet.service.kylin.dao.KylinArtistOperationLogDao;
import com.liquidnet.service.kylin.dao.KylinArtistPerformanceDao;
import com.liquidnet.service.kylin.dto.param.ArtistParam; import com.liquidnet.service.kylin.dto.param.ArtistParam;
import com.liquidnet.service.kylin.dto.param.ArtistSearchParam; import com.liquidnet.service.kylin.dto.param.ArtistSearchParam;
import com.liquidnet.service.kylin.dto.vo.ArtistVo; import com.liquidnet.service.kylin.dto.vo.ArtistVo;
import com.liquidnet.service.kylin.entity.KylinArtistPerformance;
import com.liquidnet.service.kylin.mapper.KylinArtistPerformanceMapper;
import com.liquidnet.service.kylin.service.admin.IKylinArtistService; import com.liquidnet.service.kylin.service.admin.IKylinArtistService;
import com.liquidnet.service.kylin.service.admin.IKylinArtistOperationLogService; import com.liquidnet.service.kylin.service.admin.IKylinArtistOperationLogService;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
...@@ -21,7 +26,10 @@ import org.springframework.stereotype.Controller; ...@@ -21,7 +26,10 @@ import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap; import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.List; import com.liquidnet.service.kylin.entity.KylinArtist;
import java.util.*;
import java.util.stream.Collectors;
/** /**
* <p> * <p>
...@@ -47,13 +55,15 @@ public class KylinArtistController extends BaseController { ...@@ -47,13 +55,15 @@ public class KylinArtistController extends BaseController {
@Autowired @Autowired
private IKylinArtistOperationLogService operationLogService; private IKylinArtistOperationLogService operationLogService;
@Autowired
private KylinArtistPerformanceMapper artistPerformanceMapper;
@GetMapping("/create") @GetMapping("/create")
@RequiresPermissions("kylin:artist:create") @RequiresPermissions("kylin:artist:create")
public String create(ModelMap mmap) { public String create(ModelMap mmap) {
mmap.put("platformUrl", platformUrl); mmap.put("platformUrl", platformUrl);
return prefix + "/create"; return prefix + "/create";
} }
@GetMapping("/update/{artistId}") @GetMapping("/update/{artistId}")
@RequiresPermissions("kylin:artist:update") @RequiresPermissions("kylin:artist:update")
public String update(@PathVariable("artistId") String artistId, ModelMap mmap) { public String update(@PathVariable("artistId") String artistId, ModelMap mmap) {
...@@ -200,4 +210,167 @@ public class KylinArtistController extends BaseController { ...@@ -200,4 +210,167 @@ public class KylinArtistController extends BaseController {
operationLogService.getOperationLogs(artistId, pageNum, pageSize); operationLogService.getOperationLogs(artistId, pageNum, pageSize);
return getDataTable(result.getList()); return getDataTable(result.getList());
} }
/**
* 获取指定场次的艺人阵容
*/
@GetMapping("/getSessionArtists")
@ResponseBody
public AjaxResult getSessionArtists(String performancesId, String timesId) {
try {
if (performancesId == null || timesId == null) {
return error("参数不能为空");
}
// 查询该场次的艺人
List<KylinArtistPerformanceDao> artists = artistPerformanceMapper.selectArtistsByPerformanceAndTimes(
performancesId, timesId
);
return AjaxResult.success(artists);
} catch (Exception e) {
return error("获取艺人阵容失败: " + e.getMessage());
}
}
/**
* 更新艺人排序
*/
@Log(title = "更新演出艺人排序", businessType = BusinessType.UPDATE)
@PostMapping("/updateArtistOrder")
@ResponseBody
public AjaxResult updateArtistOrder(@RequestBody Map<String, Object> params) {
try {
String performancesId = (String) params.get("performancesId");
String timesId = (String) params.get("timesId");
List<Map<String, Object>> orderData = (List<Map<String, Object>>) params.get("orderData");
if (orderData == null || orderData.isEmpty()) {
return error("排序数据不能为空");
}
// 更新每个艺人的排序
for (Map<String, Object> item : orderData) {
Long mid = Long.valueOf(item.get("mid").toString());
Integer sort = Integer.valueOf(item.get("sort").toString());
KylinArtistPerformance entity = artistPerformanceMapper.selectById(mid);
if (entity != null) {
entity.setSort(sort);
artistPerformanceMapper.updateById(entity);
}
}
return success("排序更新成功");
} catch (Exception e) {
return error("更新排序失败: " + e.getMessage());
}
}
/**
* 删除演出艺人关联
*/
@Log(title = "删除演出艺人", businessType = BusinessType.DELETE)
@PostMapping("/deletePerformanceArtist")
@ResponseBody
public AjaxResult deletePerformanceArtist(Long mid, String performancesId, String timesId) {
try {
if (mid == null) {
return error("参数错误");
}
int result = artistPerformanceMapper.deleteById(mid);
if (result > 0) {
return success("删除成功");
} else {
return error("删除失败");
}
} catch (Exception e) {
return error("删除失败: " + e.getMessage());
}
}
/**
* 获取所有艺人以供关联
*/
@GetMapping("/getAllArtists")
@ResponseBody
public AjaxResult getAllArtists(String performancesId, String timesId, String keyword) {
try {
// 1. 获取所有艺人
List<KylinArtist> allArtists = kylinArtistService.list(new QueryWrapper<KylinArtist>()
.like(keyword != null && !keyword.isEmpty(), "artist_name", keyword)
.orderByDesc("created_at"));
// 2. 获取当前场次已关联的艺人,用 Map 保留各自的 sort 值
List<KylinArtistPerformanceDao> associatedArtists =
artistPerformanceMapper.selectArtistsByPerformanceAndTimes(performancesId, timesId);
// key: artistId value: 演出关联表中的 sort(越大越靠前)
Map<String, Integer> associatedSortMap = associatedArtists.stream()
.collect(Collectors.toMap(
KylinArtistPerformanceDao::getArtistId,
KylinArtistPerformanceDao::getSort,
(existing, replacement) -> existing // 重复 key 保留已有值
));
// 3. 组装返回结果
List<KylinArtistAssociationStatusDto> resultList = allArtists.stream().map(artist -> {
KylinArtistAssociationStatusDto dto = new KylinArtistAssociationStatusDto();
dto.setArtistId(artist.getArtistId());
dto.setArtistName(artist.getArtistName());
dto.setAvatarUrl(artist.getAvatarUrl());
boolean associated = associatedSortMap.containsKey(artist.getArtistId());
dto.setAssociated(associated);
// sort 取演出关联表的值;未关联的艺人 sort 置 0
dto.setSort(associated ? associatedSortMap.get(artist.getArtistId()) : 0);
return dto;
}).collect(Collectors.toList());
return AjaxResult.success(resultList);
} catch (Exception e) {
return error("获取艺人列表失败: " + e.getMessage());
}
}
/**
* 修改艺人关联
* @param payload
* @return
*/
@PostMapping("/updateArtistAssociations")
@ResponseBody
public AjaxResult updateArtistAssociations(@RequestBody Map<String, Object> payload) {
try {
String performancesId = (String) payload.get("performancesId");
String timesId = (String) payload.get("timesId");
List<Map<String, Object>> artistOrders = (List<Map<String, Object>>) payload.get("artistOrders");
// 1. 删除当前场次所有已关联的艺人
artistPerformanceMapper.delete(new QueryWrapper<KylinArtistPerformance>()
.eq("performances_id", performancesId)
.eq("times_id", timesId));
// 2. 按前端传入的顺序重新关联,sort 越大越靠前
if (artistOrders != null && !artistOrders.isEmpty()) {
for (Map<String, Object> item : artistOrders) {
String artistId = (String) item.get("artistId");
Integer sort = item.get("sort") != null
? Integer.valueOf(item.get("sort").toString()) : 0;
KylinArtistPerformance newAssociation = new KylinArtistPerformance();
newAssociation.setPerformancesId(performancesId);
newAssociation.setTimesId(timesId);
newAssociation.setArtistId(artistId);
newAssociation.setSort(sort);
artistPerformanceMapper.insert(newAssociation);
}
}
return success("关联更新成功");
} catch (Exception e) {
logger.error("error", e);
return error("更新关联失败: " + e.getMessage());
}
}
} }
...@@ -26,6 +26,7 @@ import com.liquidnet.service.kylin.dto.param.SysDamaiParam; ...@@ -26,6 +26,7 @@ import com.liquidnet.service.kylin.dto.param.SysDamaiParam;
import com.liquidnet.service.kylin.dto.vo.admin.*; import com.liquidnet.service.kylin.dto.vo.admin.*;
import com.liquidnet.service.kylin.dto.vo.partner.KylinPerformanceMisVo; import com.liquidnet.service.kylin.dto.vo.partner.KylinPerformanceMisVo;
import com.liquidnet.service.kylin.dto.vo.partner.KylinPerformancesVo; import com.liquidnet.service.kylin.dto.vo.partner.KylinPerformancesVo;
import com.liquidnet.service.kylin.dto.vo.partner.TicketTimesTicketCreatePartnerVo;
import com.liquidnet.service.kylin.entity.KylinOrderImport; import com.liquidnet.service.kylin.entity.KylinOrderImport;
import com.liquidnet.service.kylin.service.admin.IKylinPerformancesAdminService; import com.liquidnet.service.kylin.service.admin.IKylinPerformancesAdminService;
import com.liquidnet.service.kylin.service.other.DamaiService; import com.liquidnet.service.kylin.service.other.DamaiService;
...@@ -258,6 +259,38 @@ public class KylinPerformancesController extends BaseController { ...@@ -258,6 +259,38 @@ public class KylinPerformancesController extends BaseController {
return prefix + "/subscribe"; return prefix + "/subscribe";
} }
/**
* 演出阵容页面
*/
@GetMapping(value = "/artistLineup/{performancesId}")
public String artistLineup(@PathVariable("performancesId") String performancesId, ModelMap mmap) {
try {
// 获取演出信息
KylinPerformanceMisVo performance = kylinPerformancesService.performanceDetails(performancesId);
if (performance == null) {
mmap.put("errorMsg", "演出不存在");
return prefix + "/artistLineup";
}
// 将场次数据转换为JSON
List<TicketTimesTicketCreatePartnerVo> ticketTimes = performance.getTicketTimes();
String ticketTimesJson = "[]";
if (ticketTimes != null && !ticketTimes.isEmpty()) {
com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper();
ticketTimesJson = mapper.writeValueAsString(ticketTimes);
}
mmap.put("performancesId", performancesId);
mmap.put("performanceTitle", performance.getTitle());
mmap.put("ticketTimesJson", ticketTimesJson);
return prefix + "/artistLineup";
} catch (Exception e) {
mmap.put("errorMsg", "加载演出阵容失败: " + e.getMessage());
return prefix + "/artistLineup";
}
}
@Log(title = "预约统计:导出列表") @Log(title = "预约统计:导出列表")
@PostMapping("/subscribe/export") @PostMapping("/subscribe/export")
@ResponseBody @ResponseBody
......
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
<th:block th:include="include :: header('演出阵容')" />
<style>
/* ===== 演出阵容主体样式 ===== */
.lineup-session {
margin-bottom: 30px;
border: 1px solid #e7eaec;
padding: 20px;
background: #fff;
border-radius: 4px;
}
.lineup-session-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 15px;
color: #676a6c;
padding-bottom: 10px;
border-bottom: 2px solid #1ab394;
}
.artist-list {
display: flex;
flex-wrap: wrap;
gap: 20px;
}
.artist-item {
position: relative;
text-align: center;
cursor: default;
width: 100px;
transition: all 0.3s;
}
.edit-mode .artist-item {
cursor: move;
}
.artist-item:hover {
transform: translateY(-5px);
}
.artist-item.dragging {
opacity: 0.5;
}
.artist-avatar {
width: 80px;
height: 80px;
border-radius: 50%;
object-fit: cover;
border: 2px solid #e7eaec;
transition: border-color 0.3s;
}
.artist-item:hover .artist-avatar {
border-color: #1ab394;
}
.artist-name {
margin-top: 8px;
font-size: 14px;
color: #676a6c;
word-break: break-all;
}
.delete-icon {
display: none;
position: absolute;
top: -5px;
right: 10px;
width: 20px;
height: 20px;
background: #ed5565;
color: white;
border-radius: 50%;
cursor: pointer;
align-items: center;
justify-content: center;
font-size: 14px;
line-height: 1;
transition: background-color 0.3s;
}
.edit-mode .delete-icon {
display: flex;
}
.delete-icon:hover {
background: #da4453;
}
.no-artists-tip {
text-align: center;
padding: 40px;
color: #999;
}
/* 编辑模式下的拖拽提示(标题旁小字) */
.drag-hint {
display: none;
margin-left: 10px;
color: #aaa;
font-size: 11px;
font-weight: normal;
}
.edit-mode .drag-hint {
display: inline;
}
/* ===== 关联艺人弹窗样式 ===== */
/* 搜索框容器:内嵌 × 按钮 */
.artist-search-wrap {
position: relative;
margin-bottom: 12px;
}
.artist-search-wrap .form-control {
padding-right: 34px;
border-radius: 4px;
}
.artist-search-clear {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
cursor: pointer;
color: #aaa;
font-size: 16px;
line-height: 1;
display: none;
background: none;
border: none;
padding: 0;
}
.artist-search-clear:hover {
color: #555;
}
/* 搜索结果列表 */
.modal-artist-list {
max-height: 300px;
overflow-y: auto;
border: 1px solid #eee;
border-radius: 4px;
}
.modal-artist-item {
display: flex;
align-items: center;
padding: 10px 14px;
cursor: pointer;
border-bottom: 1px solid #f0f0f0;
transition: background 0.15s;
}
.modal-artist-item:last-child {
border-bottom: none;
}
.modal-artist-item:hover {
background: #f7f9fc;
}
.modal-artist-avatar {
width: 44px;
height: 44px;
border-radius: 50%;
object-fit: cover;
margin-right: 12px;
flex-shrink: 0;
border: 2px solid #e0e0e0;
}
.modal-artist-name {
flex: 1;
font-size: 14px;
color: #333;
}
/* 单选按钮样式 */
.artist-radio-btn {
width: 22px;
height: 22px;
border-radius: 50%;
border: 2px solid #ccc;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
transition: border-color 0.2s, background 0.2s;
}
.artist-radio-btn.selected {
border-color: #1ab394;
background: #1ab394;
}
.artist-radio-btn.selected::after {
content: '';
display: block;
width: 8px;
height: 5px;
border-left: 2px solid #fff;
border-bottom: 2px solid #fff;
transform: rotate(-45deg) translateY(-1px);
}
/* 加载动画 */
.modal-loading {
text-align: center;
padding: 30px;
color: #999;
}
/* 空状态 */
.modal-empty {
text-align: center;
padding: 30px;
color: #aaa;
font-size: 13px;
}
/* 已选艺人区域 */
.selected-artists-section {
margin-top: 14px;
}
.selected-artists-section h5 {
font-size: 14px;
font-weight: 600;
color: #555;
margin-bottom: 10px;
}
.selected-artists-container {
display: flex;
flex-wrap: nowrap;
/* 不换行,支持横向滚动 */
overflow-x: auto;
gap: 12px;
padding: 10px;
min-height: 90px;
border: 1px solid #eee;
border-radius: 4px;
background: #fafafa;
align-items: flex-start;
}
.selected-artists-container::-webkit-scrollbar {
height: 4px;
}
.selected-artists-container::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 2px;
}
.selected-artist-card {
position: relative;
text-align: center;
flex-shrink: 0;
width: 72px;
cursor: grab;
transition: opacity 0.2s, transform 0.15s;
}
.selected-artist-card:active {
cursor: grabbing;
}
.selected-artist-card.dragging {
opacity: 0.4;
transform: scale(0.95);
}
.selected-artist-card.drag-over {
outline: 2px dashed #1ab394;
outline-offset: 2px;
border-radius: 6px;
}
.selected-artist-avatar {
width: 56px;
height: 56px;
border-radius: 50%;
object-fit: cover;
border: 2px solid #1ab394;
}
.selected-artist-name {
margin-top: 5px;
font-size: 11px;
color: #555;
word-break: break-all;
line-height: 1.3;
}
.remove-selected-artist {
position: absolute;
top: -4px;
right: 4px;
width: 18px;
height: 18px;
background: #ed5565;
color: white;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
line-height: 1;
transition: background-color 0.2s;
}
.remove-selected-artist:hover {
background: #da4453;
}
.selected-empty-tip {
width: 100%;
text-align: center;
color: #bbb;
font-size: 13px;
padding: 20px 0;
align-self: center;
}
/* 弹窗底部按钮 */
.modal-footer .btn-cancel {
border: 1px solid #ccc;
color: #555;
background: #fff;
}
.modal-footer .btn-cancel:hover {
background: #f5f5f5;
}
</style>
</head>
<body class="gray-bg">
<div class="wrapper wrapper-content animated fadeInRight">
<div class="row">
<div class="col-sm-12">
<div class="ibox">
<div class="ibox-title">
<h5>演出阵容管理</h5>
<div class="ibox-tools">
<span th:text="${performanceTitle}"
style="font-size:15px;font-weight:600;color:#333;vertical-align:middle;"></span>
</div>
</div>
<div class="ibox-content">
<div style="margin-bottom:15px;">
<button class="btn btn-primary btn-sm" id="edit-lineup-btn">编辑演出阵容</button>
</div>
<div id="artist-lineup-container">
<div class="text-center">
<i class="fa fa-spinner fa-spin fa-3x"></i>
<p>加载中...</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 关联艺人弹窗 -->
<div class="modal fade" id="associateArtistModal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">关联艺人</h4>
</div>
<div class="modal-body">
<!-- 搜索框 -->
<div class="artist-search-wrap">
<input type="text" class="form-control" id="artist-search-input" placeholder="搜索艺人姓名"
autocomplete="off">
<button class="artist-search-clear" id="clear-search-btn" title="清空">&times;</button>
</div>
<!-- 搜索结果列表 -->
<div class="modal-artist-list" id="modal-artist-list">
<!-- 动态渲染 -->
</div>
<!-- 已选艺人区 -->
<div class="selected-artists-section">
<h5>已选艺人 <small style="font-weight:normal;color:#aaa;font-size:11px;">(可拖拽调整顺序)</small></h5>
<div class="selected-artists-container" id="selected-artists-container">
<!-- 动态渲染 -->
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-cancel" id="cancel-associate-btn">取消</button>
<button type="button" class="btn btn-primary" onclick="saveAssociatedArtists()">保存</button>
</div>
</div>
</div>
</div>
<th:block th:include="include :: footer" />
<script th:inline="javascript">
const performancesId = [[${ performancesId }]];
let ticketTimes = [];
try {
const rawTicketTimes = /*[[${ticketTimesJson}]]*/ '[]';
ticketTimes = JSON.parse(rawTicketTimes);
} catch (e) {
console.error('Invalid ticketTimesJson:', e);
ticketTimes = [];
}
$(function () {
loadArtistLineup();
// 编辑模式切换
$('#edit-lineup-btn').on('click', function () {
const container = $('#artist-lineup-container');
const isEditMode = container.hasClass('edit-mode');
if (isEditMode) {
container.removeClass('edit-mode');
$(this).text('编辑演出阵容');
$('.associate-artist-btn').hide();
destroyDragAndDrop();
} else {
container.addClass('edit-mode');
$(this).text('完成编辑');
$('.associate-artist-btn').show();
initDragAndDrop();
}
});
// 搜索框输入事件(防抖)
$(document).on('input', '#artist-search-input', function () {
$('#clear-search-btn').toggle($(this).val().length > 0);
debouncedSearch();
});
// 清空搜索框
$(document).on('click', '#clear-search-btn', function () {
$('#artist-search-input').val('').trigger('input');
});
// 取消按钮:关闭弹窗,不保存
$(document).on('click', '#cancel-associate-btn', function () {
$('#associateArtistModal').modal('hide');
});
});
// ===== 演出阵容加载与渲染 =====
function loadArtistLineup() {
const times = Array.isArray(ticketTimes) ? ticketTimes : [];
if (!times || times.length === 0) {
$('#artist-lineup-container').html(
'<div class="alert alert-info">该演出暂无场次信息</div>'
);
return;
}
let loadedCount = 0;
const allSessionData = [];
times.forEach(function (ticketTime, index) {
$.ajax({
url: ctx + "kylin/artist/getSessionArtists",
type: "GET",
data: {
performancesId: performancesId,
timesId: ticketTime.ticketTimesId
},
success: function (response) {
if (response.code === 0) {
allSessionData[index] = {
timesId: ticketTime.ticketTimesId,
sessionDate: ticketTime.title.replace(/ 00:00/g, ''),
artists: response.data || []
};
}
loadedCount++;
if (loadedCount === times.length) {
renderArtistLineup(allSessionData);
}
},
error: function () {
loadedCount++;
allSessionData[index] = {
timesId: ticketTime.ticketTimesId,
sessionDate: ticketTime.title.replace(/ 00:00/g, ''),
artists: []
};
if (loadedCount === times.length) {
renderArtistLineup(allSessionData);
}
}
});
});
}
function renderArtistLineup(data) {
const container = $('#artist-lineup-container');
if (!data || data.length === 0) {
container.html('<div class="alert alert-info">暂无演出阵容数据</div>');
return;
}
let html = '';
data.forEach(function (session) {
html += '<div class="lineup-session">';
html += '<div class="lineup-session-title">' + session.sessionDate;
html += '<span class="drag-hint">注:可拖拽调整出场顺序,松开后实时更新</span>';
html += '<button class="btn btn-xs btn-primary associate-artist-btn" style="display:none; float: right;" onclick="openAssociateArtistModal(\'' + session.timesId + '\')">关联艺人</button>';
html += '</div>';
html += '<div class="artist-list" data-times-id="' + session.timesId + '">';
if (session.artists && session.artists.length > 0) {
session.artists.forEach(function (artist) {
html += '<div class="artist-item" draggable="true" data-artist-id="' + artist.artistId + '" data-mid="' + artist.mid + '">';
html += '<div class="delete-icon" onclick="deleteArtist(\'' + artist.mid + '\', \'' + session.timesId + '\')">×</div>';
html += '<img class="artist-avatar" src="' + artist.avatarUrl + '" alt="' + artist.artistName + '" onerror="this.src=\'/img/profile.jpg\'">';
html += '<div class="artist-name">' + artist.artistName + '</div>';
html += '</div>';
});
} else {
html += '<div class="no-artists-tip">该场次暂无艺人</div>';
}
html += '</div>';
html += '</div>';
});
container.html(html);
// 渲染完成后,若当前仍处于编辑模式,恢复按钮显示和拖拽绑定
if (container.hasClass('edit-mode')) {
$('.associate-artist-btn').show();
initDragAndDrop();
}
}
// ===== 拖拽排序 =====
function initDragAndDrop() {
if (!$('#artist-lineup-container').hasClass('edit-mode')) return;
const artistItems = document.querySelectorAll('.artist-item');
artistItems.forEach(item => {
item.setAttribute('draggable', 'true');
item.addEventListener('dragstart', handleDragStart);
item.addEventListener('dragend', handleDragEnd);
item.addEventListener('dragover', handleDragOver);
item.addEventListener('drop', handleDrop);
});
}
function destroyDragAndDrop() {
const artistItems = document.querySelectorAll('.artist-item');
artistItems.forEach(item => {
item.setAttribute('draggable', 'false');
item.removeEventListener('dragstart', handleDragStart);
item.removeEventListener('dragend', handleDragEnd);
item.removeEventListener('dragover', handleDragOver);
item.removeEventListener('drop', handleDrop);
});
}
let draggedElement = null;
function handleDragStart(e) {
draggedElement = this;
this.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
}
function handleDragEnd(e) {
this.classList.remove('dragging');
draggedElement = null;
}
function handleDragOver(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
if (draggedElement && draggedElement !== this) {
const parent = this.parentNode;
const draggedParent = draggedElement.parentNode;
if (parent === draggedParent) {
const allItems = Array.from(parent.querySelectorAll('.artist-item'));
const draggedIndex = allItems.indexOf(draggedElement);
const targetIndex = allItems.indexOf(this);
if (draggedIndex < targetIndex) {
parent.insertBefore(draggedElement, this.nextSibling);
} else {
parent.insertBefore(draggedElement, this);
}
}
}
}
function handleDrop(e) {
e.preventDefault();
e.stopPropagation();
saveArtistOrder(this.parentNode);
}
function saveArtistOrder(artistListElement) {
const timesId = artistListElement.getAttribute('data-times-id');
const artistItems = artistListElement.querySelectorAll('.artist-item');
const orderData = [];
artistItems.forEach(function (item, index) {
orderData.push({
mid: item.getAttribute('data-mid'),
sort: artistItems.length - index
});
});
$.ajax({
url: ctx + "kylin/artist/updateArtistOrder",
type: "POST",
contentType: "application/json",
data: JSON.stringify({
performancesId: performancesId,
timesId: timesId,
orderData: orderData
}),
success: function (response) {
if (response.code === 0) {
$.modal.msgSuccess("排序已更新");
} else {
$.modal.msgError(response.msg || "更新排序失败");
loadArtistLineup();
}
},
error: function () {
$.modal.msgError("更新排序失败");
loadArtistLineup();
}
});
}
// ===== 删除艺人 =====
function deleteArtist(mid, timesId) {
$.modal.confirm("确定要删除该艺人吗?", function () {
$.ajax({
url: ctx + "kylin/artist/deletePerformanceArtist",
type: "POST",
data: {
mid: mid,
performancesId: performancesId,
timesId: timesId
},
success: function (response) {
if (response.code === 0) {
$.modal.msgSuccess("删除成功");
loadArtistLineup();
} else {
$.modal.msgError(response.msg || "删除失败");
}
},
error: function () {
$.modal.msgError("删除失败");
}
});
});
}
// ===== 关联艺人弹窗逻辑 =====
let selectedArtists = []; // 当前已选艺人列表
let currentModalArtists = []; // 当前搜索结果中的艺人列表
function openAssociateArtistModal(timesId) {
$('#associateArtistModal').data('timesId', timesId);
// 重置状态
selectedArtists = [];
currentModalArtists = [];
$('#artist-search-input').val('');
$('#clear-search-btn').hide();
$('#modal-artist-list').html('');
$('#selected-artists-container').html('');
// 打开弹窗
$('#associateArtistModal').modal('show');
// 加载初始艺人列表(含已关联状态)
loadAllArtistsForModal(timesId, true);
}
// 防抖
function debounce(func, wait) {
let timeout;
return function (...args) {
const context = this;
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(context, args), wait);
};
}
const debouncedSearch = debounce(function () {
const timesId = $('#associateArtistModal').data('timesId');
loadAllArtistsForModal(timesId, false);
}, 300);
/**
* 从后端加载艺人列表
* @param {string} timesId
* @param {boolean} isInitialLoad 是否首次加载(用于初始化已选艺人)
*/
function loadAllArtistsForModal(timesId, isInitialLoad) {
const keyword = $('#artist-search-input').val().trim();
// 显示加载中
$('#modal-artist-list').html(
'<div class="modal-loading"><i class="fa fa-spinner fa-spin"></i> 搜索中...</div>'
);
$.ajax({
url: ctx + "kylin/artist/getAllArtists",
type: "GET",
data: {
performancesId: performancesId,
timesId: timesId,
keyword: keyword
},
success: function (response) {
if (response.code === 0) {
currentModalArtists = response.data || [];
renderArtistsInModal(currentModalArtists, isInitialLoad);
} else {
$('#modal-artist-list').html(
'<div class="modal-empty">加载艺人列表失败,请重试</div>'
);
}
},
error: function () {
$('#modal-artist-list').html(
'<div class="modal-empty">加载艺人列表失败,请重试</div>'
);
}
});
}
/**
* 渲染搜索结果列表
* @param {Array} artists
* @param {boolean} isInitialLoad
*/
function renderArtistsInModal(artists, isInitialLoad) {
const listContainer = $('#modal-artist-list');
if (!artists || artists.length === 0) {
listContainer.html('<div class="modal-empty">暂无匹配艺人</div>');
updateSelectedArtistsView();
return;
}
// 首次加载时,从后端的 associated 字段初始化已选列表,并按 sort 降序排列(sort 越大越靠前)
if (isInitialLoad) {
selectedArtists = artists
.filter(a => a.associated)
.map(a => ({ artistId: a.artistId, artistName: a.artistName, avatarUrl: a.avatarUrl, sort: a.sort || 0 }))
.sort((a, b) => b.sort - a.sort);
}
let html = '';
artists.forEach(function (artist) {
const isSelected = selectedArtists.some(a => a.artistId === artist.artistId);
html += '<div class="modal-artist-item" data-artist-id="' + artist.artistId + '">';
html += '<img src="' + (artist.avatarUrl || '/img/profile.jpg') + '" class="modal-artist-avatar" onerror="this.src=\'/img/profile.jpg\'">';
html += '<div class="modal-artist-name">' + escapeHtml(artist.artistName) + '</div>';
html += '<div class="artist-radio-btn' + (isSelected ? ' selected' : '') + '"></div>';
html += '</div>';
});
listContainer.html(html);
updateSelectedArtistsView();
// 点击整行切换选中状态
listContainer.find('.modal-artist-item').on('click', function () {
const artistId = $(this).data('artist-id');
const artist = currentModalArtists.find(a => a.artistId === artistId);
if (!artist) return;
const radioBtn = $(this).find('.artist-radio-btn');
const isSelected = selectedArtists.some(a => a.artistId === artistId);
if (isSelected) {
// 取消选中
selectedArtists = selectedArtists.filter(a => a.artistId !== artistId);
radioBtn.removeClass('selected');
} else {
// 选中,防止重复
if (!selectedArtists.some(a => a.artistId === artistId)) {
selectedArtists.push({
artistId: artist.artistId,
artistName: artist.artistName,
avatarUrl: artist.avatarUrl
});
}
radioBtn.addClass('selected');
}
updateSelectedArtistsView();
});
}
/**
* 更新"已选艺人"区域视图,并初始化拖拽排序
*/
function updateSelectedArtistsView() {
const container = $('#selected-artists-container');
if (selectedArtists.length === 0) {
container.html('<div class="selected-empty-tip">尚未选择艺人</div>');
return;
}
let html = '';
selectedArtists.forEach(function (artist) {
html += '<div class="selected-artist-card" draggable="true" data-artist-id="' + artist.artistId + '">';
html += '<img src="' + (artist.avatarUrl || '/img/profile.jpg') + '" class="selected-artist-avatar" onerror="this.src=\'/img/profile.jpg\'">';
html += '<div class="selected-artist-name">' + escapeHtml(artist.artistName) + '</div>';
html += '<div class="remove-selected-artist" title="移除">&times;</div>';
html += '</div>';
});
container.html(html);
// 初始化已选区域拖拽排序
initSelectedArtistsDrag();
}
// 点击"已选艺人"区域的 × 移除艺人
$(document).on('click', '.remove-selected-artist', function () {
const artistId = $(this).closest('.selected-artist-card').data('artist-id');
selectedArtists = selectedArtists.filter(a => a.artistId !== artistId);
updateSelectedArtistsView();
// 同步取消搜索列表中对应的选中状态
const item = $('#modal-artist-list .modal-artist-item[data-artist-id="' + artistId + '"]');
item.find('.artist-radio-btn').removeClass('selected');
});
/**
* 初始化已选艺人卡片的拖拽排序
*/
let modalDraggedCard = null;
function initSelectedArtistsDrag() {
const cards = document.querySelectorAll('#selected-artists-container .selected-artist-card');
cards.forEach(card => {
card.addEventListener('dragstart', onModalDragStart);
card.addEventListener('dragend', onModalDragEnd);
card.addEventListener('dragover', onModalDragOver);
card.addEventListener('dragleave', onModalDragLeave);
card.addEventListener('drop', onModalDrop);
});
}
function onModalDragStart(e) {
// 若点击的是 × 按钮则不触发拖拽
if (e.target.classList.contains('remove-selected-artist')) {
e.preventDefault();
return;
}
modalDraggedCard = this;
this.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
}
function onModalDragEnd() {
if (modalDraggedCard) {
modalDraggedCard.classList.remove('dragging');
}
document.querySelectorAll('#selected-artists-container .selected-artist-card')
.forEach(c => c.classList.remove('drag-over'));
modalDraggedCard = null;
}
function onModalDragOver(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
if (modalDraggedCard && modalDraggedCard !== this) {
this.classList.add('drag-over');
}
}
function onModalDragLeave() {
this.classList.remove('drag-over');
}
function onModalDrop(e) {
e.preventDefault();
e.stopPropagation();
this.classList.remove('drag-over');
if (!modalDraggedCard || modalDraggedCard === this) return;
const container = document.getElementById('selected-artists-container');
const cards = Array.from(container.querySelectorAll('.selected-artist-card'));
const fromIndex = cards.indexOf(modalDraggedCard);
const toIndex = cards.indexOf(this);
if (fromIndex === -1 || toIndex === -1) return;
// 更新 selectedArtists 数组顺序
const moved = selectedArtists.splice(fromIndex, 1)[0];
selectedArtists.splice(toIndex, 0, moved);
// 重新渲染(保持拖拽事件绑定)
updateSelectedArtistsView();
}
/**
* 保存关联艺人
*/
function saveAssociatedArtists() {
const timesId = $('#associateArtistModal').data('timesId');
if (selectedArtists.length === 0) {
$.modal.msgWarning("请至少选择一位艺人");
return;
}
// 根据当前显示顺序计算 sort:越靠前的 sort 值越大
const total = selectedArtists.length;
const artistOrders = selectedArtists.map((a, index) => ({
artistId: a.artistId,
sort: total - index // 第 0 位 → sort 最大
}));
$.ajax({
url: ctx + "kylin/artist/updateArtistAssociations",
type: "POST",
contentType: "application/json",
data: JSON.stringify({
performancesId: performancesId,
timesId: timesId,
artistOrders: artistOrders
}),
success: function (response) {
if (response.code === 0) {
$.modal.msgSuccess("关联成功");
$('#associateArtistModal').modal('hide');
loadArtistLineup();
} else {
$.modal.msgError(response.msg || "关联失败");
}
},
error: function () {
$.modal.msgError("关联失败");
}
});
}
/**
* HTML 转义,防止 XSS
*/
function escapeHtml(str) {
if (!str) return '';
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
</script>
</body>
</html>
\ No newline at end of file
...@@ -43,6 +43,8 @@ ...@@ -43,6 +43,8 @@
</li> </li>
<li id="li-tab-11"><a data-toggle="tab" href="#tab-11" aria-expanded="false" onclick="subscribeInfo()">预约统计</a> <li id="li-tab-11"><a data-toggle="tab" href="#tab-11" aria-expanded="false" onclick="subscribeInfo()">预约统计</a>
</li> </li>
<li id="li-tab-12"><a data-toggle="tab" href="#tab-12" aria-expanded="false" onclick="artistLineupInfo()">演出阵容</a>
</li>
</ul> </ul>
<div class="tab-content"> <div class="tab-content">
<div id="tab-1" class="tab-pane"> <div id="tab-1" class="tab-pane">
...@@ -364,6 +366,13 @@ ...@@ -364,6 +366,13 @@
height=800px frameborder=0></iframe> height=800px frameborder=0></iframe>
</div> </div>
</div> </div>
<div id="tab-12" class="tab-pane">
<div class="panel-body">
<iframe id="artist_lineup_iframe" name="artist_lineup_iframe" marginwidth=0 marginheight=0
width=100%
height=800px frameborder=0></iframe>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
...@@ -563,6 +572,11 @@ ...@@ -563,6 +572,11 @@
document.getElementById("subscribe_iframe").src = "../subscribe/" + '[[${kylinPerformanceMisVo.performancesId}]]'.replaceAll("\"", ""); document.getElementById("subscribe_iframe").src = "../subscribe/" + '[[${kylinPerformanceMisVo.performancesId}]]'.replaceAll("\"", "");
} }
//演出阵容
function artistLineupInfo() {
document.getElementById("artist_lineup_iframe").src = "../artistLineup/" + '[[${kylinPerformanceMisVo.performancesId}]]'.replaceAll("\"", "");
}
$("#tab-nav-1").bind("click", function () { $("#tab-nav-1").bind("click", function () {
$("#tab_iframe_1").attr("src", prefix + "/performanceStatic/" + '[[${kylinPerformanceMisVo.performancesId}]]'.replaceAll("\"", "")); $("#tab_iframe_1").attr("src", prefix + "/performanceStatic/" + '[[${kylinPerformanceMisVo.performancesId}]]'.replaceAll("\"", ""));
}); });
......
package com.liquidnet.service.kylin.dao;
import lombok.Data;
@Data
public class KylinArtistAssociationStatusDto {
/**
* 艺人ID
*/
private String artistId;
/**
* 艺人名称
*/
private String artistName;
/**
* 艺人头像地址
*/
private String avatarUrl;
/**
* 是否关联
*/
private boolean isAssociated;
/**
* 排序 越大越靠前
*/
private int sort;
}
...@@ -11,6 +11,26 @@ public class KylinArtistPerformanceDao implements Serializable { ...@@ -11,6 +11,26 @@ public class KylinArtistPerformanceDao implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
/**
* 关联记录ID
*/
private Long mid;
/**
* 艺人ID
*/
private String artistId;
/**
* 艺人名称
*/
private String artistName;
/**
* 艺人头像
*/
private String avatarUrl;
/** /**
* 演出ID * 演出ID
*/ */
...@@ -36,4 +56,9 @@ public class KylinArtistPerformanceDao implements Serializable { ...@@ -36,4 +56,9 @@ public class KylinArtistPerformanceDao implements Serializable {
*/ */
private String timeTitle; private String timeTitle;
/**
* 排序权重
*/
private Integer sort;
} }
...@@ -17,4 +17,13 @@ public interface KylinArtistPerformanceMapper extends BaseMapper<KylinArtistPerf ...@@ -17,4 +17,13 @@ public interface KylinArtistPerformanceMapper extends BaseMapper<KylinArtistPerf
*/ */
List<KylinArtistPerformanceDao> selectPerformanceDaoByArtistId(@Param("artistId") String artistId); List<KylinArtistPerformanceDao> selectPerformanceDaoByArtistId(@Param("artistId") String artistId);
/**
* 根据演出ID和场次ID查询艺人阵容
*
* @param performancesId 演出ID
* @param timesId 场次ID
* @return 艺人阵容列表
*/
List<KylinArtistPerformanceDao> selectArtistsByPerformanceAndTimes(@Param("performancesId") String performancesId, @Param("timesId") String timesId);
} }
...@@ -4,12 +4,18 @@ ...@@ -4,12 +4,18 @@
<select id="selectPerformanceDaoByArtistId" parameterType="java.lang.String" resultType="com.liquidnet.service.kylin.dao.KylinArtistPerformanceDao"> <select id="selectPerformanceDaoByArtistId" parameterType="java.lang.String" resultType="com.liquidnet.service.kylin.dao.KylinArtistPerformanceDao">
SELECT SELECT
ap.mid,
ap.artist_id as artistId,
a.artist_name as artistName,
a.avatar_url as avatarUrl,
p.performances_id as performanceId, p.performances_id as performanceId,
p.title, p.title,
DATE_FORMAT(p.time_start, '%Y-%m-%d %H:%i:%s') as timeStart, DATE_FORMAT(p.time_start, '%Y-%m-%d %H:%i:%s') as timeStart,
t.ticket_times_id as timesId, t.ticket_times_id as timesId,
t.title as timeTitle t.title as timeTitle,
ap.sort
FROM kylin_artist_performance ap FROM kylin_artist_performance ap
INNER JOIN kylin_artist a ON ap.artist_id = a.artist_id
INNER JOIN kylin_performances p ON ap.performances_id = p.performances_id INNER JOIN kylin_performances p ON ap.performances_id = p.performances_id
INNER JOIN kylin_ticket_times t ON ap.times_id = t.ticket_times_id INNER JOIN kylin_ticket_times t ON ap.times_id = t.ticket_times_id
WHERE ap.artist_id = #{artistId} WHERE ap.artist_id = #{artistId}
...@@ -24,4 +30,28 @@ ...@@ -24,4 +30,28 @@
AND status = 1 AND status = 1
</select> </select>
<!-- 根据演出ID和场次ID查询艺人阵容 -->
<select id="selectArtistsByPerformanceAndTimes" resultType="com.liquidnet.service.kylin.dao.KylinArtistPerformanceDao">
SELECT
ap.mid,
ap.artist_id as artistId,
ap.times_id as timesId,
ap.sort,
a.artist_name as artistName,
a.avatar_url as avatarUrl,
p.performances_id as performanceId,
p.title,
DATE_FORMAT(p.time_start, '%Y-%m-%d %H:%i:%s') as timeStart,
t.title as timeTitle
FROM kylin_artist_performance ap
INNER JOIN kylin_artist a ON ap.artist_id = a.artist_id
INNER JOIN kylin_performances p ON ap.performances_id = p.performances_id
INNER JOIN kylin_ticket_times t ON ap.times_id = t.ticket_times_id
WHERE ap.performances_id = #{performancesId}
<if test="timesId != null and timesId != ''">
AND ap.times_id = #{timesId}
</if>
ORDER BY ap.sort DESC, ap.created_at ASC
</select>
</mapper> </mapper>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment