Compare commits
5 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
561a373e40 | 1 month ago |
|
|
b0eda7277d | 1 month ago |
|
|
03d33118ce | 1 month ago |
|
|
1dd39bbda2 | 1 month ago |
|
|
48fb8045a0 | 1 month ago |
26 changed files with 2779 additions and 262 deletions
@ -0,0 +1,81 @@ |
|||
package com.ruoyi.web.controller.system; |
|||
|
|||
import java.util.List; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.security.access.prepost.PreAuthorize; |
|||
import org.springframework.web.bind.annotation.DeleteMapping; |
|||
import org.springframework.web.bind.annotation.GetMapping; |
|||
import org.springframework.web.bind.annotation.PathVariable; |
|||
import org.springframework.web.bind.annotation.PostMapping; |
|||
import org.springframework.web.bind.annotation.PutMapping; |
|||
import org.springframework.web.bind.annotation.RequestBody; |
|||
import org.springframework.web.bind.annotation.RequestMapping; |
|||
import org.springframework.web.bind.annotation.RestController; |
|||
import com.ruoyi.common.annotation.Log; |
|||
import com.ruoyi.common.core.controller.BaseController; |
|||
import com.ruoyi.common.core.domain.AjaxResult; |
|||
import com.ruoyi.common.core.domain.entity.SysTimelineSegment; |
|||
import com.ruoyi.common.enums.BusinessType; |
|||
import com.ruoyi.system.service.ISysTimelineSegmentService; |
|||
|
|||
@RestController |
|||
@RequestMapping("/system/timeline") |
|||
public class SysTimelineSegmentController extends BaseController |
|||
{ |
|||
@Autowired |
|||
private ISysTimelineSegmentService segmentService; |
|||
|
|||
@PreAuthorize("@ss.hasPermi('system:timeline:list')") |
|||
@GetMapping("/list") |
|||
public AjaxResult list(SysTimelineSegment segment) |
|||
{ |
|||
List<SysTimelineSegment> list = segmentService.selectSegmentList(segment); |
|||
return success(list); |
|||
} |
|||
|
|||
@GetMapping("/listByRoomId/{roomId}") |
|||
public AjaxResult listByRoomId(@PathVariable Long roomId) |
|||
{ |
|||
List<SysTimelineSegment> list = segmentService.selectSegmentListByRoomId(roomId); |
|||
return success(list); |
|||
} |
|||
|
|||
@PreAuthorize("@ss.hasPermi('system:timeline:query')") |
|||
@GetMapping(value = "/{segmentId}") |
|||
public AjaxResult getInfo(@PathVariable Long segmentId) |
|||
{ |
|||
return success(segmentService.selectSegmentById(segmentId)); |
|||
} |
|||
|
|||
@PreAuthorize("@ss.hasPermi('system:timeline:add')") |
|||
@Log(title = "时间轴管理", businessType = BusinessType.INSERT) |
|||
@PostMapping |
|||
public AjaxResult add(@RequestBody SysTimelineSegment segment) |
|||
{ |
|||
return toAjax(segmentService.insertSegment(segment)); |
|||
} |
|||
|
|||
@PreAuthorize("@ss.hasPermi('system:timeline:edit')") |
|||
@Log(title = "时间轴管理", businessType = BusinessType.UPDATE) |
|||
@PutMapping |
|||
public AjaxResult edit(@RequestBody SysTimelineSegment segment) |
|||
{ |
|||
return toAjax(segmentService.updateSegment(segment)); |
|||
} |
|||
|
|||
@PreAuthorize("@ss.hasPermi('system:timeline:remove')") |
|||
@Log(title = "时间轴管理", businessType = BusinessType.DELETE) |
|||
@DeleteMapping("/{segmentId}") |
|||
public AjaxResult remove(@PathVariable Long segmentId) |
|||
{ |
|||
return toAjax(segmentService.deleteSegmentById(segmentId)); |
|||
} |
|||
|
|||
@PreAuthorize("@ss.hasPermi('system:timeline:remove')") |
|||
@Log(title = "时间轴管理", businessType = BusinessType.DELETE) |
|||
@DeleteMapping("/room/{roomId}") |
|||
public AjaxResult removeByRoomId(@PathVariable Long roomId) |
|||
{ |
|||
return toAjax(segmentService.deleteSegmentByRoomId(roomId)); |
|||
} |
|||
} |
|||
@ -0,0 +1,89 @@ |
|||
package com.ruoyi.common.core.domain.entity; |
|||
|
|||
import javax.validation.constraints.NotBlank; |
|||
import javax.validation.constraints.NotNull; |
|||
import javax.validation.constraints.Size; |
|||
import org.apache.commons.lang3.builder.ToStringBuilder; |
|||
import org.apache.commons.lang3.builder.ToStringStyle; |
|||
|
|||
public class SysTimelineSegment |
|||
{ |
|||
private static final long serialVersionUID = 1L; |
|||
|
|||
private Long segmentId; |
|||
|
|||
private Long roomId; |
|||
|
|||
@NotBlank(message = "时间点不能为空") |
|||
@Size(min = 0, max = 8, message = "时间点长度不能超过8个字符") |
|||
private String segmentTime; |
|||
|
|||
@NotBlank(message = "时间段名称不能为空") |
|||
@Size(min = 0, max = 50, message = "时间段名称长度不能超过50个字符") |
|||
private String segmentName; |
|||
|
|||
@Size(max = 500, message = "时间段描述长度不能超过500个字符") |
|||
private String segmentDesc; |
|||
|
|||
public Long getSegmentId() |
|||
{ |
|||
return segmentId; |
|||
} |
|||
|
|||
public void setSegmentId(Long segmentId) |
|||
{ |
|||
this.segmentId = segmentId; |
|||
} |
|||
|
|||
@NotNull(message = "房间ID不能为空") |
|||
public Long getRoomId() |
|||
{ |
|||
return roomId; |
|||
} |
|||
|
|||
public void setRoomId(Long roomId) |
|||
{ |
|||
this.roomId = roomId; |
|||
} |
|||
|
|||
public String getSegmentTime() |
|||
{ |
|||
return segmentTime; |
|||
} |
|||
|
|||
public void setSegmentTime(String segmentTime) |
|||
{ |
|||
this.segmentTime = segmentTime; |
|||
} |
|||
|
|||
public String getSegmentName() |
|||
{ |
|||
return segmentName; |
|||
} |
|||
|
|||
public void setSegmentName(String segmentName) |
|||
{ |
|||
this.segmentName = segmentName; |
|||
} |
|||
|
|||
public String getSegmentDesc() |
|||
{ |
|||
return segmentDesc; |
|||
} |
|||
|
|||
public void setSegmentDesc(String segmentDesc) |
|||
{ |
|||
this.segmentDesc = segmentDesc; |
|||
} |
|||
|
|||
@Override |
|||
public String toString() { |
|||
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE) |
|||
.append("segmentId", getSegmentId()) |
|||
.append("roomId", getRoomId()) |
|||
.append("segmentTime", getSegmentTime()) |
|||
.append("segmentName", getSegmentName()) |
|||
.append("segmentDesc", getSegmentDesc()) |
|||
.toString(); |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
package com.ruoyi.system.mapper; |
|||
|
|||
import java.util.List; |
|||
import org.apache.ibatis.annotations.Param; |
|||
import com.ruoyi.common.core.domain.entity.SysTimelineSegment; |
|||
|
|||
public interface SysTimelineSegmentMapper |
|||
{ |
|||
public List<SysTimelineSegment> selectSegmentList(SysTimelineSegment segment); |
|||
|
|||
public List<SysTimelineSegment> selectSegmentListByRoomId(@Param("roomId") Long roomId); |
|||
|
|||
public SysTimelineSegment selectSegmentById(Long segmentId); |
|||
|
|||
public int insertSegment(SysTimelineSegment segment); |
|||
|
|||
public int updateSegment(SysTimelineSegment segment); |
|||
|
|||
public int deleteSegmentById(Long segmentId); |
|||
|
|||
public int deleteSegmentByRoomId(@Param("roomId") Long roomId); |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
package com.ruoyi.system.service; |
|||
|
|||
import java.util.List; |
|||
import com.ruoyi.common.core.domain.entity.SysTimelineSegment; |
|||
|
|||
public interface ISysTimelineSegmentService |
|||
{ |
|||
public List<SysTimelineSegment> selectSegmentList(SysTimelineSegment segment); |
|||
|
|||
public List<SysTimelineSegment> selectSegmentListByRoomId(Long roomId); |
|||
|
|||
public SysTimelineSegment selectSegmentById(Long segmentId); |
|||
|
|||
public int insertSegment(SysTimelineSegment segment); |
|||
|
|||
public int updateSegment(SysTimelineSegment segment); |
|||
|
|||
public int deleteSegmentById(Long segmentId); |
|||
|
|||
public int deleteSegmentByRoomId(Long roomId); |
|||
} |
|||
@ -0,0 +1,57 @@ |
|||
package com.ruoyi.system.service.impl; |
|||
|
|||
import java.util.List; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.stereotype.Service; |
|||
import com.ruoyi.common.core.domain.entity.SysTimelineSegment; |
|||
import com.ruoyi.system.mapper.SysTimelineSegmentMapper; |
|||
import com.ruoyi.system.service.ISysTimelineSegmentService; |
|||
|
|||
@Service |
|||
public class SysTimelineSegmentServiceImpl implements ISysTimelineSegmentService |
|||
{ |
|||
@Autowired |
|||
private SysTimelineSegmentMapper segmentMapper; |
|||
|
|||
@Override |
|||
public List<SysTimelineSegment> selectSegmentList(SysTimelineSegment segment) |
|||
{ |
|||
return segmentMapper.selectSegmentList(segment); |
|||
} |
|||
|
|||
@Override |
|||
public List<SysTimelineSegment> selectSegmentListByRoomId(Long roomId) |
|||
{ |
|||
return segmentMapper.selectSegmentListByRoomId(roomId); |
|||
} |
|||
|
|||
@Override |
|||
public SysTimelineSegment selectSegmentById(Long segmentId) |
|||
{ |
|||
return segmentMapper.selectSegmentById(segmentId); |
|||
} |
|||
|
|||
@Override |
|||
public int insertSegment(SysTimelineSegment segment) |
|||
{ |
|||
return segmentMapper.insertSegment(segment); |
|||
} |
|||
|
|||
@Override |
|||
public int updateSegment(SysTimelineSegment segment) |
|||
{ |
|||
return segmentMapper.updateSegment(segment); |
|||
} |
|||
|
|||
@Override |
|||
public int deleteSegmentById(Long segmentId) |
|||
{ |
|||
return segmentMapper.deleteSegmentById(segmentId); |
|||
} |
|||
|
|||
@Override |
|||
public int deleteSegmentByRoomId(Long roomId) |
|||
{ |
|||
return segmentMapper.deleteSegmentByRoomId(roomId); |
|||
} |
|||
} |
|||
@ -0,0 +1,77 @@ |
|||
<?xml version="1.0" encoding="UTF-8" ?> |
|||
<!DOCTYPE mapper |
|||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" |
|||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd"> |
|||
<mapper namespace="com.ruoyi.system.mapper.SysTimelineSegmentMapper"> |
|||
|
|||
<resultMap type="SysTimelineSegment" id="SysTimelineSegmentResult"> |
|||
<id property="segmentId" column="segment_id" /> |
|||
<result property="roomId" column="room_id" /> |
|||
<result property="segmentTime" column="segment_time" /> |
|||
<result property="segmentName" column="segment_name" /> |
|||
<result property="segmentDesc" column="segment_desc" /> |
|||
</resultMap> |
|||
|
|||
<sql id="selectSegmentVo"> |
|||
select s.segment_id, s.room_id, s.segment_time, s.segment_name, s.segment_desc |
|||
from sys_timeline_segment s |
|||
</sql> |
|||
|
|||
<select id="selectSegmentList" parameterType="SysTimelineSegment" resultMap="SysTimelineSegmentResult"> |
|||
<include refid="selectSegmentVo"/> |
|||
where 1=1 |
|||
<if test="roomId != null and roomId != 0"> |
|||
AND room_id = #{roomId} |
|||
</if> |
|||
<if test="segmentName != null and segmentName != ''"> |
|||
AND segment_name like concat('%', #{segmentName}, '%') |
|||
</if> |
|||
order by s.segment_time |
|||
</select> |
|||
|
|||
<select id="selectSegmentListByRoomId" parameterType="Long" resultMap="SysTimelineSegmentResult"> |
|||
<include refid="selectSegmentVo"/> |
|||
where room_id = #{roomId} |
|||
order by s.segment_time |
|||
</select> |
|||
|
|||
<select id="selectSegmentById" parameterType="Long" resultMap="SysTimelineSegmentResult"> |
|||
<include refid="selectSegmentVo"/> |
|||
where segment_id = #{segmentId} |
|||
</select> |
|||
|
|||
<insert id="insertSegment" parameterType="SysTimelineSegment"> |
|||
insert into sys_timeline_segment( |
|||
room_id, |
|||
segment_time, |
|||
segment_name, |
|||
<if test="segmentDesc != null and segmentDesc != ''">segment_desc,</if> |
|||
segment_id |
|||
)values( |
|||
#{roomId}, |
|||
#{segmentTime}, |
|||
#{segmentName}, |
|||
<if test="segmentDesc != null and segmentDesc != ''">#{segmentDesc},</if> |
|||
#{segmentId} |
|||
) |
|||
</insert> |
|||
|
|||
<update id="updateSegment" parameterType="SysTimelineSegment"> |
|||
update sys_timeline_segment |
|||
<set> |
|||
<if test="segmentTime != null and segmentTime != ''">segment_time = #{segmentTime},</if> |
|||
<if test="segmentName != null and segmentName != ''">segment_name = #{segmentName},</if> |
|||
<if test="segmentDesc != null">segment_desc = #{segmentDesc},</if> |
|||
</set> |
|||
where segment_id = #{segmentId} |
|||
</update> |
|||
|
|||
<delete id="deleteSegmentById" parameterType="Long"> |
|||
delete from sys_timeline_segment where segment_id = #{segmentId} |
|||
</delete> |
|||
|
|||
<delete id="deleteSegmentByRoomId" parameterType="Long"> |
|||
delete from sys_timeline_segment where room_id = #{roomId} |
|||
</delete> |
|||
|
|||
</mapper> |
|||
@ -1,12 +1,12 @@ |
|||
module.exports = { |
|||
presets: [ |
|||
// https://github.com/vuejs/vue-cli/tree/master/packages/@vue/babel-preset-app
|
|||
'@vue/cli-plugin-babel/preset' |
|||
], |
|||
plugins: [ |
|||
'@babel/plugin-proposal-object-rest-spread' |
|||
], |
|||
'env': { |
|||
'development': { |
|||
// babel-plugin-dynamic-import-node plugin only does one thing by converting all import() to require().
|
|||
// This plugin can significantly increase the speed of hot updates, when you have a large number of pages.
|
|||
'plugins': ['dynamic-import-node'] |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,691 @@ |
|||
<!DOCTYPE html> |
|||
<html lang="zh-CN"> |
|||
<head> |
|||
<meta charset="UTF-8"> |
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|||
<title>数据卡</title> |
|||
<link rel="stylesheet" href="./element-ui.css"> |
|||
<style> |
|||
* { |
|||
margin: 0; |
|||
padding: 0; |
|||
box-sizing: border-box; |
|||
} |
|||
|
|||
body { |
|||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; |
|||
background: #fafafa; |
|||
} |
|||
|
|||
#app { |
|||
width: 100%; |
|||
height: 100vh; |
|||
} |
|||
|
|||
.data-card-container { |
|||
display: flex; |
|||
flex-direction: column; |
|||
height: 100vh; |
|||
background: #fafafa; |
|||
} |
|||
|
|||
.data-card-header { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
padding: 0 32px; |
|||
height: 56px; |
|||
background: #fff; |
|||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); |
|||
} |
|||
|
|||
.header-left { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 16px; |
|||
} |
|||
|
|||
.back-btn { |
|||
padding: 8px 12px; |
|||
font-size: 14px; |
|||
color: #666; |
|||
cursor: pointer; |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 4px; |
|||
} |
|||
|
|||
.back-btn:hover { |
|||
color: #409EFF; |
|||
} |
|||
|
|||
.data-card-title { |
|||
font-size: 18px; |
|||
font-weight: 600; |
|||
color: #333; |
|||
} |
|||
|
|||
.header-right { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 12px; |
|||
} |
|||
|
|||
.data-card-toolbar { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 12px; |
|||
padding: 16px 32px; |
|||
background: #fff; |
|||
border-bottom: 1px solid #e8e8e8; |
|||
} |
|||
|
|||
.toolbar-group { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 8px; |
|||
} |
|||
|
|||
.toolbar-label { |
|||
font-size: 14px; |
|||
color: #666; |
|||
} |
|||
|
|||
.data-card-content { |
|||
flex: 1; |
|||
padding: 24px 32px; |
|||
overflow: auto; |
|||
} |
|||
|
|||
.table-container { |
|||
background: #fff; |
|||
border-radius: 8px; |
|||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); |
|||
padding: 24px; |
|||
display: inline-block; |
|||
} |
|||
|
|||
.data-table { |
|||
border-collapse: collapse; |
|||
table-layout: fixed; |
|||
} |
|||
|
|||
.data-table td { |
|||
border: 1px solid #ddd; |
|||
min-width: 60px; |
|||
height: 30px; |
|||
position: relative; |
|||
cursor: cell; |
|||
transition: background-color 0.2s; |
|||
} |
|||
|
|||
.data-table td:hover { |
|||
background-color: #f5f7fa; |
|||
} |
|||
|
|||
.data-table td.selected { |
|||
background-color: #ecf5ff; |
|||
border-color: #409EFF; |
|||
} |
|||
|
|||
.resize-handle-col { |
|||
position: absolute; |
|||
right: 0; |
|||
top: 0; |
|||
width: 5px; |
|||
height: 100%; |
|||
cursor: col-resize; |
|||
background: transparent; |
|||
z-index: 10; |
|||
} |
|||
|
|||
.resize-handle-col:hover { |
|||
background: #409EFF; |
|||
} |
|||
|
|||
.resize-handle-row { |
|||
position: absolute; |
|||
bottom: 0; |
|||
left: 0; |
|||
width: 100%; |
|||
height: 5px; |
|||
cursor: row-resize; |
|||
background: transparent; |
|||
z-index: 10; |
|||
} |
|||
|
|||
.resize-handle-row:hover { |
|||
background: #409EFF; |
|||
} |
|||
|
|||
.cell-input { |
|||
width: 100%; |
|||
height: 100%; |
|||
border: none; |
|||
outline: none; |
|||
padding: 4px 8px; |
|||
font-size: 14px; |
|||
background: transparent; |
|||
} |
|||
|
|||
.color-picker-panel { |
|||
display: flex; |
|||
flex-wrap: wrap; |
|||
gap: 4px; |
|||
width: 180px; |
|||
} |
|||
|
|||
.color-option { |
|||
width: 24px; |
|||
height: 24px; |
|||
border-radius: 4px; |
|||
cursor: pointer; |
|||
border: 2px solid transparent; |
|||
} |
|||
|
|||
.color-option:hover { |
|||
border-color: #409EFF; |
|||
} |
|||
|
|||
.color-option.selected { |
|||
border-color: #409EFF; |
|||
} |
|||
</style> |
|||
</head> |
|||
<body> |
|||
<div id="app"> |
|||
<div class="data-card-container"> |
|||
<div class="data-card-header"> |
|||
<div class="header-left"> |
|||
<div class="back-btn" @click="goBack"> |
|||
<i class="el-icon-arrow-left"></i> |
|||
返回 |
|||
</div> |
|||
<div class="data-card-title">数据卡</div> |
|||
</div> |
|||
<div class="header-right"> |
|||
<el-button type="primary" size="small" @click="exportImage"> |
|||
<i class="el-icon-picture-outline"></i> |
|||
导出图片 |
|||
</el-button> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="data-card-toolbar"> |
|||
<div class="toolbar-group"> |
|||
<span class="toolbar-label">行数:</span> |
|||
<el-input-number v-model="rows" :min="1" :max="50" size="small" @change="updateTableSize"></el-input-number> |
|||
</div> |
|||
<div class="toolbar-group"> |
|||
<span class="toolbar-label">列数:</span> |
|||
<el-input-number v-model="cols" :min="1" :max="20" size="small" @change="updateTableSize"></el-input-number> |
|||
</div> |
|||
<el-divider direction="vertical"></el-divider> |
|||
<div class="toolbar-group"> |
|||
<span class="toolbar-label">单元格背景色:</span> |
|||
<el-popover |
|||
placement="bottom" |
|||
width="200" |
|||
trigger="click" |
|||
v-model="colorPopoverVisible"> |
|||
<div class="color-picker-panel"> |
|||
<div |
|||
v-for="color in colors" |
|||
:key="color" |
|||
class="color-option" |
|||
:style="{ backgroundColor: color }" |
|||
:class="{ selected: selectedColor === color }" |
|||
@click="selectColor(color)"> |
|||
</div> |
|||
</div> |
|||
<el-button slot="reference" size="small"> |
|||
<span :style="{ display: 'inline-block', width: '16px', height: '16px', backgroundColor: selectedColor, marginRight: '8px', verticalAlign: 'middle', borderRadius: '2px' }"></span> |
|||
选择颜色 |
|||
</el-button> |
|||
</el-popover> |
|||
</div> |
|||
<el-divider direction="vertical"></el-divider> |
|||
<div class="toolbar-group"> |
|||
<el-button size="small" @click="undo">撤回</el-button> |
|||
<el-button size="small" @click="mergeCells">合并单元格</el-button> |
|||
<el-button type="primary" size="small" @click="saveTable">保存</el-button> |
|||
<el-button type="danger" size="small" @click="clearCell">清空选中</el-button> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="data-card-content"> |
|||
<div class="table-container" ref="tableContainer"> |
|||
<table class="data-table" ref="dataTable"> |
|||
<tbody> |
|||
<tr v-for="(row, rowIndex) in tableData" :key="rowIndex"> |
|||
<td |
|||
v-for="(cell, colIndex) in row" |
|||
:key="colIndex" |
|||
:class="{ selected: selectedCells.some(c => c.row === rowIndex && c.col === colIndex) }" |
|||
:rowspan="getRowspan(rowIndex, colIndex)" |
|||
:colspan="getColspan(rowIndex, colIndex)" |
|||
:style="getCellStyle(rowIndex, colIndex)" |
|||
@click="editCell(rowIndex, colIndex)" |
|||
@mousedown="startSelect($event, rowIndex, colIndex)" |
|||
@mouseover="continueSelect($event, rowIndex, colIndex)" |
|||
@mouseup="endSelect"> |
|||
<div class="resize-handle-col" @mousedown.stop="startResizeCol($event, colIndex)"></div> |
|||
<div class="resize-handle-row" @mousedown.stop="startResizeRow($event, rowIndex)"></div> |
|||
<span v-if="!editingCell || editingCell.row !== rowIndex || editingCell.col !== colIndex">{{ cell.content }}</span> |
|||
<input |
|||
v-else |
|||
class="cell-input" |
|||
v-model="tableData[rowIndex][colIndex].content" |
|||
@blur="finishEditing" |
|||
@keyup.enter="finishEditing" |
|||
ref="cellInput" |
|||
autofocus> |
|||
</td> |
|||
</tr> |
|||
</tbody> |
|||
</table> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<script src="https://unpkg.com/vue@2.6.12/dist/vue.js"></script> |
|||
<script src="https://unpkg.com/element-ui/lib/index.js"></script> |
|||
<script src="https://unpkg.com/html2canvas@1.4.1/dist/html2canvas.min.js"></script> |
|||
<script> |
|||
new Vue({ |
|||
el: '#app', |
|||
data() { |
|||
return { |
|||
rows: 10, |
|||
cols: 8, |
|||
tableData: [], |
|||
cellWidths: [], |
|||
cellHeights: [], |
|||
selectedCells: [], |
|||
isSelecting: false, |
|||
startCell: null, |
|||
editingCell: null, |
|||
selectedColor: '#ffffff', |
|||
colorPopoverVisible: false, |
|||
colors: [ |
|||
'#ffffff', '#f5f7fa', '#e6a23c', '#67c23a', '#409eff', |
|||
'#909399', '#f0f9eb', '#fdf6ec', '#ecf5ff', '#f4f4f5', |
|||
'#ff6b6b', '#51cf66', '#339af0', '#cc5de8', '#ff922b', |
|||
'#ffd43b', '#20c997', '#22b8cf', '#748ffc', '#be4bdb' |
|||
], |
|||
resizingCol: null, |
|||
resizingRow: null, |
|||
startX: 0, |
|||
startY: 0, |
|||
startWidth: 0, |
|||
startHeight: 0, |
|||
mergedCells: [], |
|||
history: [], |
|||
historyIndex: -1 |
|||
}; |
|||
}, |
|||
mounted() { |
|||
this.loadTable(); |
|||
if (this.tableData.length === 0) { |
|||
this.generateTable(); |
|||
// 初始化历史记录 |
|||
this.saveState(); |
|||
} |
|||
document.addEventListener('mouseup', this.endSelect); |
|||
document.addEventListener('mousemove', this.handleResize); |
|||
document.addEventListener('mouseup', this.endResize); |
|||
}, |
|||
beforeDestroy() { |
|||
document.removeEventListener('mouseup', this.endSelect); |
|||
document.removeEventListener('mousemove', this.handleResize); |
|||
document.removeEventListener('mouseup', this.endResize); |
|||
}, |
|||
methods: { |
|||
goBack() { |
|||
window.close(); |
|||
}, |
|||
generateTable() { |
|||
this.tableData = []; |
|||
this.cellWidths = []; |
|||
this.cellHeights = []; |
|||
this.mergedCells = []; |
|||
for (let i = 0; i < this.rows; i++) { |
|||
const row = []; |
|||
for (let j = 0; j < this.cols; j++) { |
|||
row.push({ content: '', bgColor: '#ffffff' }); |
|||
} |
|||
this.tableData.push(row); |
|||
this.cellHeights.push(30); |
|||
} |
|||
for (let j = 0; j < this.cols; j++) { |
|||
this.cellWidths.push(100); |
|||
} |
|||
this.selectedCells = []; |
|||
// 重置历史记录 |
|||
this.history = []; |
|||
this.historyIndex = -1; |
|||
}, |
|||
|
|||
updateTableSize() { |
|||
const oldRows = this.tableData.length; |
|||
const oldCols = oldRows > 0 ? this.tableData[0].length : 0; |
|||
|
|||
if (this.rows !== oldRows || this.cols !== oldCols) { |
|||
// 保存当前状态 |
|||
this.saveState(); |
|||
|
|||
// 处理行的增减 |
|||
if (this.rows > oldRows) { |
|||
// 增加行 |
|||
for (let i = oldRows; i < this.rows; i++) { |
|||
const row = []; |
|||
for (let j = 0; j < this.cols; j++) { |
|||
row.push({ content: '', bgColor: '#ffffff' }); |
|||
} |
|||
this.tableData.push(row); |
|||
this.cellHeights.push(30); |
|||
} |
|||
} else if (this.rows < oldRows) { |
|||
// 减少行 |
|||
this.tableData.splice(this.rows); |
|||
this.cellHeights.splice(this.rows); |
|||
} |
|||
|
|||
// 处理列的增减 |
|||
if (this.cols > oldCols) { |
|||
// 增加列 |
|||
for (let i = 0; i < this.tableData.length; i++) { |
|||
for (let j = oldCols; j < this.cols; j++) { |
|||
this.tableData[i].push({ content: '', bgColor: '#ffffff' }); |
|||
} |
|||
} |
|||
for (let j = oldCols; j < this.cols; j++) { |
|||
this.cellWidths.push(100); |
|||
} |
|||
} else if (this.cols < oldCols) { |
|||
// 减少列 |
|||
for (let i = 0; i < this.tableData.length; i++) { |
|||
this.tableData[i].splice(this.cols); |
|||
} |
|||
this.cellWidths.splice(this.cols); |
|||
} |
|||
} |
|||
}, |
|||
|
|||
mergeCells() { |
|||
if (this.selectedCells.length < 2) { |
|||
this.$message.warning('请至少选择两个单元格进行合并'); |
|||
return; |
|||
} |
|||
|
|||
// 保存当前状态 |
|||
this.saveState(); |
|||
|
|||
// 计算合并区域的最小和最大行、列 |
|||
const minRow = Math.min(...this.selectedCells.map(c => c.row)); |
|||
const maxRow = Math.max(...this.selectedCells.map(c => c.row)); |
|||
const minCol = Math.min(...this.selectedCells.map(c => c.col)); |
|||
const maxCol = Math.max(...this.selectedCells.map(c => c.col)); |
|||
|
|||
// 获取左上角单元格的内容和背景色 |
|||
const firstCell = this.tableData[minRow][minCol]; |
|||
const content = firstCell.content; |
|||
const bgColor = firstCell.bgColor; |
|||
|
|||
// 清除其他单元格的内容和背景色 |
|||
for (let r = minRow; r <= maxRow; r++) { |
|||
for (let c = minCol; c <= maxCol; c++) { |
|||
if (r !== minRow || c !== minCol) { |
|||
this.tableData[r][c].content = ''; |
|||
this.tableData[r][c].bgColor = '#ffffff'; |
|||
} |
|||
} |
|||
} |
|||
|
|||
// 记录合并信息 |
|||
this.mergedCells.push({ |
|||
startRow: minRow, |
|||
startCol: minCol, |
|||
endRow: maxRow, |
|||
endCol: maxCol |
|||
}); |
|||
|
|||
this.$message.success('单元格合并成功'); |
|||
}, |
|||
|
|||
saveTable() { |
|||
const tableData = { |
|||
rows: this.rows, |
|||
cols: this.cols, |
|||
tableData: this.tableData, |
|||
cellWidths: this.cellWidths, |
|||
cellHeights: this.cellHeights, |
|||
mergedCells: this.mergedCells |
|||
}; |
|||
localStorage.setItem('dataCardTable', JSON.stringify(tableData)); |
|||
this.$message.success('表格保存成功!'); |
|||
}, |
|||
|
|||
loadTable() { |
|||
const savedData = localStorage.getItem('dataCardTable'); |
|||
if (savedData) { |
|||
try { |
|||
const data = JSON.parse(savedData); |
|||
this.rows = data.rows; |
|||
this.cols = data.cols; |
|||
this.tableData = data.tableData; |
|||
this.cellWidths = data.cellWidths; |
|||
this.cellHeights = data.cellHeights; |
|||
this.mergedCells = data.mergedCells || []; |
|||
this.$message.success('表格加载成功!'); |
|||
// 初始化历史记录 |
|||
this.saveState(); |
|||
} catch (e) { |
|||
console.error('加载表格失败:', e); |
|||
} |
|||
} |
|||
}, |
|||
|
|||
saveState() { |
|||
// 保存当前状态到历史记录 |
|||
const state = { |
|||
rows: this.rows, |
|||
cols: this.cols, |
|||
tableData: JSON.parse(JSON.stringify(this.tableData)), |
|||
cellWidths: [...this.cellWidths], |
|||
cellHeights: [...this.cellHeights], |
|||
mergedCells: JSON.parse(JSON.stringify(this.mergedCells)) |
|||
}; |
|||
|
|||
// 如果当前不是在历史记录的最后,删除后面的历史记录 |
|||
if (this.historyIndex < this.history.length - 1) { |
|||
this.history = this.history.slice(0, this.historyIndex + 1); |
|||
} |
|||
|
|||
// 添加新状态到历史记录 |
|||
this.history.push(state); |
|||
|
|||
// 限制历史记录长度,防止内存占用过大 |
|||
if (this.history.length > 50) { |
|||
this.history.shift(); |
|||
// 调整历史记录索引 |
|||
if (this.historyIndex > 0) { |
|||
this.historyIndex--; |
|||
} |
|||
} else { |
|||
this.historyIndex++; |
|||
} |
|||
}, |
|||
|
|||
undo() { |
|||
if (this.historyIndex > 0) { |
|||
this.historyIndex--; |
|||
const state = this.history[this.historyIndex]; |
|||
this.rows = state.rows; |
|||
this.cols = state.cols; |
|||
this.tableData = state.tableData; |
|||
this.cellWidths = state.cellWidths; |
|||
this.cellHeights = state.cellHeights; |
|||
this.mergedCells = state.mergedCells; |
|||
this.$message.success('操作已撤回'); |
|||
} else { |
|||
this.$message.info('没有可撤回的操作'); |
|||
} |
|||
}, |
|||
|
|||
startSelect(event, row, col) { |
|||
if (event.button !== 0) return; |
|||
// 只有在按下鼠标并移动时才开始选择 |
|||
this.isSelecting = true; |
|||
this.startCell = { row, col }; |
|||
this.selectedCells = [{ row, col }]; |
|||
}, |
|||
continueSelect(event, row, col) { |
|||
if (!this.isSelecting || !this.startCell) return; |
|||
const minRow = Math.min(this.startCell.row, row); |
|||
const maxRow = Math.max(this.startCell.row, row); |
|||
const minCol = Math.min(this.startCell.col, col); |
|||
const maxCol = Math.max(this.startCell.col, col); |
|||
this.selectedCells = []; |
|||
for (let r = minRow; r <= maxRow; r++) { |
|||
for (let c = minCol; c <= maxCol; c++) { |
|||
this.selectedCells.push({ row: r, col: c }); |
|||
} |
|||
} |
|||
}, |
|||
endSelect() { |
|||
this.isSelecting = false; |
|||
}, |
|||
editCell(row, col) { |
|||
// 停止任何正在进行的选择 |
|||
this.isSelecting = false; |
|||
this.selectedCells = [{ row, col }]; |
|||
|
|||
// 进入编辑模式 |
|||
this.editingCell = { row, col }; |
|||
this.$nextTick(() => { |
|||
const cellInputs = this.$refs.cellInput; |
|||
if (cellInputs) { |
|||
// 确保输入框获得焦点 |
|||
if (Array.isArray(cellInputs)) { |
|||
cellInputs.forEach(input => { |
|||
if (input) input.focus(); |
|||
}); |
|||
} else if (cellInputs) { |
|||
cellInputs.focus(); |
|||
} |
|||
} |
|||
}); |
|||
}, |
|||
finishEditing() { |
|||
// 保存当前状态 |
|||
this.saveState(); |
|||
this.editingCell = null; |
|||
}, |
|||
selectColor(color) { |
|||
if (this.selectedCells.length === 0) return; |
|||
|
|||
// 保存当前状态 |
|||
this.saveState(); |
|||
|
|||
this.selectedColor = color; |
|||
this.colorPopoverVisible = false; |
|||
this.selectedCells.forEach(cell => { |
|||
this.tableData[cell.row][cell.col].bgColor = color; |
|||
}); |
|||
}, |
|||
clearCell() { |
|||
if (this.selectedCells.length === 0) return; |
|||
|
|||
// 保存当前状态 |
|||
this.saveState(); |
|||
|
|||
this.selectedCells.forEach(cell => { |
|||
this.tableData[cell.row][cell.col].content = ''; |
|||
this.tableData[cell.row][cell.col].bgColor = '#ffffff'; |
|||
}); |
|||
}, |
|||
startResizeCol(event, col) { |
|||
this.resizingCol = col; |
|||
this.startX = event.clientX; |
|||
this.startWidth = this.cellWidths[col]; |
|||
}, |
|||
startResizeRow(event, row) { |
|||
this.resizingRow = row; |
|||
this.startY = event.clientY; |
|||
this.startHeight = this.cellHeights[row]; |
|||
}, |
|||
handleResize(event) { |
|||
if (this.resizingCol !== null) { |
|||
const deltaX = event.clientX - this.startX; |
|||
const newWidth = Math.max(60, this.startWidth + deltaX); |
|||
this.$set(this.cellWidths, this.resizingCol, newWidth); |
|||
} |
|||
if (this.resizingRow !== null) { |
|||
const deltaY = event.clientY - this.startY; |
|||
const newHeight = Math.max(30, this.startHeight + deltaY); |
|||
this.$set(this.cellHeights, this.resizingRow, newHeight); |
|||
} |
|||
}, |
|||
endResize() { |
|||
this.resizingCol = null; |
|||
this.resizingRow = null; |
|||
}, |
|||
getRowspan(row, col) { |
|||
// 检查当前单元格是否是合并单元格的起始单元格 |
|||
const mergedCell = this.mergedCells.find(mc => mc.startRow === row && mc.startCol === col); |
|||
return mergedCell ? mergedCell.endRow - mergedCell.startRow + 1 : 1; |
|||
}, |
|||
|
|||
getColspan(row, col) { |
|||
// 检查当前单元格是否是合并单元格的起始单元格 |
|||
const mergedCell = this.mergedCells.find(mc => mc.startRow === row && mc.startCol === col); |
|||
return mergedCell ? mergedCell.endCol - mergedCell.startCol + 1 : 1; |
|||
}, |
|||
|
|||
getCellStyle(row, col) { |
|||
// 检查当前单元格是否是合并单元格的一部分 |
|||
const mergedCell = this.mergedCells.find(mc => |
|||
row >= mc.startRow && row <= mc.endRow && |
|||
col >= mc.startCol && col <= mc.endCol |
|||
); |
|||
|
|||
// 如果是合并单元格的非起始单元格,隐藏它 |
|||
if (mergedCell && (row !== mergedCell.startRow || col !== mergedCell.startCol)) { |
|||
return { |
|||
display: 'none', |
|||
width: this.cellWidths[col] + 'px', |
|||
height: this.cellHeights[row] + 'px' |
|||
}; |
|||
} |
|||
|
|||
// 正常单元格的样式 |
|||
return { |
|||
width: this.cellWidths[col] + 'px', |
|||
height: this.cellHeights[row] + 'px', |
|||
backgroundColor: this.tableData[row][col].bgColor || '#fff' |
|||
}; |
|||
}, |
|||
|
|||
exportImage() { |
|||
const container = this.$refs.tableContainer; |
|||
html2canvas(container, { |
|||
backgroundColor: '#ffffff', |
|||
scale: 2, |
|||
useCORS: true |
|||
}).then(canvas => { |
|||
const link = document.createElement('a'); |
|||
link.download = '数据卡.png'; |
|||
link.href = canvas.toDataURL('image/png'); |
|||
link.click(); |
|||
this.$message.success('图片导出成功!'); |
|||
}).catch(err => { |
|||
this.$message.error('图片导出失败:' + err.message); |
|||
}); |
|||
} |
|||
} |
|||
}); |
|||
</script> |
|||
</body> |
|||
</html> |
|||
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 28 KiB |
@ -0,0 +1,53 @@ |
|||
import request from '@/utils/request' |
|||
|
|||
export function listTimelineSegments(query) { |
|||
return request({ |
|||
url: '/system/timeline/list', |
|||
method: 'get', |
|||
params: query |
|||
}) |
|||
} |
|||
|
|||
export function getTimelineSegmentsByRoomId(roomId) { |
|||
return request({ |
|||
url: '/system/timeline/listByRoomId/' + roomId, |
|||
method: 'get' |
|||
}) |
|||
} |
|||
|
|||
export function getTimelineSegment(id) { |
|||
return request({ |
|||
url: '/system/timeline/' + id, |
|||
method: 'get' |
|||
}) |
|||
} |
|||
|
|||
export function addTimelineSegment(data) { |
|||
return request({ |
|||
url: '/system/timeline', |
|||
method: 'post', |
|||
data: data |
|||
}) |
|||
} |
|||
|
|||
export function updateTimelineSegment(data) { |
|||
return request({ |
|||
url: '/system/timeline', |
|||
method: 'put', |
|||
data: data |
|||
}) |
|||
} |
|||
|
|||
export function delTimelineSegment(id) { |
|||
return request({ |
|||
url: '/system/timeline/' + id, |
|||
method: 'delete' |
|||
}) |
|||
} |
|||
|
|||
export function delTimelineSegmentsByRoomId(roomId) { |
|||
return request({ |
|||
url: '/system/timeline/room/' + roomId, |
|||
method: 'delete' |
|||
}) |
|||
} |
|||
|
After Width: | Height: | Size: 1.3 KiB |
File diff suppressed because it is too large
@ -0,0 +1,22 @@ |
|||
-- ---------------------------- |
|||
-- 时间轴信息表 |
|||
-- ---------------------------- |
|||
drop table if exists sys_timeline_segment; |
|||
create table sys_timeline_segment ( |
|||
segment_id bigint(20) not null auto_increment comment '时间段ID', |
|||
room_id bigint(20) not null comment '房间ID', |
|||
segment_time varchar(8) not null comment '时间点(HH:mm:ss)', |
|||
segment_name varchar(50) not null comment '时间段名称', |
|||
segment_desc varchar(500) default '' comment '时间段描述', |
|||
primary key (segment_id), |
|||
key idx_room_id (room_id) |
|||
) engine=innodb auto_increment=100 comment = '时间轴信息表'; |
|||
|
|||
-- ---------------------------- |
|||
-- 初始化时间轴信息表数据(示例数据) |
|||
-- ---------------------------- |
|||
insert into sys_timeline_segment values(1, 1, '08:00:00', '任务准备', '开始准备任务所需的资源和设备'); |
|||
insert into sys_timeline_segment values(2, 1, '10:00:00', '资源调配', '完成资源的调配和分配'); |
|||
insert into sys_timeline_segment values(3, 1, '12:00:00', '任务执行', '开始执行主要任务'); |
|||
insert into sys_timeline_segment values(4, 1, '14:00:00', '任务监控', '监控任务执行进度'); |
|||
insert into sys_timeline_segment values(5, 1, '16:00:00', '任务完成', '任务完成,进行总结'); |
|||
Loading…
Reference in new issue