<template>
  <div>
    <!-- 可选的标题区 -->
    <template v-if="showHeader">
      <h2>日期 → 弹窗精确到分钟的时间范围选择器</h2>
    </template>

    <div class="row">
      <el-date-picker
        v-model="selectedDate"
        type="date"
        placeholder="选择日期"
        value-format="yyyy-MM-dd"
      />
      <el-button type="primary" @click="openPicker">
        选择时间范围
      </el-button>

    </div>

    <template v-if="showHeader">
      <h3 style="margin-top:20px;">已保存的范围</h3>
    </template>
    <ul>
      <li v-for="(r, i) in ranges" :key="i">
        日期 <b>{{ r.date }}</b>：
        <code>{{ fmt(r.startMs) }}</code> ~ <code>{{ fmt(r.endMs) }}</code>
        <el-button type="text" size="mini" @click="removeRange(i)" style="color:red;">删除</el-button>
      </li>
      <li v-if="!ranges.length" class="hint">暂无数据</li>

    </ul>

    <!-- 弹窗 -->
    <el-dialog
      :title="'为 ' + selectedDate + ' 选择时间范围（分钟级禁用，秒补 00）'"
      :visible.sync="show"
      width="980px"
      :close-on-click-modal="false"
      append-to-body
    >
      <div class="modal-body">
        <!-- 左侧主区域 -->
        <div>
          <div class="day-bar-wrap">
            <div class="day-bar-header">
              <div class="row">
                <el-button @click="shiftDay(-1)" size="mini">← 上一天</el-button>
                <el-date-picker
                  v-model="viewDay"
                  type="date"
                  size="mini"
                  value-format="yyyy-MM-dd"
                />
                <el-button @click="shiftDay(+1)" size="mini">下一天 →</el-button>
              </div>
              <div class="bar-legend">
                <span><i class="dot dot-free"></i>可选</span>
                <span><i class="dot dot-busy"></i>已占用</span>
                <span><i class="dot dot-range"></i>当前选择</span>
              </div>
            </div>

            <canvas ref="canvas"
                    @mousemove="onHover"
                    @mouseleave="hoverSec=null"
                    @click="onClick"></canvas>
            <div class="ticks">
              <span>00:00</span><span>06:00</span><span>12:00</span><span>18:00</span><span>24:00</span>
            </div>
          </div>

          <div style="margin-top:12px;" class="kv">
            <div>当前在选：<span class="chip">{{ selectingLabel }}</span>
              <span v-if="hoverSec !== null" class="hint">光标：{{ secToHMS(hoverSec) }}</span>
            </div>
            <div>开始：<b v-if="draft.startMs">{{ fmt(draft.startMs) }}</b><span v-else class="hint">未设置</span></div>
            <div>结束：<b v-if="draft.endMs">{{ fmt(draft.endMs) }}</b><span v-else class="hint">未设置</span></div>
            <div v-if="conflictMsg" class="danger">{{ conflictMsg }}</div>
            <div v-else-if="draft.startMs && draft.endMs" class="ok">选择有效 ✅</div>
          </div>
        </div>

        <!-- 右侧辅助输入 -->
        <div class="aside">
          <div class="kv">
            <div><b>辅助输入（分钟级）</b></div>
            <div style="margin:6px 0 10px" class="hint">秒数会自动补 00</div>
            <div class="row">
              <el-date-picker
                v-model="startStr"
                type="datetime"
                value-format="yyyy-MM-ddTHH:mm"
                format="yyyy-MM-dd HH:mm"
                placeholder="开始时间"
                :picker-options="{ selectableRange: '00:00:00 - 23:59:00' }"
                @change="applyStartStr"
              />
              <el-button size="mini" @click="selecting='end'">下一步：选结束</el-button>
            </div>
            <div class="row" style="margin-top:8px;">
              <el-date-picker
                v-model="endStr"
                type="datetime"
                value-format="yyyy-MM-ddTHH:mm"
                format="yyyy-MM-dd HH:mm"
                placeholder="结束时间"
                :picker-options="{ selectableRange: '00:00:00 - 23:59:00' }"
                @change="applyEndStr"
              />
              <el-button size="mini" @click="selecting='start'">切回：选开始</el-button>
            </div>
            <div class="hint" style="margin-top:8px;">已占用区间会被自动挡住或剪齐到边界。</div>

            <div style="margin-top:18px;"><b>当天禁用切片（分钟）</b></div>
            <ul style="max-height:140px; overflow:auto; border:1px dashed #e5e7eb; border-radius:8px; padding:8px;">
              <li v-for="(seg, i) in daySegments" :key="i" class="hint">
                {{ secToHMS(seg[0]) }} ~ {{ secToHMS(seg[1]) }}
              </li>
              <li v-if="!daySegments.length" class="hint">无</li>
            </ul>
          </div>
        </div>
      </div>

      <span slot="footer" class="dialog-footer">
        <el-button @click="show=false">关闭</el-button>
        <el-button type="primary" :disabled="!canSave" @click="save">保存</el-button>
      </span>
    </el-dialog>
  </div>
</template>

<script>
export default {
  name: "TimeRangePicker",
  props: {
    showHeader: { type: Boolean, default: false },
    emitOnSave: { type: Boolean, default: true }, // 保存时向父组件通知
    value: { type: Array, default: () => [] } // 新增，用来接收外部 ranges
  },
  data() {
    return {
      timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone || "Local",
      selectedDate: new Date().toISOString().split("T")[0],
      show: false,

      ranges: [], // [{date,startMs,endMs}]
      draft: { startMs: null, endMs: null },

      viewDay: "",
      hoverSec: null,
      daySegments: [],

      selecting: "start",
      startStr: "",
      endStr: "",
      conflictMsg: "",
    };
  },
  computed: {
    canSave() {
      return (
        this.draft.startMs &&
        this.draft.endMs &&
        this.draft.startMs < this.draft.endMs &&
        !this.conflictMsg
      );
    },
    selectingLabel() {
      return this.selecting === "start"
        ? "正在选择：开始时间"
        : "正在选择：结束时间";
    },
  },
  methods: {

    removeRange(index) {
      this.ranges.splice(index, 1);
      // 同步更新到父组件
      this.$emit("input", JSON.parse(JSON.stringify(this.ranges)));
      this.$emit("saved", JSON.parse(JSON.stringify(this.ranges)));
    },

    pad(n){ return String(n).padStart(2,'0'); },
    fmt(ms) {
      const d = new Date(ms);
      return `${d.getFullYear()}-${this.pad(d.getMonth()+1)}-${this.pad(d.getDate())} ${this.pad(d.getHours())}:${this.pad(d.getMinutes())}:00`;
    },
    toMs(str) {
      const [d,t] = str.split(' ');
      const [y,M,D] = d.split('-').map(Number);
      const [h,m,s] = t.split(':').map(Number);
      const dt = new Date(y, M-1, D, h, m, s);
      dt.setSeconds(0,0);
      return dt.getTime();
    },
    dayStartMs(dayStr){ const [y,M,D]=dayStr.split('-').map(Number); const dt=new Date(y,M-1,D,0,0,0); dt.setSeconds(0,0); return dt.getTime(); },
    dayEndMs(dayStr){ const [y,M,D]=dayStr.split('-').map(Number); return new Date(y,M-1,D,23,59,59).getTime(); },
    secToHMS(min){ const h=Math.floor(min/60),m=min%60; return `${this.pad(h)}:${this.pad(m)}:00`; },

    refreshRanges() {
      // 优先来源：props.value（父组件可能在任何时刻更新）
      // 如果父组件没给，则保留已有 this.ranges
      const source = (Array.isArray(this.value) && this.value.length) ? this.value : this.ranges;
      // 规范化（使用你已有 normalizeRanges 实现）
      this.ranges = this.normalizeRanges(source || []);
      // 如果当前 viewDay 没有占用段，优先切到第一个有占用的日期
      if (!this.viewDay && this.ranges.length) {
        this.viewDay = this.ranges[0].date;
      }
      const hasOnView = this.viewDay && this.ranges.some(r => r.date === this.viewDay);
      if (!hasOnView && this.ranges.length) {
        // 切换到第一个有占用的日期，便于查看
        this.viewDay = this.ranges[0].date;
      }
      // 强制重算并重绘
      this.computeDaySegments();
      this.$nextTick(this.drawCanvas);
    },

// 放在 methods: { ... } 里
    normalizeRanges(arr) {
      if (!Array.isArray(arr)) return [];
      const pad = (n) => String(n).padStart(2,'0');

      // 把各种可能的值解析为毫秒（返回 null 表示无法解析）
      const parseToMs = (v, dateForHHmm) => {
        if (v == null) return null;
        if (typeof v === 'number') return v;
        if (v instanceof Date) return v.getTime();
        if (typeof v !== 'string') return null;

        // 纯数字字符串（时间戳毫秒）
        if (/^\d+$/.test(v)) return Number(v);

        // ISO 或带 T 的 datetime
        if (v.includes('T')) {
          const dt = new Date(v);
          return isNaN(dt.getTime()) ? null : dt.getTime();
        }

        // "yyyy-MM-dd HH:mm" 或 "yyyy-MM-dd HH:mm:ss"
        if (/^\d{4}-\d{2}-\d{2} /.test(v)) {
          const iso = v.replace(' ', 'T');
          const dt = new Date(iso);
          if (!isNaN(dt.getTime())) return dt.getTime();
          // fallback 手动解析
          const [d, t] = v.split(' ');
          const [y, mo, da] = d.split('-').map(Number);
          const [hh = 0, mm = 0] = (t || '00:00').split(':').map(Number);
          const dt2 = new Date(y, mo - 1, da, hh, mm, 0);
          return isNaN(dt2.getTime()) ? null : dt2.getTime();
        }

        // 只有 "HH:mm" 且同时提供 dateForHHmm（'yyyy-MM-dd'）
        if (/^\d{1,2}:\d{2}$/.test(v) && dateForHHmm) {
          const [y, mo, da] = dateForHHmm.split('-').map(Number);
          const [hh, mm] = v.split(':').map(Number);
          const dt = new Date(y, mo - 1, da, hh, mm, 0);
          return isNaN(dt.getTime()) ? null : dt.getTime();
        }

        // 最后尝试 new Date()
        const dtry = new Date(v);
        return isNaN(dtry.getTime()) ? null : dtry.getTime();
      };

      const getDateStrFromMs = (ms) => {
        const d = new Date(ms);
        return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
      };
      const getTimePartsFromMs = (ms) => {
        const d = new Date(ms);
        return { h: d.getHours(), m: d.getMinutes(), s: d.getSeconds() };
      };

      const out = [];
      for (const raw of arr) {
        if (!raw) continue;
        const r = Object.assign({}, raw);

        // 尝试从各种命名拿到 start/end
        let startMs = parseToMs(r.startMs ?? r.start ?? r.startTime ?? r.startAt, r.date);
        let endMs = parseToMs(r.endMs ?? r.end ?? r.endTime ?? r.endAt, r.date);

        // 支持 timeRange 字段 "09:00-10:00"
        if ((startMs == null || endMs == null) && typeof r.timeRange === 'string' && r.date) {
          const parts = r.timeRange.split('-').map(s => s.trim());
          if (parts.length === 2) {
            const sMs = parseToMs(parts[0], r.date);
            const eMs = parseToMs(parts[1], r.date);
            if (sMs != null && eMs != null) {
              startMs = startMs ?? sMs;
              endMs = endMs ?? eMs;
            }
          }
        }

        // 仍解析失败则跳过并打印 warn（便于调试）
        if (typeof startMs !== 'number' || typeof endMs !== 'number' || isNaN(startMs) || isNaN(endMs)) {
          console.warn('[TimeRangePicker] normalizeRanges 跳过无法解析的 range：', raw);
          continue;
        }

        // 若有 r.date，但 startMs/endMs 对应的本地日期与 r.date 不一致，
        // 则把 startMs/endMs 的 时分秒“贴到” r.date 上进行修正（这是为了解决你传入 date 与 timestamp 不对应的问题）
        if (r.date) {
          const sDateStr = getDateStrFromMs(startMs);
          const eDateStr = getDateStrFromMs(endMs);
          if (sDateStr !== r.date) {
            const tp = getTimePartsFromMs(startMs);
            const [y, mo, da] = r.date.split('-').map(Number);
            const fixed = new Date(y, mo - 1, da, tp.h, tp.m, tp.s || 0);
            console.warn('[TimeRangePicker] 修正 startMs 日期不匹配：', sDateStr, '-> 贴到', r.date, '（new:', fixed.toString(), '）');
            startMs = fixed.getTime();
          }
          if (eDateStr !== r.date) {
            const tp = getTimePartsFromMs(endMs);
            const [y, mo, da] = r.date.split('-').map(Number);
            const fixed = new Date(y, mo - 1, da, tp.h, tp.m, tp.s || 0);
            console.warn('[TimeRangePicker] 修正 endMs 日期不匹配：', eDateStr, '-> 贴到', r.date, '（new:', fixed.toString(), '）');
            endMs = fixed.getTime();
          }
        }

        // 保证 start <= end
        if (startMs > endMs) {
          const t = startMs; startMs = endMs; endMs = t;
        }

        r.startMs = Math.floor(startMs);
        r.endMs = Math.floor(endMs);

        // 如果没有 date 字段则根据 startMs 补上（本地日期）
        if (!r.date) {
          const d = new Date(r.startMs);
          r.date = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
        }

        out.push(r);
      }

      return out;
    },

    // 自带按钮打开弹窗（父组件无需 $refs）
    openPicker(){
      if (!this.selectedDate) {
        this.$message.warning("请先选择日期");
        return;
      }

      // 规范并刷新 ranges（从 props.value 或内部 ranges）
      this.refreshRanges();

      // 选择视图日期：优先显示 selectedDate（如果有占用），否则显示 first busy date
      const hasOnSelected = this.ranges.some(r => r.date === this.selectedDate);
      if (hasOnSelected) this.viewDay = this.selectedDate;
      else if (this.ranges.length) this.viewDay = this.ranges[0].date || this.selectedDate;
      else this.viewDay = this.selectedDate;

      this.show=true;
      this.viewDay=this.selectedDate;
      this.draft={startMs:null,endMs:null};
      this.startStr='';this.endStr='';
      this.selecting='start';
      this.computeDaySegments();
      this.$nextTick(this.drawCanvas);
    },
    shiftDay(delta){
      const base=this.dayStartMs(this.viewDay);
      const next=new Date(base+delta*24*3600*1000);
      this.viewDay=`${next.getFullYear()}-${this.pad(next.getMonth()+1)}-${this.pad(next.getDate())}`;
      this.computeDaySegments();
      this.drawCanvas();
    },
    computeDaySegments(){
      const ds=this.dayStartMs(this.viewDay),de=this.dayEndMs(this.viewDay);
      let segs=[];
      for(const r of this.ranges){
        const s=Math.max(r.startMs,ds),e=Math.min(r.endMs,de);
        if(s<=e){
          const from=Math.floor((s-ds)/60000);
          const to=Math.floor((e-ds)/60000);
          segs.push([Math.max(0,from),Math.min(1439,to)]);
        }
      }
      segs.sort((a,b)=>a[0]-b[0]);
      const merged=[];
      for(const seg of segs){
        if(!merged.length||seg[0]>merged[merged.length-1][1]+1) merged.push(seg);
        else merged[merged.length-1][1]=Math.max(merged[merged.length-1][1],seg[1]);
      }
      this.daySegments=merged;
      this.validateConflict();
    },
    validateConflict(){
      this.conflictMsg='';
      const {startMs,endMs}=this.draft;
      if(!startMs||!endMs||startMs>=endMs) return;
      for(const r of this.ranges){
        if(!(endMs<r.startMs||startMs>r.endMs)){
          const touchStart=startMs===r.endMs+60000;
          const touchEnd=endMs===r.startMs-60000;
          if(touchStart||touchEnd) continue;
          this.conflictMsg='所选范围与已占用区间发生重叠，请重新选择或使用辅助输入自动剪齐。';
          this.$message.warning("所选范围与已占用区间发生重叠，请重新选择或使用辅助输入自动剪齐")
          break;
        }
      }
    },
    dateOfMs(ms){ const d=new Date(ms); return `${d.getFullYear()}-${this.pad(d.getMonth()+1)}-${this.pad(d.getDate())}`; },
    drawCanvas(){
      const el=this.$refs.canvas;if(!el)return;
      const dpr=window.devicePixelRatio||1;
      const cssW=el.clientWidth,cssH=el.clientHeight;
      el.width=Math.floor(cssW*dpr);el.height=Math.floor(cssH*dpr);
      const ctx=el.getContext('2d');ctx.scale(dpr,dpr);
      ctx.clearRect(0,0,cssW,cssH);
      const W=cssW,H=cssH;
      const minToX=(min)=>min/1439*(W-2)+1;
      ctx.fillStyle='#e5e7eb';ctx.strokeStyle='#d1d5db';ctx.lineWidth=1;
      for(const [a,b] of this.daySegments){const x=minToX(a),w=minToX(b)-x;ctx.fillRect(x,1,w,H-2);ctx.strokeRect(x,1,Math.max(1,w),H-2);}
      ctx.strokeStyle='#f3f4f6';
      for(let h=0;h<=24;h++){const min=h*60;const x=Math.floor(minToX(min));ctx.beginPath();ctx.moveTo(x,0);ctx.lineTo(x,H);ctx.stroke();}
      const {startMs,endMs}=this.draft;
      if(startMs){const s=Math.floor((startMs-this.dayStartMs(this.viewDay))/60000);if(this.dateOfMs(startMs)===this.viewDay){ctx.fillStyle='#93c5fd';ctx.fillRect(minToX(s)-1,0,2,H);}}
      if(startMs&&endMs){const ds=this.dayStartMs(this.viewDay),de=this.dayEndMs(this.viewDay);const visS=Math.max(startMs,ds),visE=Math.min(endMs,de);if(visS<=visE){const a=Math.floor((visS-ds)/60000);const b=Math.floor((visE-ds)/60000);ctx.fillStyle='#60a5fa';const x=minToX(a),w=minToX(b)-x;ctx.fillRect(x,6,Math.max(2,w),H-12);}}
      if(this.hoverSec!==null){ctx.fillStyle='#111827';const x=Math.floor(minToX(this.hoverSec));ctx.fillRect(x,0,1,H);}
    },
    pxToSec(evt){const el=this.$refs.canvas;const rect=el.getBoundingClientRect();const x=evt.clientX-rect.left;const w=el.clientWidth;let min=Math.round(x/w*1439);return Math.max(0,Math.min(1439,min));},
    onHover(evt){this.hoverSec=this.pxToSec(evt);this.drawCanvas();},
    onClick(evt){
      const min=this.pxToSec(evt);
      if(this.isSecDisabled(min)) return;
      const ms=this.dayStartMs(this.viewDay)+min*60000;
      if(this.selecting==='start'){this.draft.startMs=ms;if(this.draft.endMs&&this.draft.endMs<=ms)this.draft.endMs=null;this.startStr=this.msToLocalInput(ms);this.selecting='end';}
      else{const fixed=this.clipEndAuto(ms);this.draft.endMs=fixed;this.endStr=this.msToLocalInput(fixed);}
      this.computeDaySegments();this.validateConflict();this.drawCanvas();
    },
    isSecDisabled(min){
      for(const [a,b] of this.daySegments){if(min>=a&&min<=b)return true;}
      if(this.selecting==='end'&&this.draft.startMs){const minMs=this.draft.startMs+60000;const candidate=this.dayStartMs(this.viewDay)+min*60000;if(candidate<minMs)return true;}
      return false;
    },
    clipEndAuto(endMs){
      const startMs=this.draft.startMs;if(!startMs)return endMs;if(endMs<=startMs)return startMs+60000;
      for(const r of this.ranges){if(!(endMs<=r.startMs||startMs>=r.endMs)){if(r.startMs>startMs){return Math.min(endMs,r.startMs-60000);}}}
      return endMs;
    },
    msToLocalInput(ms){const d=new Date(ms);const Y=d.getFullYear(),M=this.pad(d.getMonth()+1),D=this.pad(d.getDate());const h=this.pad(d.getHours()),m=this.pad(d.getMinutes());return `${Y}-${M}-${D}T${h}:${m}`;},
    parseLocalInput(str){if(!str)return null;const [date,time]=str.split('T');if(!time)return null;const [y,M,D]=date.split('-').map(Number);const parts=time.split(':').map(Number);const h=parts[0]||0;const m=parts[1]||0;return new Date(y,M-1,D,h,m,0).getTime();},
    applyStartStr(){
      const ms=this.parseLocalInput(this.startStr);if(!ms)return;
      if(this.isMsInBusy(ms)){const next=this.snapOut(ms,+1);this.draft.startMs=next;this.startStr=this.msToLocalInput(next);}
      else{this.draft.startMs=ms;}
      if(this.draft.endMs&&this.draft.endMs<=this.draft.startMs)this.draft.endMs=null;
      this.selecting='end';this.computeDaySegments();this.validateConflict();this.drawCanvas();
    },
    applyEndStr(){
      let ms=this.parseLocalInput(this.endStr);if(!ms)return;
      if(!this.draft.startMs){this.$message.warning('请先设置开始时间');this.endStr='';return;}
      if(ms<=this.draft.startMs) ms=this.draft.startMs+60000;
      if(this.isMsRangeOverBusy(this.draft.startMs,ms)){ms=this.clipEndAuto(ms);}
      this.draft.endMs=ms;this.selecting='start';this.computeDaySegments();this.validateConflict();this.drawCanvas();
    },
    isMsInBusy(ms){for(const r of this.ranges){if(!(ms<r.startMs||ms>r.endMs))return true;}return false;},
    isMsRangeOverBusy(s,e){for(const r of this.ranges){if(!(e<=r.startMs||s>=r.endMs))return true;}return false;},
    snapOut(ms,dir){if(dir===0)return ms;let cur=ms;const step=60000*Math.sign(dir);for(let i=0;i<1440;i++){let inside=false;for(const r of this.ranges){if(!(cur<r.startMs||cur>r.endMs)){inside=true;break;}}if(!inside)return cur;cur+=step;}return ms;},
    save() {
      if (!this.canSave) return;

      const start = new Date(this.draft.startMs);
      start.setSeconds(0, 0);
      const end = new Date(this.draft.endMs);
      end.setSeconds(0, 0);

      const pad = (n) => String(n).padStart(2, '0');
      const dateStr = `${start.getFullYear()}-${pad(start.getMonth() + 1)}-${pad(start.getDate())}`;

      const rec = {
        date: dateStr,
        startMs: start.getTime(),
        endMs: end.getTime()
      };

      // 直接 push
      this.ranges.push(rec);

      // emit 给父组件，让它来驱动
      const out = JSON.parse(JSON.stringify(this.ranges));
      if (this.emitOnSave) {
        this.$emit('saved', out);
        this.$emit('input', out);
      }

      // 更新界面状态
      this.selectedDate = dateStr;
      this.viewDay = dateStr;
      this.show = false;
    }


  },
  watch:{
    viewDay(){this.computeDaySegments();this.$nextTick(this.drawCanvas);},
    daySegments(){this.$nextTick(this.drawCanvas);},
    draft:{deep:true,handler(){this.validateConflict();this.$nextTick(this.drawCanvas);}},
    value: {
      deep: true,
      handler(val) {
        this.ranges = JSON.parse(JSON.stringify(val));
      }
    }
  },
  mounted() {
    this.ranges = JSON.parse(JSON.stringify(this.value));
  },
};
</script>

<style scoped>
/* 复用你的样式（略有精简） */
.row { display:flex; gap:12px; align-items:center; flex-wrap: wrap; }
.hint { font-size:12px; color:#6b7280; }
.chip { display:inline-flex; align-items:center; padding:3px 8px; border-radius:999px; background:#eef2ff; color:#3730a3; font-size:12px; margin-left:6px; }
.day-bar-wrap { border:1px solid #e5e7eb; border-radius:12px; padding:12px; }
.day-bar-header { display:flex; align-items:center; justify-content:space-between; margin-bottom:10px; }
.bar-legend { font-size:12px; color:#6b7280; display:flex; gap:12px; align-items:center; }
.dot { width:10px; height:10px; border-radius:2px; display:inline-block; vertical-align:middle; margin-right:6px; }
.dot-free { background:#10b981; }
.dot-busy { background:#e5e7eb; border:1px solid #d1d5db; }
.dot-range { background:#60a5fa; }
canvas { width:100%; height:52px; border-radius:8px; display:block; background:linear-gradient(180deg, #fafafa, #fff); border:1px solid #e5e7eb; }
.ticks { font-size:12px; color:#9ca3af; display:flex; justify-content:space-between; margin-top:6px; }
.kv { font-size:14px; line-height:1.8; }
.kv b { color:#111827; }
.danger { color:#b91c1c; font-size: 20px;background-color: wheat }
.ok { color:#065f46; }
.aside { border-left:1px dashed #e5e7eb; padding-left:16px; }
</style>
