From 40782a8905f0786e7a8a66e2046113a69c6e226d Mon Sep 17 00:00:00 2001 From: zhonghua Date: Tue, 24 Mar 2026 15:11:29 +0800 Subject: [PATCH] =?UTF-8?q?feat(theme):=20=E4=B8=BB=E9=A2=98=E5=AD=97?= =?UTF-8?q?=E5=85=B8=E9=A2=9C=E8=89=B2=E3=80=81=E8=AF=BE=E7=A8=8B=E4=B8=BB?= =?UTF-8?q?=E9=A2=98=20Tag=20=E5=B1=95=E7=A4=BA=E4=B8=8E=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E8=A7=84=E8=8C=83=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端:theme 表增加 color 字段;主题创建/更新/课程响应返回 themeColor - 前端:主题管理页颜色选择器与列表;管理端课程列表与详情主题 Tag - 课程中心/课程包卡片展示主题 Tag,course-center 规范化接口字段 - 隐藏管理端课程配置列与筛选;课程详情关联主题使用 themeName/color Made-with: Cursor --- .../src/api/course-center.ts | 54 +++++++- reading-platform-frontend/src/api/course.ts | 2 + reading-platform-frontend/src/api/theme.ts | 3 + .../src/constants/themeColors.ts | 50 +++++++ .../src/utils/tagMaps.ts | 39 ++++++ .../views/admin/courses/CourseDetailView.vue | 11 +- .../views/admin/courses/CourseListView.vue | 31 ++--- .../src/views/admin/themes/ThemeListView.vue | 129 +++++++++++++++++- .../school/courses-new/CourseCenterView.vue | 20 --- .../components/CoursePackageCard.vue | 61 +++++---- .../views/school/courses/CourseDetailView.vue | 12 +- .../teacher/courses-new/CourseCenterView.vue | 21 --- .../components/CoursePackageCard.vue | 59 ++++---- .../teacher/courses/CourseDetailView.vue | 11 +- .../components/content/CourseBasicInfo.vue | 10 +- .../admin/AdminThemeController.java | 3 + .../dto/request/ThemeCreateRequest.java | 3 + .../dto/response/CoursePackageResponse.java | 3 + .../platform/dto/response/CourseResponse.java | 3 + .../platform/dto/response/ThemeResponse.java | 3 + .../com/reading/platform/entity/Theme.java | 3 + .../platform/service/ThemeService.java | 4 +- .../impl/CourseCollectionServiceImpl.java | 49 ++++++- .../impl/CoursePackageServiceImpl.java | 1 + .../service/impl/ThemeServiceImpl.java | 8 +- .../db/migration/V49__add_theme_color.sql | 2 + 26 files changed, 457 insertions(+), 138 deletions(-) create mode 100644 reading-platform-frontend/src/constants/themeColors.ts create mode 100644 reading-platform-java/src/main/resources/db/migration/V49__add_theme_color.sql diff --git a/reading-platform-frontend/src/api/course-center.ts b/reading-platform-frontend/src/api/course-center.ts index 83da8c0..eb97433 100644 --- a/reading-platform-frontend/src/api/course-center.ts +++ b/reading-platform-frontend/src/api/course-center.ts @@ -33,6 +33,8 @@ export interface CoursePackage { courses?: CoursePackageCourseItem[]; // 用于课程配置展示 themeId?: number; themeName?: string; + /** 主题颜色(hex),来自主题字典 */ + themeColor?: string; durationMinutes?: number; usageCount?: number; avgRating?: number; @@ -66,6 +68,48 @@ export interface FilterMetaResponse { themes?: ThemeOption[]; } +// ============= 响应规范化(学校端/教师端共用同一接口,兼容字段形态) ============= + +function normalizeGradeTags(raw: unknown): string[] { + if (raw == null) return []; + if (Array.isArray(raw)) return raw.map(String).filter(Boolean); + if (typeof raw === 'string' && raw.trim()) { + const s = raw.trim(); + if (s.startsWith('[')) { + try { + const j = JSON.parse(s); + return Array.isArray(j) ? j.map(String).filter(Boolean) : []; + } catch { + /* fallthrough */ + } + } + return s.split(',').map((x) => x.trim()).filter(Boolean); + } + return []; +} + +/** 统一课程包卡片所需字段,避免学校端/代理层返回 snake_case 或嵌套 theme 时主题不显示 */ +export function normalizeCoursePackage(raw: any): CoursePackage { + if (!raw || typeof raw !== 'object') { + return raw as CoursePackage; + } + const nested = + raw.theme && typeof raw.theme === 'object' ? (raw.theme as Record) : null; + const themeName = String( + raw.themeName ?? raw.theme_name ?? nested?.name ?? '' + ).trim(); + const themeColor = (raw.themeColor ?? raw.theme_color ?? nested?.color) as string | undefined; + const themeId = (raw.themeId ?? raw.theme_id ?? nested?.id) as number | undefined; + + return { + ...raw, + themeId: themeId ?? raw.themeId, + themeName: themeName || undefined, + themeColor: themeColor || undefined, + gradeTags: normalizeGradeTags(raw.gradeTags ?? raw.grade_tags), + }; +} + // ============= API 接口 ============= /** @@ -87,9 +131,13 @@ export function getPackages( keyword?: string; } ): Promise { - return http.get(`/v1/school/packages/${collectionId}/packages`, { - params, - }); + return http + .get(`/v1/school/packages/${collectionId}/packages`, { + params, + }) + .then((list) => + Array.isArray(list) ? list.map((item) => normalizeCoursePackage(item)) : [] + ); } /** diff --git a/reading-platform-frontend/src/api/course.ts b/reading-platform-frontend/src/api/course.ts index 5be0cdc..c14b0a7 100644 --- a/reading-platform-frontend/src/api/course.ts +++ b/reading-platform-frontend/src/api/course.ts @@ -49,6 +49,8 @@ export interface Course { themeId?: number; theme?: { id: number; name: string }; themeName?: string; + /** 主题颜色(hex),来自主题字典 */ + themeColor?: string; coreContent?: string; coverImagePath?: string; domainTags?: string[]; diff --git a/reading-platform-frontend/src/api/theme.ts b/reading-platform-frontend/src/api/theme.ts index dcbad4b..355d0fd 100644 --- a/reading-platform-frontend/src/api/theme.ts +++ b/reading-platform-frontend/src/api/theme.ts @@ -4,6 +4,7 @@ export interface Theme { id: number; name: string; description?: string; + color?: string; sortOrder: number; status: string; createdAt: string; @@ -17,12 +18,14 @@ export interface Theme { export interface CreateThemeData { name: string; description?: string; + color?: string; sortOrder?: number; } export interface UpdateThemeData { name?: string; description?: string; + color?: string; sortOrder?: number; status?: string; } diff --git a/reading-platform-frontend/src/constants/themeColors.ts b/reading-platform-frontend/src/constants/themeColors.ts new file mode 100644 index 0000000..034612f --- /dev/null +++ b/reading-platform-frontend/src/constants/themeColors.ts @@ -0,0 +1,50 @@ +/** + * 主题可选颜色配置 + * 从预设调色板中挑选,去除相近色,供主题字典选择 + */ + +function hexToRgb(hex: string): [number, number, number] { + const r = parseInt(hex.slice(1, 3), 16); + const g = parseInt(hex.slice(3, 5), 16); + const b = parseInt(hex.slice(5, 7), 16); + return [r, g, b]; +} + +function colorDistance(a: [number, number, number], b: [number, number, number]): number { + return Math.sqrt( + (a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2 + (a[2] - b[2]) ** 2 + ); +} + +/** 保留与已选颜色距离均大于该值的颜色,约 55 可去除明显相近色 */ +const SIMILARITY_THRESHOLD = 55; + +const RAW_COLORS = [ + '#0780cf', '#73c0de', '#009db2', '#3ba272', '#024b51', '#0e2c82', + '#5470c6', '#91cc75', '#ee6666', '#9a60b4', '#fc8452', '#ea7ccc', + '#2196F3', '#08C9C9', '#00C345', '#F52222', '#FAD714', '#D3D3D3', + '#FF9F7F', '#FAC858', '#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', + '#9966FF', '#FF9F40', '#8AC24A', '#EA5F89', '#7CDDDD', '#F77825', + '#8FD3E8', '#A4DE6C', '#D0ED57', '#F7CAC9', '#92A8D1', '#88B04B', + '#E15D44', '#F3D6E4', '#B565A7', '#009B77', + '#FFD1DC', '#E1F7D5', '#C4C1E0', '#FFDAC1', '#B5EAD7', '#F8B195', + '#FF9AA2', '#FFB7B2', '#E2F0CB', '#C7CEEA', '#F67280', '#C06C84', + '#6C7A89', '#95A5A6', '#BDC3C7', '#2E86AB', '#A23B72', '#F18F01', + '#04A777', '#59114D', + '#4ECDC4', '#45B7D1', '#96CEB4', '#FF6B6B', '#FFEAA7', '#DDA0DD', + '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E9', '#F8C471', '#82E0AA', + '#58D68D', '#5DADE2', '#52BE80', '#7DCEA0', '#76D7C4', +]; + +export const THEME_COLOR_OPTIONS: string[] = (() => { + const result: string[] = []; + for (const hex of RAW_COLORS) { + const rgb = hexToRgb(hex); + const tooClose = result.some((kept) => { + const d = colorDistance(rgb, hexToRgb(kept)); + return d < SIMILARITY_THRESHOLD; + }); + if (!tooClose) result.push(hex); + } + return result; +})(); diff --git a/reading-platform-frontend/src/utils/tagMaps.ts b/reading-platform-frontend/src/utils/tagMaps.ts index 25f3c28..469e0c8 100644 --- a/reading-platform-frontend/src/utils/tagMaps.ts +++ b/reading-platform-frontend/src/utils/tagMaps.ts @@ -819,3 +819,42 @@ export function getUserRoleStyle(role: string): { border: "none", }; } + +const THEME_HEX = /^#[0-9A-Fa-f]{6}$/; + +function themeHexToRgb(hex: string): [number, number, number] { + const h = hex.startsWith("#") ? hex : `#${hex}`; + if (!THEME_HEX.test(h)) { + return [89, 89, 89]; + } + return [ + parseInt(h.slice(1, 3), 16), + parseInt(h.slice(3, 5), 16), + parseInt(h.slice(5, 7), 16), + ]; +} + +/** + * 课程主题 Tag 样式(与主题字典中配置的颜色一致,浅底 + 主题色文字/描边) + */ +export function getThemeTagStyle(themeColor?: string | null): { + background: string; + color: string; + border: string; +} { + const raw = (themeColor || "").trim(); + const hex = raw.startsWith("#") ? raw : raw ? `#${raw}` : ""; + if (!hex || !THEME_HEX.test(hex)) { + return { + background: "#fafafa", + color: "#595959", + border: "1px solid #d9d9d9", + }; + } + const [r, g, b] = themeHexToRgb(hex); + return { + background: `rgba(${r},${g},${b},0.12)`, + color: hex, + border: `1px solid rgba(${r},${g},${b},0.35)`, + }; +} diff --git a/reading-platform-frontend/src/views/admin/courses/CourseDetailView.vue b/reading-platform-frontend/src/views/admin/courses/CourseDetailView.vue index bef118a..c0c1fd3 100644 --- a/reading-platform-frontend/src/views/admin/courses/CourseDetailView.vue +++ b/reading-platform-frontend/src/views/admin/courses/CourseDetailView.vue @@ -43,7 +43,7 @@
关联主题 - {{ course.theme.name }} + {{ themeDisplayName }} 未设置
@@ -404,7 +404,7 @@ import { EnvironmentOutlined, } from '@ant-design/icons-vue'; import * as courseApi from '@/api/course'; -import { translateDomainTags } from '@/utils/tagMaps'; +import { translateDomainTags, getThemeTagStyle } from '@/utils/tagMaps'; import FilePreviewModal from '@/components/FilePreviewModal.vue'; const router = useRouter(); @@ -499,6 +499,13 @@ const domainTags = computed(() => { } }); +const themeDisplayName = computed(() => + (course.value.theme?.name || course.value.themeName || '').trim() +); +const themeTagStyle = computed(() => + getThemeTagStyle(course.value.theme?.color || course.value.themeColor) +); + // 审核中(待审核)不可编辑,仅草稿/已驳回/已下架可编辑 const canEdit = computed(() => { const s = course.value.status; diff --git a/reading-platform-frontend/src/views/admin/courses/CourseListView.vue b/reading-platform-frontend/src/views/admin/courses/CourseListView.vue index 5069109..58e1290 100644 --- a/reading-platform-frontend/src/views/admin/courses/CourseListView.vue +++ b/reading-platform-frontend/src/views/admin/courses/CourseListView.vue @@ -54,22 +54,19 @@ - -