Compare commits

...

2 Commits

Author SHA1 Message Date
Guwan 431888797d 稳定提交 2025-03-26 21:02:36 +08:00
Guwan 3d11c02067 稳定提交 2025-03-21 00:07:42 +08:00
24 changed files with 3332 additions and 161 deletions

View File

@ -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"
}
}

View File

@ -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>

View File

@ -6,6 +6,7 @@ export function getCourses(params) {
}
export function getCoursesMethod(params) {
console.log(params)
return get('/bs/courses', params)
}

View File

@ -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 })
}
/**

21
src/api/shop/index.js Normal file
View File

@ -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 })
}

25
src/api/test.js Normal file
View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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')

View File

@ -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',

View File

@ -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()

View File

@ -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) => {

View File

@ -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>

View File

@ -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>

View File

@ -7,10 +7,9 @@
<script setup>
</script>
<style lang="scss" scoped>
.announcement {
max-width: 1200px;
margin: 0 auto;
}
</style>
</style>

View File

@ -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,

View File

@ -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,

View File

@ -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>

View File

@ -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>

203
src/views/oj/shop/Shop.vue Normal file
View File

@ -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

View File

@ -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'
}
}
},