feat: Enhance exam management features and add sensitive word filtering
- Implement global sensitive word filtering in HitokotoCard component. - Add a test button for Jinrishici API in HitokotoSettings component. - Introduce ConciseExamCard and ExamScheduleCard components for better exam display. - Add functionality to create and manage exam configurations in examschedule.vue. - Implement upcoming exam notifications in the main index page. - Create a new exam store for managing exam data and fetching exam details. - Add RelativeTimeDisplay component for displaying relative time in a user-friendly format.
This commit is contained in:
+856
-128
File diff suppressed because it is too large
Load Diff
@@ -24,6 +24,19 @@
|
||||
import { SettingsManager, watchSettings } from '@/utils/settings'
|
||||
import dataProvider from '@/utils/dataProvider'
|
||||
import axios from 'axios'
|
||||
import { Base64 } from 'js-base64'
|
||||
|
||||
// 全局敏感词列表,强制生效。
|
||||
const GLOBAL_SENSITIVE_WORDS_ENCODED = [
|
||||
'6IO4',
|
||||
'5Lmz',
|
||||
'6JCd6I6J',
|
||||
'5rer',
|
||||
'5aW4',
|
||||
]
|
||||
|
||||
// 解码敏感词列表
|
||||
const GLOBAL_SENSITIVE_WORDS = GLOBAL_SENSITIVE_WORDS_ENCODED.map(word => Base64.decode(word))
|
||||
|
||||
export default {
|
||||
name: 'HitokotoCard',
|
||||
@@ -123,10 +136,7 @@ export default {
|
||||
}
|
||||
} else if (source === 'jinrishici') {
|
||||
if (this.kvConfig.jinrishiciToken) {
|
||||
const res = await axios.get('https://v2.jinrishici.com/sentence', {
|
||||
headers: {
|
||||
'X-User-Token': this.kvConfig.jinrishiciToken
|
||||
}
|
||||
const res = await axios.get('https://v2.jinrishici.com/one.json?client=npm-sdk/1.0&X-User-Token='+encodeURIComponent(this.kvConfig.jinrishiciToken), {
|
||||
})
|
||||
if (res.data.status === 'success') {
|
||||
data = res.data.data
|
||||
@@ -145,8 +155,9 @@ export default {
|
||||
}
|
||||
|
||||
if (content) {
|
||||
// Sensitive word check
|
||||
const hasSensitiveWord = this.kvConfig.sensitiveWords.some(word => content.includes(word))
|
||||
// Sensitive word check (global + KV)
|
||||
const combinedWords = [...GLOBAL_SENSITIVE_WORDS, ...this.kvConfig.sensitiveWords]
|
||||
const hasSensitiveWord = combinedWords.some(word => word && content.includes(word))
|
||||
if (hasSensitiveWord) {
|
||||
// Retry
|
||||
this.loading = false
|
||||
|
||||
@@ -61,6 +61,29 @@
|
||||
/>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item v-if="kvConfig.sources.includes('jinrishici')">
|
||||
<div class="d-flex flex-column flex-sm-row align-center w-100">
|
||||
<v-btn
|
||||
:loading="testLoading"
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
class="mr-sm-4 mb-2 mb-sm-0"
|
||||
@click="testJinrishici"
|
||||
>
|
||||
测试今日诗词接口
|
||||
</v-btn>
|
||||
<v-alert
|
||||
v-if="testMessage"
|
||||
:type="testColor"
|
||||
density="comfortable"
|
||||
border="start"
|
||||
class="flex-grow-1"
|
||||
>
|
||||
{{ testMessage }}
|
||||
</v-alert>
|
||||
</div>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item>
|
||||
<v-textarea
|
||||
v-model="kvConfig.sensitiveWords"
|
||||
@@ -74,12 +97,167 @@
|
||||
@change="saveKvSettings"
|
||||
/>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-checkbox
|
||||
label="启用云端敏感词列表"
|
||||
hide-details
|
||||
v-model="enableCloudSensitiveWords"
|
||||
density="compact"
|
||||
disabled
|
||||
class="mb-2"
|
||||
/>
|
||||
<div class="text-caption text-grey">
|
||||
已启用的数据源将在获取一言时随机尝试,直到成功获取内容为止。<br/>
|
||||
敏感词过滤会将包含任意敏感词的句子过滤掉,避免显示不当内容。<br/>
|
||||
</div>
|
||||
</v-list-item>
|
||||
|
||||
<div v-if="loading" class="text-center pb-4">
|
||||
<v-progress-circular indeterminate size="24" color="primary" />
|
||||
<span class="ml-2 text-caption">正在同步配置...</span>
|
||||
</div>
|
||||
</setting-group>
|
||||
|
||||
<v-dialog v-model="testResultDialog" max-width="600">
|
||||
<v-card v-if="testResultData" class="rounded-lg">
|
||||
|
||||
|
||||
<v-card-text class="pa-0">
|
||||
<v-list lines="two" class="py-0">
|
||||
<v-list-item class="px-4 py-3">
|
||||
<template v-slot:prepend>
|
||||
<v-avatar color="primary" variant="tonal" class="mr-2">
|
||||
<v-icon icon="mdi-key-variant" />
|
||||
</v-avatar>
|
||||
</template>
|
||||
<v-list-item-title class="text-subtitle-2 font-weight-bold mb-1">Token</v-list-item-title>
|
||||
<v-list-item-subtitle class="text-body-2 text-high-emphasis" style="word-break: break-all;">
|
||||
{{ testResultData.data.token }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-row no-gutters>
|
||||
<v-col cols="6">
|
||||
<v-list-item class="px-4 py-2">
|
||||
<template v-slot:prepend>
|
||||
<v-icon icon="mdi-ip-network" color="grey-darken-1" class="mr-2" />
|
||||
</template>
|
||||
<v-list-item-title class="text-caption text-grey-darken-1">IP 地址</v-list-item-title>
|
||||
<v-list-item-subtitle class="text-body-2">{{ testResultData.data.ip }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<v-list-item class="px-4 py-2">
|
||||
<template v-slot:prepend>
|
||||
<v-icon icon="mdi-map-marker-radius" color="grey-darken-1" class="mr-2" />
|
||||
</template>
|
||||
<v-list-item-title class="text-caption text-grey-darken-1">地区</v-list-item-title>
|
||||
<v-list-item-subtitle class="text-body-2">{{ testResultData.data.region }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-divider />
|
||||
|
||||
|
||||
|
||||
<v-container class="px-4 py-3">
|
||||
<v-row dense>
|
||||
<v-col cols="6" sm="4">
|
||||
<div class="d-flex align-center mb-2">
|
||||
<v-icon icon="mdi-thermometer" color="orange" class="mr-2" />
|
||||
<div>
|
||||
<div class="text-caption text-grey">温度</div>
|
||||
<div class="text-body-1 font-weight-medium">{{ testResultData.data.weatherData.temperature }}°C</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="6" sm="4">
|
||||
<div class="d-flex align-center mb-2">
|
||||
<v-icon icon="mdi-weather-cloudy" color="blue-grey" class="mr-2" />
|
||||
<div>
|
||||
<div class="text-caption text-grey">天气</div>
|
||||
<div class="text-body-1 font-weight-medium">{{ testResultData.data.weatherData.weather }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="6" sm="4">
|
||||
<div class="d-flex align-center mb-2">
|
||||
<v-icon icon="mdi-water-percent" color="blue" class="mr-2" />
|
||||
<div>
|
||||
<div class="text-caption text-grey">湿度</div>
|
||||
<div class="text-body-1 font-weight-medium">{{ testResultData.data.weatherData.humidity }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="6" sm="4">
|
||||
<div class="d-flex align-center mb-2">
|
||||
<v-icon icon="mdi-weather-windy" color="teal" class="mr-2" />
|
||||
<div>
|
||||
<div class="text-caption text-grey">风向/风力</div>
|
||||
<div class="text-body-2 font-weight-medium">
|
||||
{{ testResultData.data.weatherData.windDirection }} {{ testResultData.data.weatherData.windPower }}级
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="6" sm="4">
|
||||
<div class="d-flex align-center mb-2">
|
||||
<v-icon icon="mdi-blur" color="grey" class="mr-2" />
|
||||
<div>
|
||||
<div class="text-caption text-grey">PM2.5</div>
|
||||
<div class="text-body-1 font-weight-medium">{{ testResultData.data.weatherData.pm25 }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="6" sm="4">
|
||||
<div class="d-flex align-center mb-2">
|
||||
<v-icon icon="mdi-eye" color="indigo" class="mr-2" />
|
||||
<div>
|
||||
<div class="text-caption text-grey">能见度</div>
|
||||
<div class="text-body-1 font-weight-medium">{{ testResultData.data.weatherData.visibility }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<div class="px-4 py-3">
|
||||
<div class="text-caption text-grey mb-2">环境标签</div>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<v-chip
|
||||
v-for="tag in testResultData.data.tags"
|
||||
:key="tag"
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
class="mr-1 mb-1"
|
||||
>
|
||||
{{ tag }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-list-item class="px-4 py-2">
|
||||
<template v-slot:prepend>
|
||||
<v-icon icon="mdi-clock-outline" size="small" class="mr-2" />
|
||||
</template>
|
||||
<v-list-item-title class="text-caption text-grey-darken-1">
|
||||
北京时间: {{ new Date(testResultData.data.beijingTime).toLocaleString() }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
|
||||
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -102,7 +280,13 @@ export default {
|
||||
sensitiveWords: '',
|
||||
jinrishiciToken: null
|
||||
},
|
||||
loading: false
|
||||
loading: false,
|
||||
testLoading: false,
|
||||
testMessage: '',
|
||||
testColor: 'info',
|
||||
testResultDialog: false,
|
||||
testResultData: null,
|
||||
enableCloudSensitiveWords: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@@ -152,6 +336,39 @@ export default {
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
async testJinrishici() {
|
||||
this.testLoading = true
|
||||
this.testMessage = ''
|
||||
this.testColor = 'info'
|
||||
try {
|
||||
const headers = {}
|
||||
if (this.kvConfig.jinrishiciToken) {
|
||||
headers['X-User-Token'] = this.kvConfig.jinrishiciToken
|
||||
}
|
||||
const res = await axios.get('https://v2.jinrishici.com/info?X-User-Token='+encodeURIComponent(this.kvConfig.jinrishiciToken))
|
||||
if (res.data && res.data.status === 'success') {
|
||||
this.testResultData = res.data
|
||||
this.testResultDialog = true
|
||||
|
||||
const token = res.data.data?.token
|
||||
const region = res.data.data?.region
|
||||
const consistent = this.kvConfig.jinrishiciToken ? token === this.kvConfig.jinrishiciToken : true
|
||||
this.testColor = consistent ? 'success' : 'warning'
|
||||
this.testMessage = consistent
|
||||
? `接口正常,Token 一致:${token}${region ? `,地区:${region}` : ''}`
|
||||
: `接口返回 Token 与当前设置不一致:${token}${region ? `,地区:${region}` : ''}`
|
||||
} else {
|
||||
this.testColor = 'error'
|
||||
this.testMessage = '接口返回非 success,请检查网络或 Token 配置。'
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to test jinrishici info', e)
|
||||
this.testColor = 'error'
|
||||
this.testMessage = '接口测试失败,请检查网络或 Token。'
|
||||
} finally {
|
||||
this.testLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<span>{{ displayTime }}</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'RelativeTimeDisplay',
|
||||
props: {
|
||||
time: {
|
||||
type: [String, Date, Number],
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
displayTime() {
|
||||
if (!this.time) return ''
|
||||
const date = new Date(this.time)
|
||||
const now = new Date()
|
||||
|
||||
// Reset hours to compare dates only
|
||||
const d = new Date(date.getFullYear(), date.getMonth(), date.getDate())
|
||||
const n = new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
||||
|
||||
const diffTime = d.getTime() - n.getTime()
|
||||
const diffDays = Math.round(diffTime / (1000 * 60 * 60 * 24))
|
||||
|
||||
if (diffDays === 0) return '今天'
|
||||
if (diffDays === 1) return '明天'
|
||||
if (diffDays === 2) return '后天'
|
||||
if (diffDays === -1) return '昨天'
|
||||
if (diffDays === -2) return '前天'
|
||||
|
||||
// Check if in same week (assuming Monday start)
|
||||
const nDay = n.getDay() || 7 // 1-7
|
||||
|
||||
// Start of current week for n
|
||||
const nStartOfWeek = new Date(n)
|
||||
nStartOfWeek.setDate(n.getDate() - nDay + 1)
|
||||
|
||||
// End of current week for n
|
||||
const nEndOfWeek = new Date(n)
|
||||
nEndOfWeek.setDate(n.getDate() + (7 - nDay))
|
||||
|
||||
if (d >= nStartOfWeek && d <= nEndOfWeek) {
|
||||
const weekDays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
|
||||
return weekDays[date.getDay()]
|
||||
}
|
||||
|
||||
// Default format M月D日
|
||||
const month = date.getMonth() + 1
|
||||
const day = date.getDate()
|
||||
return `${month}月${day}日`
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,161 @@
|
||||
<template>
|
||||
<v-card
|
||||
border
|
||||
class="fill-height d-flex flex-column cursor-pointer hover-elevation"
|
||||
elevation="0"
|
||||
@click="$emit('click')"
|
||||
>
|
||||
<v-card-title
|
||||
class="d-flex align-center py-2 px-3 bg-primary-lighten-5 text-subtitle-1 font-weight-bold"
|
||||
>
|
||||
<span class="text-truncate">{{ exam?.examName || "加载中..." }}</span>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="flex-grow-1 pa-4 overflow-y-auto" :style="contentStyle">
|
||||
<div v-if="loading" class="d-flex justify-center align-center py-4">
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
size="24"
|
||||
color="primary"
|
||||
></v-progress-circular>
|
||||
</div>
|
||||
|
||||
<template v-else-if="exam">
|
||||
<!--<div v-if="exam.message" class="text-caption text-grey mb-2 px-1">
|
||||
{{ exam.message }}
|
||||
</div>-->
|
||||
|
||||
<div class="d-flex flex-column">
|
||||
<div v-for="(group, gIndex) in groupedExamInfos" :key="gIndex" class="mb-3">
|
||||
<div class="text-subtitle-2 font-weight-bold text-primary mb-1">
|
||||
<RelativeTimeDisplay :time="group.date" />
|
||||
</div>
|
||||
<div
|
||||
v-for="(info, index) in group.infos"
|
||||
:key="index"
|
||||
class="d-flex align-center justify-space-between py-1 border-b-sm"
|
||||
:class="{
|
||||
'border-none': index === group.infos.length - 1,
|
||||
'text-grey': isPast(info.end),
|
||||
}"
|
||||
>
|
||||
<div class="font-weight-bold mr-2" style="font-size: 1.1em">
|
||||
{{ info.name }}
|
||||
</div>
|
||||
<div
|
||||
class="font-weight-medium text-grey-darken-2"
|
||||
style="font-size: 0.85em"
|
||||
>
|
||||
{{ formatTimeOnly(info.start) }} -
|
||||
{{ formatTimeOnly(info.end) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-else class="text-center text-caption text-grey py-2">无法加载</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useExamStore } from "@/stores/examStore";
|
||||
import { mapState, mapActions } from "pinia";
|
||||
import RelativeTimeDisplay from "@/components/RelativeTimeDisplay.vue";
|
||||
|
||||
export default {
|
||||
name: "ConciseExamCard",
|
||||
components: {
|
||||
RelativeTimeDisplay,
|
||||
},
|
||||
props: {
|
||||
examId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
contentStyle: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(useExamStore, ["exams", "loadingDetails"]),
|
||||
exam() {
|
||||
return this.exams[this.examId];
|
||||
},
|
||||
loading() {
|
||||
return this.loadingDetails[this.examId];
|
||||
},
|
||||
groupedExamInfos() {
|
||||
if (!this.exam || !this.exam.examInfos) return [];
|
||||
|
||||
const sortedInfos = [...this.exam.examInfos].sort(
|
||||
(a, b) => new Date(a.start) - new Date(b.start)
|
||||
);
|
||||
const groups = [];
|
||||
let currentGroup = null;
|
||||
|
||||
sortedInfos.forEach((info) => {
|
||||
const date = new Date(info.start);
|
||||
const dateKey = date.toDateString();
|
||||
|
||||
if (!currentGroup || currentGroup.key !== dateKey) {
|
||||
currentGroup = {
|
||||
key: dateKey,
|
||||
date: info.start,
|
||||
infos: [],
|
||||
};
|
||||
groups.push(currentGroup);
|
||||
}
|
||||
currentGroup.infos.push(info);
|
||||
});
|
||||
|
||||
return groups;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.fetchExam(this.examId);
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useExamStore, ["fetchExam"]),
|
||||
formatTimeOnly(timeStr) {
|
||||
if (!timeStr) return "";
|
||||
try {
|
||||
const date = new Date(timeStr);
|
||||
const hours = date.getHours().toString().padStart(2, "0");
|
||||
const minutes = date.getMinutes().toString().padStart(2, "0");
|
||||
return `${hours}:${minutes}`;
|
||||
} catch (e) {
|
||||
return "";
|
||||
}
|
||||
},
|
||||
isPast(timeStr) {
|
||||
if (!timeStr) return false;
|
||||
return new Date(timeStr) < new Date();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.gap-1 {
|
||||
gap: 0px;
|
||||
}
|
||||
.border-b-sm {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
.border-none {
|
||||
border-bottom: none;
|
||||
}
|
||||
.hover-elevation {
|
||||
transition: box-shadow 0.2s;
|
||||
}
|
||||
.hover-elevation:hover {
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<v-card class="fill-height d-flex flex-column rounded-xl" elevation="2">
|
||||
<v-card-title class="d-flex align-center py-3 px-4 bg-primary text-white">
|
||||
<v-icon class="mr-2">mdi-calendar-clock</v-icon>
|
||||
<span class="text-truncate">{{ exam?.examName || '加载中...' }}</span>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn icon="mdi-close" variant="text" density="comfortable" @click="$emit('close')"></v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="flex-grow-1 pa-4 overflow-y-auto" :style="contentStyle">
|
||||
<div v-if="loading" class="d-flex justify-center align-center fill-height" style="min-height: 200px;">
|
||||
<v-progress-circular indeterminate color="primary"></v-progress-circular>
|
||||
</div>
|
||||
|
||||
<template v-else-if="exam">
|
||||
<v-alert
|
||||
v-if="exam.message"
|
||||
color="info"
|
||||
variant="tonal"
|
||||
class="mb-4 text-body-1"
|
||||
border="start"
|
||||
density="compact"
|
||||
>
|
||||
{{ exam.message }}
|
||||
</v-alert>
|
||||
|
||||
<v-list density="comfortable" class="pa-0 bg-transparent">
|
||||
<v-list-item v-for="(info, index) in exam.examInfos" :key="index" class="px-0 mb-3">
|
||||
<template #prepend>
|
||||
<v-avatar color="primary" variant="tonal" size="large" class="mr-3 font-weight-bold elevation-1">
|
||||
{{ info.name.charAt(0) }}
|
||||
</v-avatar>
|
||||
</template>
|
||||
|
||||
<v-list-item-title class="font-weight-bold text-h6 mb-1">
|
||||
{{ info.name }}
|
||||
</v-list-item-title>
|
||||
|
||||
<v-list-item-subtitle class="text-body-1">
|
||||
<div class="d-flex align-center mb-1">
|
||||
<v-icon size="small" color="success" class="mr-2">mdi-clock-start</v-icon>
|
||||
<span class="font-weight-medium">{{ formatTime(info.start) }}</span>
|
||||
</div>
|
||||
<div class="d-flex align-center">
|
||||
<v-icon size="small" color="error" class="mr-2">mdi-clock-end</v-icon>
|
||||
<span class="font-weight-medium">{{ formatTime(info.end) }}</span>
|
||||
</div>
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</template>
|
||||
|
||||
<div v-else class="d-flex flex-column align-center justify-center fill-height text-grey mt-4">
|
||||
<v-icon size="large" class="mb-2">mdi-alert-circle-outline</v-icon>
|
||||
无法加载考试信息
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useExamStore } from '@/stores/examStore'
|
||||
import { mapState, mapActions } from 'pinia'
|
||||
|
||||
export default {
|
||||
name: 'ExamScheduleCard',
|
||||
props: {
|
||||
examId: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
contentStyle: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(useExamStore, ['exams', 'loadingDetails']),
|
||||
exam() {
|
||||
return this.exams[this.examId]
|
||||
},
|
||||
loading() {
|
||||
return this.loadingDetails[this.examId]
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.fetchExam(this.examId)
|
||||
},
|
||||
methods: {
|
||||
...mapActions(useExamStore, ['fetchExam']),
|
||||
formatTime(timeStr) {
|
||||
if (!timeStr) return ''
|
||||
try {
|
||||
const date = new Date(timeStr)
|
||||
// Format: MM-DD HH:mm
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0')
|
||||
const day = date.getDate().toString().padStart(2, '0')
|
||||
const hours = date.getHours().toString().padStart(2, '0')
|
||||
const minutes = date.getMinutes().toString().padStart(2, '0')
|
||||
return `${month}-${day} ${hours}:${minutes}`
|
||||
} catch (e) {
|
||||
return timeStr
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.v-list-item-title {
|
||||
white-space: normal;
|
||||
}
|
||||
</style>
|
||||
@@ -24,16 +24,26 @@
|
||||
>
|
||||
随机点名
|
||||
</v-btn>
|
||||
<v-btn
|
||||
<v-btn-group
|
||||
v-if="showExamScheduleButton"
|
||||
class="ml-2"
|
||||
color="green"
|
||||
prepend-icon="mdi-calendar-check"
|
||||
size="large"
|
||||
@click="$router.push('/examschedule')"
|
||||
variant="elevated"
|
||||
divided
|
||||
>
|
||||
考试看板
|
||||
</v-btn>
|
||||
<v-btn
|
||||
prepend-icon="mdi-calendar-check"
|
||||
size="large"
|
||||
@click="$router.push('/examschedule')"
|
||||
>
|
||||
考试看板
|
||||
</v-btn>
|
||||
<v-btn
|
||||
icon="mdi-plus"
|
||||
size="large"
|
||||
@click="$emit('add-exam-card')"
|
||||
/>
|
||||
</v-btn-group>
|
||||
<v-btn
|
||||
v-if="showListCardButton"
|
||||
class="ml-2"
|
||||
|
||||
@@ -16,6 +16,15 @@
|
||||
<hitokoto-card />
|
||||
</div>
|
||||
|
||||
<!-- 考试卡片 -->
|
||||
<div v-else-if="item.type === 'exam'" style="height: 100%">
|
||||
<concise-exam-card
|
||||
:exam-id="item.data.examId"
|
||||
:content-style="contentStyle"
|
||||
@click="$emit('open-exam-detail', item.data.examId)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 出勤卡片 -->
|
||||
<v-card
|
||||
v-else-if="item.type === 'attendance'"
|
||||
@@ -185,11 +194,13 @@
|
||||
|
||||
<script>
|
||||
import HitokotoCard from "@/components/HitokotoCard.vue";
|
||||
import ConciseExamCard from "@/components/home/ConciseExamCard.vue";
|
||||
|
||||
export default {
|
||||
name: "HomeworkGrid",
|
||||
components: {
|
||||
HitokotoCard,
|
||||
ConciseExamCard,
|
||||
},
|
||||
props: {
|
||||
sortedItems: {
|
||||
|
||||
Reference in New Issue
Block a user