npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@kevinchang1207/cx-ui

v0.1.2

Published

Cx UI components library

Readme

Cx UI(基于 Element Plus 的业务组件库)

轻量封装 Element Plus,提供统一风格与常用增强组件:CxInputCxButtonCxFormCxTable(列筛选)、CxDialogCxDrawerCxPaginationCxSelectCxDateRange

安装

npm i @kevinchang1207/cx-ui element-plus
# 或者
pnpm add @kevinchang1207/cx-ui element-plus

要求:Vue 3.2+,Element Plus 2.x。

快速开始(全局注册)

// main.ts / main.js
import { createApp } from 'vue'
import App from './App.vue'
import CxUI from '@kevinchang1207/cx-ui'
import 'element-plus/dist/index.css'

createApp(App).use(CxUI).mount('#app')

按需使用(局部注册)

import { CxButton, CxInput } from '@kevinchang1207/cx-ui'
export default { components: { CxButton, CxInput } }

组件示例

CxButton

<cx-button type="primary" @click="doSomething">提交</cx-button>
<cx-button type="success" plain>成功</cx-button>
<cx-button type="danger" :loading="loading">删除中</cx-button>

Props:type(primary/success/warning/danger/info/default)、size(sm/md/lg)、plainroundcircleloadingdisabled

CxInput

<cx-input v-model="keyword" placeholder="请输入关键词" clearable />

支持 prefixsuffix 插槽。

CxForm(基于 schema)

<cx-form :model="form" :rules="rules" :schema="[
  { label: '姓名', prop: 'name', component: 'input', placeholder: '请输入姓名' },
  { label: '状态', prop: 'status', component: 'select', options: [
    { label: '启用', value: 'enabled' },
    { label: '停用', value: 'disabled' }
  ]},
  { label: '日期', prop: 'date', component: 'date' }
]" @submit="onSubmit" />

事件:submit(values)reset()validate({ valid, error })

CxTable(列筛选/排序/选择)

<cx-table :data="rows" :columns="[
  { type: 'selection' },
  { prop: 'name', label: '姓名', sortable: 'custom' },
  { prop: 'status', label: '状态', filters: [
    { text: '启用', value: 'enabled' },
    { text: '停用', value: 'disabled' }
  ], filterMethod: (val, row) => row.status === val },
  { prop: 'age', label: '年龄', sortable: 'custom' }
]" @filter-change="onFilter" @sort-change="onSort" />

支持插槽列:columns 中定义 slot: 'xxx',并提供 <template #xxx="{ row }">

CxDialog / CxDrawer

<cx-dialog v-model="visible" title="提示" @confirm="ok">内容</cx-dialog>
<cx-drawer v-model="drawer" title="侧边">内容</cx-drawer>

CxSelect / CxDateRange

<cx-select v-model="val" :options="opts" />
<cx-date-range v-model="range" />

分页

<cx-pagination :current="page" :page-size="size" :total="total" @change="onPage" />

常见问题

  • 按钮/输入框尺寸:库内 sm/md/lg 自动映射到 Element Plus 的 small/default/large
  • 主题:直接使用 Element Plus 主题定制方案(如变量覆盖)。
  • Tree-shaking:本库为 UMD/CommonJS 构建,建议在应用层做路由/页面级懒加载。

License

MIT

<template>
  <div class="question-create-page">
    <!-- 顶部导航栏 -->
    <div class="page-header">
      <div class="header-content">
        <h1 class="page-title">新建题目</h1>
        <el-button text circle class="close-btn" @click="handleClose">
          <el-icon><i-ep-close /></el-icon>
        </el-button>
      </div>
    </div>

    <!-- 顶部工具栏 -->
    <div class="top-toolbar">
      <div class="toolbar-content">
        <!-- 题目类型选择 -->
        <div class="type-selector">
          <div
            v-for="type in questionTypes"
            :key="type.value"
            :class="['type-item', { active: formData.type === type.value }]"
            @click="formData.type = type.value"
          >
            <span class="type-name">{{ type.label }}</span>
          </div>
        </div>

        <!-- 基础信息 -->
        <div class="info-panel">
          <div class="info-item">
            <label class="info-label">所属题集</label>
            <el-input
              v-model="formData.questionSet"
              disabled
              size="small"
              style="width: 180px"
            />
          </div>
          <div class="info-item">
            <label class="info-label">题目分类</label>
            <el-select
              v-model="formData.category"
              placeholder="请选择分类"
              clearable
              filterable
              size="small"
              style="width: 160px"
            >
              <el-option
                v-for="item in categoryOptions"
                :key="item.value"
                :label="item.label"
                :value="item.value"
              />
            </el-select>
          </div>
          <div class="info-item">
            <label class="info-label">难度</label>
            <div class="difficulty-selector">
              <div
                v-for="level in 5"
                :key="level"
                :class="[
                  'difficulty-item',
                  { active: formData.difficulty === level },
                ]"
                @click="formData.difficulty = level"
              >
                {{ level }}
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>

    <!-- 主内容区 -->
    <div class="main-content">
      <!-- 编辑区 -->
      <div class="editor-area">
        <div class="editor-container">
          <el-scrollbar class="editor-scrollbar">
            <!-- 题干编辑 -->
            <div class="editor-block">
              <div class="block-header">
                <span class="block-title">题干</span>
                <span class="block-required">*</span>
              </div>
              <div class="block-content">
                <div class="editor-wrapper-border">
                  <WangEditor v-model="formData.content" />
                </div>
              </div>
            </div>

            <!-- 题干附件 -->
            <div class="editor-block">
              <div class="block-header">
                <span class="block-title">题干附件</span>
              </div>
              <div class="block-content">
                <el-button
                  type="primary"
                  :icon="ElIconUpload"
                  @click="handleSelectAttachment"
                >
                  选择附件
                </el-button>
                <div
                  v-if="formData.attachments.length > 0"
                  class="attachment-tags"
                >
                  <el-tag
                    v-for="(att, idx) in formData.attachments"
                    :key="idx"
                    closable
                    @close="removeAttachment(idx)"
                  >
                    {{ att.name }}
                  </el-tag>
                </div>
              </div>
            </div>

            <!-- 选项编辑(仅选择题) -->
            <div v-if="showOptions" class="editor-block">
              <div class="block-header">
                <span class="block-title">选项</span>
                <span class="block-required">*</span>
              </div>
              <div class="options-list">
                <div
                  v-for="(option, index) in formData.options"
                  :key="index"
                  class="option-card"
                >
                  <div class="option-header">
                    <div class="option-badge">{{ getOptionLabel(index) }}</div>
                    <el-button
                      text
                      type="danger"
                      :icon="ElIconDelete"
                      :disabled="formData.options.length <= 2"
                      @click="removeOption(index)"
                    >
                      删除
                    </el-button>
                  </div>
                  <div class="option-editor">
                    <WangEditor v-model="option.text" />
                  </div>
                </div>
                <el-button
                  type="primary"
                  size="small"
                  :icon="ElIconPlus"
                  class="add-option-btn"
                  @click="addOption"
                >
                  添加选项
                </el-button>
              </div>
            </div>

            <!-- 正确答案 -->
            <div class="editor-block">
              <div class="block-header">
                <span class="block-title">正确答案</span>
                <span class="block-required">*</span>
              </div>
              <div class="block-content">
                <!-- 单选题 -->
                <div v-if="formData.type === 'single'" class="answer-selector">
                  <div
                    v-for="(option, index) in formData.options"
                    :key="index"
                    :class="[
                      'answer-option',
                      { selected: formData.correctAnswer === index },
                    ]"
                    @click="formData.correctAnswer = index"
                  >
                    <div class="check-icon">
                      <i-ep-check
                        v-if="formData.correctAnswer === index"
                        class="icon-checked"
                      />
                      <span v-else class="icon-unchecked"></span>
                    </div>
                    <span>{{ getOptionLabel(index) }}</span>
                  </div>
                </div>

                <!-- 多选题/不定项 -->
                <div
                  v-else-if="
                    formData.type === 'multiple' ||
                    formData.type === 'uncertain'
                  "
                  class="answer-selector"
                >
                  <div
                    v-for="(option, index) in formData.options"
                    :key="index"
                    :class="['answer-option', { selected: (formData.correctAnswer as number[])?.includes(index) }]"
                    @click="toggleAnswer(index)"
                  >
                    <div class="check-icon">
                      <i-ep-check
                        v-if="(formData.correctAnswer as number[])?.includes(index)"
                        class="icon-checked"
                      />
                      <span v-else class="icon-unchecked"></span>
                    </div>
                    <span>{{ getOptionLabel(index) }}</span>
                  </div>
                </div>

                <!-- 判断题 -->
                <div
                  v-else-if="formData.type === 'judge'"
                  class="answer-selector"
                >
                  <div
                    :class="[
                      'answer-option',
                      { selected: formData.correctAnswer === true },
                    ]"
                    @click="formData.correctAnswer = true"
                  >
                    <div class="check-icon">
                      <i-ep-check
                        v-if="formData.correctAnswer === true"
                        class="icon-checked"
                      />
                      <span v-else class="icon-unchecked"></span>
                    </div>
                    <span>正确</span>
                  </div>
                  <div
                    :class="[
                      'answer-option',
                      { selected: formData.correctAnswer === false },
                    ]"
                    @click="formData.correctAnswer = false"
                  >
                    <div class="check-icon">
                      <i-ep-check
                        v-if="formData.correctAnswer === false"
                        class="icon-checked"
                      />
                      <span v-else class="icon-unchecked"></span>
                    </div>
                    <span>错误</span>
                  </div>
                </div>

                <!-- 填空题/问答题 -->
                <el-input
                  v-else
                  v-model="formData.correctAnswer"
                  type="textarea"
                  :rows="formData.type === 'essay' ? 6 : 3"
                  placeholder="请输入答案"
                  class="answer-textarea"
                />
              </div>
            </div>

            <!-- 题目解析 -->
            <div class="editor-block">
              <div class="block-header">
                <span class="block-title">题目解析</span>
              </div>
              <div class="block-content">
                <div class="editor-wrapper-border">
                  <WangEditor v-model="formData.analysis" />
                </div>
              </div>
            </div>
          </el-scrollbar>
        </div>
      </div>
    </div>

    <!-- 底部操作栏 -->
    <div class="page-footer">
      <div class="footer-content">
        <div class="footer-left">
          <span class="zoom-text">1:1</span>
        </div>
        <div class="footer-actions">
          <el-button size="small" @click="handleClose">取消</el-button>
          <el-button type="primary" size="small" @click="handleSave">
            <el-icon><i-ep-check /></el-icon>
            保存
          </el-button>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, reactive, computed, watch } from "vue";
import { useRouter } from "vue-router";
import { ElMessage, ElMessageBox } from "element-plus";
import {
  Upload as ElIconUpload,
  Plus as ElIconPlus,
  Delete as ElIconDelete,
} from "@element-plus/icons-vue";
import WangEditor from "@/components/WangEditor/index.vue";
import type { QuestionType, QuestionOption } from "@/api/kaoshi/types";

const router = useRouter();

// 题目类型选项
const questionTypes = [
  { label: "单选题", value: "single" },
  { label: "多选题", value: "multiple" },
  { label: "不定项", value: "uncertain" },
  { label: "判断题", value: "judge" },
  { label: "填空题", value: "fill" },
  { label: "问答题", value: "essay" },
];

// 表单数据
const formData = reactive({
  type: "single" as QuestionType,
  questionSet: "花雨精讲(学员端)",
  category: "",
  difficulty: 1,
  content: "",
  attachments: [] as Array<{ name: string; url: string }>,
  options: [] as QuestionOption[],
  correctAnswer: undefined as any,
  analysis: "",
});

// 分类选项
const categoryOptions = ref([
  { label: "未分类", value: "uncategorized" },
  { label: "复制章节", value: "copy_chapter" },
  { label: "学习收获", value: "learning_outcomes" },
  { label: "最后讨论了", value: "finally_discussed" },
  { label: "图文", value: "graphics_text" },
  { label: "文档", value: "document" },
  { label: "学党史", value: "party_history" },
  { label: "音频任务测试", value: "audio_task_test" },
  { label: "线上考试抽查", value: "online_exam_check" },
  { label: "随堂小测作业", value: "class_quiz" },
  { label: "练习效果", value: "practice_effect" },
]);

// 是否显示选项
const showOptions = computed(() => {
  return ["single", "multiple", "uncertain"].includes(formData.type);
});

// 监听题目类型变化
watch(
  () => formData.type,
  (newType) => {
    if (showOptions.value) {
      if (formData.options.length === 0) {
        formData.options = [{ text: "" }, { text: "" }];
      }
      if (newType === "single") {
        formData.correctAnswer = undefined;
      } else {
        formData.correctAnswer = [];
      }
    } else {
      formData.options = [];
      formData.correctAnswer = undefined;
    }
  },
  { immediate: true }
);

/**
 * 获取选项标签
 */
function getOptionLabel(index: number): string {
  return String.fromCharCode(65 + index);
}

/**
 * 添加选项
 */
function addOption() {
  formData.options.push({ text: "" });
}

/**
 * 删除选项
 */
function removeOption(index: number) {
  if (formData.options.length <= 2) {
    ElMessage.warning("至少需要保留2个选项");
    return;
  }
  formData.options.splice(index, 1);

  if (formData.type === "single") {
    if (formData.correctAnswer === index) {
      formData.correctAnswer = undefined;
    } else if (formData.correctAnswer > index) {
      formData.correctAnswer = (formData.correctAnswer as number) - 1;
    }
  } else {
    const answerArray = formData.correctAnswer as number[];
    if (answerArray) {
      formData.correctAnswer = answerArray
        .filter((ans) => ans !== index)
        .map((ans) => (ans > index ? ans - 1 : ans));
    }
  }
}

/**
 * 切换多选题答案
 */
function toggleAnswer(index: number) {
  const answerArray = (formData.correctAnswer as number[]) || [];
  const idx = answerArray.indexOf(index);
  if (idx > -1) {
    answerArray.splice(idx, 1);
  } else {
    answerArray.push(index);
  }
  formData.correctAnswer = [...answerArray];
}

/**
 * 选择附件
 */
function handleSelectAttachment() {
  ElMessage.info("附件选择功能待实现");
}

/**
 * 删除附件
 */
function removeAttachment(index: number) {
  formData.attachments.splice(index, 1);
}

/**
 * 关闭页面
 */
function handleClose() {
  ElMessageBox.confirm("确定要关闭吗?未保存的内容将丢失", "提示", {
    confirmButtonText: "确定",
    cancelButtonText: "取消",
    type: "warning",
  })
    .then(() => {
      router.back();
    })
    .catch(() => {});
}

/**
 * 保存题目
 */
function handleSave() {
  if (!formData.content || formData.content.trim() === "") {
    ElMessage.warning("请输入题干内容");
    return;
  }

  if (showOptions.value) {
    if (formData.options.length < 2) {
      ElMessage.warning("至少需要2个选项");
      return;
    }

    const emptyOptions = formData.options.some(
      (opt) => !opt.text || opt.text.trim() === ""
    );
    if (emptyOptions) {
      ElMessage.warning("请填写所有选项内容");
      return;
    }

    if (formData.type === "single") {
      if (
        formData.correctAnswer === undefined ||
        formData.correctAnswer === null
      ) {
        ElMessage.warning("请选择正确答案");
        return;
      }
    } else {
      const answerArray = formData.correctAnswer as number[];
      if (!answerArray || answerArray.length === 0) {
        ElMessage.warning("请选择至少一个正确答案");
        return;
      }
    }
  } else {
    if (
      formData.correctAnswer === undefined ||
      formData.correctAnswer === null ||
      formData.correctAnswer === ""
    ) {
      ElMessage.warning("请输入正确答案");
      return;
    }
  }

  console.log("保存题目数据:", formData);
  ElMessage.success("保存成功");

  setTimeout(() => {
    router.back();
  }, 1000);
}
</script>

<style lang="scss" scoped>
.question-create-page {
  display: flex;
  flex-direction: column;
  height: 100vh;
  background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
  overflow: hidden;
}

.page-header {
  background: #fff;
  border-bottom: 1px solid #e4e7ed;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
  z-index: 10;

  .header-content {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 12px 20px;
    max-width: 1600px;
    margin: 0 auto;

    .page-title {
      margin: 0;
      font-size: 16px;
      font-weight: 600;
      color: #1f2937;
    }

    .close-btn {
      width: 32px;
      height: 32px;
      color: #6b7280;
      transition: all 0.2s;

      &:hover {
        color: #ef4444;
        background-color: #fef2f2;
      }
    }
  }
}

.top-toolbar {
  background: #fff;
  border-bottom: 1px solid #e4e7ed;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);

  .toolbar-content {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 24px;
    padding: 12px 20px;
    max-width: 1600px;
    margin: 0 auto;
    flex-wrap: wrap;
  }

  .type-selector {
    display: flex;
    gap: 6px;
    flex: 1;
    min-width: 0;

    .type-item {
      padding: 6px 16px;
      border-radius: 6px;
      cursor: pointer;
      transition: all 0.2s;
      background: #f9fafb;
      border: 1.5px solid transparent;
      white-space: nowrap;
      user-select: none;

      .type-name {
        font-size: 12px;
        font-weight: 500;
        color: #6b7280;
      }

      &:hover {
        background: #f3f4f6;
        border-color: #e5e7eb;
      }

      &.active {
        background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
        border-color: #667eea;
        box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);

        .type-name {
          color: #fff;
          font-weight: 600;
        }
      }
    }
  }

  .info-panel {
    display: flex;
    align-items: center;
    gap: 16px;
    flex-wrap: wrap;

    .info-item {
      display: flex;
      align-items: center;
      gap: 6px;

      .info-label {
        font-size: 12px;
        font-weight: 500;
        color: #6b7280;
        white-space: nowrap;
      }

      .difficulty-selector {
        display: flex;
        gap: 4px;

        .difficulty-item {
          width: 28px;
          height: 28px;
          display: flex;
          align-items: center;
          justify-content: center;
          border-radius: 4px;
          background: #f3f4f6;
          color: #6b7280;
          font-weight: 600;
          font-size: 13px;
          cursor: pointer;
          transition: all 0.2s;

          &:hover {
            background: #e5e7eb;
            transform: translateY(-1px);
          }

          &.active {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: #fff;
            box-shadow: 0 2px 6px rgba(102, 126, 234, 0.3);
          }
        }
      }
    }
  }
}

.main-content {
  flex: 1;
  padding: 16px 20px;
  overflow: hidden;
  max-width: 1600px;
  margin: 0 auto;
  width: 100%;
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.editor-area {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  min-height: 0;

  .editor-container {
    flex: 1;
    min-height: 0;
    background: #fff;
    border-radius: 12px;
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
    overflow: hidden;
    display: flex;
    flex-direction: column;

    .editor-scrollbar {
      flex: 1;
      min-height: 0;
    }
  }
}

.editor-block {
  padding: 16px 20px;
  border-bottom: 1px solid #f3f4f6;

  &:last-child {
    border-bottom: none;
  }

  .editor-wrapper-border {
    border: 1px solid #dcdfe6;
    border-radius: 4px;
    overflow: hidden;
    transition: all 0.2s;
    min-height: 180px;

    &:focus-within {
      border-color: #667eea;
      box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.1);
    }

    :deep(.editor-wrapper) {
      border: none;
      min-height: 180px;
      display: flex;
      flex-direction: column;
    }

    :deep(#toolbar-container) {
      flex-shrink: 0;
      border-bottom: 1px solid #e4e7ed;
    }

    :deep(#editor-container) {
      flex: 1;
      min-height: 130px;
    }

    :deep(.w-e-text-container) {
      min-height: 130px !important;
      padding: 6px !important;
    }
  }

  .block-header {
    display: flex;
    align-items: center;
    gap: 6px;
    margin-bottom: 10px;

    .block-title {
      font-size: 13px;
      font-weight: 600;
      color: #1f2937;
    }

    .block-required {
      color: #ef4444;
      font-size: 12px;
    }

    .add-option-btn {
      margin-top: 8px;
      width: 100%;
    }
  }

  .block-content {
    .attachment-tags {
      margin-top: 12px;
      display: flex;
      flex-wrap: wrap;
      gap: 8px;
    }

    .answer-selector {
      display: flex;
      flex-direction: column;
      gap: 8px;

      .answer-option {
        display: flex;
        align-items: center;
        gap: 10px;
        padding: 10px 12px;
        border: 1.5px solid #e5e7eb;
        border-radius: 6px;
        cursor: pointer;
        transition: all 0.2s;
        background: #f9fafb;

        .check-icon {
          display: flex;
          align-items: center;
          justify-content: center;
          width: 18px;
          height: 18px;
          flex-shrink: 0;

          .icon-checked {
            font-size: 16px;
            color: #667eea;
          }

          .icon-unchecked {
            width: 14px;
            height: 14px;
            border: 2px solid #9ca3af;
            border-radius: 50%;
            background: transparent;
            display: inline-block;
          }
        }

        span {
          font-size: 13px;
          font-weight: 500;
          color: #374151;
        }

        &:hover {
          border-color: #667eea;
          background: #f3f4f6;
          transform: translateX(2px);
        }

        &.selected {
          border-color: #667eea;
          background: linear-gradient(135deg, #f0f4ff 0%, #e0e7ff 100%);

          .check-icon {
            .icon-unchecked {
              border-color: #667eea;
            }
          }
        }
      }
    }

    .answer-textarea {
      :deep(.el-textarea__inner) {
        border-radius: 6px;
        border-color: #e5e7eb;
        font-size: 13px;
        line-height: 1.5;
        padding: 8px 12px;

        &:focus {
          border-color: #667eea;
        }
      }
    }
  }

  .options-list {
    display: flex;
    flex-direction: column;
    gap: 10px;

    .option-card {
      border: 1.5px solid #e5e7eb;
      border-radius: 6px;
      padding: 12px;
      background: #fafbfc;
      transition: all 0.2s;

      &:hover {
        border-color: #667eea;
        box-shadow: 0 2px 8px rgba(102, 126, 234, 0.1);
      }

      .option-header {
        display: flex;
        justify-content: space-between;
        align-items: center;
        margin-bottom: 8px;

        .option-badge {
          width: 28px;
          height: 28px;
          display: flex;
          align-items: center;
          justify-content: center;
          border-radius: 4px;
          background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
          color: #fff;
          font-weight: 700;
          font-size: 13px;
          box-shadow: 0 2px 4px rgba(102, 126, 234, 0.3);
        }
      }

      .option-editor {
        margin-top: 8px;
        min-height: 150px;
        position: relative;
        overflow: hidden;
        border: 1px solid #dcdfe6;
        border-radius: 4px;
        transition: all 0.2s;

        :deep(.editor-wrapper) {
          min-height: 150px;
          display: flex;
          flex-direction: column;
          border: none;
        }

        :deep(#toolbar-container) {
          flex-shrink: 0;
          border-bottom: 1px solid #e4e7ed;
        }

        :deep(#editor-container) {
          flex: 1;
          min-height: 100px;
        }

        :deep(.w-e-text-container) {
          min-height: 100px !important;
          padding: 6px !important;
        }

        &:focus-within {
          border-color: #667eea;
          box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.1);
        }
      }
    }
  }
}

.page-footer {
  background: #fff;
  border-top: 1px solid #e4e7ed;
  box-shadow: 0 -1px 4px rgba(0, 0, 0, 0.04);
  z-index: 10;

  .footer-content {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 12px 20px;
    max-width: 1600px;
    margin: 0 auto;

    .footer-left {
      .zoom-text {
        color: #9ca3af;
        font-size: 12px;
        font-weight: 500;
      }
    }

    .footer-actions {
      display: flex;
      gap: 8px;

      .el-button {
        padding: 8px 20px;
        font-size: 13px;
        font-weight: 500;
        border-radius: 6px;
        transition: all 0.2s;

        &.el-button--primary {
          background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
          border: none;
          box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);

          &:hover {
            transform: translateY(-1px);
            box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
          }
        }
      }
    }
  }
}
</style>
<template>
  <div class="question-edit-container">
    <el-card shadow="never" class="header-card">
      <template #header>
        <div class="flex justify-between items-center">
          <h3>试题编辑</h3>
          <div class="flex gap-2">
            <el-button @click="handleBack">返 回</el-button>
            <el-button type="primary" @click="handleSave">保 存</el-button>
          </div>
        </div>
      </template>
    </el-card>

    <div class="content-wrapper">
      <!-- 左侧题号导航 -->
      <div class="left-nav">
        <el-card shadow="never" class="nav-card">
          <template #header>
            <div class="nav-header">
              <span>试题列表</span>
              <span class="text-sm text-gray-500"
                >(共{{ questionList.length }}题)</span
              >
            </div>
          </template>
          <el-scrollbar height="calc(100vh - 200px)">
            <div class="answer-sheet">
              <div
                v-for="(item, index) in questionList"
                :key="item.id || index"
                :class="[
                  'answer-item',
                  {
                    active: currentIndex === index,
                    error: item.hasError,
                  },
                ]"
                @click="scrollToQuestion(index)"
              >
                <div class="answer-number">{{ index + 1 }}</div>
                <div class="answer-type-badge">
                  <el-tag
                    :type="getTypeTagType(item.type)"
                    size="small"
                    effect="plain"
                  >
                    {{ getTypeName(item.type) }}
                  </el-tag>
                </div>
                <el-icon v-if="item.hasError" class="error-icon">
                  <i-ep-warning-filled />
                </el-icon>
              </div>
            </div>
          </el-scrollbar>
        </el-card>
      </div>

      <!-- 右侧试题编辑区 -->
      <div class="right-content">
        <el-card shadow="never" class="content-card">
          <el-scrollbar height="calc(100vh - 200px)">
            <div class="question-list">
              <div
                v-for="(question, index) in questionList"
                :key="question.id || index"
                :id="`question-${index}`"
                :ref="(el) => setQuestionRef(el, index)"
                class="question-item"
              >
                <div class="question-header">
                  <div class="flex items-center gap-2">
                    <span class="question-title">第{{ index + 1 }}题</span>
                    <el-tag :type="getTypeTagType(question.type)" size="small">
                      {{ getTypeName(question.type) }}
                    </el-tag>
                    <el-tag v-if="question.hasError" type="danger" size="small">
                      需要修改
                    </el-tag>
                  </div>
                  <el-button
                    v-if="index > 0"
                    link
                    size="small"
                    @click="scrollToQuestion(index - 1)"
                  >
                    <i-ep-arrow-up />上一题
                  </el-button>
                  <el-button
                    v-if="index < questionList.length - 1"
                    link
                    size="small"
                    @click="scrollToQuestion(index + 1)"
                  >
                    下一题<i-ep-arrow-down />
                  </el-button>
                </div>

                <!-- 题目内容编辑 -->
                <el-form
                  :model="question"
                  label-width="100px"
                  class="question-form"
                >
                  <el-form-item label="题目内容">
                    <el-input
                      v-model="question.content"
                      type="textarea"
                      :rows="3"
                      placeholder="请输入题目内容"
                    />
                  </el-form-item>

                  <!-- 选择题 -->
                  <template
                    v-if="
                      question.type === 'single' || question.type === 'multiple'
                    "
                  >
                    <el-form-item
                      v-for="(option, optIndex) in question.options"
                      :key="optIndex"
                      :label="`选项${String.fromCharCode(65 + optIndex)}`"
                    >
                      <div class="flex items-center gap-2">
                        <el-input
                          v-model="option.text"
                          placeholder="请输入选项内容"
                          class="flex-1"
                        />
                        <el-checkbox
                          v-if="question.type === 'multiple'"
                          v-model="option.isCorrect"
                        >
                          正确答案
                        </el-checkbox>
                        <el-radio
                          v-else
                          :model-value="question.correctAnswer"
                          :label="optIndex"
                          @change="question.correctAnswer = optIndex"
                        >
                          正确答案
                        </el-radio>
                        <el-button
                          link
                          type="danger"
                          size="small"
                          @click="removeOption(question, optIndex)"
                        >
                          <i-ep-delete />
                        </el-button>
                      </div>
                    </el-form-item>
                    <el-form-item>
                      <el-button
                        link
                        type="primary"
                        @click="addOption(question)"
                      >
                        <i-ep-plus />添加选项
                      </el-button>
                    </el-form-item>
                  </template>

                  <!-- 判断题 -->
                  <template v-if="question.type === 'judge'">
                    <el-form-item label="正确答案">
                      <el-radio-group v-model="question.correctAnswer">
                        <el-radio :label="true">正确</el-radio>
                        <el-radio :label="false">错误</el-radio>
                      </el-radio-group>
                    </el-form-item>
                  </template>

                  <!-- 填空题 -->
                  <template v-if="question.type === 'fill'">
                    <el-form-item label="参考答案">
                      <el-input
                        v-model="question.correctAnswer"
                        type="textarea"
                        :rows="2"
                        placeholder="请输入参考答案(多个答案用逗号分隔)"
                      />
                    </el-form-item>
                  </template>

                  <!-- 简答题 -->
                  <template v-if="question.type === 'essay'">
                    <el-form-item label="参考答案">
                      <el-input
                        v-model="question.correctAnswer"
                        type="textarea"
                        :rows="5"
                        placeholder="请输入参考答案"
                      />
                    </el-form-item>
                  </template>

                  <!-- 解析 -->
                  <el-form-item label="题目解析">
                    <el-input
                      v-model="question.analysis"
                      type="textarea"
                      :rows="3"
                      placeholder="请输入题目解析(可选)"
                    />
                  </el-form-item>

                  <!-- 难度和分值 -->
                  <el-form-item label="难度">
                    <el-rate v-model="question.difficulty" :max="5" />
                  </el-form-item>
                  <el-form-item label="分值">
                    <el-input-number
                      v-model="question.score"
                      :min="0"
                      :max="100"
                      :precision="1"
                    />
                  </el-form-item>
                </el-form>

                <!-- 错误提示 -->
                <el-alert
                  v-if="question.hasError && question.errorMessage"
                  :title="question.errorMessage"
                  type="warning"
                  :closable="false"
                  class="mt-2"
                />
              </div>
            </div>
          </el-scrollbar>
        </el-card>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
defineOptions({
  name: "QuestionEdit",
});

import { ref, onMounted, nextTick } from "vue";
import { useRoute, useRouter } from "vue-router";
import { ElMessage, ElMessageBox } from "element-plus";
// import { getQuestionList, saveQuestions } from "@/api/kaoshi";
// import type { Question } from "@/api/kaoshi/types";

/**
 * 试题类型定义
 * - single: 单选题(只有一个正确答案)
 * - multiple: 多选题(可以有多个正确答案)
 * - judge: 判断题(正确/错误)
 * - fill: 填空题(需要填写答案)
 * - essay: 简答题(需要文字描述答案)
 */
type QuestionType = "single" | "multiple" | "judge" | "fill" | "essay";

/**
 * 试题选项(用于选择题)
 */
interface QuestionOption {
  /** 选项文本内容 */
  text: string;
  /** 是否为正确答案(多选题使用,单选题使用 correctAnswer 字段) */
  isCorrect?: boolean;
}

/**
 * 试题实体
 */
interface Question {
  /**
   * 试题ID
   * - 新建题目时可能为空
   * - 从后端获取的题目会有ID
   */
  id?: string | number;

  /**
   * 试题类型
   * - single: 单选题
   * - multiple: 多选题
   * - judge: 判断题
   * - fill: 填空题
   * - essay: 简答题
   */
  type: QuestionType;

  /**
   * 题目内容
   * - 题目的题干部分
   * - 必填项,不能为空
   * - 支持多行文本
   */
  content: string;

  /**
   * 选项列表(仅选择题使用)
   * - 单选题和多选题都需要此字段
   * - 至少需要2个选项
   * - 每个选项包含文本和是否正确答案的标记
   */
  options?: QuestionOption[];

  /**
   * 正确答案
   * - 单选题:存储选项的索引(number),如 0、1、2
   * - 多选题:存储选项索引数组(number[]),如 [0, 2, 3]
   * - 判断题:存储布尔值(boolean),true 表示正确,false 表示错误
   * - 填空题:存储答案字符串(string),多个答案用逗号分隔
   * - 简答题:存储参考答案字符串(string),支持多行文本
   */
  correctAnswer?: any;

  /**
   * 题目解析
   * - 对题目的解释说明
   * - 可选字段,用于帮助理解题目
   * - 支持多行文本
   */
  analysis?: string;

  /**
   * 难度等级
   * - 取值范围:1-5
   * - 1: 非常简单
   * - 2: 简单
   * - 3: 中等
   * - 4: 困难
   * - 5: 非常困难
   * - 用于题目筛选和组卷
   */
  difficulty?: number;

  /**
   * 分值
   * - 该题目的分数
   * - 取值范围:0-100
   * - 用于计算总分
   * - 默认值根据题目类型设置
   */
  score?: number;

  /**
   * 是否有错误标记
   * - 导入时如果解析出现问题,会标记为 true
   * - 需要用户修改后才能保存
   * - 界面上会显示错误提示
   */
  hasError?: boolean;

  /**
   * 错误信息
   * - 当 hasError 为 true 时,此字段包含具体的错误描述
   * - 例如:"题目内容不能为空"、"至少需要2个选项"等
   * - 用于提示用户需要修改的内容
   */
  errorMessage?: string;
}

const route = useRoute();
const router = useRouter();

const questionList = ref<Question[]>([]);
const currentIndex = ref(0);
const questionRefs = ref<(HTMLElement | null)[]>([]);

// 设置题目引用
function setQuestionRef(el: HTMLElement | null, index: number) {
  if (el) {
    questionRefs.value[index] = el;
  }
}

// 获取类型名称
function getTypeName(type: QuestionType): string {
  const typeMap: Record<QuestionType, string> = {
    single: "单选题",
    multiple: "多选题",
    judge: "判断题",
    fill: "填空题",
    essay: "简答题",
  };
  return typeMap[type] || "未知类型";
}

// 获取类型标签类型
function getTypeTagType(type: QuestionType): string {
  const typeMap: Record<QuestionType, string> = {
    single: "primary",
    multiple: "success",
    judge: "warning",
    fill: "info",
    essay: "danger",
  };
  return typeMap[type] || "";
}

// 滚动到指定题目
function scrollToQuestion(index: number) {
  if (index < 0 || index >= questionList.value.length) return;

  currentIndex.value = index;
  const questionId = `question-${index}`;
  const element = document.getElementById(questionId);

  if (element) {
    element.scrollIntoView({
      behavior: "smooth",
      block: "start",
    });
  }
}

// 添加选项
function addOption(question: Question) {
  if (!question.options) {
    question.options = [];
  }
  question.options.push({
    text: "",
    isCorrect: false,
  });
}

// 移除选项
function removeOption(question: Question, index: number) {
  if (question.options && question.options.length > 2) {
    question.options.splice(index, 1);
  } else {
    ElMessage.warning("至少需要保留2个选项");
  }
}

// 返回
function handleBack() {
  router.back();
}

/**
 * 保存试题
 * 验证所有题目的必填项,如果有错误则提示用户修改
 * 验证通过后调用后端接口保存数据
 */
function handleSave() {
  // 验证所有题目的必填项
  const hasError = questionList.value.some((q) => {
    if (!q.content || q.content.trim() === "") {
      q.hasError = true;
      q.errorMessage = "题目内容不能为空";
      return true;
    }

    if (q.type === "single" || q.type === "multiple") {
      if (!q.options || q.options.length < 2) {
        q.hasError = true;
        q.errorMessage = "至少需要2个选项";
        return true;
      }
      if (q.options.some((opt) => !opt.text || opt.text.trim() === "")) {
        q.hasError = true;
        q.errorMessage = "选项内容不能为空";
        return true;
      }
      if (q.type === "single" && q.correctAnswer === undefined) {
        q.hasError = true;
        q.errorMessage = "请选择正确答案";
        return true;
      }
      if (q.type === "multiple" && !q.options.some((opt) => opt.isCorrect)) {
        q.hasError = true;
        q.errorMessage = "至少需要选择一个正确答案";
        return true;
      }
    }

    if (q.type === "judge" && q.correctAnswer === undefined) {
      q.hasError = true;
      q.errorMessage = "请选择正确答案";
      return true;
    }

    if (
      (q.type === "fill" || q.type === "essay") &&
      (!q.correctAnswer || q.correctAnswer.trim() === "")
    ) {
      q.hasError = true;
      q.errorMessage = "参考答案不能为空";
      return true;
    }

    q.hasError = false;
    q.errorMessage = "";
    return false;
  });

  if (hasError) {
    ElMessage.warning("请修改标记为错误的题目后再保存");
    return;
  }

  // 调用保存接口
  ElMessageBox.confirm("确定要保存所有修改吗?", "提示", {
    confirmButtonText: "确定",
    cancelButtonText: "取消",
    type: "info",
  })
    .then(async () => {
      // 获取导入批次ID
      const importId = (route.params.id || route.query.id) as string;

      // 调用后端API保存试题数据
      // await saveQuestions(importId, questionList.value);
      // ElMessage.success("保存成功");
      // router.back();

      // 模拟保存(实际开发时删除此部分)
      ElMessage.success("保存成功");
      router.back();
    })
    .catch(() => {});
}

/**
 * 加载试题数据
 * 从后端接口获取试题列表,如果接口返回的数据有错误标记,会在界面上显示
 */
async function loadQuestions() {
  // 从路由参数获取导入批次ID
  const importId = (route.params.id || route.query.id) as string;

  if (importId) {
    try {
      // 调用后端接口获取试题数据
      // 后端返回的试题数据可能包含 hasError 和 errorMessage 字段
      // 表示哪些题目在解析时出现问题,需要用户修改
      // const { data } = await getQuestionList(importId);
      // questionList.value = data;

      // 模拟数据(实际开发时删除此部分,使用上面的接口调用)
      questionList.value = mockQuestions;
    } catch (error) {
      ElMessage.error("加载试题数据失败");
      console.error("加载失败:", error);
    }
  } else {
    // 如果没有ID,可能是直接导入后跳转,使用模拟数据
    questionList.value = mockQuestions;
  }
}

// 模拟试题数据
const mockQuestions: Question[] = [
  {
    id: 1,
    type: "single",
    content: "Vue 3 的 Composition API 中,哪个函数用于创建响应式引用?",
    options: [
      { text: "ref()", isCorrect: false },
      { text: "reactive()", isCorrect: false },
      { text: "ref() 或 reactive()", isCorrect: true },
      { text: "createRef()", isCorrect: false },
    ],
    correctAnswer: 2,
    analysis: "Vue 3 中可以使用 ref() 或 reactive() 创建响应式数据",
    difficulty: 3,
    score: 5,
  },
  {
    id: 2,
    type: "multiple",
    content: "以下哪些是 Vue 3 的新特性?",
    options: [
      { text: "Composition API", isCorrect: true },
      { text: "TypeScript 支持", isCorrect: true },
      { text: "Teleport", isCorrect: true },
      { text: "Options API", isCorrect: false },
    ],
    analysis:
      "Vue 3 引入了 Composition API、更好的 TypeScript 支持和 Teleport 等新特性",
    difficulty: 4,
    score: 10,
  },
  {
    id: 3,
    type: "judge",
    content: "Vue 3 完全兼容 Vue 2 的语法",
    correctAnswer: false,
    analysis: "Vue 3 虽然保持了大部分 Vue 2 的语法,但有一些破坏性变更",
    difficulty: 2,
    score: 3,
  },
  {
    id: 4,
    type: "fill",
    content: "Vue 3 使用 _____ 作为构建工具",
    correctAnswer: "Vite",
    analysis: "Vue 3 推荐使用 Vite 作为构建工具",
    difficulty: 2,
    score: 3,
  },
  {
    id: 5,
    type: "essay",
    content: "请简述 Vue 3 相比 Vue 2 的主要改进",
    correctAnswer:
      "1. 性能提升:更小的包体积、更快的渲染速度\n2. Composition API:更好的逻辑复用\n3. 更好的 TypeScript 支持\n4. 新的内置组件:Teleport、Suspense",
    analysis: "Vue 3 在性能、开发体验和功能方面都有显著提升",
    difficulty: 4,
    score: 15,
  },
];

onMounted(() => {
  loadQuestions();
});
</script>

<style lang="scss" scoped>
.question-edit-container {
  padding: 20px;
  height: 100vh;
  display: flex;
  flex-direction: column;
}

.header-card {
  margin-bottom: 20px;
}

.content-wrapper {
  display: flex;
  gap: 20px;
  flex: 1;
  overflow: hidden;
}

.left-nav {
  width: 250px;
  flex-shrink: 0;
}

.nav-card {
  height: 100%;

  .nav-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
  }
}

.answer-sheet {
  display: grid;
  grid-template-columns: repeat(3, 1fr); // 一行3个
  gap: 6px;
  padding: 6px;

  .answer-item {
    position: relative;
    aspect-ratio: 1; // 保持正方形
    border: 1px solid var(--el-border-color);
    border-radius: 4px;
    cursor: pointer;
    transition: all 0.3s;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    background-color: #fff;
    padding: 2px;

    &:hover {
      border-color: var(--el-color-primary);
      box-shadow: 0 1px 4px rgba(64, 158, 255, 0.2);
      transform: translateY(-1px);
    }

    &.active {
      background-color: var(--el-color-primary-light-9);
      border-color: var(--el-color-primary);
      border-width: 2px;
      box-shadow: 0 2px 6px rgba(64, 158, 255, 0.3);
    }

    &.error {
      border-color: var(--el-color-danger);
      background-color: var(--el-color-danger-light-9);

      &.active {
        border-color: var(--el-color-danger);
        background-color: var(--el-color-danger-light-8);
      }
    }

    .answer-number {
      font-size: 12px;
      font-weight: bold;
      color: var(--el-text-color-primary);
      margin-bottom: 1px;
      line-height: 1.2;
    }

    .answer-type-badge {
      margin-top: 1px;

      :deep(.el-tag) {
        font-size: 9px;
        padding: 0 3px;
        height: 16px;
        line-height: 16px;
      }
    }

    .error-icon {
      position: absolute;
      top: 1px;
      right: 1px;
      color: var(--el-color-danger);
      font-size: 10px;
    }
  }
}

.right-content {
  flex: 1;
  overflow: hidden;
}

.content-card {
  height: 100%;
}

.question-list {
  padding: 20px;
}

.question-item {
  margin-bottom: 40px;
  padding: 20px;
  border: 1px solid var(--el-border-color-light);
  border-radius: 8px;
  background-color: #fff;

  &:last-child {
    margin-bottom: 0;
  }
}

.question-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--el-border-color-lighter);
}

.question-title {
  font-size: 16px;
  font-weight: bold;
  color: var(--el-text-color-primary);
}

.question-form {
  margin-top: 20px;
}
</style>
async function handleQuestionImport(file: File, fileType: "word" | "excel") {
  console.log("导入试题文件:", file.name, "文件类型:", fileType);

  try {
    // 调用后端API上传文件并解析试题
    // 后端会解析Word/Excel文件,提取试题内容,返回结构化数据
    // const { data } = await importQuestions(file, fileType);
    // const importId = data.id; // 导入批次ID
    // const questions = data.questions; // 解析后的试题列表

    // 模拟接口调用(实际开发时删除此部分)
    ElMessage.success("试题导入成功,正在跳转到编辑页面...");
    await new Promise((resolve) => setTimeout(resolve, 500));
    const questionId = "mock-id-" + Date.now();

    // 跳转到试题编辑页面
    // 传递导入批次ID,编辑页面会根据ID从后端获取试题数据
    router.push({
      path: "/kaoshi/question-edit",
      query: {
        id: questionId, // 导入批次ID
        fileName: file.name,
        fileType: fileType,
      },
    });
  } catch (error) {
    ElMessage.error("试题导入失败,请检查文件格式");
    console.error("导入失败:", error);
  }
}