appsnbcbweicheng 1.2.27 → 1.2.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appsnbcbweicheng",
3
- "version": "1.2.27",
3
+ "version": "1.2.28",
4
4
  "main": "index.js",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1"
Binary file
package/readme.md CHANGED
@@ -1,3 +1,4 @@
1
+ 1.2.28 双击复制
1
2
  1.2.27 日历编辑
2
3
  1.2.26 投研平台
3
4
  1.2.25 投研平台
@@ -1,679 +0,0 @@
1
- <template>
2
- <div class="workday-calendar">
3
- <div class="toolbar">
4
- <div class="year-nav">
5
- <el-button icon="el-icon-arrow-left" circle size="small" @click="changeYear(-1)" />
6
- <span class="year-label">{{ year }} 年</span>
7
- <el-button icon="el-icon-arrow-right" circle size="small" @click="changeYear(1)" />
8
- </div>
9
-
10
- <div class="toolbar-actions">
11
- <el-radio-group v-model="selectMode" size="small" @change="onSelectModeChange">
12
- <el-radio-button label="single">单选</el-radio-button>
13
- <el-radio-button label="multi">复选</el-radio-button>
14
- </el-radio-group>
15
-
16
- <el-button v-if="selectMode === 'multi'" :type="rangePickMode ? 'primary' : 'default'" size="small"
17
- @click="toggleRangePickMode">
18
- 起止选择
19
- </el-button>
20
-
21
- <el-button type="primary" size="small" :disabled="!hasSelection" @click="applyType('work')">
22
- 设为工作日
23
- </el-button>
24
- <el-button type="warning" size="small" :disabled="!hasSelection" @click="applyType('rest')">
25
- 设为非工作日
26
- </el-button>
27
-
28
- <el-button type="success" size="small" @click="handleSubmit">
29
- 提交({{ changeCount }} 项变更)
30
- </el-button>
31
- </div>
32
- </div>
33
-
34
- <div v-if="selectMode === 'multi' && rangePickMode && rangeAnchor" class="range-hint">
35
- 已选起始日 {{ rangeAnchor }},请点击同月内的结束日完成范围选择,或
36
- <el-button type="text" size="mini" @click="clearRangeAnchor">取消</el-button>
37
- </div>
38
-
39
- <div class="legend">
40
- <span class="legend-item"><i class="dot work" />工作日</span>
41
- <span class="legend-item"><i class="dot rest" />非工作日</span>
42
- <span class="legend-item"><i class="dot modified" />已修改</span>
43
- <span class="legend-item"><i class="dot selected" />已选中</span>
44
- </div>
45
-
46
- <div class="months-grid">
47
- <div v-for="month in 12" :key="month" class="month-card">
48
- <div class="month-title">{{ month }} 月</div>
49
- <div class="month-calendar">
50
- <div class="week-header">
51
- <span v-for="w in weekLabels" :key="w">{{ w }}</span>
52
- </div>
53
- <div class="days-grid" @mouseleave="onMonthMouseLeave(month)">
54
- <template v-for="(cell, idx) in getMonthCells(month)">
55
- <div v-if="cell === null" :key="`empty-${month}-${idx}`" class="day-cell empty" />
56
- <div v-else :key="`${month}-${cell}`" class="day-cell" :class="dayCellClass(month, cell)"
57
- @mousedown.prevent="onDayMouseDown(month, cell, $event)" @mouseenter="onDayMouseEnter(month, cell)"
58
- @mouseup="onDayMouseUp(month)" @click="onDayClick(month, cell)">
59
- {{ cell }}
60
- </div>
61
- </template>
62
- <!-- 框选遮罩 -->
63
- <div v-if="dragState.active && dragState.month === month && dragBoxStyle" class="drag-box"
64
- :style="dragBoxStyle" />
65
- </div>
66
- </div>
67
- </div>
68
- </div>
69
- </div>
70
- </template>
71
-
72
- <script>
73
- const WEEK_LABELS = ['一', '二', '三', '四', '五', '六', '日']
74
-
75
- function pad2(n) {
76
- return String(n).padStart(2, '0')
77
- }
78
-
79
- function toDateKey(year, month, day) {
80
- return `${year}-${pad2(month)}-${pad2(day)}`
81
- }
82
-
83
- function parseDateKey(key) {
84
- const [y, m, d] = key.split('-').map(Number)
85
- return { year: y, month: m, day: d }
86
- }
87
-
88
- export default {
89
- name: 'WorkdayCalendar',
90
- props: {
91
- /** 初始年份 */
92
- initialYear: {
93
- type: Number,
94
- default: () => new Date().getFullYear(),
95
- },
96
- /**
97
- * 初始日历数据 { '2026-01-01': 'work' | 'rest' }
98
- * 未指定的日期默认:周六日为 rest,其余为 work
99
- */
100
- value: {
101
- type: Object,
102
- default: () => ({}),
103
- },
104
- },
105
- data() {
106
- return {
107
- year: this.initialYear,
108
- weekLabels: WEEK_LABELS,
109
- selectMode: 'multi',
110
- /** 当前日期类型 */
111
- dayMap: {},
112
- /** 提交对比用的原始快照 */
113
- originalMap: {},
114
- /** 选中日期集合 */
115
- selected: {},
116
- /** 复选模式下的起止范围起点 */
117
- rangeAnchor: null,
118
- rangePickMode: false,
119
- dragMoved: false,
120
- /** 框选状态 */
121
- dragState: {
122
- active: false,
123
- month: null,
124
- startIdx: null,
125
- endIdx: null,
126
- },
127
- dragBoxStyle: null,
128
- }
129
- },
130
- computed: {
131
- hasSelection() {
132
- return Object.keys(this.selected).length > 0
133
- },
134
- changeCount() {
135
- return this.getChanges().length
136
- },
137
- },
138
- watch: {
139
- year() {
140
- this.initYearData()
141
- this.clearSelection()
142
- },
143
- value: {
144
- deep: true,
145
- handler() {
146
- this.initYearData()
147
- },
148
- },
149
- },
150
- created() {
151
- this.initYearData()
152
- document.addEventListener('mouseup', this.onDocumentMouseUp)
153
- },
154
- beforeDestroy() {
155
- document.removeEventListener('mouseup', this.onDocumentMouseUp)
156
- },
157
- methods: {
158
- initYearData() {
159
- const map = {}
160
- for (let m = 1; m <= 12; m++) {
161
- const days = new Date(this.year, m, 0).getDate()
162
- for (let d = 1; d <= days; d++) {
163
- const key = toDateKey(this.year, m, d)
164
- if (this.value[key] !== undefined) {
165
- map[key] = this.value[key]
166
- } else {
167
- map[key] = this.defaultTypeForDate(this.year, m, d)
168
- }
169
- }
170
- }
171
- console.log(map, 'map')
172
- this.dayMap = { ...map }
173
- this.originalMap = { ...map }
174
- },
175
-
176
- defaultTypeForDate(year, month, day) {
177
- const wd = new Date(year, month - 1, day).getDay()
178
- return wd === 0 || wd === 6 ? 'rest' : 'work'
179
- },
180
-
181
- getMonthCells(month) {
182
- const first = new Date(this.year, month - 1, 1)
183
- const daysInMonth = new Date(this.year, month, 0).getDate()
184
- let offset = first.getDay()
185
- offset = offset === 0 ? 6 : offset - 1
186
-
187
- const cells = []
188
- for (let i = 0; i < offset; i++) cells.push(null)
189
- for (let d = 1; d <= daysInMonth; d++) cells.push(d)
190
- return cells
191
- },
192
-
193
- dayCellClass(month, day) {
194
- const key = toDateKey(this.year, month, day)
195
- const type = this.dayMap[key]
196
- return {
197
- work: type === 'work',
198
- rest: type === 'rest',
199
- selected: !!this.selected[key],
200
- modified: this.dayMap[key] !== this.originalMap[key],
201
- 'range-anchor': this.rangeAnchor === key,
202
- }
203
- },
204
-
205
- changeYear(delta) {
206
- this.year += delta
207
- },
208
-
209
- onSelectModeChange() {
210
- this.clearSelection()
211
- this.clearRangeAnchor()
212
- this.rangePickMode = false
213
- },
214
-
215
- toggleRangePickMode() {
216
- this.rangePickMode = !this.rangePickMode
217
- this.clearRangeAnchor()
218
- },
219
-
220
- clearSelection() {
221
- this.selected = {}
222
- },
223
-
224
- clearRangeAnchor() {
225
- this.rangeAnchor = null
226
- },
227
-
228
- toggleSelect(key, force) {
229
- const next = { ...this.selected }
230
- if (force === true) {
231
- next[key] = true
232
- } else if (force === false) {
233
- delete next[key]
234
- } else if (next[key]) {
235
- delete next[key]
236
- } else {
237
- next[key] = true
238
- }
239
- this.selected = next
240
- },
241
-
242
- selectSingle(key) {
243
- this.selected = { [key]: true }
244
- },
245
-
246
- selectRangeInMonth(month, startDay, endDay) {
247
- const min = Math.min(startDay, endDay)
248
- const max = Math.max(startDay, endDay)
249
- const next = { ...this.selected }
250
- for (let d = min; d <= max; d++) {
251
- next[toDateKey(this.year, month, d)] = true
252
- }
253
- this.selected = next
254
- },
255
-
256
- getCellIndex(month, day) {
257
- const cells = this.getMonthCells(month)
258
- return cells.findIndex((c) => c === day)
259
- },
260
-
261
- indexToDay(month, idx) {
262
- const cells = this.getMonthCells(month)
263
- return cells[idx]
264
- },
265
-
266
- getKeysInBox(month, startIdx, endIdx) {
267
- const cells = this.getMonthCells(month)
268
- const col = 7
269
- const min = Math.min(startIdx, endIdx)
270
- const max = Math.max(startIdx, endIdx)
271
- const r0 = Math.floor(min / col)
272
- const c0 = min % col
273
- const r1 = Math.floor(max / col)
274
- const c1 = max % col
275
- const rowStart = Math.min(r0, r1)
276
- const rowEnd = Math.max(r0, r1)
277
- const colStart = Math.min(c0, c1)
278
- const colEnd = Math.max(c0, c1)
279
- const keys = []
280
- for (let r = rowStart; r <= rowEnd; r++) {
281
- for (let c = colStart; c <= colEnd; c++) {
282
- const idx = r * col + c
283
- if (cells[idx] !== null) {
284
- keys.push(toDateKey(this.year, month, cells[idx]))
285
- }
286
- }
287
- }
288
- return keys
289
- },
290
-
291
- getDragMetrics() {
292
- const grid = this.$el && this.$el.querySelector('.days-grid')
293
- if (!grid) {
294
- return { cellSize: 28, gap: 2 }
295
- }
296
- const style = window.getComputedStyle(grid)
297
- const gap = parseFloat(style.columnGap || style.gap) || 2
298
- const cell = grid.querySelector('.day-cell:not(.empty)')
299
- const cellSize = cell ? cell.offsetWidth : 28
300
- return { cellSize, gap }
301
- },
302
-
303
- updateDragBoxStyle() {
304
- const { startIdx, endIdx } = this.dragState
305
- if (!this.dragState.active || startIdx === null || endIdx === null) {
306
- this.dragBoxStyle = null
307
- return
308
- }
309
- const min = Math.min(startIdx, endIdx)
310
- const max = Math.max(startIdx, endIdx)
311
- const col = 7
312
- const r0 = Math.floor(min / col)
313
- const c0 = min % col
314
- const r1 = Math.floor(max / col)
315
- const c1 = max % col
316
- const rowStart = Math.min(r0, r1)
317
- const rowEnd = Math.max(r0, r1)
318
- const colStart = Math.min(c0, c1)
319
- const colEnd = Math.max(c0, c1)
320
- const { cellSize, gap } = this.getDragMetrics()
321
- const unit = cellSize + gap
322
- this.dragBoxStyle = {
323
- left: `${colStart * unit}px`,
324
- top: `${rowStart * unit}px`,
325
- width: `${(colEnd - colStart + 1) * unit - gap}px`,
326
- height: `${(rowEnd - rowStart + 1) * unit - gap}px`,
327
- }
328
- },
329
-
330
- onDayMouseDown(month, day, e) {
331
- if (this.selectMode !== 'multi' || this.rangePickMode || e.button !== 0) return
332
- const idx = this.getCellIndex(month, day)
333
- this.dragMoved = false
334
- this.dragState = {
335
- active: true,
336
- month,
337
- startIdx: idx,
338
- endIdx: idx,
339
- }
340
- this.updateDragBoxStyle()
341
- },
342
-
343
- onDayMouseEnter(month, day) {
344
- if (!this.dragState.active || this.dragState.month !== month) return
345
- const idx = this.getCellIndex(month, day)
346
- if (idx !== this.dragState.startIdx) {
347
- this.dragMoved = true
348
- }
349
- this.dragState.endIdx = idx
350
- this.updateDragBoxStyle()
351
- },
352
-
353
- onDayMouseUp(month) {
354
- if (!this.dragState.active || this.dragState.month !== month) return
355
- this.finishDragSelect()
356
- },
357
-
358
- onMonthMouseLeave() {
359
- /* 保持 drag,由 document mouseup 结束 */
360
- },
361
-
362
- onDocumentMouseUp() {
363
- if (this.dragState.active) {
364
- this.finishDragSelect()
365
- }
366
- },
367
-
368
- finishDragSelect() {
369
- const { month, startIdx, endIdx } = this.dragState
370
- if (startIdx !== null && endIdx !== null && month !== null) {
371
- const keys = this.getKeysInBox(month, startIdx, endIdx)
372
- const next = { ...this.selected }
373
- keys.forEach((k) => {
374
- next[k] = true
375
- })
376
- this.selected = next
377
- }
378
- this.dragState = {
379
- active: false,
380
- month: null,
381
- startIdx: null,
382
- endIdx: null,
383
- }
384
- this.dragBoxStyle = null
385
- },
386
-
387
- onDayClick(month, day) {
388
- const key = toDateKey(this.year, month, day)
389
-
390
- if (this.selectMode === 'single') {
391
- this.selectSingle(key)
392
- return
393
- }
394
-
395
- if (this.dragMoved) return
396
-
397
- if (this.rangePickMode) {
398
- if (!this.rangeAnchor) {
399
- this.rangeAnchor = key
400
- this.toggleSelect(key, true)
401
- return
402
- }
403
- const anchor = parseDateKey(this.rangeAnchor)
404
- if (anchor.month === month) {
405
- this.selectRangeInMonth(month, anchor.day, day)
406
- } else {
407
- this.$message.warning('结束日需与起始日在同一月份')
408
- }
409
- this.clearRangeAnchor()
410
- return
411
- }
412
-
413
- this.toggleSelect(key)
414
- },
415
-
416
- applyType(type) {
417
- const keys = Object.keys(this.selected)
418
- if (!keys.length) return
419
- const next = { ...this.dayMap }
420
- keys.forEach((k) => {
421
- next[k] = type
422
- })
423
- this.dayMap = next
424
- this.clearSelection()
425
- this.clearRangeAnchor()
426
- },
427
-
428
- getChanges() {
429
- const keys = new Set([
430
- ...Object.keys(this.dayMap),
431
- ...Object.keys(this.originalMap),
432
- ])
433
- const changes = []
434
- keys.forEach((date) => {
435
- if (this.dayMap[date] !== this.originalMap[date]) {
436
- changes.push({
437
- date,
438
- type: this.dayMap[date],
439
- previous: this.originalMap[date],
440
- })
441
- }
442
- })
443
- return changes
444
- },
445
-
446
- handleSubmit() {
447
- const changes = this.getChanges()
448
- if (!changes.length) {
449
- this.$message.info('没有需要提交的变更')
450
- return
451
- }
452
- this.$emit('submit', {
453
- year: this.year,
454
- changes,
455
- })
456
- // 提交后更新快照
457
- changes.forEach(({ date, type }) => {
458
- this.$set(this.originalMap, date, type)
459
- })
460
- console.log(changes, 'changes')
461
- this.$message.success(`已提交 ${changes.length} 项变更`)
462
- },
463
- },
464
- }
465
- </script>
466
-
467
- <style scoped lang="scss">
468
- .workday-calendar {
469
- /* 6 列月历:每卡 7 列格子 + 内边距,避免周日被裁切 */
470
- --cell-size: 28px;
471
- --cell-gap: 2px;
472
- --calendar-cols: 7;
473
- --calendar-width: calc(var(--calendar-cols) * var(--cell-size) + (var(--calendar-cols) - 1) * var(--cell-gap));
474
- --month-card-padding: 24px;
475
- --month-card-min: calc(var(--calendar-width) + var(--month-card-padding));
476
- max-width: calc(6 * var(--month-card-min) + 5 * 16px);
477
- margin: 0 auto;
478
- }
479
-
480
- .toolbar {
481
- display: flex;
482
- flex-wrap: wrap;
483
- align-items: center;
484
- justify-content: space-between;
485
- gap: 12px;
486
- margin-bottom: 16px;
487
- padding: 16px;
488
- background: #fff;
489
- border-radius: 8px;
490
- box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
491
- }
492
-
493
- .year-nav {
494
- display: flex;
495
- align-items: center;
496
- gap: 12px;
497
-
498
- .year-label {
499
- font-size: 20px;
500
- font-weight: 600;
501
- min-width: 100px;
502
- text-align: center;
503
- }
504
- }
505
-
506
- .toolbar-actions {
507
- display: flex;
508
- flex-wrap: wrap;
509
- align-items: center;
510
- gap: 8px;
511
- }
512
-
513
- .range-hint {
514
- margin-bottom: 12px;
515
- padding: 8px 12px;
516
- background: #ecf5ff;
517
- border-radius: 4px;
518
- color: #409eff;
519
- font-size: 13px;
520
- }
521
-
522
- .legend {
523
- display: flex;
524
- flex-wrap: wrap;
525
- gap: 16px;
526
- margin-bottom: 16px;
527
- font-size: 13px;
528
- color: #606266;
529
-
530
- .legend-item {
531
- display: flex;
532
- align-items: center;
533
- gap: 6px;
534
- }
535
-
536
- .dot {
537
- display: inline-block;
538
- width: 12px;
539
- height: 12px;
540
- border-radius: 2px;
541
-
542
- &.work {
543
- background: #e1f3d8;
544
- border: 1px solid #67c23a;
545
- }
546
-
547
- &.rest {
548
- background: #fde2e2;
549
- border: 1px solid #f56c6c;
550
- }
551
-
552
- &.modified {
553
- background: #fff;
554
- border: 2px solid #e6a23c;
555
- }
556
-
557
- &.selected {
558
- background: #fff;
559
- box-shadow: 0 0 0 2px #409eff;
560
- }
561
- }
562
- }
563
-
564
- .months-grid {
565
- display: grid;
566
- grid-template-columns: repeat(6, minmax(var(--month-card-min), 1fr));
567
- gap: 16px;
568
- }
569
-
570
- @media (max-width: 1500px) {
571
- .months-grid {
572
- grid-template-columns: repeat(auto-fill, minmax(var(--month-card-min), 1fr));
573
- }
574
- }
575
-
576
- .month-card {
577
- box-sizing: border-box;
578
- min-width: var(--month-card-min);
579
- background: #fff;
580
- border-radius: 8px;
581
- padding: 12px;
582
- box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
583
- overflow: visible;
584
- }
585
-
586
- .month-calendar {
587
- width: var(--calendar-width);
588
- margin: 0 auto;
589
- }
590
-
591
- .month-title {
592
- font-weight: 600;
593
- margin-bottom: 8px;
594
- text-align: center;
595
- color: #303133;
596
- }
597
-
598
- .week-header {
599
- display: grid;
600
- grid-template-columns: repeat(7, var(--cell-size));
601
- gap: var(--cell-gap);
602
- width: var(--calendar-width);
603
- margin-bottom: 4px;
604
- font-size: 11px;
605
- color: #909399;
606
- text-align: center;
607
- }
608
-
609
- .days-grid {
610
- position: relative;
611
- display: grid;
612
- grid-template-columns: repeat(7, var(--cell-size));
613
- gap: var(--cell-gap);
614
- width: var(--calendar-width);
615
- user-select: none;
616
- }
617
-
618
- .day-cell {
619
- box-sizing: border-box;
620
- width: var(--cell-size);
621
- height: var(--cell-size);
622
- display: flex;
623
- align-items: center;
624
- justify-content: center;
625
- font-size: 12px;
626
- border-radius: 4px;
627
- cursor: pointer;
628
- transition: background 0.15s, box-shadow 0.15s;
629
-
630
- &.empty {
631
- cursor: default;
632
- pointer-events: none;
633
- }
634
-
635
- &.work {
636
- background: #e1f3d8;
637
- color: #529b2e;
638
- }
639
-
640
- &.rest {
641
- background: #fde2e2;
642
- color: #c45656;
643
- }
644
-
645
- &.selected {
646
- box-shadow: 0 0 0 2px #409eff;
647
- }
648
-
649
- &.modified::after {
650
- content: '';
651
- position: absolute;
652
- top: 2px;
653
- right: 2px;
654
- width: 4px;
655
- height: 4px;
656
- border-radius: 50%;
657
- background: #e6a23c;
658
- }
659
-
660
- &.range-anchor {
661
- box-shadow: 0 0 0 2px #e6a23c;
662
- }
663
-
664
- position: relative;
665
-
666
- &:hover:not(.empty) {
667
- filter: brightness(0.95);
668
- }
669
- }
670
-
671
- .drag-box {
672
- position: absolute;
673
- border: 1px dashed #409eff;
674
- background: rgba(64, 158, 255, 0.12);
675
- pointer-events: none;
676
- z-index: 2;
677
- border-radius: 4px;
678
- }
679
- </style>