| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729 |
- <template>
- <schedule-wrapper
- class="c-schedule-calendar"
- v-bind="scheduleOptions"
- :dirty="dirty"
- @add="addProgram"
- @submit="submit"
- @save="onSave"
- @click.stop
- >
- <template #header>
- <button
- v-if="editable"
- class="l-flex__none c-sibling-item o-button"
- @click="today"
- >
- 本月
- </button>
- <i
- class="c-schedule-calendar__btn el-icon-arrow-left c-sibling-item u-pointer"
- :class="{ show: canToPresent }"
- @click="toPresent"
- />
- <span class="c-schedule-calendar__time u-readonly">{{ date }}</span>
- <i
- class="c-schedule-calendar__btn el-icon-arrow-right u-pointer"
- :class="{ show: canToNext }"
- @click="toNext"
- />
- </template>
- <div class="l-flex__auto l-flex--col c-schedule-calendar__main">
- <div class="l-flex__none l-flex c-schedule-calendar__header">
- <div class="l-flex__auto u-readonly">周日</div>
- <div class="l-flex__auto u-readonly">周一</div>
- <div class="l-flex__auto u-readonly">周二</div>
- <div class="l-flex__auto u-readonly">周三</div>
- <div class="l-flex__auto u-readonly">周四</div>
- <div class="l-flex__auto u-readonly">周五</div>
- <div class="l-flex__auto u-readonly">周六</div>
- </div>
- <div class="c-schedule-calendar__content">
- <div
- v-for="(week, weekIndex) in weeks"
- :key="weekIndex"
- class="l-flex c-schedule-calendar__row"
- >
- <div
- v-for="(day, dayIndex) in week"
- :key="dayIndex"
- class="c-schedule-calendar__col day c-day-card"
- :class="{ 'enabled': day.editable, 'disabled': day.disabled, 'invalid': day.invalid }"
- @dblclick="addProgramByDay(day)"
- >
- <span class="c-day-card__day">{{ day.value }}</span>
- <div
- v-if="day.programs.length > 0"
- class="l-flex--row"
- >
- <pop-detail
- class="l-flex__auto"
- :program="day.programs[0]"
- >
- <program-item
- class="c-day-card__program"
- :program="day.programs[0]"
- :editable="editable"
- @edit="editProgram"
- @remove="removeProgram"
- />
- </pop-detail>
- </div>
- <pop-list
- v-if="day.programs.length > 1"
- class="l-flex__auto"
- :programs="day.programs"
- :editable="editable"
- @edit="editProgram"
- @remove="removeProgram"
- >
- <div class="c-day-card__count u-ellipsis u-pointer">
- +{{ day.programs.length - 1 }}
- </div>
- </pop-list>
- </div>
- </div>
- </div>
- </div>
- <el-dialog
- title="排期设置"
- :visible.sync="editing"
- custom-class="c-dialog"
- :close-on-click-modal="false"
- :before-close="handleCloseEditDialog"
- >
- <schedule-edit
- v-if="program"
- ref="editor"
- :program="program"
- :ratio="ratio"
- :min-date="min"
- :max-date="max"
- />
- <template #footer>
- <button
- class="o-button"
- @click="saveProgram"
- >
- 确定
- </button>
- <button
- class="o-button cancel"
- @click="handleCloseEditDialog"
- >
- 取消
- </button>
- </template>
- </el-dialog>
- <el-dialog
- title="冲突提醒"
- :visible.sync="conflicting"
- custom-class="c-dialog"
- :close-on-click-modal="false"
- append-to-body
- >
- <div class="has-bottom-padding u-bold">
- <div>局部覆盖:被冲突的节目的时间段将被切割</div>
- <div>覆盖:被冲突的节目将被移除</div>
- </div>
- <div
- v-for="conflict in conflicts"
- :key="conflict.key"
- >
- 与 {{ conflict.target.name }} 于 {{ conflict.start }} ~ {{ conflict.end }} 冲突
- </div>
- <template #footer>
- <button
- class="o-button"
- @click="_coverConflicts"
- >
- 局部覆盖
- </button>
- <button
- class="o-button"
- @click="_coverConflictsFull"
- >
- 覆盖
- </button>
- <button
- class="o-button cancel"
- @click="conflicting = false"
- >
- 取消
- </button>
- </template>
- </el-dialog>
- </schedule-wrapper>
- </template>
- <script>
- import { parseTime } from '@/utils'
- import scheduleMixin from '../mixins/schedule'
- import ScheduleWrapper from '../components/ScheduleWrapper'
- import ScheduleEdit from './ScheduleEdit'
- import ProgramItem from './ProgramItem'
- import PopDetail from './PopDetail'
- import PopList from './PopList'
- export default {
- name: 'ScheduleCalendar',
- components: {
- ScheduleWrapper,
- ScheduleEdit,
- ProgramItem,
- PopDetail,
- PopList
- },
- mixins: [scheduleMixin],
- data () {
- return {
- weeks: [],
- min: null,
- max: null,
- current: null,
- editing: false,
- program: null,
- dirty: false,
- conflicting: false,
- conflicts: []
- }
- },
- computed: {
- date () {
- return this.current ? `${this.current.year}.${this.current.month + 1}` : ''
- },
- canToPresent () {
- return this.current ? this._compare(this.current, this.min, false) > 0 : false
- },
- canToNext () {
- return this.current && this.max ? this._compare(this.current, this.max, false) < 0 : !!this.current
- }
- },
- methods: {
- init () {
- const now = this._transform(Date.now())
- let current = {
- year: now.year,
- month: now.month
- }
- if (this.editable) {
- this._setMinDate()
- if (this.programs.length) {
- const minProgramDate = this._transform(this.programs[0].startDateTime)
- if (this._compare(now, minProgramDate, false) < 0) {
- current = {
- year: minProgramDate.year,
- month: minProgramDate.month
- }
- }
- }
- } else {
- if (this.programs.length) {
- this.min = this._transform(this.programs[0].startDateTime)
- this.max = this.programs.reduce((curr, { endDateTime }) => {
- const date = this._transform(endDateTime)
- if (curr) {
- return this._compare(curr, date) < 0 ? date : curr
- }
- return date
- }, null)
- } else {
- this.max = this.min = now
- }
- if (this._compare(now, this.max, false) > 0) {
- current = {
- year: this.min.year,
- month: this.min.month
- }
- }
- }
- this.current = current
- this._calculate()
- },
- fix () {
- let endTime = Date.now() + 60000
- if (this.programs.some(({ endDateTime }) => new Date(endDateTime).getTime() <= endTime)) {
- return this.$confirm(
- '存在过期或将要过期(<=60s)节目,是否移除?',
- '提示',
- {
- type: 'warning',
- distinguishCancelAndClose: true,
- confirmButtonText: '移除',
- cancelButtonText: '保留'
- }
- ).then(() => {
- endTime = Date.now() + 60000
- this.scheduleOptions.programs = this.programs.filter(({ endDateTime }) => new Date(endDateTime).getTime() > endTime)
- if (this._compare(this.current, this._transform(endTime), false) <= 0) {
- this._calculate()
- }
- return this.programs
- }, action => {
- if (action === 'cancel') {
- return this.programs
- }
- return Promise.reject()
- })
- }
- return Promise.resolve(this.programs)
- },
- onSave () {
- this.fix().then(programs => {
- this.save(programs.map(({ id, name, startDateTime, endDateTime }) => {
- endDateTime = this._offsetDateTime(endDateTime, 1)
- return {
- programId: id,
- programName: name,
- startDateTime, endDateTime
- }
- })).then(() => {
- this.dirty = false
- })
- })
- },
- _transformPrograms (programs) {
- return programs.sort(this._sortPrograms).map(this._transformProgram)
- },
- _sortPrograms (a, b) {
- return new Date(a.startDateTime).getTime() - new Date(b.startDateTime).getTime()
- },
- _transformProgram ({ programId, programName, startDateTime, endDateTime }) {
- // 服务器保存的时间采用左闭右开原则
- return this._createProgram(programId, programName, startDateTime, this._offsetDateTime(endDateTime, -1))
- },
- _createProgram (id, name, startDateTime, endDateTime) {
- return {
- id, name, startDateTime, endDateTime,
- key: Math.random().toString(16).slice(2),
- days: this._getDays(startDateTime, endDateTime)
- }
- },
- _offsetDateTime (dateTime, val) {
- return parseTime(new Date(dateTime).getTime() + val * 1000, '{y}-{m}-{d} {h}:{i}:{s}')
- },
- _getDays (startDateTime, endDateTime) {
- const days = {}
- const { year: endYear, month: endMonth, day: endDay } = this._transform(endDateTime)
- const end = `${endYear}-${endMonth + 1}-${endDay}`
- let { year, month, day } = this._transform(startDateTime)
- let start = `${year}-${month + 1}-${day}`
- while (start !== end) {
- days[start] = 1
- ;({ year, month, day } = this._transform(new Date(year, month, day + 1)))
- start = `${year}-${month + 1}-${day}`
- }
- days[start] = 1
- return days
- },
- removeProgram ({ key, name }) {
- this.$confirm(
- `确定移除节目 ${name}`,
- '提示',
- {
- type: 'warning',
- confirmButtonText: '确定',
- cancelButtonText: '取消'
- }
- ).then(() => {
- this.dirty = true
- this._removeProgram(key)
- this._calculate()
- })
- },
- _removeProgram (key) {
- if (!key) {
- return
- }
- const index = this.programs.findIndex(program => program.key === key)
- this.programs.splice(index, 1)
- if (index === 0) {
- this._setMinDate()
- if (this._compare(this.current, this.min, false) < 0) {
- this.current = {
- year: this.min.year,
- month: this.min.month
- }
- }
- }
- },
- _setMinDate () {
- let minDate = this._transform(Date.now())
- if (this.programs.length) {
- const minProgramDate = this._transform(this.programs[0].startDateTime)
- if (this._compare(minDate, minProgramDate, false) > 0) {
- minDate = minProgramDate
- }
- }
- if (!this.min || this._compare(this.min, minDate) < 0) {
- this.min = minDate
- }
- },
- addProgram () {
- this.editProgram({})
- },
- addProgramByDay (day) {
- if (day.editable) {
- const { year, month } = this.current
- const date = day.value
- this.editProgram({
- startDateTime: `${year}-${(month + 1).toString().padStart(2, '0')}-${date} ${this._compare({ year, month, day: date }, this._transform(Date.now())) === 0 ? parseTime(new Date(), '{h}:{i}:{s}') : '00:00:00'}`,
- endDateTime: `${year}-${(month + 1).toString().padStart(2, '0')}-${date} 23:59:59`
- })
- }
- },
- editProgram (program) {
- if (this.editable) {
- this.program = program
- this.editing = true
- }
- },
- saveProgram () {
- const { id, name, startDateTime, endDateTime } = this.$refs.editor.getValue()
- if (!startDateTime || !endDateTime) {
- this.$message({
- type: 'warning',
- message: '请选择生效时间'
- })
- return
- }
- if (!id) {
- this.$message({
- type: 'warning',
- message: '请选择节目'
- })
- return
- }
- if (this.program.id &&
- id === this.program.id &&
- startDateTime === this.program.startDateTime &&
- endDateTime === this.program.endDateTime
- ) {
- this.handleCloseEditDialog()
- return
- }
- this._program = { id, name, startDateTime, endDateTime }
- if (this._checkConflict(this._program, this.program.key)) {
- this._saveProgram()
- }
- },
- _saveProgram () {
- this._removeProgram(this.program.key)
- this._mergeOrAdd()
- this.dirty = true
- this._calculate()
- this.handleCloseEditDialog()
- },
- _mergeOrAdd () {
- let program = this._program
- const timestamp = new Date(program.startDateTime).getTime()
- const insertIndex = this.programs.findIndex(item => new Date(item.startDateTime).getTime() > timestamp)
- let pre = null
- let next = null
- let add = true
- if (~insertIndex) {
- pre = this.programs[insertIndex - 1]
- next = this.programs[insertIndex]
- } else {
- pre = this.programs[this.programs.length - 1]
- }
- if (next && program.id === next.id && new Date(program.endDateTime).getTime() + 1000 === new Date(next.startDateTime).getTime()) {
- next.startDateTime = program.startDateTime
- program = next
- add = false
- }
- if (pre && program.id === pre.id && new Date(pre.endDateTime).getTime() + 1000 === new Date(program.startDateTime).getTime()) {
- pre.endDateTime = program.endDateTime
- this._removeProgram(program.key)
- program = pre
- add = false
- }
- program.key = Math.random().toString(16).slice(2)
- program.days = this._getDays(program.startDateTime, program.endDateTime)
- if (add) {
- if (next) {
- this.programs.splice(insertIndex, 0, program)
- } else {
- this.programs.push(program)
- }
- }
- },
- _checkConflict (program, key) {
- if (this.programs.length) {
- const cstartDateTime = new Date(program.startDateTime)
- const cendDateTime = new Date(program.endDateTime)
- this.conflicts = this.programs.filter(item => {
- const startDateTime = new Date(item.startDateTime)
- const endDateTime = new Date(item.endDateTime)
- return item.key !== key && !(cstartDateTime > endDateTime || cendDateTime < startDateTime)
- }).map(item => {
- const startDateTime = new Date(item.startDateTime)
- const endDateTime = new Date(item.endDateTime)
- return {
- target: item,
- key: item.key,
- start: cstartDateTime > startDateTime ? program.startDateTime : item.startDateTime,
- end: cendDateTime > endDateTime ? item.endDateTime : program.endDateTime
- }
- })
- if (this.conflicts.length) {
- this.conflicting = true
- return false
- }
- }
- return true
- },
- _coverConflicts () {
- const now = Date.now()
- this.conflicts.forEach(({ target, key, start, end }) => {
- const { startDateTime, endDateTime } = target
- if (new Date(endDateTime).getTime() <= now || startDateTime === start && endDateTime === end) {
- this._removeProgram(key)
- return
- }
- if (startDateTime === start) {
- target.startDateTime = this._offsetDateTime(end, 1)
- } else {
- if (endDateTime !== end) {
- const index = this.programs.findIndex(({ key }) => key === target.key)
- this.programs.splice(index + 1, 0, this._createProgram(target.id, target.name, this._offsetDateTime(end, 1), target.endDateTime))
- }
- target.endDateTime = this._offsetDateTime(start, -1)
- }
- target.days = this._getDays(target.startDateTime, target.endDateTime)
- })
- this.conflicting = false
- this._saveProgram()
- },
- _coverConflictsFull () {
- this.conflicts.forEach(({ key }) => this._removeProgram(key))
- this.conflicting = false
- this._saveProgram()
- },
- handleCloseEditDialog () {
- this.program = null
- this.editing = false
- },
- today () {
- const { year, month } = this._transform(Date.now())
- this.current = { year, month }
- this._calculate()
- },
- _transform (timestamp) {
- const date = new Date(timestamp)
- return {
- year: date.getFullYear(),
- month: date.getMonth(),
- day: date.getDate()
- }
- },
- _compare (a, b, full = true) {
- if (a.year === b.year) {
- if (full && a.month === b.month) {
- return a.day - b.day
- }
- return a.month - b.month
- }
- return a.year - b.year
- },
- _createItem (date, disabled) {
- const dateObj = this._transform(date)
- const invalid = this._compare(dateObj, this._transform(Date.now())) < 0
- const day = `${dateObj.year}-${dateObj.month + 1}-${dateObj.day}`
- return {
- value: dateObj.day,
- editable: this.editable && !disabled && !invalid,
- invalid,
- disabled,
- programs: this.programs.length ? this.programs.filter(program => {
- return program.days[day]
- }) : []
- }
- },
- _calculate () {
- const { year, month } = this.current
- const monthDays = this._getPresentMonthDays(year, month).concat(this._currentMonthDays(year, month))
- const diff = 42 - monthDays.length
- const dates = diff === 0 ? monthDays : monthDays.concat(this._getNextMonthDays(year, month, diff))
- const weeks = []
- let row
- for (let i = 0; i < dates.length; i++) {
- if (i % 7 === 0) {
- row = weeks.length
- weeks[row] = []
- }
- weeks[row][i % 7] = dates[i]
- }
- this.weeks = weeks
- },
- _currentMonthDays (year, month) {
- const dateArr = []
- const lastDay = new Date(year, month + 1, 0).getDate()
- for (let i = 1; i <= lastDay; i++) {
- dateArr.push(this._createItem(new Date(year, month, i)))
- }
- return dateArr
- },
- _getPresentMonthDays (year, month) {
- const dateArr = []
- const week = new Date(year, month, 1).getDay()
- const fill = week === 0 ? 7 : week
- for (let i = fill; i > 0; i--) {
- dateArr.push(this._createItem(new Date(year, month, 1 - i), true))
- }
- return dateArr
- },
- _getNextMonthDays (year, month, count) {
- const dateArr = []
- for (let i = 0; i < count; i++) {
- dateArr.push(this._createItem(new Date(year, month + 1, 1 + i), true))
- }
- return dateArr
- },
- _offsetMonth (offset) {
- if (offset < 0) {
- if (this.current.month === 0) {
- this.current.year -= 1
- this.current.month = 11
- } else {
- this.current.month -= 1
- }
- } else if (offset > 0) {
- if (this.current.month === 11) {
- this.current.year += 1
- this.current.month = 0
- } else {
- this.current.month += 1
- }
- }
- this._calculate()
- },
- toPresent () {
- if (this.canToPresent) {
- this._offsetMonth(-1)
- }
- },
- toNext () {
- if (this.canToNext) {
- this._offsetMonth(1)
- }
- }
- }
- }
- </script>
- <style lang="scss" scoped>
- .c-schedule-calendar {
- min-height: 200px;
- &__tip {
- color: $black;
- line-height: 1;
- }
- &__time {
- width: 100px;
- color: $black;
- font-size: 24px;
- font-weight: bold;
- text-align: center;
- }
- &__btn {
- color: $blue;
- font-size: 18px;
- font-weight: bold;
- visibility: hidden;
- &.show {
- visibility: visible;
- }
- }
- &__main {
- color: $black;
- font-size: 16px;
- }
- &__content {
- min-height: 0;
- border: 1px solid $gray--light;
- overflow-y: auto;
- }
- &__row + &__row {
- border-top: 1px solid $gray--light;
- }
- &__col {
- flex: 1 1 0;
- display: flex;
- min-width: 0;
- height: 108px;
- user-select: none;
- &.disabled {
- color: $gray;
- }
- &.invalid {
- background-color: lighten($gray--light, 4%);
- cursor: not-allowed;
- }
- &.day.enabled {
- cursor: pointer;
- &:hover {
- transition: background-color 0.4s;
- background-color: #f2f8fe;
- }
- }
- & + & {
- border-left: 1px solid $gray--light;
- }
- }
- &__header {
- height: 40px;
- font-weight: bold;
- text-align: center;
- }
- }
- .c-day-card {
- display: inline-flex;
- flex-direction: column;
- padding: 10px 10px 0;
- font-size: 14px;
- line-height: 1;
- text-align: left;
- &__day {
- align-self: flex-end;
- font-size: 16px;
- }
- &__program {
- max-width: 100%;
- margin-top: 8px;
- }
- &__count {
- align-self: flex-start;
- display: inline-block;
- padding: 6px 10px;
- margin-top: 10px;
- color: #fff;
- font-weight: bold;
- border-radius: 4px;
- background-color: #fb8885;
- }
- }
- </style>
|