Compare commits
2 Commits
Author | SHA1 | Date |
---|---|---|
|
431888797d | |
|
3d11c02067 |
49
package.json
49
package.json
|
@ -9,29 +9,50 @@
|
|||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.4.0",
|
||||
"vue-router": "^4.2.0",
|
||||
"element-plus": "^2.5.0",
|
||||
"@codemirror/basic-setup": "^0.20.0",
|
||||
"@codemirror/commands": "^6.8.0",
|
||||
"@codemirror/lang-cpp": "^6.0.2",
|
||||
"@codemirror/lang-javascript": "^6.2.3",
|
||||
"@codemirror/language": "^6.11.0",
|
||||
"@codemirror/rangeset": "^0.19.9",
|
||||
"@codemirror/state": "^6.5.2",
|
||||
"@codemirror/theme-one-dark": "^6.1.2",
|
||||
"@codemirror/view": "^6.36.4",
|
||||
"@element-plus/icons-vue": "^2.3.0",
|
||||
"echarts": "^5.5.0",
|
||||
"vue-echarts": "^6.6.0",
|
||||
"three": "^0.160.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"xlsx": "^0.18.5",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||
"@fortawesome/vue-fontawesome": "^3.0.8",
|
||||
"@iconify/vue": "^4.3.0",
|
||||
"@monaco-editor/loader": "^1.5.0",
|
||||
"@vicons/ionicons5": "^0.13.0",
|
||||
"@vueuse/core": "^13.0.0",
|
||||
"animate.css": "^4.1.1",
|
||||
"axios": "^1.6.0",
|
||||
"pinia": "^2.1.0",
|
||||
"dayjs": "^1.11.0",
|
||||
|
||||
|
||||
"echarts": "^5.5.0",
|
||||
"element-plus": "^2.5.0",
|
||||
"file-saver": "^2.0.5",
|
||||
"js-cookie": "^3.0.5",
|
||||
"monaco-editor": "^0.30.1",
|
||||
"js-cookie": "^3.0.5"
|
||||
"naive-ui": "^2.41.0",
|
||||
"pinia": "^2.1.0",
|
||||
"qs": "^6.14.0",
|
||||
"three": "^0.160.0",
|
||||
"vfonts": "^0.0.3",
|
||||
"vtron": "^0.7.8",
|
||||
"vue": "^3.4.0",
|
||||
"vue-codemirror": "^6.1.1",
|
||||
"vue-echarts": "^6.6.0",
|
||||
"vue-monaco-editor": "^0.0.19",
|
||||
"vue-router": "^4.2.0",
|
||||
"vue-windows": "^0.3.0",
|
||||
"xlsx": "^0.18.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.0",
|
||||
"vite": "^5.0.0",
|
||||
"sass": "^1.70.0",
|
||||
"unplugin-auto-import": "^0.17.0",
|
||||
"unplugin-vue-components": "^0.26.0"
|
||||
"unplugin-vue-components": "^0.26.0",
|
||||
"vite": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<template>
|
||||
<div class="app">
|
||||
<AppHeader />
|
||||
<AppHeader v-if="!$route.meta.hideNav"/>
|
||||
<main class="main-content">
|
||||
<router-view />
|
||||
</main>
|
||||
<AppFooter />
|
||||
<AppFooter v-if="!$route.meta.hideNav"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -18,11 +18,11 @@ import AppFooter from './components/AppFooter.vue'
|
|||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
margin-top: 60px; // 为固定的头部留出空间
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
@ -6,6 +6,7 @@ export function getCourses(params) {
|
|||
}
|
||||
|
||||
export function getCoursesMethod(params) {
|
||||
console.log(params)
|
||||
return get('/bs/courses', params)
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import { post } from '@/utils/request'
|
|||
* @param data
|
||||
*/
|
||||
export function fetchDetail(id) {
|
||||
return post('/exam/api/exam/exam/detail', { id: id })
|
||||
return post('/api/common/exam/api/exam/exam/detail', { id: id })
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import { get, post } from '@/utils/request'
|
||||
|
||||
// 获取用户积分
|
||||
export function getUserPoints() {
|
||||
return get('/api/shop/points')
|
||||
}
|
||||
|
||||
// 获取积分历史
|
||||
export function getPointsHistory() {
|
||||
return get('/api/shop/points/history')
|
||||
}
|
||||
|
||||
// 获取商品列表
|
||||
export function getGoodsList() {
|
||||
return get('/api/shop/goods')
|
||||
}
|
||||
|
||||
// 兑换商品
|
||||
export function exchangeGoods(goodsId) {
|
||||
return post('/api/shop/exchange', { goodsId })
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import {postRequest, post, get, postRequestJSON, getRequest, getRequestWithqs} from "@/utils/request.js";
|
||||
|
||||
|
||||
|
||||
export const testPostUseParamMethod = (param) => {
|
||||
return postRequest("/api/common/testPostUseParam", param)
|
||||
}
|
||||
|
||||
export const testPostUseBodyMethod = (data) =>{
|
||||
return post("/api/common/testPostUseBody", data)
|
||||
}
|
||||
|
||||
export const testGetParamMethod = (param) =>{
|
||||
return get("/api/common/testGetParam", param)
|
||||
}
|
||||
|
||||
export const testGetParamMethodList = (param) =>{
|
||||
return getRequest("/api/common/testGetParam", param)
|
||||
}
|
||||
|
||||
export const testGetParamMethodqs = (param) =>{
|
||||
return getRequestWithqs("/api/common/testGetParam", param)
|
||||
}
|
||||
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 14 KiB |
|
@ -0,0 +1,192 @@
|
|||
<template>
|
||||
<div class="editor-container">
|
||||
<div ref="editor" class="code-editor"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted, onBeforeUnmount, defineProps, defineEmits } from "vue";
|
||||
import { EditorView, keymap, gutter, GutterMarker, lineNumbers } from "@codemirror/view";
|
||||
import { EditorState, StateField, StateEffect } from "@codemirror/state";
|
||||
import { defaultKeymap, indentWithTab } from "@codemirror/commands";
|
||||
import { RangeSet } from "@codemirror/rangeset";
|
||||
import { javascript } from "@codemirror/lang-javascript";
|
||||
import { oneDark } from "@codemirror/theme-one-dark";
|
||||
import { indentUnit } from "@codemirror/language";
|
||||
|
||||
class BreakpointMarker extends GutterMarker {
|
||||
toDOM() {
|
||||
const marker = document.createElement("div");
|
||||
marker.className = "breakpoint-marker";
|
||||
marker.textContent = "●";
|
||||
return marker;
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: "CodeEditor",
|
||||
props: {
|
||||
modelValue: String, // 双向绑定代码内容
|
||||
language: String, // 代码语言,默认 JS
|
||||
height: {
|
||||
type: String,
|
||||
default: '600px'
|
||||
}
|
||||
},
|
||||
emits: ["update:modelValue"], // 监听代码变更
|
||||
setup(props, { emit }) {
|
||||
const editor = ref(null);
|
||||
let editorView = null;
|
||||
|
||||
const setBreakpoint = StateEffect.define();
|
||||
const breakpointsField = StateField.define({
|
||||
create: () => new Set(),
|
||||
update(breakpoints, tr) {
|
||||
const newBreakpoints = new Set(breakpoints);
|
||||
for (let effect of tr.effects) {
|
||||
if (effect.is(setBreakpoint)) {
|
||||
const line = effect.value;
|
||||
if (newBreakpoints.has(line)) {
|
||||
newBreakpoints.delete(line);
|
||||
} else {
|
||||
newBreakpoints.add(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
return newBreakpoints;
|
||||
},
|
||||
});
|
||||
|
||||
const breakpointGutter = gutter({
|
||||
class: "cm-breakpoint-gutter",
|
||||
markers: (view) => {
|
||||
const breakpoints = view.state.field(breakpointsField);
|
||||
const markers = [];
|
||||
for (const line of breakpoints) {
|
||||
try {
|
||||
const lineInfo = view.state.doc.line(line);
|
||||
markers.push(new BreakpointMarker().range(lineInfo.from));
|
||||
} catch (e) {
|
||||
console.warn("Invalid line number skipped:", line);
|
||||
}
|
||||
}
|
||||
return RangeSet.of(markers, true);
|
||||
},
|
||||
domEventHandlers: {
|
||||
click: (view, block) => {
|
||||
const lineNumber = view.state.doc.lineAt(block.from).number;
|
||||
view.dispatch({
|
||||
effects: [setBreakpoint.of(lineNumber)],
|
||||
});
|
||||
return true;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (!editor.value) return;
|
||||
|
||||
editorView = new EditorView({
|
||||
state: EditorState.create({
|
||||
doc: props.modelValue || "",
|
||||
extensions: [
|
||||
lineNumbers(),
|
||||
breakpointGutter,
|
||||
breakpointsField,
|
||||
javascript(),
|
||||
oneDark,
|
||||
indentUnit.of(" "), // 使用4个空格作为缩进
|
||||
keymap.of([
|
||||
...defaultKeymap,
|
||||
indentWithTab
|
||||
]),
|
||||
EditorView.updateListener.of((update) => {
|
||||
if (update.docChanged) {
|
||||
emit("update:modelValue", update.state.doc.toString());
|
||||
}
|
||||
}),
|
||||
EditorView.theme({
|
||||
"&": {
|
||||
fontSize: "14px",
|
||||
height: "100%"
|
||||
},
|
||||
".cm-content": {
|
||||
fontFamily: "'Fira Code', monospace",
|
||||
padding: "12px 0",
|
||||
},
|
||||
".cm-line": {
|
||||
padding: "0 12px",
|
||||
lineHeight: "1.6"
|
||||
},
|
||||
".cm-gutters": {
|
||||
backgroundColor: "#1e1e1e",
|
||||
color: "#666",
|
||||
border: "none"
|
||||
},
|
||||
".cm-activeLineGutter": {
|
||||
backgroundColor: "#333"
|
||||
},
|
||||
".cm-activeLine": {
|
||||
backgroundColor: "rgba(255,255,255,0.03)"
|
||||
}
|
||||
}),
|
||||
EditorView.baseTheme({
|
||||
".cm-breakpoint-gutter": {
|
||||
width: "30px",
|
||||
backgroundColor: "#1e1e1e",
|
||||
borderRight: "1px solid #333"
|
||||
},
|
||||
".breakpoint-marker": {
|
||||
color: "#ff5555",
|
||||
fontSize: "14px",
|
||||
cursor: "pointer",
|
||||
textAlign: "center"
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
parent: editor.value,
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
editorView?.destroy();
|
||||
});
|
||||
|
||||
return { editor };
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.editor-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #282c34;
|
||||
}
|
||||
|
||||
.code-editor {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* 自定义滚动条样式 */
|
||||
.code-editor ::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.code-editor ::-webkit-scrollbar-track {
|
||||
background: #21252b;
|
||||
}
|
||||
|
||||
.code-editor ::-webkit-scrollbar-thumb {
|
||||
background: #454c59;
|
||||
border-radius: 6px;
|
||||
border: 3px solid #21252b;
|
||||
}
|
||||
|
||||
.code-editor ::-webkit-scrollbar-thumb:hover {
|
||||
background: #535b6a;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,393 @@
|
|||
<template>
|
||||
<div class="debugger-container">
|
||||
<div class="debugger-toolbar">
|
||||
<el-button-group>
|
||||
<el-button type="primary" size="small" @click="startDebug" :disabled="isDebugging">
|
||||
<el-icon><VideoPlay /></el-icon> 开始调试
|
||||
</el-button>
|
||||
<el-button type="primary" size="small" @click="continueExecution" :disabled="!isDebugging">
|
||||
<el-icon><Right /></el-icon> 继续
|
||||
</el-button>
|
||||
<el-button type="primary" size="small" @click="stepOver" :disabled="!isDebugging">
|
||||
<el-icon><Bottom /></el-icon> 单步跳过
|
||||
</el-button>
|
||||
<el-button type="primary" size="small" @click="stepInto" :disabled="!isDebugging">
|
||||
<el-icon><TopRight /></el-icon> 单步进入
|
||||
</el-button>
|
||||
<el-button type="primary" size="small" @click="stepOut" :disabled="!isDebugging">
|
||||
<el-icon><TopLeft /></el-icon> 单步跳出
|
||||
</el-button>
|
||||
<el-button type="danger" size="small" @click="stopDebug" :disabled="!isDebugging">
|
||||
<el-icon><VideoPause /></el-icon> 停止调试
|
||||
</el-button>
|
||||
</el-button-group>
|
||||
</div>
|
||||
|
||||
<div class="debugger-content">
|
||||
<div class="debugger-code">
|
||||
<code-editor
|
||||
v-model="code"
|
||||
:language="language"
|
||||
:readOnly="isDebugging"
|
||||
height="400px"
|
||||
@line-click="toggleBreakpoint"
|
||||
/>
|
||||
<div
|
||||
v-for="(bp, index) in breakpoints"
|
||||
:key="index"
|
||||
class="breakpoint"
|
||||
:style="{ top: (bp.line * 18) + 'px' }"
|
||||
></div>
|
||||
<div
|
||||
v-if="currentLine"
|
||||
class="current-line"
|
||||
:style="{ top: (currentLine * 18) + 'px' }"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div class="debugger-panels">
|
||||
<el-tabs v-model="activePanel">
|
||||
<el-tab-pane label="控制台" name="console">
|
||||
<div class="console-output" ref="consoleOutputEl">
|
||||
<div v-for="(line, index) in consoleOutput" :key="index" class="console-line">
|
||||
{{ line }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="console-input">
|
||||
<el-input
|
||||
v-model="consoleInput"
|
||||
placeholder="输入GDB命令..."
|
||||
@keyup.enter="executeCommand"
|
||||
:disabled="!isDebugging"
|
||||
>
|
||||
<template #append>
|
||||
<el-button @click="executeCommand" :disabled="!isDebugging">执行</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="变量" name="variables">
|
||||
<el-table :data="variables" style="width: 100%">
|
||||
<el-table-column prop="name" label="名称" />
|
||||
<el-table-column prop="value" label="值" />
|
||||
<el-table-column prop="type" label="类型" />
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="调用栈" name="callstack">
|
||||
<el-table :data="callstack" style="width: 100%">
|
||||
<el-table-column prop="level" label="层级" width="60" />
|
||||
<el-table-column prop="function" label="函数" />
|
||||
<el-table-column prop="location" label="位置" />
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, reactive, onMounted, onBeforeUnmount, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { VideoPlay, VideoPause, Right, Bottom, TopRight, TopLeft } from '@element-plus/icons-vue'
|
||||
import CodeEditor from '@/components/CodeEditor/index.vue'
|
||||
import { startDebugSession, executeDebugCommand, getDebugStatus, endDebugSession } from '@/api/exam/exam'
|
||||
|
||||
export default {
|
||||
name: 'Debugger',
|
||||
components: {
|
||||
CodeEditor,
|
||||
VideoPlay, VideoPause, Right, Bottom, TopRight, TopLeft
|
||||
},
|
||||
props: {
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
language: {
|
||||
type: String,
|
||||
default: 'cpp'
|
||||
},
|
||||
problemId: {
|
||||
type: [String, Number],
|
||||
required: true
|
||||
}
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const code = ref(props.modelValue)
|
||||
const isDebugging = ref(false)
|
||||
const sessionId = ref(null)
|
||||
const breakpoints = ref([])
|
||||
const currentLine = ref(null)
|
||||
const consoleOutput = ref([])
|
||||
const consoleInput = ref('')
|
||||
const activePanel = ref('console')
|
||||
const variables = ref([])
|
||||
const callstack = ref([])
|
||||
const consoleOutputEl = ref(null)
|
||||
const statusInterval = ref(null)
|
||||
|
||||
// 监听代码变化
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
if (code.value !== newVal) {
|
||||
code.value = newVal
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => code.value, (newVal) => {
|
||||
if (props.modelValue !== newVal) {
|
||||
emit('update:modelValue', newVal)
|
||||
}
|
||||
})
|
||||
|
||||
// 开始调试
|
||||
const startDebug = async () => {
|
||||
try {
|
||||
const response = await startDebugSession({
|
||||
code: code.value,
|
||||
language: props.language,
|
||||
problemId: props.problemId,
|
||||
breakpoints: breakpoints.value.map(bp => bp.line)
|
||||
})
|
||||
|
||||
if (response.code === 200) {
|
||||
sessionId.value = response.data.sessionId
|
||||
isDebugging.value = true
|
||||
consoleOutput.value = ['调试会话已启动...']
|
||||
|
||||
// 开始定期获取调试状态
|
||||
statusInterval.value = setInterval(updateDebugStatus, 1000)
|
||||
|
||||
ElMessage.success('调试会话已启动')
|
||||
} else {
|
||||
ElMessage.error(response.msg || '启动调试失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('启动调试出错:', error)
|
||||
ElMessage.error('启动调试出错')
|
||||
}
|
||||
}
|
||||
|
||||
// 停止调试
|
||||
const stopDebug = async () => {
|
||||
if (!sessionId.value) return
|
||||
|
||||
try {
|
||||
await endDebugSession(sessionId.value)
|
||||
clearInterval(statusInterval.value)
|
||||
isDebugging.value = false
|
||||
sessionId.value = null
|
||||
currentLine.value = null
|
||||
variables.value = []
|
||||
callstack.value = []
|
||||
consoleOutput.value.push('调试会话已结束')
|
||||
ElMessage.info('调试会话已结束')
|
||||
} catch (error) {
|
||||
console.error('结束调试出错:', error)
|
||||
ElMessage.error('结束调试出错')
|
||||
}
|
||||
}
|
||||
|
||||
// 执行GDB命令
|
||||
const executeCommand = async () => {
|
||||
if (!sessionId.value || !consoleInput.value) return
|
||||
|
||||
try {
|
||||
consoleOutput.value.push(`> ${consoleInput.value}`)
|
||||
|
||||
const response = await executeDebugCommand(sessionId.value, consoleInput.value)
|
||||
|
||||
if (response.code === 200) {
|
||||
if (response.data.output) {
|
||||
consoleOutput.value.push(response.data.output)
|
||||
}
|
||||
|
||||
// 更新调试状态
|
||||
updateDebugStatus()
|
||||
} else {
|
||||
consoleOutput.value.push(`错误: ${response.msg || '命令执行失败'}`)
|
||||
}
|
||||
|
||||
consoleInput.value = ''
|
||||
|
||||
// 滚动到控制台底部
|
||||
scrollToBottom()
|
||||
} catch (error) {
|
||||
console.error('执行命令出错:', error)
|
||||
consoleOutput.value.push(`错误: ${error.message || '命令执行出错'}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 更新调试状态
|
||||
const updateDebugStatus = async () => {
|
||||
if (!sessionId.value) return
|
||||
|
||||
try {
|
||||
const response = await getDebugStatus(sessionId.value)
|
||||
|
||||
if (response.code === 200) {
|
||||
const { currentLine: line, variables: vars, callstack: stack } = response.data
|
||||
|
||||
currentLine.value = line
|
||||
variables.value = vars || []
|
||||
callstack.value = stack || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取调试状态出错:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 切换断点
|
||||
const toggleBreakpoint = (line) => {
|
||||
const index = breakpoints.value.findIndex(bp => bp.line === line)
|
||||
|
||||
if (index >= 0) {
|
||||
breakpoints.value.splice(index, 1)
|
||||
} else {
|
||||
breakpoints.value.push({ line })
|
||||
}
|
||||
|
||||
// 如果正在调试,更新断点
|
||||
if (isDebugging.value) {
|
||||
executeCommand(`break ${line}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 继续执行
|
||||
const continueExecution = () => {
|
||||
executeCommand('continue')
|
||||
}
|
||||
|
||||
// 单步跳过
|
||||
const stepOver = () => {
|
||||
executeCommand('next')
|
||||
}
|
||||
|
||||
// 单步进入
|
||||
const stepInto = () => {
|
||||
executeCommand('step')
|
||||
}
|
||||
|
||||
// 单步跳出
|
||||
const stepOut = () => {
|
||||
executeCommand('finish')
|
||||
}
|
||||
|
||||
// 滚动到控制台底部
|
||||
const scrollToBottom = () => {
|
||||
if (consoleOutputEl.value) {
|
||||
consoleOutputEl.value.scrollTop = consoleOutputEl.value.scrollHeight
|
||||
}
|
||||
}
|
||||
|
||||
// 组件卸载前清理
|
||||
onBeforeUnmount(() => {
|
||||
if (statusInterval.value) {
|
||||
clearInterval(statusInterval.value)
|
||||
}
|
||||
|
||||
if (sessionId.value) {
|
||||
endDebugSession(sessionId.value).catch(console.error)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
code,
|
||||
isDebugging,
|
||||
breakpoints,
|
||||
currentLine,
|
||||
consoleOutput,
|
||||
consoleOutputEl,
|
||||
consoleInput,
|
||||
activePanel,
|
||||
variables,
|
||||
callstack,
|
||||
startDebug,
|
||||
stopDebug,
|
||||
executeCommand,
|
||||
toggleBreakpoint,
|
||||
continueExecution,
|
||||
stepOver,
|
||||
stepInto,
|
||||
stepOut
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.debugger-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.debugger-toolbar {
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid #dcdfe6;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.debugger-content {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.debugger-code {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
border-right: 1px solid #dcdfe6;
|
||||
}
|
||||
|
||||
.breakpoint {
|
||||
position: absolute;
|
||||
left: 3px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: #f56c6c;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.current-line {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 18px;
|
||||
background-color: rgba(64, 158, 255, 0.1);
|
||||
border-left: 2px solid #409eff;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.debugger-panels {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.console-output {
|
||||
height: calc(100% - 40px);
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
background-color: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.console-line {
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.console-input {
|
||||
padding: 8px;
|
||||
border-top: 1px solid #dcdfe6;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,236 @@
|
|||
<template>
|
||||
<div class="echarts-wrapper">
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="loading-mask">
|
||||
<div class="loading-spinner"></div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref="chartRef"
|
||||
:style="{
|
||||
width: width,
|
||||
height: height,
|
||||
minHeight: '100px'
|
||||
}"
|
||||
class="echarts-container"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
import debounce from 'lodash/debounce'
|
||||
|
||||
export default {
|
||||
name: 'EChartsComponent',
|
||||
props: {
|
||||
// 图表配置项
|
||||
options: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
// 图表主题
|
||||
theme: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 图表宽度
|
||||
width: {
|
||||
type: String,
|
||||
default: '100%'
|
||||
},
|
||||
// 图表高度
|
||||
height: {
|
||||
type: String,
|
||||
default: '400px'
|
||||
},
|
||||
// 是否自动调整大小
|
||||
autoResize: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 加载状态
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 动画时长
|
||||
animationDuration: {
|
||||
type: Number,
|
||||
default: 1000
|
||||
},
|
||||
// 是否平滑曲线(用于折线图)
|
||||
smooth: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 图表类型
|
||||
chartType: {
|
||||
type: String,
|
||||
default: 'line',
|
||||
validator: (value) => ['line', 'bar', 'pie', 'scatter'].includes(value)
|
||||
}
|
||||
},
|
||||
emits: ['chartReady', 'click', 'legendselectchanged'],
|
||||
setup(props, { emit }) {
|
||||
const chartRef = ref(null)
|
||||
let chartInstance = null
|
||||
|
||||
// 处理配置项
|
||||
const processOptions = (options) => {
|
||||
const defaultOptions = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
right: '3%',
|
||||
left: '3%',
|
||||
bottom: '3%',
|
||||
top: '3%',
|
||||
containLabel: true
|
||||
},
|
||||
animation: true,
|
||||
animationDuration: props.animationDuration
|
||||
}
|
||||
|
||||
// 处理折线图的平滑设置
|
||||
if (props.chartType === 'line' && options.series) {
|
||||
options.series = options.series.map(series => ({
|
||||
...series,
|
||||
smooth: props.smooth
|
||||
}))
|
||||
}
|
||||
|
||||
return {
|
||||
...defaultOptions,
|
||||
...options
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化图表
|
||||
const initChart = () => {
|
||||
if (!chartRef.value) return
|
||||
|
||||
chartInstance = echarts.init(chartRef.value, props.theme)
|
||||
|
||||
// 绑定事件
|
||||
chartInstance.on('click', (params) => {
|
||||
emit('click', params)
|
||||
})
|
||||
|
||||
chartInstance.on('legendselectchanged', (params) => {
|
||||
emit('legendselectchanged', params)
|
||||
})
|
||||
|
||||
const processedOptions = processOptions(props.options)
|
||||
chartInstance.setOption(processedOptions)
|
||||
emit('chartReady', chartInstance)
|
||||
}
|
||||
|
||||
// 更新图表
|
||||
const updateChart = () => {
|
||||
if (!chartInstance) return
|
||||
|
||||
const processedOptions = processOptions(props.options)
|
||||
chartInstance.setOption(processedOptions)
|
||||
}
|
||||
|
||||
// 调整图表大小
|
||||
const resizeChart = debounce(() => {
|
||||
if (!chartInstance) return
|
||||
chartInstance.resize()
|
||||
}, 100)
|
||||
|
||||
// 监听配置变化
|
||||
watch(
|
||||
() => props.options,
|
||||
() => updateChart(),
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
// 监听主题变化
|
||||
watch(
|
||||
() => props.theme,
|
||||
() => {
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose()
|
||||
}
|
||||
initChart()
|
||||
}
|
||||
)
|
||||
|
||||
// 监听加载状态
|
||||
watch(
|
||||
() => props.loading,
|
||||
(val) => {
|
||||
if (chartInstance) {
|
||||
val ? chartInstance.showLoading() : chartInstance.hideLoading()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
initChart()
|
||||
if (props.autoResize) {
|
||||
window.addEventListener('resize', resizeChart)
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (props.autoResize) {
|
||||
window.removeEventListener('resize', resizeChart)
|
||||
}
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose()
|
||||
chartInstance = null
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
chartRef
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.echarts-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.echarts-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.loading-mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid #f3f3f3;
|
||||
border-top: 3px solid #3498db;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
|
@ -6,9 +6,7 @@ import ElementPlus from 'element-plus'
|
|||
import 'element-plus/dist/index.css'
|
||||
import './styles/element.scss'
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
// 暂时注释掉这两行,直到我们创建了相应的文件
|
||||
// import 'animate.css'
|
||||
// import './assets/styles/main.scss'
|
||||
import 'vtron/distlib/style.css';
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
|
@ -21,4 +19,4 @@ app.use(createPinia())
|
|||
app.use(router)
|
||||
app.use(ElementPlus)
|
||||
|
||||
app.mount('#app')
|
||||
app.mount('#app')
|
||||
|
|
|
@ -67,7 +67,16 @@ const routes = [
|
|||
path: '/exam/start/:id',
|
||||
component: () => import('../views/paper/exam/exam.vue'),
|
||||
name: 'StartExam',
|
||||
meta: { title: '开始考试' },
|
||||
|
||||
meta: {
|
||||
|
||||
title: '开始考试',
|
||||
|
||||
isExam: true,
|
||||
|
||||
hideNav: true
|
||||
|
||||
},
|
||||
hidden: true
|
||||
},
|
||||
|
||||
|
@ -87,6 +96,11 @@ const routes = [
|
|||
name: 'ActivityCenter',
|
||||
component: () => import('../views/activity/ActivityCenter.vue')
|
||||
},
|
||||
{
|
||||
path: '/test',
|
||||
name: 'TestCenter',
|
||||
component: () => import('../views/activity/TestCenter.vue')
|
||||
},
|
||||
{
|
||||
path: '/mall',
|
||||
name: 'Mall',
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import axios from 'axios'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { useUserStore } from '../stores/user'
|
||||
import qs from 'qs';
|
||||
|
||||
// 创建 axios 实例
|
||||
const service = axios.create({
|
||||
|
@ -23,7 +24,7 @@ service.interceptors.request.use(
|
|||
|
||||
|
||||
|
||||
config.headers['token'] = `eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3NDE3ODAxNDMsInVzZXJuYW1lIjoiYWRtaW4ifQ._jKmdu1T-zpf_qSWRTBtovJ51v2ONC6CGF-60MLJOOE`
|
||||
config.headers['token'] = `eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3NDI0NzI5MzUsInVzZXJuYW1lIjoiYWRtaW4ifQ.-4e5HlHyM9gYlfkaraNdIcPDAeegiPoe3KPRaID9fRY`
|
||||
|
||||
|
||||
// 根据请求方法和数据类型动态设置 Content-Type
|
||||
|
@ -206,6 +207,65 @@ export function del(url, params) {
|
|||
})
|
||||
}
|
||||
|
||||
// 封装 `@RequestParam` 方式的 POST 请求
|
||||
export function postRequest(url, params) {
|
||||
const formData = new FormData();
|
||||
for (const key in params) {
|
||||
console.log(params[key]);
|
||||
formData.append(key, params[key]);
|
||||
}
|
||||
for (let pair of formData.entries()) {
|
||||
console.log(pair[0] + ': ' + pair[1]);
|
||||
}
|
||||
return service({
|
||||
url,
|
||||
method: 'post',
|
||||
data: formData,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export function getRequest(url, params) {
|
||||
const searchParams = new URLSearchParams();
|
||||
for (const key in params) {
|
||||
if (Array.isArray(params[key])) {
|
||||
params[key].forEach(item => {
|
||||
searchParams.append(key, item); // 注意这里直接使用 key,不加 []
|
||||
});
|
||||
} else {
|
||||
searchParams.append(key, params[key]);
|
||||
}
|
||||
}
|
||||
return service({
|
||||
url: `${url}?${searchParams.toString()}`,
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export function getRequestWithqs(url, params) {
|
||||
return service({
|
||||
url,
|
||||
method: 'get',
|
||||
params,
|
||||
paramsSerializer: params => {
|
||||
// 设置 arrayFormat 为 'repeat' 表示 ids=1&ids=2&ids=3
|
||||
// 设置 arrayFormat 为 'comma' 表示 ds=1%2C2%2C3
|
||||
return qs.stringify(params, { arrayFormat: 'repeat' });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export function postRequestJSON(url, params) {
|
||||
return service({
|
||||
url,
|
||||
method: 'post',
|
||||
data: JSON.stringify(params), // 发送 JSON
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
// 封装上传文件的请求
|
||||
export function upload(url, file, onUploadProgress) {
|
||||
const formData = new FormData()
|
||||
|
|
|
@ -170,11 +170,18 @@ const features = [
|
|||
|
||||
const popularCourses = ref([])
|
||||
|
||||
|
||||
|
||||
const getCourses = async () => {
|
||||
var axiosResponse = await getCoursesMethod();
|
||||
|
||||
const params = {
|
||||
page: 1, // 当前页码
|
||||
size: 4, // 每页数量
|
||||
};
|
||||
|
||||
var axiosResponse = await getCoursesMethod(params);
|
||||
console.log(axiosResponse)
|
||||
popularCourses.value = axiosResponse.data
|
||||
|
||||
}
|
||||
|
||||
const viewCourse = (courseId) => {
|
||||
|
|
|
@ -1,11 +1,174 @@
|
|||
<template>
|
||||
<div class="activity-center">
|
||||
<h1 class="page-title">活动大厅</h1>
|
||||
<p>活动大厅功能正在开发中,敬请期待...</p>
|
||||
</div>
|
||||
<el-tree-select
|
||||
v-model="selectedValue"
|
||||
:data="treeData"
|
||||
:props="treeProps"
|
||||
show-checkbox
|
||||
@check="handleCheck"
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
testGetParamMethod,
|
||||
testGetParamMethodList,
|
||||
testGetParamMethodqs,
|
||||
testPostUseBodyMethod,
|
||||
testPostUseParamMethod
|
||||
} from "@/api/test.js";
|
||||
import { ref, reactive, getCurrentInstance, nextTick, onMounted} from 'vue';
|
||||
|
||||
const { proxy } = getCurrentInstance();
|
||||
|
||||
onMounted(() => {
|
||||
console.log(111)
|
||||
// 方式1 post方法 @RequestParam("ids") List<String> ids
|
||||
// 可以
|
||||
// Spring 内部有个转换机制,会自动把逗号分隔的字符串拆分成 List
|
||||
/* testPostUseParamMethod({
|
||||
ids: "122ss,22s2,3ss11aa",
|
||||
str: 1
|
||||
})*/
|
||||
//可以
|
||||
// 当你直接将数组作为一个值传入 FormData 或 URLSearchParams 的append 方法时,
|
||||
// JavaScript 会自动将数组转换为字符串,默认行为是调用数组的 toString() 方法,
|
||||
// 这个方法会将数组元素以逗号分隔拼接成一个字符串。
|
||||
|
||||
//方式二 post方法 @RequestBody 实体内部 List<String> ids;
|
||||
|
||||
//可以
|
||||
// testPostUseParamMethod({
|
||||
// ids: [1, 2, 3],
|
||||
// str: 1
|
||||
// })
|
||||
//可以
|
||||
// testPostUseBodyMethod({
|
||||
// ids: [1, 2, 3],
|
||||
// str: 1
|
||||
// })
|
||||
// 不可以
|
||||
// 在反序列化 JSON 时,试图将一个字符串(例如:"122ss,22s2,3ss11aa")转换成一个 ArrayList 类型,
|
||||
// 但它找不到合适的构造函数或工厂方法来从单个字符串创建一个 List 对象。
|
||||
/* testPostUseBodyMethod({
|
||||
ids: "122ss,22s2,3ss11aa",
|
||||
str: 1
|
||||
})*/
|
||||
// get 方法 @RequestParam("ids") List<String> ids
|
||||
// ids = 122ss,22s2,3ss11a
|
||||
testGetParamMethod({
|
||||
ids: "122ss,22s2,3ss11aa",
|
||||
str: 1
|
||||
})
|
||||
//不行会变成 ids[]=1&ids[]=2&ids[]=3&str=1
|
||||
/* testGetParamMethod({
|
||||
ids: [1, 2, 3],
|
||||
str: 1
|
||||
})*/
|
||||
|
||||
//手动处理
|
||||
//ids=1&ids=2&ids=3
|
||||
testGetParamMethodList({
|
||||
ids: [1, 2, 3],
|
||||
str: 1
|
||||
})
|
||||
|
||||
//ids=1&ids=2&ids=3
|
||||
testGetParamMethodqs({
|
||||
ids: [1, 2, 3],
|
||||
str: 1
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
})
|
||||
|
||||
// 树形数据
|
||||
const treeData = ref([
|
||||
{
|
||||
id: 1,
|
||||
label: '父级1',
|
||||
children: [
|
||||
{ id: 11, label: '子级1-1' },
|
||||
{ id: 12, label: '子级1-2' },
|
||||
{
|
||||
id: 13,
|
||||
label: '子级1-3',
|
||||
children: [
|
||||
{ id: 131, label: '子子级1-3-1' },
|
||||
{ id: 132, label: '子子级1-3-2' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
label: '父级2',
|
||||
children: [
|
||||
{ id: 21, label: '子级2-1' },
|
||||
{ id: 22, label: '子级2-2' },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
// 树形选择的配置属性
|
||||
const treeProps = {
|
||||
value: 'id',
|
||||
label: 'label',
|
||||
children: 'children',
|
||||
};
|
||||
|
||||
// 当前选中的值
|
||||
const selectedValue = ref(null);
|
||||
|
||||
// 处理节点选中事件
|
||||
const handleCheck = (currentNode, checkedNodes) => {
|
||||
// 获取所有选中的节点
|
||||
const checked = checkedNodes.checkedNodes;
|
||||
|
||||
// 用于存储最终的叶子节点id
|
||||
const finalIds = new Set(); // 使用Set避免重复
|
||||
|
||||
checked.forEach(node => {
|
||||
// 判断是否是叶子节点(即没有子节点)
|
||||
if (!node.children || node.children.length === 0) {
|
||||
//在这 可以 node.属性 去个性化判断
|
||||
finalIds.add(node.id);
|
||||
} else {
|
||||
// 如果是父级节点,递归获取其下的叶子节点id
|
||||
const leafIds = getLeafIds(node, new Set());
|
||||
leafIds.forEach(id => finalIds.add(id));
|
||||
}
|
||||
});
|
||||
|
||||
console.log('最终的叶子节点id列表:', Array.from(finalIds));
|
||||
};
|
||||
|
||||
// 递归获取叶子节点id
|
||||
const getLeafIds = (node, visited) => {
|
||||
const ids = new Set();
|
||||
if (node.children) {
|
||||
node.children.forEach(child => {
|
||||
if (!child.children || child.children.length === 0) {
|
||||
//同理这里 //在这 可以 child.属性 去个性化判断
|
||||
// 如果是叶子节点,添加其id
|
||||
ids.add(child.id);
|
||||
} else {
|
||||
// 如果不是叶子节点,继续递归
|
||||
if (!visited.has(child.id)) {
|
||||
visited.add(child.id);
|
||||
const childIds = getLeafIds(child, visited);
|
||||
childIds.forEach(id => ids.add(id));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return ids;
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
@ -13,4 +176,4 @@
|
|||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,462 @@
|
|||
<template>
|
||||
<div class="test-center">
|
||||
<!-- 第一行搜索区域 -->
|
||||
<div class="search-section">
|
||||
<el-tree-select
|
||||
v-model="selectedValue"
|
||||
:data="treeData"
|
||||
:props="treeProps"
|
||||
show-checkbox
|
||||
@check="handleCheck"
|
||||
style="width: 600px"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 第二行操作区域 -->
|
||||
<div class="operation-section">
|
||||
<div class="left-area">
|
||||
<el-select v-model="value" placeholder="Select" style="width: 240px">
|
||||
<el-option
|
||||
v-for="item in options"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
:disabled="item.disabled"
|
||||
/>
|
||||
</el-select>
|
||||
|
||||
<el-tree-select
|
||||
v-model="selectedValue"
|
||||
:data="treeData"
|
||||
:props="treeProps"
|
||||
show-checkbox
|
||||
@check="handleCheck"
|
||||
style="width: 600px"
|
||||
/>
|
||||
</div>
|
||||
<div class="right-area">
|
||||
<el-button type="primary" disabled>操作1</el-button>
|
||||
<el-button type="success" disabled>操作2</el-button>
|
||||
<el-button type="warning" disabled>操作3</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表格区域 -->
|
||||
<div class="table-section">
|
||||
<el-table :data="tableData" style="width: 100%">
|
||||
<el-table-column prop="date" label="Date" width="180" />
|
||||
<el-table-column prop="name" label="Name" width="180" />
|
||||
<el-table-column prop="address" label="Address" />
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 分页区域 -->
|
||||
<div class="pagination-section">
|
||||
<el-pagination
|
||||
background
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="1000"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="chart-demo">
|
||||
<!-- 柱状图示例 -->
|
||||
<ECharts
|
||||
:options="barChartOptions"
|
||||
height="300px"
|
||||
chartType="bar"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import ECharts from '@/components/ECharts/index.vue'
|
||||
|
||||
const value = ref('')
|
||||
const options = [
|
||||
{
|
||||
value: 'Option1',
|
||||
label: 'Option1',
|
||||
},
|
||||
{
|
||||
value: 'Option2',
|
||||
label: 'Option2',
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
value: 'Option3',
|
||||
label: 'Option3',
|
||||
},
|
||||
{
|
||||
value: 'Option4',
|
||||
label: 'Option4',
|
||||
},
|
||||
{
|
||||
value: 'Option5',
|
||||
label: 'Option5',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
|
||||
// 树形数据
|
||||
const treeData = ref([
|
||||
{
|
||||
id: 1,
|
||||
label: '父级1',
|
||||
children: [
|
||||
{ id: 11, label: '子级1-1' },
|
||||
{ id: 12, label: '子级1-2' },
|
||||
{
|
||||
id: 13,
|
||||
label: '子级1-3',
|
||||
children: [
|
||||
{ id: 131, label: '子子级1-3-1' },
|
||||
{ id: 132, label: '子子级1-3-2' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
label: '父级2',
|
||||
children: [
|
||||
{ id: 21, label: '子级2-1' },
|
||||
{ id: 22, label: '子级2-2' },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
|
||||
const tableData = [
|
||||
{
|
||||
date: '2016-05-03',
|
||||
name: 'Tom',
|
||||
address: 'No. 189, Grove St, Los Angeles',
|
||||
},
|
||||
{
|
||||
date: '2016-05-02',
|
||||
name: 'Tom',
|
||||
address: 'No. 189, Grove St, Los Angeles',
|
||||
},
|
||||
{
|
||||
date: '2016-05-04',
|
||||
name: 'Tom',
|
||||
address: 'No. 189, Grove St, Los Angeles',
|
||||
},
|
||||
{
|
||||
date: '2016-05-01',
|
||||
name: 'Tom',
|
||||
address: 'No. 189, Grove St, Los Angeles',
|
||||
},
|
||||
{
|
||||
date: '2016-05-03',
|
||||
name: 'Tom',
|
||||
address: 'No. 189, Grove St, Los Angeles',
|
||||
},
|
||||
{
|
||||
date: '2016-05-02',
|
||||
name: 'Tom',
|
||||
address: 'No. 189, Grove St, Los Angeles',
|
||||
},
|
||||
{
|
||||
date: '2016-05-04',
|
||||
name: 'Tom',
|
||||
address: 'No. 189, Grove St, Los Angeles',
|
||||
},
|
||||
{
|
||||
date: '2016-05-01',
|
||||
name: 'Tom',
|
||||
address: 'No. 189, Grove St, Los Angeles',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
// 树形选择的配置属性
|
||||
const treeProps = {
|
||||
value: 'id',
|
||||
label: 'label',
|
||||
children: 'children',
|
||||
};
|
||||
|
||||
// 当前选中的值
|
||||
const selectedValue = ref(null);
|
||||
|
||||
// 处理节点选中事件
|
||||
const handleCheck = (currentNode, checkedNodes) => {
|
||||
// 获取所有选中的节点
|
||||
const checked = checkedNodes.checkedNodes;
|
||||
|
||||
// 用于存储最终的叶子节点id
|
||||
const finalIds = new Set(); // 使用Set避免重复
|
||||
|
||||
checked.forEach(node => {
|
||||
// 判断是否是叶子节点(即没有子节点)
|
||||
if (!node.children || node.children.length === 0) {
|
||||
finalIds.add(node.id);
|
||||
} else {
|
||||
// 如果是父级节点,递归获取其下的叶子节点id
|
||||
const leafIds = getLeafIds(node, new Set());
|
||||
leafIds.forEach(id => finalIds.add(id));
|
||||
}
|
||||
});
|
||||
|
||||
console.log('最终的叶子节点id列表:', Array.from(finalIds));
|
||||
};
|
||||
|
||||
// 递归获取叶子节点id
|
||||
const getLeafIds = (node, visited) => {
|
||||
const ids = new Set();
|
||||
if (node.children) {
|
||||
node.children.forEach(child => {
|
||||
if (!child.children || child.children.length === 0) {
|
||||
// 如果是叶子节点,添加其id
|
||||
ids.add(child.id);
|
||||
} else {
|
||||
// 如果不是叶子节点,继续递归
|
||||
if (!visited.has(child.id)) {
|
||||
visited.add(child.id);
|
||||
const childIds = getLeafIds(child, visited);
|
||||
childIds.forEach(id => ids.add(id));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return ids;
|
||||
};
|
||||
|
||||
// 定义图表数据结构
|
||||
const chartData = ref({
|
||||
categories: ['手机', '电脑', '平板', '耳机', '手表'],
|
||||
series: [
|
||||
{
|
||||
name: '上月销量',
|
||||
type: 'bar',
|
||||
data: [120, 200, 150, 80, 70],
|
||||
color: '#409EFF',
|
||||
showLabel: true,
|
||||
labelFormatter: '{c}台'
|
||||
},
|
||||
{
|
||||
name: '本月销量',
|
||||
type: 'bar',
|
||||
data: [140, 180, 180, 120, 90],
|
||||
color: '#67C23A',
|
||||
showLabel: true,
|
||||
labelFormatter: '{c}台'
|
||||
},
|
||||
{
|
||||
name: '环比增长',
|
||||
type: 'line',
|
||||
yAxisIndex: 1,
|
||||
data: [16.7, -10, 20, 50, 28.6],
|
||||
color: '#E6A23C',
|
||||
showLabel: true,
|
||||
labelFormatter: '{c}%'
|
||||
}
|
||||
],
|
||||
yAxis: [
|
||||
{
|
||||
name: '销量',
|
||||
min: 0,
|
||||
nameTextStyle: {
|
||||
padding: [0, 0, 0, 50]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '增长率',
|
||||
min: 0,
|
||||
max: 100,
|
||||
axisLabel: '{value}%',
|
||||
nameTextStyle: {
|
||||
padding: [0, 0, 0, 50]
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// 生成图表配置的函数
|
||||
const generateChartOptions = (chartData) => {
|
||||
const barSeries = chartData.series.filter(item => item.type === 'bar')
|
||||
const lineSeries = chartData.series.filter(item => item.type === 'line')
|
||||
|
||||
return {
|
||||
title: {
|
||||
text: '各类商品销量对比'
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross'
|
||||
},
|
||||
formatter: function(params) {
|
||||
let result = `${params[0].axisValue}<br/>`;
|
||||
// 计算同比增长
|
||||
const currentValue = params.find(p => p.seriesName === '本月销量')?.value || 0;
|
||||
const lastValue = params.find(p => p.seriesName === '上月销量')?.value || 0;
|
||||
const growth = params.find(p => p.seriesName === '环比增长')?.value || 0;
|
||||
|
||||
// 添加所有系列的数据
|
||||
params.forEach(param => {
|
||||
const marker = param.marker;
|
||||
const seriesName = param.seriesName;
|
||||
const value = param.value;
|
||||
const unit = param.seriesName.includes('增长') ? '%' : '台';
|
||||
result += `${marker}${seriesName}: ${value}${unit}<br/>`;
|
||||
});
|
||||
|
||||
// 添加额外统计信息
|
||||
if (currentValue && lastValue) {
|
||||
const diff = currentValue - lastValue;
|
||||
result += '<br/>';
|
||||
result += `<span style="color: #666">销量差额: ${diff > 0 ? '+' : ''}${diff}台</span><br/>`;
|
||||
result += `<span style="color: ${growth >= 0 ? '#67C23A' : '#F56C6C'}">环比: ${growth}%</span>`;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: chartData.series.map(item => item.name),
|
||||
selected: chartData.series.reduce((acc, item) => {
|
||||
acc[item.name] = true;
|
||||
return acc;
|
||||
}, {})
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: chartData.categories,
|
||||
axisLabel: {
|
||||
interval: 0,
|
||||
rotate: chartData.categories.length > 5 ? 30 : 0
|
||||
}
|
||||
},
|
||||
yAxis: chartData.yAxis.map((axis, index) => ({
|
||||
type: 'value',
|
||||
name: axis.name,
|
||||
min: axis.min,
|
||||
max: axis.max,
|
||||
position: index === 0 ? 'left' : 'right',
|
||||
splitLine: {
|
||||
show: index === 0
|
||||
},
|
||||
axisLabel: axis.axisLabel ? {
|
||||
formatter: axis.axisLabel
|
||||
} : undefined
|
||||
})),
|
||||
series: chartData.series.map(item => ({
|
||||
name: item.name,
|
||||
type: item.type,
|
||||
yAxisIndex: item.yAxisIndex || 0,
|
||||
data: item.data,
|
||||
barMaxWidth: 50, // 限制柱子最大宽度
|
||||
barGap: '30%', // 柱子之间的间距
|
||||
itemStyle: {
|
||||
color: item.color,
|
||||
borderRadius: item.type === 'bar' ? [4, 4, 0, 0] : 0
|
||||
},
|
||||
label: item.showLabel ? {
|
||||
show: true,
|
||||
position: item.type === 'line' ? 'top' : 'inside',
|
||||
formatter: item.labelFormatter,
|
||||
fontSize: 12,
|
||||
color: item.type === 'line' ? item.color : '#fff'
|
||||
} : undefined,
|
||||
emphasis: {
|
||||
focus: 'series',
|
||||
label: {
|
||||
show: true,
|
||||
position: item.type === 'line' ? 'top' : 'inside',
|
||||
formatter: item.labelFormatter || '{c}',
|
||||
fontSize: 12,
|
||||
color: item.type === 'line' ? item.color : '#fff'
|
||||
}
|
||||
}
|
||||
})),
|
||||
grid: {
|
||||
top: '15%',
|
||||
bottom: '15%',
|
||||
left: '10%',
|
||||
right: '10%',
|
||||
containLabel: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 生成图表配置
|
||||
const barChartOptions = computed(() => generateChartOptions(chartData.value))
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.test-center {
|
||||
padding: 16px;
|
||||
background-color: #f5f7fa;
|
||||
min-height: 100vh;
|
||||
|
||||
.search-section {
|
||||
background-color: #fff;
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.operation-section {
|
||||
background-color: #fff;
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.left-area {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.right-area {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.table-section {
|
||||
background-color: #fff;
|
||||
padding: 12px 16px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
|
||||
margin-bottom: 12px;
|
||||
|
||||
:deep(.el-table) {
|
||||
// 移除表格的外边框
|
||||
--el-table-border: none;
|
||||
// 减小表格行的上下padding
|
||||
--el-table-row-height: 45px;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-section {
|
||||
background-color: #fff;
|
||||
padding: 12px 16px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.chart-demo {
|
||||
background-color: #fff;
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -7,10 +7,9 @@
|
|||
|
||||
<script setup>
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.announcement {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
@ -204,7 +204,7 @@ const courses = ref([
|
|||
id: 1,
|
||||
title: 'C++面向对象编程精讲',
|
||||
description: '深入理解C++面向对象编程思想,掌握核心概念和实践技巧',
|
||||
coverImg: '/src/assets/images/course-covers/cpp.jpg',
|
||||
coverImg: 'http://localhost:9000/file/c6d07740-7306-4fc1-b1f8-670684a73ed9/img/31c258e7-3fd4-402c-8f2f-c9455c6cfdaf.png',
|
||||
teacher: '张教授',
|
||||
studentCount: 1234,
|
||||
rating: 4.8,
|
||||
|
|
|
@ -252,7 +252,7 @@ const courseData = ref({
|
|||
docCount: 5,
|
||||
totalDuration: '24小时',
|
||||
enrolled: false,
|
||||
coverImg: 'http://example.com/course-cover.jpg',
|
||||
coverImg: 'http://localhost:9000/file/c6d07740-7306-4fc1-b1f8-670684a73ed9/img/31c258e7-3fd4-402c-8f2f-c9455c6cfdaf.png',
|
||||
chapters: [
|
||||
{
|
||||
id: 1,
|
||||
|
|
|
@ -1,16 +1,410 @@
|
|||
<template>
|
||||
<div class="mall">
|
||||
<h1 class="page-title">商城</h1>
|
||||
<p>商城功能正在开发中,敬请期待...</p>
|
||||
<div class="challenge-container">
|
||||
<div class="challenge-header">
|
||||
<div class="title-section">
|
||||
<h2>{{ challenge.title }}</h2>
|
||||
<div class="difficulty easy">简单</div>
|
||||
</div>
|
||||
<div class="challenge-info">
|
||||
<span class="info-tag">
|
||||
<i class="fas fa-memory"></i>
|
||||
内存限制:{{ challenge.memoryLimit }} MB
|
||||
</span>
|
||||
<span class="info-tag">
|
||||
<i class="fas fa-code"></i>
|
||||
{{ challenge.language }}
|
||||
</span>
|
||||
<span class="info-tag">
|
||||
<i class="fas fa-clock"></i>
|
||||
时间限制:1000ms
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="challenge-content">
|
||||
<div class="test-cases-section">
|
||||
<div class="section-header">
|
||||
<h3><i class="fas fa-vial"></i> 测试用例</h3>
|
||||
<span class="case-count">{{ challenge.testCases.length }} 个用例</span>
|
||||
</div>
|
||||
<div class="test-cases">
|
||||
<div v-for="(test, index) in challenge.testCases"
|
||||
:key="index"
|
||||
class="test-case-card">
|
||||
<div class="test-case-header">
|
||||
<span class="case-number">#{{ index + 1 }}</span>
|
||||
<span class="case-status success">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div class="test-case-content">
|
||||
<div class="test-input">
|
||||
<div class="label">输入:</div>
|
||||
<code>{{ test.input }}</code>
|
||||
</div>
|
||||
<div class="test-output">
|
||||
<div class="label">期望输出:</div>
|
||||
<code>{{ test.expectedOutput }}</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="editor-section">
|
||||
<div class="editor-header">
|
||||
<div class="editor-tabs">
|
||||
<div class="tab active">
|
||||
<i class="fas fa-code"></i>
|
||||
代码编辑器
|
||||
</div>
|
||||
</div>
|
||||
<div class="editor-actions">
|
||||
<button class="action-btn">
|
||||
<i class="fas fa-redo"></i>
|
||||
重置
|
||||
</button>
|
||||
<button class="submit-btn" @click="submitCode">
|
||||
<i class="fas fa-paper-plane"></i>
|
||||
提交代码
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CodeEditor
|
||||
v-model="userCode"
|
||||
:language="challenge.language"
|
||||
class="code-editor-wrapper"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="result" class="result-section">
|
||||
<div class="section-header">
|
||||
<h3><i class="fas fa-poll"></i> 执行结果</h3>
|
||||
</div>
|
||||
<pre class="result-content">{{ result }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
<script>
|
||||
import { ref } from "vue";
|
||||
import CodeEditor from "@/components/CodeEditor/CodeEditor.vue";
|
||||
|
||||
export default {
|
||||
components: { CodeEditor },
|
||||
setup() {
|
||||
const challenge = {
|
||||
title: "两数之和",
|
||||
memoryLimit: 256,
|
||||
language: "javascript",
|
||||
testCases: [
|
||||
{ input: "[2,7,11,15], 9", expectedOutput: "[0,1]" },
|
||||
{ input: "[3,2,4], 6", expectedOutput: "[1,2]" },
|
||||
{ input: "[3,3], 6", expectedOutput: "[0,1]" },
|
||||
],
|
||||
};
|
||||
|
||||
const userCode = ref('// 在这里编写你的代码\nfunction twoSum(nums, target) {\n return [];\n}');
|
||||
|
||||
const result = ref(null);
|
||||
|
||||
const submitCode = () => {
|
||||
result.value = {
|
||||
code: userCode.value,
|
||||
challenge,
|
||||
};
|
||||
console.log("提交代码:", result.value);
|
||||
};
|
||||
|
||||
return { challenge, userCode, submitCode, result };
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mall {
|
||||
<style scoped>
|
||||
.challenge-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
margin: 20px auto;
|
||||
padding: 20px;
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
</style>
|
||||
|
||||
.challenge-header {
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid #eef2f7;
|
||||
}
|
||||
|
||||
.title-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.challenge-header h2 {
|
||||
margin: 0;
|
||||
color: #1a202c;
|
||||
font-size: 26px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.difficulty {
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.difficulty.easy {
|
||||
background: #e6f6e6;
|
||||
color: #2f9e44;
|
||||
}
|
||||
|
||||
.challenge-info {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.info-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 8px 16px;
|
||||
background: #f8fafc;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
color: #64748b;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.info-tag:hover {
|
||||
background: #f1f5f9;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.info-tag i {
|
||||
margin-right: 8px;
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.challenge-content {
|
||||
display: grid;
|
||||
grid-template-columns: 320px 1fr;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.section-header h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
color: #1a202c;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.section-header h3 i {
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.case-count {
|
||||
font-size: 14px;
|
||||
color: #64748b;
|
||||
background: #f1f5f9;
|
||||
padding: 4px 12px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.test-cases {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.test-case-card {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.test-case-card:hover {
|
||||
border-color: #cbd5e1;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.test-case-header {
|
||||
padding: 12px 16px;
|
||||
background: #f8fafc;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.case-number {
|
||||
font-weight: 500;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.case-status {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.case-status.success i {
|
||||
color: #2f9e44;
|
||||
}
|
||||
|
||||
.test-case-content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.test-input, .test-output {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-weight: 500;
|
||||
color: #475569;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
code {
|
||||
display: block;
|
||||
background: #f8fafc;
|
||||
padding: 8px 12px;
|
||||
border-radius: 6px;
|
||||
font-family: 'Fira Code', monospace;
|
||||
font-size: 13px;
|
||||
color: #334155;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.editor-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.editor-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.editor-tabs {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
color: #64748b;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
background: #f1f5f9;
|
||||
color: #3b82f6;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.editor-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 8px 16px;
|
||||
border: 1px solid #e2e8f0;
|
||||
background: white;
|
||||
border-radius: 6px;
|
||||
color: #64748b;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
border-color: #cbd5e1;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
padding: 8px 20px;
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.submit-btn:hover {
|
||||
background: #2563eb;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.code-editor-wrapper {
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #e2e8f0;
|
||||
flex-grow: 1;
|
||||
height: 600px;
|
||||
}
|
||||
|
||||
.result-section {
|
||||
grid-column: 1 / -1;
|
||||
background: #f8fafc;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.result-content {
|
||||
margin: 0;
|
||||
padding: 16px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e2e8f0;
|
||||
font-family: 'Fira Code', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.challenge-content {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.test-cases-section, .editor-section {
|
||||
grid-column: 1;
|
||||
}
|
||||
|
||||
.info-tag {
|
||||
padding: 6px 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
<template>
|
||||
<div class="goods-detail">
|
||||
<NavBar :active="'/shop'"></NavBar>
|
||||
|
||||
<el-card class="detail-card">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<img :src="goods.image" class="goods-image">
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<h1>{{ goods.name }}</h1>
|
||||
<div class="goods-price">{{ goods.points }} 积分</div>
|
||||
<div class="goods-desc">{{ goods.description }}</div>
|
||||
<div class="goods-action">
|
||||
<el-button type="primary" size="large" @click="handleExchange">
|
||||
立即兑换
|
||||
</el-button>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,203 @@
|
|||
<template>
|
||||
<div class="shop-container">
|
||||
<NavBar :active="'/shop'"></NavBar>
|
||||
|
||||
<!-- 积分余额显示 -->
|
||||
<div class="points-balance">
|
||||
<el-card>
|
||||
<div class="balance-content">
|
||||
<i class="el-icon-coin"></i>
|
||||
<span class="points">当前积分: {{ userPoints }}</span>
|
||||
<el-button type="primary" size="small" @click="showPointsHistory">
|
||||
积分明细
|
||||
</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 商品列表 -->
|
||||
<div class="goods-list">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="6" v-for="item in goodsList" :key="item.id">
|
||||
<el-card :body-style="{ padding: '0px' }" class="goods-card">
|
||||
<img :src="item.image" class="goods-image">
|
||||
<div class="goods-info">
|
||||
<h3>{{ item.name }}</h3>
|
||||
<div class="price-info">
|
||||
<span class="points-price">{{ item.points }} 积分</span>
|
||||
<el-button type="primary" size="small" @click="handleExchange(item)">
|
||||
立即兑换
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 积分明细对话框 -->
|
||||
<el-dialog
|
||||
title="积分明细"
|
||||
v-model="pointsHistoryVisible"
|
||||
width="60%">
|
||||
<el-table :data="pointsHistory">
|
||||
<el-table-column prop="time" label="时间" width="180"/>
|
||||
<el-table-column prop="type" label="类型" width="120"/>
|
||||
<el-table-column prop="points" label="积分变动"/>
|
||||
<el-table-column prop="description" label="说明"/>
|
||||
</el-table>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import NavBar from '@/components/oj/common/NavBar.vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
export default {
|
||||
name: 'Shop',
|
||||
components: {
|
||||
NavBar
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
userPoints: 1000, // 用户积分余额
|
||||
pointsHistoryVisible: false,
|
||||
goodsList: [
|
||||
{
|
||||
id: 1,
|
||||
name: '力扣会员1个月',
|
||||
points: 500,
|
||||
image: 'https://example.com/leetcode.png',
|
||||
description: '力扣会员1个月使用权限'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '编程书籍优惠券',
|
||||
points: 300,
|
||||
image: 'https://example.com/book.png',
|
||||
description: '任意编程书籍立减30元'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'IDE专业版',
|
||||
points: 800,
|
||||
image: 'https://example.com/ide.png',
|
||||
description: 'JetBrains IDE 1个月使用授权'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '在线课程优惠券',
|
||||
points: 400,
|
||||
image: 'https://example.com/course.png',
|
||||
description: '指定在线课程优惠50元'
|
||||
}
|
||||
],
|
||||
pointsHistory: [
|
||||
{
|
||||
time: '2024-03-20 10:00:00',
|
||||
type: '题目通过',
|
||||
points: '+10',
|
||||
description: '完成题目【两数之和】'
|
||||
},
|
||||
{
|
||||
time: '2024-03-19 15:30:00',
|
||||
type: '商品兑换',
|
||||
points: '-300',
|
||||
description: '兑换【编程书籍优惠券】'
|
||||
},
|
||||
{
|
||||
time: '2024-03-18 09:15:00',
|
||||
type: '每日登录',
|
||||
points: '+5',
|
||||
description: '每日登录奖励'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showPointsHistory() {
|
||||
this.pointsHistoryVisible = true
|
||||
},
|
||||
handleExchange(item) {
|
||||
if (this.userPoints < item.points) {
|
||||
ElMessage.warning('积分不足!')
|
||||
return
|
||||
}
|
||||
|
||||
ElMessage.success('兑换成功!')
|
||||
this.userPoints -= item.points
|
||||
// TODO: 调用后端API完成兑换
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.shop-container {
|
||||
padding: 20px;
|
||||
margin-top: 60px;
|
||||
}
|
||||
|
||||
.points-balance {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.balance-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.points {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #409EFF;
|
||||
}
|
||||
|
||||
.goods-list {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.goods-card {
|
||||
margin-bottom: 20px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.goods-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);
|
||||
}
|
||||
|
||||
.goods-image {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.goods-info {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.goods-info h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.price-info {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.points-price {
|
||||
color: #F56C6C;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
File diff suppressed because it is too large
Load Diff
|
@ -139,7 +139,7 @@ export default {
|
|||
// 可批量操作
|
||||
multi: false,
|
||||
// 列表请求URL
|
||||
listUrl: '/exam/api/exam/exam/online-paging'
|
||||
listUrl: '/api/common/exam/api/exam/exam/online-paging'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue