1
build / build (push) Failing after 6m21s

This commit is contained in:
wanghu
2026-10-14 16:08:25 +08:00
parent 9d387692b4
commit 2a4d5b8e03
2 changed files with 871 additions and 1 deletions
+870
View File
@@ -0,0 +1,870 @@
<template>
<div class="ov-page">
<!-- 页头:标题 + 统计 -->
<div class="ov-pagehead">
<div>
<h1 class="ov-h1">排课情况</h1>
<p class="ov-sub">按班级汇总电子课表与照片课表的编排情况,点击卡片进入该班编排。</p>
</div>
<div class="ov-head-tools">
<div class="ov-stats">
<div class="ov-stat"><b class="num">{{ rows.length }}</b><span>班级总数</span></div>
<div class="ov-stat"><b class="num">{{ electronicCount }}</b><span>电子版已设置</span></div>
<div class="ov-stat"><b class="num">{{ photoCount }}</b><span>照片版已上传</span></div>
<div v-if="yearTermText !== '—'" class="ov-stat"><b>{{ yearTermText }}</b><span>当前学年学期</span></div>
</div>
<button class="ov-btn ghost" type="button" @click="goEdit">返回编排</button>
</div>
</div>
<section class="ov-card">
<!-- 加载中 -->
<div v-if="loading" class="ov-state">
<span class="spinner"></span>正在获取排课情况…
</div>
<!-- 加载失败 -->
<div v-else-if="loadError" class="ov-state">
<b>{{ loadError }}</b>
<button class="ov-btn ghost" type="button" @click="loadAll">重新加载</button>
</div>
<!-- 无班级 -->
<div v-else-if="!rows.length" class="ov-state">
<b>暂无班级数据</b>
<span>请确认班级服务可用后刷新重试。</span>
</div>
<!-- 年级 Tab 切换 + 当前年级的班级卡片 -->
<div v-else class="ov-scroll">
<div v-if="groups.length > 1" class="ov-tabs" role="tablist" aria-label="按年级切换">
<button
v-for="group in groups"
:key="group.name"
type="button"
class="ov-tab"
role="tab"
:class="{ on: activeGrade === group.name }"
:aria-selected="activeGrade === group.name"
@click="activeGrade = group.name"
>
{{ group.name || "未分年级" }}
<span class="tab-cnt num">{{ group.items.length }}</span>
</button>
</div>
<section v-if="activeGroup" class="ov-group">
<header class="ov-group-head">
<!-- 年级统一设置:启用 + 使用电子版/照片版(作用于「我的课表」电子/照片视图切换) -->
<span v-if="activeGroup.name" class="ov-grade-set" :class="{ busy: savingGrade === activeGroup.name }">
<label
class="ov-switch"
:class="{ on: cfgOf(activeGroup.name).enabled }"
title="统一启用/停用该年级课表查看(作用于「我的课表」)"
>
<input
type="checkbox"
:checked="cfgOf(activeGroup.name).enabled"
:disabled="savingGrade === activeGroup.name"
@change="toggleGradeEnabled(activeGroup.name)"
/>
<span class="track"><span class="thumb"></span></span>
{{ cfgOf(activeGroup.name).enabled ? "启用" : "未启用" }}
</label>
<label
class="ov-chk"
:class="{ on: cfgOf(activeGroup.name).useElectronic, off: !cfgOf(activeGroup.name).enabled }"
title="开放「我的课表」电子课表视图"
>
<input
type="checkbox"
:checked="cfgOf(activeGroup.name).useElectronic"
:disabled="!cfgOf(activeGroup.name).enabled || savingGrade === activeGroup.name"
@change="toggleGradeFlag(activeGroup.name, 'useElectronic')"
/>
使用电子版
</label>
<label
class="ov-chk"
:class="{ on: cfgOf(activeGroup.name).usePhoto, off: !cfgOf(activeGroup.name).enabled }"
title="开放「我的课表」照片课表视图"
>
<input
type="checkbox"
:checked="cfgOf(activeGroup.name).usePhoto"
:disabled="!cfgOf(activeGroup.name).enabled || savingGrade === activeGroup.name"
@change="toggleGradeFlag(activeGroup.name, 'usePhoto')"
/>
使用照片版
</label>
</span>
<span class="ov-group-head-right">
<button
class="ov-dl"
type="button"
:class="{ busy: downloadingGrade === activeGroup.name }"
:disabled="!activeGroup.items.some((c) => c.photoSet)"
:title="activeGroup.items.some((c) => c.photoSet)
? `将「${activeGroup.name || '未分年级'}」已上传的照片课表打包为 ZIP 下载`
: '该年级暂无已上传的照片课表'"
@click="downloadGradeZip"
>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 4v11" /><path d="M7 11l5 5 5-5" /><path d="M4 20h16" /></svg>
{{ downloadingGrade === activeGroup.name ? "打包中…" : "图片打包下载" }}
</button>
<span class="mini num">电子版 {{ activeGroup.items.filter((c) => c.electronicSet).length }} · 照片版 {{ activeGroup.items.filter((c) => c.photoSet).length }}</span>
</span>
</header>
<div class="ov-grid">
<article
v-for="item in activeGroup.items"
:key="item.classId"
class="ov-card-item"
role="link"
tabindex="0"
:aria-label="fullClassName(item) + ',进入班级编排'"
@click="goEdit(item.classId)"
@keydown.enter.prevent="goEdit(item.classId)"
@keydown.space.prevent="goEdit(item.classId)"
>
<div class="ci-head">
<div class="ci-title">
<span class="name">{{ item.className }}</span>
</div>
<span class="chip" :class="item.electronicSet ? 'ok' : 'off'">电子版:{{ item.electronicSet ? "已设置" : "未设置" }}</span>
<span class="chip" :class="item.photoSet ? 'ok' : 'off'">照片版:{{ item.photoSet ? "已上传" : "未上传" }}</span>
</div>
<div class="ci-body">
<div class="ci-pane">
<div class="pane-title">电子版</div>
<template v-if="item.electronicSet">
<div class="pane-line">已排课程</div>
<div class="pane-stats num">
<span>节次:<b>{{ item.periodCount }}</b> 节</span>
<span>科目:<b>{{ item.subjectCount }}</b> 门</span>
</div>
</template>
<div v-else class="pane-empty">尚未编排电子课表</div>
</div>
<div class="ci-pane">
<div class="pane-title">照片版</div>
<div v-if="item.photoSet && thumbUrl(item)" class="pane-thumb">
<img :src="thumbUrl(item)!" :alt="fullClassName(item) + ' 课程表照片'" loading="lazy" @error="onThumbError(item)" />
</div>
<div v-else class="pane-empty">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M3 8h3.5L8 5.5h8L17.5 8H21v11H3z" /><circle cx="12" cy="13.5" r="3.5" /></svg>
尚未上传照片
</div>
</div>
</div>
<div class="ci-foot">点击进入班级编排
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 6l6 6-6 6" /></svg>
</div>
</article>
</div>
</section>
</div>
</section>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from "vue";
import { useRouter } from "vue-router";
import { Message } from "view-ui-plus";
import { filePreviewUrl, gradePhotoZipUrl, syllabusApi } from "@/api/module/syllabus";
import type { GradeSettingVo, NowYearTermVo, TimetableOverviewVo } from "@/types/syllabus";
/* ============================================================
* 状态
* ============================================================ */
const rows = ref<TimetableOverviewVo[]>([]);
const loading = ref(false);
const loadError = ref("");
const nowYearTerm = ref<NowYearTermVo | null>(null);
/** 年级设置(启用 + 使用电子版/照片版),按年级名索引;未加载到的年级走默认全启用 */
const gradeCfg = ref<Record<string, GradeSettingVo>>({});
/** 正在保存年级设置的年级名(保存中禁用该组控件) */
const savingGrade = ref("");
/** 正在打包下载的年级名(按钮转「打包中…」防重复点击) */
const downloadingGrade = ref("");
/** 加载失败 / 出错后重试前清空重取的附件地址缓存 */
const thumbTokens = new Map<number, string>();
const router = useRouter();
/* ============================================================
* 派生
* ============================================================ */
const termName = computed(() => (nowYearTerm.value?.term === 2 ? "第二学期" : "第一学期"));
const yearTermText = computed(() => (nowYearTerm.value ? `${nowYearTerm.value.year} · ${termName.value}` : "—"));
const electronicCount = computed(() => rows.value.filter((r) => r.electronicSet).length);
const photoCount = computed(() => rows.value.filter((r) => r.photoSet).length);
/** 接口已按学生树年级→班级顺序返回,这里把相邻同年级班级归为一组 */
const groups = computed<Array<{ name: string; items: TimetableOverviewVo[] }>>(() => {
const out: Array<{ name: string; items: TimetableOverviewVo[] }> = [];
for (const item of rows.value) {
const name = item.gradeName || "";
const last = out[out.length - 1];
if (last && last.name === name) {
last.items.push(item);
} else {
out.push({ name, items: [item] });
}
}
return out;
});
/** 当前选中的年级 Tab(年级名,空串表示未分年级) */
const activeGrade = ref("");
/** 当前年级的分组数据 */
const activeGroup = computed(() => groups.value.find((g) => g.name === activeGrade.value) || null);
function fullClassName(item: TimetableOverviewVo): string {
return (item.gradeName || "") + (item.className || "");
}
/* ============================================================
* 年级设置(启用 + 使用电子版/照片版;即改即存)
* ============================================================ */
function cfgOf(name: string): GradeSettingVo {
return (
gradeCfg.value[name] || { gradeName: name, enabled: true, useElectronic: true, usePhoto: true }
);
}
async function persistGrade(name: string, next: GradeSettingVo) {
gradeCfg.value = { ...gradeCfg.value, [name]: next };
savingGrade.value = name;
try {
await syllabusApi.saveGradeSettings([next]);
if (next.enabled && !next.useElectronic && !next.usePhoto) {
Message.warning(`「${name}」电子版与照片版均未勾选,「我的课表」将不展示该年级课表`);
} else {
Message.success(`「${name}」年级设置已保存`);
}
} catch (e: any) {
Message.error(e?.message || "保存年级设置失败,请重试");
console.error("保存年级设置失败", e);
} finally {
savingGrade.value = "";
}
}
function toggleGradeEnabled(name: string) {
const cur = cfgOf(name);
void persistGrade(name, { ...cur, enabled: !cur.enabled });
}
function toggleGradeFlag(name: string, key: "useElectronic" | "usePhoto") {
const cur = cfgOf(name);
void persistGrade(name, { ...cur, [key]: !cur[key] });
}
/** 照片缩略图直链(加载失败过一次的班级换缓存标记绕过浏览器缓存) */
function thumbUrl(item: TimetableOverviewVo): string | null {
if (!item.photoFileId) return null;
const token = thumbTokens.get(item.classId);
return filePreviewUrl(item.photoFileId) + (token ? `&t=${token}` : "");
}
function onThumbError(item: TimetableOverviewVo) {
thumbTokens.set(item.classId, String(Date.now()));
}
/* ============================================================
* 操作
* ============================================================ */
function goEdit(classId?: number) {
router.push(classId ? { path: "/timetable", query: { classId } } : "/timetable");
}
/** 年级照片课表打包下载(ZIP;后端取当前学年学期该年级已上传照片,条目按班级命名) */
async function downloadGradeZip() {
const group = activeGroup.value;
if (!group || downloadingGrade.value) return;
const withPhoto = group.items.filter((c) => c.photoSet);
if (!withPhoto.length) {
Message.warning(`「${group.name || "未分年级"}」暂无已上传的照片课表`);
return;
}
downloadingGrade.value = group.name;
try {
const res = await fetch(gradePhotoZipUrl(group.name), { credentials: "include" });
// 业务异常(如附件缺失)返回 JSON,正常返回 ZIP 流
if ((res.headers.get("content-type") || "").includes("application/json")) {
const data = await res.json().catch(() => null);
throw new Error(data?.msg || "打包下载失败,请稍后重试");
}
if (!res.ok) {
throw new Error("打包下载失败,请稍后重试");
}
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `${group.name || "未分年级"}照片课表_${yearTermText.value.replace(" · ", "")}.zip`;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
Message.success(`已打包 ${withPhoto.length} 个班级的照片课表`);
} catch (e: any) {
Message.error(e?.message || "打包下载失败,请稍后重试");
console.error("年级照片课表打包下载失败", e);
} finally {
downloadingGrade.value = "";
}
}
async function loadAll() {
loading.value = true;
loadError.value = "";
try {
const [overview, yearTerm, gradeSettings] = await Promise.all([
syllabusApi.overview().send(),
syllabusApi.nowYearTerm().send().catch(() => null),
// 年级设置加载失败不阻断页面:控件按默认全启用展示,改动会重新保存
syllabusApi.gradeSettings().send().catch(() => [] as GradeSettingVo[]),
]);
rows.value = overview || [];
nowYearTerm.value = yearTerm;
const map: Record<string, GradeSettingVo> = {};
for (const g of gradeSettings || []) {
map[g.gradeName] = g;
}
gradeCfg.value = map;
// 默认选中第一个年级;刷新后原年级仍存在则保持选中
if (!groups.value.some((g) => g.name === activeGrade.value)) {
activeGrade.value = groups.value[0]?.name ?? "";
}
if (!rows.value.length) {
Message.info("未获取到班级数据,请确认班级服务可用");
}
} catch (e: any) {
rows.value = [];
loadError.value = "排课情况获取失败,请稍后重试";
console.error("获取排课情况失败", e);
} finally {
loading.value = false;
}
}
onMounted(loadAll);
</script>
<style lang="scss" scoped>
/* ============================================================
* 设计令牌(与课表编排页同源,$color-primary #5979ea)
* ============================================================ */
.ov-page {
--brand-50: #eef1fd;
--brand-100: #dde4fb;
--brand-400: #7d93ee;
--brand: #5979ea;
--brand-600: #4560d4;
--ink-900: #22262e;
--ink-700: #404042;
--ink-600: #5a5f6a;
--ink-400: #8c8f99;
--ink-300: #a8abaf;
--ink-200: #d6d9df;
--line: #e9e9e9;
--line-soft: #f0f1f3;
--bg-page: #f4f6fa;
--bg-surface: #fff;
--bg-subtle: #f7f8fb;
--ok: #3fa86c;
--warn: #d99a2b;
--err: #e05b45;
--r-md: 8px;
--r-lg: 12px;
--r-xl: 16px;
--ease: cubic-bezier(0.4, 0, 0.2, 1);
height: 100%;
box-sizing: border-box;
display: flex;
flex-direction: column;
padding: 20px 24px 40px;
background: var(--bg-page);
color: var(--ink-700);
font-size: 14px;
line-height: 1.6;
overflow: auto;
}
.ov-page button {
font-family: inherit;
font-size: inherit;
color: inherit;
}
.num {
font-variant-numeric: tabular-nums;
}
/* ==================== 页头 ==================== */
.ov-pagehead {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 24px;
flex-wrap: wrap;
padding-bottom: 20px;
}
.ov-h1 {
margin: 0;
font-size: 22px;
font-weight: 600;
color: var(--ink-900);
letter-spacing: -0.2px;
}
.ov-sub {
margin: 4px 0 0;
font-size: 13px;
color: var(--ink-400);
}
.ov-head-tools {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.ov-stats {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.ov-stat {
display: flex;
flex-direction: column;
gap: 1px;
min-width: 92px;
padding: 8px 16px;
background: var(--bg-surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: 0 1px 2px rgba(34, 38, 46, 0.05);
b {
font-size: 18px;
font-weight: 600;
color: var(--ink-900);
line-height: 1.3;
}
span {
font-size: 12px;
color: var(--ink-400);
}
}
/* ==================== 按钮 ==================== */
.ov-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
height: 34px;
padding: 0 16px;
border-radius: var(--r-md);
border: 1px solid transparent;
font-size: 13px;
cursor: pointer;
transition: all 200ms var(--ease);
white-space: nowrap;
&.ghost {
background: var(--bg-surface);
border-color: var(--ink-200);
color: var(--ink-700);
&:hover {
border-color: var(--brand-400);
color: var(--brand-600);
background: var(--brand-50);
}
}
}
/* ==================== 主卡片 ==================== */
.ov-card {
flex: 1;
min-height: 240px;
display: flex;
flex-direction: column;
background: var(--bg-surface);
border: 1px solid var(--line);
border-radius: var(--r-xl);
box-shadow: 0 4px 14px rgba(34, 38, 46, 0.07);
}
.ov-state {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
padding: 60px 24px;
color: var(--ink-400);
text-align: center;
b {
font-size: 15px;
color: var(--ink-600);
}
}
.spinner {
width: 22px;
height: 22px;
border-radius: 50%;
border: 2.5px solid var(--brand-100);
border-top-color: var(--brand);
animation: ov-spin 0.8s linear infinite;
}
@keyframes ov-spin {
to {
transform: rotate(360deg);
}
}
.ov-scroll {
flex: 1;
overflow: auto;
padding: 20px 24px 28px;
}
/* ==================== 年级 Tab ==================== */
.ov-tabs {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
margin-bottom: 16px;
}
.ov-tab {
display: inline-flex;
align-items: center;
gap: 7px;
height: 34px;
padding: 0 16px;
border-radius: 999px;
border: 1px solid var(--line);
background: var(--bg-surface);
font-size: 13px;
color: var(--ink-600);
cursor: pointer;
white-space: nowrap;
transition: all 200ms var(--ease);
.tab-cnt {
min-width: 20px;
padding: 0 6px;
border-radius: 99px;
background: var(--line-soft);
font-size: 11px;
line-height: 18px;
text-align: center;
color: var(--ink-400);
}
&:hover {
border-color: var(--brand-400);
color: var(--brand-600);
background: var(--brand-50);
}
&.on {
background: var(--brand);
border-color: var(--brand);
font-weight: 500;
color: #fff;
box-shadow: 0 2px 8px rgba(89, 121, 234, 0.35);
.tab-cnt {
background: rgba(255, 255, 255, 0.22);
color: #fff;
}
}
}
/* ==================== 当前年级分组 ==================== */
.ov-group-head {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
.mini {
font-size: 12px;
color: var(--ink-400);
}
}
.ov-group-head-right {
display: inline-flex;
align-items: center;
gap: 10px;
margin-left: auto;
}
/* 年级照片打包下载按钮(小号 ghost) */
.ov-dl {
display: inline-flex;
align-items: center;
gap: 5px;
height: 26px;
padding: 0 12px;
border-radius: 999px;
border: 1px solid var(--ink-200);
background: var(--bg-surface);
font-size: 12px;
color: var(--ink-600);
cursor: pointer;
white-space: nowrap;
transition: all 200ms var(--ease);
svg {
flex: none;
}
&:hover:not(:disabled) {
border-color: var(--brand-400);
color: var(--brand-600);
background: var(--brand-50);
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
&.busy {
opacity: 0.55;
pointer-events: none;
}
}
/* 年级统一设置:启用开关 + 使用电子版/照片版(与课表编排页开关同款) */
.ov-grade-set {
display: inline-flex;
align-items: center;
gap: 12px;
margin-left: 8px;
padding: 3px 12px;
border: 1px solid var(--line-soft);
border-radius: 999px;
background: var(--bg-subtle);
&.busy {
opacity: 0.55;
pointer-events: none;
}
}
.ov-switch {
display: inline-flex;
align-items: center;
gap: 7px;
font-size: 12px;
color: var(--ink-400);
cursor: pointer;
user-select: none;
input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.track {
display: inline-flex;
align-items: center;
width: 32px;
height: 17px;
padding: 2px;
border-radius: 99px;
background: #d4d7de;
transition: background 200ms var(--ease);
}
.thumb {
width: 13px;
height: 13px;
border-radius: 50%;
background: #fff;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
transition: transform 200ms var(--ease);
}
&.on {
color: var(--brand-600);
.track {
background: var(--brand);
}
.thumb {
transform: translateX(15px);
}
}
}
.ov-chk {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 12px;
color: var(--ink-400);
cursor: pointer;
user-select: none;
input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
&::before {
content: "";
width: 12px;
height: 12px;
border-radius: 3px;
border: 1px solid var(--ink-200);
background: #fff;
transition: all 160ms var(--ease);
}
&.on {
color: var(--brand-600);
font-weight: 500;
&::before {
background: var(--brand);
border-color: var(--brand);
box-shadow: inset 0 0 0 2.5px #fff;
}
}
/* 年级未启用:两个版本复选框随之失效 */
&.off {
opacity: 0.45;
cursor: not-allowed;
}
}
/* ==================== 班级卡片 ==================== */
.ov-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 14px;
}
.ov-card-item {
display: flex;
flex-direction: column;
background: var(--bg-surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 14px 16px 10px;
cursor: pointer;
transition: border-color 200ms var(--ease), box-shadow 200ms var(--ease), transform 200ms var(--ease);
&:hover,
&:focus-visible {
border-color: var(--brand-400);
box-shadow: 0 6px 18px rgba(89, 121, 234, 0.16);
transform: translateY(-1px);
outline: none;
.ci-foot {
color: var(--brand-600);
opacity: 1;
}
}
}
.ci-head {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
padding-bottom: 10px;
border-bottom: 1px solid var(--line-soft);
}
.ci-title {
display: flex;
align-items: baseline;
gap: 4px;
margin-right: auto;
min-width: 0;
.name {
font-size: 16px;
font-weight: 600;
color: var(--ink-900);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.chip {
font-size: 11px;
padding: 2px 8px;
border-radius: 99px;
white-space: nowrap;
&.ok {
background: #e9f7ef;
color: var(--ok);
border: 1px solid #c8e9d6;
}
&.off {
background: var(--bg-subtle);
color: var(--ink-400);
border: 1px solid var(--line);
}
}
/* 卡片正文:电子版 / 照片版两栏 */
.ci-body {
display: grid;
grid-template-columns: 1fr auto;
gap: 14px;
padding: 12px 0 10px;
flex: 1;
}
.ci-pane {
min-width: 0;
display: flex;
flex-direction: column;
gap: 6px;
& + .ci-pane {
padding-left: 14px;
border-left: 1px dashed var(--line);
}
}
.pane-title {
font-size: 12px;
color: var(--ink-400);
}
.pane-line {
font-size: 13px;
font-weight: 500;
color: var(--ink-900);
}
.pane-stats {
display: flex;
gap: 12px;
flex-wrap: wrap;
font-size: 12.5px;
color: var(--ink-600);
b {
font-size: 16px;
font-weight: 600;
color: var(--ink-900);
margin: 0 1px;
}
}
.pane-empty {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--ink-300);
}
.pane-thumb {
width: 96px;
height: 64px;
border-radius: var(--r-md);
border: 1px solid var(--line);
background: var(--bg-subtle);
overflow: hidden;
img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
}
/* 卡片底部悬浮提示 */
.ci-foot {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 2px;
padding-top: 6px;
border-top: 1px dashed var(--line-soft);
font-size: 11px;
color: var(--ink-300);
opacity: 0.8;
transition: color 200ms var(--ease);
}
</style>
+1 -1
View File
@@ -92,7 +92,7 @@ export default defineConfig(({ command, mode }: ConfigEnv): UserConfig => {
},
server: {
host: "localhost",
port: 8079,
port: 8078,
proxy: {
// dev 正式模式:serviceHost 为空(同源相对路径 /api/...),经此代理转发到本地后端
// (后端 CAS 过滤器对 /api/* 不回 CORS 头,浏览器直连会被拦,必须同源代理)