From 06342737c5b3c5d20ad74abca6abc02e6034e817 Mon Sep 17 00:00:00 2001 From: lion <120344285@qq.com> Date: Tue, 17 Mar 2026 16:10:40 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/coursePlan/index.js | 56 -- src/api/coursePlan/location.js | 56 -- src/api/coursePlan/system.js | 56 -- .../coursePlan/components/addLocation.vue | 155 ---- src/views/coursePlan/components/addPlan.vue | 348 --------- .../coursePlan/components/addPlanSystem.vue | 138 ---- src/views/coursePlan/index.vue | 468 ------------ src/views/coursePlan/location.vue | 161 ---- src/views/coursePlan/mockService.js | 494 ------------ src/views/coursePlan/planSystem.vue | 156 ---- src/views/coursePlan/课程计划页面方案.md | 704 ------------------ .../components/MemberOverview.vue | 43 ++ .../components/MonthlyHeatmap.vue | 53 ++ .../components/PlanMatrix.vue | 62 ++ .../components/SummaryPanel.vue | 26 + src/views/scheduleOverview/index.vue | 292 ++------ vue.config.js | 5 +- 17 files changed, 247 insertions(+), 3026 deletions(-) delete mode 100644 src/api/coursePlan/index.js delete mode 100644 src/api/coursePlan/location.js delete mode 100644 src/api/coursePlan/system.js delete mode 100644 src/views/coursePlan/components/addLocation.vue delete mode 100644 src/views/coursePlan/components/addPlan.vue delete mode 100644 src/views/coursePlan/components/addPlanSystem.vue delete mode 100644 src/views/coursePlan/index.vue delete mode 100644 src/views/coursePlan/location.vue delete mode 100644 src/views/coursePlan/mockService.js delete mode 100644 src/views/coursePlan/planSystem.vue delete mode 100644 src/views/coursePlan/课程计划页面方案.md create mode 100644 src/views/scheduleOverview/components/MemberOverview.vue create mode 100644 src/views/scheduleOverview/components/MonthlyHeatmap.vue create mode 100644 src/views/scheduleOverview/components/PlanMatrix.vue create mode 100644 src/views/scheduleOverview/components/SummaryPanel.vue diff --git a/src/api/coursePlan/index.js b/src/api/coursePlan/index.js deleted file mode 100644 index 9a56861..0000000 --- a/src/api/coursePlan/index.js +++ /dev/null @@ -1,56 +0,0 @@ -import request from "@/utils/request"; - -function customParamsSerializer(params) { - let result = ""; - for (let key in params) { - if (Object.prototype.hasOwnProperty.call(params, key)) { - if (Array.isArray(params[key])) { - params[key].forEach((item, index) => { - if (item && item.key) { - result += `${key}[${index}][key]=${item.key}&${key}[${index}][op]=${item.op}&${key}[${index}][value]=${item.value}&`; - } else { - result += `${key}[${index}]=${item}&`; - } - }); - } else { - result += `${key}=${params[key]}&`; - } - } - } - return result.slice(0, -1); -} - -export function index(params, isLoading = false) { - return request({ - method: "get", - url: "/api/admin/course-plan/index", - params, - paramsSerializer: customParamsSerializer, - isLoading, - }); -} - -export function show(params, isLoading = true) { - return request({ - method: "get", - url: "/api/admin/course-plan/show", - params, - isLoading, - }); -} - -export function save(data) { - return request({ - method: "post", - url: "/api/admin/course-plan/save", - data, - }); -} - -export function destroy(params) { - return request({ - method: "get", - url: "/api/admin/course-plan/destroy", - params, - }); -} diff --git a/src/api/coursePlan/location.js b/src/api/coursePlan/location.js deleted file mode 100644 index 938e0e1..0000000 --- a/src/api/coursePlan/location.js +++ /dev/null @@ -1,56 +0,0 @@ -import request from "@/utils/request"; - -function customParamsSerializer(params) { - let result = ""; - for (let key in params) { - if (Object.prototype.hasOwnProperty.call(params, key)) { - if (Array.isArray(params[key])) { - params[key].forEach((item, index) => { - if (item && item.key) { - result += `${key}[${index}][key]=${item.key}&${key}[${index}][op]=${item.op}&${key}[${index}][value]=${item.value}&`; - } else { - result += `${key}[${index}]=${item}&`; - } - }); - } else { - result += `${key}=${params[key]}&`; - } - } - } - return result.slice(0, -1); -} - -export function index(params, isLoading = false) { - return request({ - method: "get", - url: "/api/admin/course-plan-location/index", - params, - paramsSerializer: customParamsSerializer, - isLoading, - }); -} - -export function show(params, isLoading = true) { - return request({ - method: "get", - url: "/api/admin/course-plan-location/show", - params, - isLoading, - }); -} - -export function save(data) { - return request({ - method: "post", - url: "/api/admin/course-plan-location/save", - data, - }); -} - -export function destroy(params) { - return request({ - method: "get", - url: "/api/admin/course-plan-location/destroy", - params, - }); -} diff --git a/src/api/coursePlan/system.js b/src/api/coursePlan/system.js deleted file mode 100644 index 145b0d2..0000000 --- a/src/api/coursePlan/system.js +++ /dev/null @@ -1,56 +0,0 @@ -import request from "@/utils/request"; - -function customParamsSerializer(params) { - let result = ""; - for (let key in params) { - if (Object.prototype.hasOwnProperty.call(params, key)) { - if (Array.isArray(params[key])) { - params[key].forEach((item, index) => { - if (item && item.key) { - result += `${key}[${index}][key]=${item.key}&${key}[${index}][op]=${item.op}&${key}[${index}][value]=${item.value}&`; - } else { - result += `${key}[${index}]=${item}&`; - } - }); - } else { - result += `${key}=${params[key]}&`; - } - } - } - return result.slice(0, -1); -} - -export function index(params, isLoading = false) { - return request({ - method: "get", - url: "/api/admin/course-plan-system/index", - params, - paramsSerializer: customParamsSerializer, - isLoading, - }); -} - -export function show(params, isLoading = true) { - return request({ - method: "get", - url: "/api/admin/course-plan-system/show", - params, - isLoading, - }); -} - -export function save(data) { - return request({ - method: "post", - url: "/api/admin/course-plan-system/save", - data, - }); -} - -export function destroy(params) { - return request({ - method: "get", - url: "/api/admin/course-plan-system/destroy", - params, - }); -} diff --git a/src/views/coursePlan/components/addLocation.vue b/src/views/coursePlan/components/addLocation.vue deleted file mode 100644 index 4c3b2a9..0000000 --- a/src/views/coursePlan/components/addLocation.vue +++ /dev/null @@ -1,155 +0,0 @@ - - - - - diff --git a/src/views/coursePlan/components/addPlan.vue b/src/views/coursePlan/components/addPlan.vue deleted file mode 100644 index bdc11e2..0000000 --- a/src/views/coursePlan/components/addPlan.vue +++ /dev/null @@ -1,348 +0,0 @@ - - - - - diff --git a/src/views/coursePlan/components/addPlanSystem.vue b/src/views/coursePlan/components/addPlanSystem.vue deleted file mode 100644 index 271e8d3..0000000 --- a/src/views/coursePlan/components/addPlanSystem.vue +++ /dev/null @@ -1,138 +0,0 @@ - - - - - diff --git a/src/views/coursePlan/index.vue b/src/views/coursePlan/index.vue deleted file mode 100644 index 0eeea7d..0000000 --- a/src/views/coursePlan/index.vue +++ /dev/null @@ -1,468 +0,0 @@ - - - - - diff --git a/src/views/coursePlan/location.vue b/src/views/coursePlan/location.vue deleted file mode 100644 index 6eaaa77..0000000 --- a/src/views/coursePlan/location.vue +++ /dev/null @@ -1,161 +0,0 @@ - - - - - diff --git a/src/views/coursePlan/mockService.js b/src/views/coursePlan/mockService.js deleted file mode 100644 index b452efc..0000000 --- a/src/views/coursePlan/mockService.js +++ /dev/null @@ -1,494 +0,0 @@ -const STORAGE_KEY = "course-plan-mock-store"; - -const getCurrentYear = () => String(new Date().getFullYear()); - -const createInitialState = () => { - const currentYear = getCurrentYear(); - return { - courseTypes: [ - { id: 1, name: "初创班", sort: 1, status: 1 }, - { id: 2, name: "高研班", sort: 2, status: 1 }, - { id: 3, name: "零单班", sort: 3, status: 1 }, - { id: 4, name: "产业加速营", sort: 4, status: 1 }, - { id: 5, name: "第二课堂", sort: 5, status: 1 }, - { id: 6, name: "人才培训", sort: 6, status: 1 }, - { id: 7, name: "科技大讲堂", sort: 7, status: 1 }, - { id: 8, name: "万人培训", sort: 8, status: 1 }, - ], - planSystems: [ - { id: 1, name: "0-1", sort: 1, status: 1, remark: "初创企业培育计划" }, - { id: 2, name: "0-10", sort: 2, status: 1, remark: "成长型企业高研计划" }, - { id: 3, name: "10-100", sort: 3, status: 1, remark: "规模企业专题计划" }, - { id: 4, name: "产业结构协同创新", sort: 4, status: 1, remark: "产业升级计划" }, - { id: 5, name: "科技人才服务", sort: 5, status: 1, remark: "科技人才专项" }, - { id: 6, name: "育民营企业家万人培训", sort: 6, status: 1, remark: "民营企业家年度培训计划" }, - ], - locations: [ - { id: 1, name: "苏州", address: "苏州校区", sort: 1, status: 1, remark: "" }, - { id: 2, name: "苏州工业园区", address: "苏州工业园区教学点", sort: 2, status: 1, remark: "" }, - { id: 3, name: "苏州高新区", address: "苏州高新区教学点", sort: 3, status: 1, remark: "" }, - { id: 4, name: "上海", address: "上海教学点", sort: 4, status: 1, remark: "" }, - { id: 5, name: "深圳", address: "深圳教学点", sort: 5, status: 1, remark: "" }, - { id: 6, name: "南京", address: "南京教学点", sort: 6, status: 1, remark: "" }, - { id: 7, name: "杭州", address: "杭州教学点", sort: 7, status: 1, remark: "" }, - { id: 8, name: "无锡", address: "无锡教学点", sort: 8, status: 1, remark: "" }, - { id: 9, name: "常州", address: "常州教学点", sort: 9, status: 1, remark: "" }, - { id: 10, name: "线上", address: "线上直播", sort: 10, status: 1, remark: "" }, - { id: 11, name: "浙江", address: "浙江教学点", sort: 11, status: 1, remark: "" }, - { id: 12, name: "北京", address: "北京教学点", sort: 12, status: 1, remark: "" }, - ], - plans: [ - { - id: 1, - year: currentYear, - plan_system_id: 1, - course_type_id: 1, - course_name: "第二期高校科技成果转化班", - details: [ - { id: 101, name: "第二期", month: 5, location_id: 1, owner_name: "王老师" }, - { id: 102, name: "第二期", month: 7, location_id: 1, owner_name: "王老师" }, - { id: 103, name: "第二期", month: 9, location_id: 1, owner_name: "王老师" }, - ], - }, - { - id: 2, - year: currentYear, - plan_system_id: 1, - course_type_id: 1, - course_name: "第二期技术经理人班", - details: [ - { id: 104, name: "南大班", month: 6, location_id: 1, owner_name: "陈老师" }, - { id: 105, name: "南大班", month: 8, location_id: 1, owner_name: "陈老师" }, - { id: 106, name: "南大班", month: 10, location_id: 1, owner_name: "陈老师" }, - ], - }, - { - id: 3, - year: currentYear, - plan_system_id: 2, - course_type_id: 2, - course_name: "第七届北大光华班", - details: [ - { id: 107, name: "第六模块", month: 3, location_id: 1, owner_name: "周老师" }, - { id: 108, name: "第六模块", month: 5, location_id: 1, owner_name: "周老师" }, - { id: 109, name: "第七模块", month: 7, location_id: 1, owner_name: "周老师" }, - ], - }, - { - id: 4, - year: currentYear, - plan_system_id: 2, - course_type_id: 2, - course_name: "第八届苏大班", - details: [ - { id: 110, name: "第二模块", month: 4, location_id: 1, owner_name: "李老师" }, - { id: 111, name: "第三模块", month: 5, location_id: 1, owner_name: "李老师" }, - { id: 112, name: "第五模块", month: 6, location_id: 1, owner_name: "李老师" }, - { id: 113, name: "六模块", month: 7, location_id: 1, owner_name: "李老师" }, - { id: 114, name: "七模块", month: 9, location_id: 4, owner_name: "李老师" }, - { id: 115, name: "八模块", month: 11, location_id: 5, owner_name: "李老师" }, - { id: 116, name: "结业", month: 12, location_id: 1, owner_name: "李老师" }, - ], - }, - { - id: 5, - year: currentYear, - plan_system_id: 2, - course_type_id: 2, - course_name: "第九期中欧班", - details: [ - { id: 117, name: "开学", month: 5, location_id: 1, owner_name: "赵老师" }, - { id: 118, name: "第二模块", month: 8, location_id: 4, owner_name: "赵老师" }, - { id: 119, name: "第三模块", month: 10, location_id: 1, owner_name: "赵老师" }, - { id: 120, name: "第四模块", month: 12, location_id: 1, owner_name: "赵老师" }, - ], - }, - { - id: 6, - year: currentYear, - plan_system_id: 2, - course_type_id: 2, - course_name: "第十期商研班", - details: [ - { id: 121, name: "开学", month: 11, location_id: 1, owner_name: "孙老师" }, - ], - }, - { - id: 7, - year: currentYear, - plan_system_id: 3, - course_type_id: 3, - course_name: "第二期肇单班", - details: [ - { id: 122, name: "开学", month: 6, location_id: 1, owner_name: "钱老师" }, - ], - }, - { - id: 8, - year: currentYear, - plan_system_id: 4, - course_type_id: 4, - course_name: "AI+能源OPC加速营", - details: [ - { id: 123, name: "", month: 4, location_id: 1, owner_name: "吴老师" }, - { id: 124, name: "", month: 5, location_id: 1, owner_name: "吴老师" }, - { id: 125, name: "", month: 6, location_id: 1, owner_name: "吴老师" }, - ], - }, - { - id: 9, - year: currentYear, - plan_system_id: 4, - course_type_id: 4, - course_name: "南大OPC加速营", - details: [ - { id: 126, name: "", month: 8, location_id: 1, owner_name: "郑老师" }, - { id: 127, name: "", month: 9, location_id: 1, owner_name: "郑老师" }, - { id: 128, name: "", month: 10, location_id: 1, owner_name: "郑老师" }, - ], - }, - { - id: 10, - year: currentYear, - plan_system_id: 4, - course_type_id: 5, - course_name: "第二课堂创新沙龙", - details: [], - }, - { - id: 11, - year: currentYear, - plan_system_id: 5, - course_type_id: 6, - course_name: "宣传部培训", - details: [ - { id: 129, name: "第一期", month: 6, location_id: 1, owner_name: "冯老师" }, - { id: 130, name: "第二期", month: 9, location_id: 1, owner_name: "冯老师" }, - { id: 131, name: "第三期", month: 12, location_id: 1, owner_name: "冯老师" }, - ], - }, - { - id: 12, - year: currentYear, - plan_system_id: 5, - course_type_id: 7, - course_name: "科技大讲堂专题课", - details: [], - }, - { - id: 13, - year: currentYear, - plan_system_id: 6, - course_type_id: 8, - course_name: "第一期万人培训", - details: [ - { id: 132, name: "南京市", month: 4, location_id: 1, owner_name: "蒋老师" }, - { id: 133, name: "苏州市", month: 5, location_id: 11, owner_name: "蒋老师" }, - { id: 134, name: "无锡市", month: 6, location_id: 1, owner_name: "蒋老师" }, - { id: 135, name: "南通市", month: 7, location_id: 1, owner_name: "蒋老师" }, - { id: 136, name: "扬州市", month: 8, location_id: 1, owner_name: "蒋老师" }, - { id: 137, name: "镇江市", month: 9, location_id: 1, owner_name: "蒋老师" }, - { id: 138, name: "盐城市", month: 10, location_id: 1, owner_name: "蒋老师" }, - { id: 139, name: "淮安市", month: 11, location_id: 1, owner_name: "蒋老师" }, - { id: 140, name: "宿迁市", month: 12, location_id: 1, owner_name: "蒋老师" }, - ], - }, - { - id: 14, - year: currentYear, - plan_system_id: 6, - course_type_id: 8, - course_name: "第二期万人培训", - details: [ - { id: 141, name: "常州市", month: 5, location_id: 12, owner_name: "韩老师" }, - { id: 142, name: "泰州市", month: 8, location_id: 1, owner_name: "韩老师" }, - { id: 143, name: "徐州市", month: 9, location_id: 1, owner_name: "韩老师" }, - { id: 144, name: "徐州市", month: 11, location_id: 1, owner_name: "韩老师" }, - ], - }, - ], - }; -}; - -const clone = (data) => JSON.parse(JSON.stringify(data)); - -const getStorage = () => { - if (typeof window === "undefined" || !window.localStorage) { - return null; - } - return window.localStorage; -}; - -const loadState = () => { - const storage = getStorage(); - if (!storage) { - return createInitialState(); - } - const cache = storage.getItem(STORAGE_KEY); - if (!cache) { - const initialState = createInitialState(); - storage.setItem(STORAGE_KEY, JSON.stringify(initialState)); - return initialState; - } - try { - return JSON.parse(cache); - } catch (error) { - const initialState = createInitialState(); - storage.setItem(STORAGE_KEY, JSON.stringify(initialState)); - return initialState; - } -}; - -let store = loadState(); - -const persist = () => { - const storage = getStorage(); - if (storage) { - storage.setItem(STORAGE_KEY, JSON.stringify(store)); - } -}; - -const nextId = (list) => list.reduce((max, item) => Math.max(max, Number(item.id) || 0), 0) + 1; - -const nextDetailId = () => { - const maxId = store.plans.reduce((max, plan) => { - const detailMax = (plan.details || []).reduce((detailCurrentMax, detail) => Math.max(detailCurrentMax, Number(detail.id) || 0), 0); - return Math.max(max, detailMax); - }, 0); - return maxId + 1; -}; - -const getFilterValue = (filter, key) => { - const target = (filter || []).find((item) => item.key === key); - return target ? target.value : ""; -}; - -const applyLikeFilter = (list, filter, key) => { - const value = getFilterValue(filter, key); - if (!value) { - return list; - } - return list.filter((item) => String(item[key] || "").includes(String(value))); -}; - -const applyEqFilter = (list, filter, key) => { - const value = getFilterValue(filter, key); - if (value === "" || value === undefined || value === null) { - return list; - } - return list.filter((item) => String(item[key]) === String(value)); -}; - -const sortBySort = (list) => clone(list).sort((a, b) => { - const sortCompare = Number(a.sort || 0) - Number(b.sort || 0); - if (sortCompare !== 0) { - return sortCompare; - } - return String(a.name || "").localeCompare(String(b.name || ""), "zh-CN"); -}); - -const getPlanSystemMap = () => { - return store.planSystems.reduce((map, item) => { - map[item.id] = item; - return map; - }, {}); -}; - -const getLocationMap = () => { - return store.locations.reduce((map, item) => { - map[item.id] = item; - return map; - }, {}); -}; - -const getCourseTypeMap = () => { - return store.courseTypes.reduce((map, item) => { - map[item.id] = item; - return map; - }, {}); -}; - -const enrichPlan = (plan) => { - const planSystemMap = getPlanSystemMap(); - const locationMap = getLocationMap(); - const courseTypeMap = getCourseTypeMap(); - return { - ...clone(plan), - plan_system: clone(planSystemMap[plan.plan_system_id] || {}), - course_type: clone(courseTypeMap[plan.course_type_id] || {}), - details: (plan.details || []).map((detail) => ({ - ...clone(detail), - location: clone(locationMap[detail.location_id] || {}), - })), - }; -}; - -export function listMockCourseTypes() { - return Promise.resolve({ - data: sortBySort(store.courseTypes.filter((item) => item.status !== 0)), - total: store.courseTypes.length, - }); -} - -export function listMockPlanSystems(params = {}) { - let list = sortBySort(store.planSystems); - list = applyLikeFilter(list, params.filter, "name"); - list = applyEqFilter(list, params.filter, "status"); - return Promise.resolve({ - data: list, - total: list.length, - }); -} - -export function showMockPlanSystem({ id }) { - const item = store.planSystems.find((planSystem) => String(planSystem.id) === String(id)); - return Promise.resolve(clone(item || {})); -} - -export function saveMockPlanSystem(data) { - if (data.id) { - const index = store.planSystems.findIndex((item) => String(item.id) === String(data.id)); - if (index > -1) { - store.planSystems.splice(index, 1, { - ...store.planSystems[index], - ...clone(data), - }); - } - } else { - store.planSystems.push({ - id: nextId(store.planSystems), - name: data.name, - sort: data.sort || 0, - status: data.status === 0 ? 0 : 1, - remark: data.remark || "", - }); - } - persist(); - return Promise.resolve({ success: true }); -} - -export function destroyMockPlanSystem({ id }) { - store.planSystems = store.planSystems.filter((item) => String(item.id) !== String(id)); - store.plans = store.plans.filter((item) => String(item.plan_system_id) !== String(id)); - persist(); - return Promise.resolve({ success: true }); -} - -export function listMockLocations(params = {}) { - let list = sortBySort(store.locations); - list = applyLikeFilter(list, params.filter, "name"); - list = applyEqFilter(list, params.filter, "status"); - return Promise.resolve({ - data: list, - total: list.length, - }); -} - -export function showMockLocation({ id }) { - const item = store.locations.find((location) => String(location.id) === String(id)); - return Promise.resolve(clone(item || {})); -} - -export function saveMockLocation(data) { - if (data.id) { - const index = store.locations.findIndex((item) => String(item.id) === String(data.id)); - if (index > -1) { - store.locations.splice(index, 1, { - ...store.locations[index], - ...clone(data), - }); - } - } else { - store.locations.push({ - id: nextId(store.locations), - name: data.name, - address: data.address || "", - sort: data.sort || 0, - status: data.status === 0 ? 0 : 1, - remark: data.remark || "", - }); - } - persist(); - return Promise.resolve({ success: true }); -} - -export function destroyMockLocation({ id }) { - store.locations = store.locations.filter((item) => String(item.id) !== String(id)); - store.plans = store.plans.map((plan) => ({ - ...plan, - details: (plan.details || []).filter((detail) => String(detail.location_id) !== String(id)), - })); - persist(); - return Promise.resolve({ success: true }); -} - -export function listMockPlans({ filter = [] } = {}) { - const year = getFilterValue(filter, "year") || getCurrentYear(); - const planSystemMap = getPlanSystemMap(); - const courseTypeMap = getCourseTypeMap(); - const list = store.plans - .filter((plan) => String(plan.year) === String(year)) - .map((plan) => enrichPlan(plan)) - .sort((a, b) => { - const planSort = Number((planSystemMap[a.plan_system_id] || {}).sort || 0) - Number((planSystemMap[b.plan_system_id] || {}).sort || 0); - if (planSort !== 0) { - return planSort; - } - const courseSort = Number((courseTypeMap[a.course_type_id] || {}).sort || 0) - Number((courseTypeMap[b.course_type_id] || {}).sort || 0); - if (courseSort !== 0) { - return courseSort; - } - return String(a.course_name || "").localeCompare(String(b.course_name || ""), "zh-CN"); - }); - - return Promise.resolve({ - data: list, - total: list.length, - }); -} - -export function showMockPlan({ id }) { - const item = store.plans.find((plan) => String(plan.id) === String(id)); - return Promise.resolve(enrichPlan(item || { details: [] })); -} - -export function saveMockPlan(data) { - const details = (data.details || []).map((detail) => ({ - id: detail.id || nextDetailId(), - name: detail.name || "", - month: Number(detail.month), - location_id: detail.location_id, - owner_name: detail.owner_name || "", - })); - - if (data.id) { - const index = store.plans.findIndex((item) => String(item.id) === String(data.id)); - if (index > -1) { - store.plans.splice(index, 1, { - ...store.plans[index], - year: String(data.year), - plan_system_id: data.plan_system_id, - course_type_id: data.course_type_id, - course_name: data.course_name, - details, - }); - } - } else { - store.plans.push({ - id: nextId(store.plans), - year: String(data.year), - plan_system_id: data.plan_system_id, - course_type_id: data.course_type_id, - course_name: data.course_name, - details, - }); - } - persist(); - return Promise.resolve({ success: true }); -} - -export function destroyMockPlan({ id }) { - store.plans = store.plans.filter((item) => String(item.id) !== String(id)); - persist(); - return Promise.resolve({ success: true }); -} diff --git a/src/views/coursePlan/planSystem.vue b/src/views/coursePlan/planSystem.vue deleted file mode 100644 index a759a09..0000000 --- a/src/views/coursePlan/planSystem.vue +++ /dev/null @@ -1,156 +0,0 @@ - - - - - diff --git a/src/views/coursePlan/课程计划页面方案.md b/src/views/coursePlan/课程计划页面方案.md deleted file mode 100644 index 802ef54..0000000 --- a/src/views/coursePlan/课程计划页面方案.md +++ /dev/null @@ -1,704 +0,0 @@ -# 课程计划页面方案 - -## 1. 背景与目标 - -课程计划用于管理某一年度内,每个月份、不同计划体系、不同课程体系下预计开设的课程。该功能的核心不是单纯维护一条课程记录,而是围绕“年度计划”集中维护某门课程在 1-12 月内的开设安排。 - -本方案目标: - -- 提供一套适配当前项目风格的前端页面设计方案。 -- 明确课程计划、计划体系、地点三类数据的职责边界。 -- 给出后端表结构、接口风格、索引与约束建议,方便前后端同步实施。 - -## 2. 业务对象拆分 - -建议将课程计划拆为 4 类业务对象,而不是把所有信息都塞进一张表。 - -### 2.1 课程计划主表 - -主表只保存“这是一条什么计划”,负责描述计划的基础维度: - -- 计划年份 -- 计划体系 -- 课程体系 -- 课程名称 - -主表不直接展开存储月份、地点、负责人,而是通过明细表维护每一个模块/期数。 - -### 2.2 课程计划明细表 - -明细表对应 `模块/期数` 数组中的每一项,一条记录代表一个模块/期数计划,字段包含: - -- 名称:非必填,可为空 -- 月份:必填,范围 1-12 -- 地点:必填 -- 负责人:必填 - -这样设计的原因: - -- 一条计划可能跨多个月份。 -- 同一月份可能出现多个模块/期数。 -- 编辑时需要单独删除某一条模块/期数明细。 -- 列表页按月份渲染时,后端可以先按主表聚合、再按月份分组返回。 - -### 2.3 计划体系表 - -计划体系属于基础数据,独立维护,作为课程计划主表的下拉来源。后续如果还有排序、停用、年度适用范围等扩展,独立成表更稳定。 - -### 2.4 地点表 - -地点同样建议作为基础数据独立维护,作为课程计划明细中的下拉来源。地点后续很可能扩展为: - -- 地址详情 -- 排序 -- 是否启用 -- 联系信息 -- 容量说明 - -因此不建议仅保存为字符串。 - -## 3. 前端页面建议 - -建议在 `coursePlan` 目录下拆分为以下页面与组件: - -- `index.vue`:课程计划主页面 -- `components/addPlan.vue`:新增/编辑计划弹窗 -- `planSystem.vue`:计划体系基础数据页 -- `components/addPlanSystem.vue`:计划体系新增/编辑弹窗 -- `location.vue`:地点基础数据页 -- `components/addLocation.vue`:地点新增/编辑弹窗 - -如果一期只做主页面,也建议先按上述结构设计,避免后续把“新增地点”“新增计划体系”继续塞在当前页面中,导致页面职责过重。 - -## 4. 课程计划主页面设计 - -### 4.1 查询与功能按钮区 - -页面顶部建议保留一行查询区和操作区,沿用项目中 `lx-header + slot content` 的风格。 - -建议包含以下内容: - -- 年份查询 - - 使用 `el-date-picker` - - `type=\"year\"` - - 默认值为当前年份 -- 查询按钮 -- 重置按钮 -- 新增计划按钮 -- 新增地点按钮 -- 新增计划体系按钮 - -说明: - -- 年份是主筛选条件,应默认带上当前年,页面进入时自动查询。 -- “新增地点”“新增计划体系”可以直接打开对应弹窗,也可以跳转到独立管理页。若从用户效率考虑,建议先做弹窗方式;若从数据管理完整性考虑,建议跳转到独立页面。 - -### 4.2 主表格列设计 - -表格建议使用 `el-table`,列如下: - -- 计划体系 -- 课程体系 -- 一月 -- 二月 -- 三月 -- 四月 -- 五月 -- 六月 -- 七月 -- 八月 -- 九月 -- 十月 -- 十一月 -- 十二月 - -### 4.3 行数据组织方式 - -列表展示建议一行代表一条课程计划主表数据,即: - -- 一条主计划 = 某年 + 某计划体系 + 某课程体系 + 某课程名称 - -但考虑到列表中不单独展示“课程名称”列,而是把课程名称写进月份单元格中,所以需要把月份列数据预处理成: - -```js -{ - id: 1, - year: "2026", - plan_system_id: 2, - plan_system_name: "年度重点计划", - course_type_id: 5, - course_type_name: "企业管理体系", - course_name: "高层管理研修班", - months: { - 1: [ - { - detail_id: 11, - module_name: "第一期", - location_name: "苏州校区", - owner_name: "张三" - } - ], - 2: [], - 3: [ - { - detail_id: 12, - module_name: "第二期", - location_name: "上海教学点", - owner_name: "李四" - }, - { - detail_id: 13, - module_name: "", - location_name: "线上", - owner_name: "王五" - } - ] - } -} -``` - -这样前端渲染逻辑会比较直接,不需要在单元格中做大量即时计算。 - -### 4.4 单元格展示规则 - -每个月份单元格中,每条明细显示格式为: - -`课程名称(模块/期数)- 地点` - -具体规则: - -- 若 `模块/期数名称` 有值:显示为 `课程名称(模块名称)- 地点` -- 若 `模块/期数名称` 为空:显示为 `课程名称 - 地点` -- 同一计划的同一月份存在多条明细时,单元格内换行展示 - -建议前端统一通过格式化方法生成显示文本,例如: - -```js -formatPlanText(courseName, moduleName, locationName) { - if (moduleName) { - return `${courseName}(${moduleName})- ${locationName}` - } - return `${courseName} - ${locationName}` -} -``` - -### 4.5 单元格悬浮交互 - -鼠标移入某月份单元格时,显示浮层,内容建议如下: - -- 第一行开始逐条展示: - - `课程名称(模块/期数)- 地点` - - `负责人:xxx` -- 底部操作区: - - 编辑按钮 - -交互建议: - -- 当该单元格没有数据时,不显示浮层。 -- 当单元格有多条明细时,浮层按列表形式展示全部明细。 -- “编辑”点击后应打开当前计划的编辑弹窗,而不是只编辑单条明细,因为需求中说明“点击后弹出该计划的编辑弹窗,可在弹窗中删除某一模块/期数”。 - -建议使用 `el-popover` 或 `el-tooltip + 自定义内容` 实现。结合项目现状,优先建议 `el-popover`,因为底部有按钮交互。 - -### 4.6 合并单元格规则 - -表格中应对“计划体系”“课程体系”执行合并单元格。 - -建议规则: - -- 如果连续多行的 `计划体系 + 课程体系` 相同,则这两列合并。 -- 合并后月份列保持逐行展示。 - -注意: - -- 只有在列表接口已经按 `计划体系排序 + 课程体系排序 + 课程名称排序` 返回时,前端 `span-method` 才容易实现。 -- 如果存在同一 `计划体系 + 课程体系` 下多门不同课程,则它们仍是多行,只是前两列被合并。 - -建议后端排序: - -- `plan_system_sort ASC` -- `course_type_sort ASC` -- `course_name ASC` -- `month ASC` - -## 5. 新增/编辑计划弹窗设计 - -建议使用一个弹窗组件同时处理新增和编辑,标题分别为: - -- 新增计划 -- 编辑计划 - -### 5.1 基础字段 - -基础区字段如下: - -| 字段 | 类型 | 必填 | 说明 | -| --- | --- | --- | --- | -| 计划年份 | 年份选择 | 是 | 默认当前年 | -| 计划体系 | 下拉选择 | 是 | 由计划体系接口返回 | -| 课程体系 | 下拉选择 | 是 | 复用 `@/api/course/courseType.js` 的 `index` 接口 | -| 课程名称 | 输入框 | 是 | 建议长度限制 100 | - -### 5.2 模块/期数数组区 - -该字段建议在弹窗中使用 `el-table` 或卡片列表维护,数组最少 1 条。 - -每项字段: - -| 字段 | 类型 | 必填 | 说明 | -| --- | --- | --- | --- | -| 名称 | 输入框 | 否 | 模块名或期数名 | -| 月份 | 下拉选择 | 是 | 1-12 月 | -| 地点 | 下拉选择 | 是 | 来源于地点接口 | -| 负责人 | 输入框 | 是 | 建议先录入姓名,后续可扩展成员选择 | - -建议操作: - -- 新增一条模块/期数 -- 删除当前条目 - -校验规则: - -- 数组不能为空 -- 至少有 1 条数据 -- 每条数据的 `月份/地点/负责人` 必填 - -### 5.3 编辑弹窗底部操作 - -编辑状态下,弹窗底部除“取消”“保存”外,增加: - -- 删除计划 - -删除逻辑建议: - -- 点击“删除计划”后弹出确认框 -- 确认文案: - `删除该计划将删除所有的模块/期数,是否确认删除?` - -删除成功后: - -- 关闭弹窗 -- 刷新列表 - -## 6. 基础数据页面建议 - -### 6.1 计划体系管理 - -建议字段: - -- 名称 -- 排序 -- 状态 -- 备注 - -页面能力: - -- 列表 -- 新增 -- 编辑 -- 删除 - -### 6.2 地点管理 - -建议字段: - -- 名称 -- 详细地址 -- 排序 -- 状态 -- 备注 - -页面能力: - -- 列表 -- 新增 -- 编辑 -- 删除 - -说明: - -- 本次需求里的“地点”更接近课程计划业务内的地点基础数据,不一定完全等同于现有预约场地类型。 -- 如果现有 `book/type` 数据能直接复用,也可以不新建地点表,而是直接复用已有接口;但从语义上看,现有接口偏“预约场地类型”,未必完全适合课程计划,因此更建议新建独立地点表。 - -## 7. 前端接口建议 - -建议延续项目当前的接口命名风格,统一采用: - -- `index` -- `show` -- `save` -- `destroy` - -### 7.1 课程计划接口 - -建议新增: - -- `@/api/coursePlan/index.js` - -建议接口: - -| 方法名 | 请求方式 | 路径建议 | 说明 | -| --- | --- | --- | --- | -| `index` | GET | `/api/admin/course-plan/index` | 课程计划列表 | -| `show` | GET | `/api/admin/course-plan/show` | 查看单条计划详情 | -| `save` | POST | `/api/admin/course-plan/save` | 新增/编辑计划 | -| `destroy` | GET | `/api/admin/course-plan/destroy` | 删除计划 | - -列表查询参数建议: - -| 参数 | 说明 | -| --- | --- | -| `page` | 页码 | -| `page_size` | 每页条数 | -| `year` | 计划年份 | -| `sort_name` | 排序字段 | -| `sort_type` | 排序方式 | - -列表返回建议除基础字段外,直接返回 `months` 聚合结构,减少前端二次整理成本。 - -### 7.2 计划体系接口 - -建议新增: - -- `@/api/coursePlan/system.js` - -建议接口: - -| 方法名 | 请求方式 | 路径建议 | 说明 | -| --- | --- | --- | --- | -| `index` | GET | `/api/admin/course-plan-system/index` | 计划体系列表 | -| `show` | GET | `/api/admin/course-plan-system/show` | 计划体系详情 | -| `save` | POST | `/api/admin/course-plan-system/save` | 新增/编辑计划体系 | -| `destroy` | GET | `/api/admin/course-plan-system/destroy` | 删除计划体系 | - -### 7.3 地点接口 - -建议新增: - -- `@/api/coursePlan/location.js` - -建议接口: - -| 方法名 | 请求方式 | 路径建议 | 说明 | -| --- | --- | --- | --- | -| `index` | GET | `/api/admin/course-plan-location/index` | 地点列表 | -| `show` | GET | `/api/admin/course-plan-location/show` | 地点详情 | -| `save` | POST | `/api/admin/course-plan-location/save` | 新增/编辑地点 | -| `destroy` | GET | `/api/admin/course-plan-location/destroy` | 删除地点 | - -### 7.4 下拉接口使用建议 - -课程体系下拉直接复用: - -- `@/api/course/courseType.js` 的 `index` - -建议前端取数时增加固定筛选: - -- 状态为启用 -- 排序按 `sort ASC` - -计划体系、地点下拉也建议提供可直接用于表单的轻量列表返回。 - -## 8. 后端表结构建议 - -建议至少新增 4 张表。 - -### 8.1 课程计划主表 `course_plan` - -| 字段名 | 类型建议 | 说明 | -| --- | --- | --- | -| `id` | bigint | 主键 | -| `year` | varchar(4) 或 int | 计划年份 | -| `plan_system_id` | bigint | 计划体系 ID | -| `course_type_id` | bigint | 课程体系 ID | -| `course_name` | varchar(100) | 课程名称 | -| `sort` | int | 排序,默认 0 | -| `status` | tinyint | 状态,默认 1 | -| `remark` | varchar(255) | 备注,可空 | -| `created_by` | bigint | 创建人,可空 | -| `updated_by` | bigint | 更新人,可空 | -| `created_at` | datetime | 创建时间 | -| `updated_at` | datetime | 更新时间 | -| `deleted_at` | datetime | 软删除时间,可空 | - -建议唯一约束: - -- 如果业务上允许“同一年、同一计划体系、同一课程体系、同一课程名称”只出现一次,则增加唯一索引: - - `uniq_year_system_course_type_course_name` - -建议普通索引: - -- `idx_year` -- `idx_plan_system_id` -- `idx_course_type_id` -- `idx_year_plan_system_course_type` - -### 8.2 课程计划明细表 `course_plan_detail` - -| 字段名 | 类型建议 | 说明 | -| --- | --- | --- | -| `id` | bigint | 主键 | -| `plan_id` | bigint | 关联 `course_plan.id` | -| `module_name` | varchar(100) | 模块/期数名称,可空 | -| `month` | tinyint | 月份,1-12 | -| `location_id` | bigint | 地点 ID | -| `owner_name` | varchar(50) | 负责人 | -| `sort` | int | 排序,默认 0 | -| `remark` | varchar(255) | 备注,可空 | -| `created_at` | datetime | 创建时间 | -| `updated_at` | datetime | 更新时间 | -| `deleted_at` | datetime | 软删除时间,可空 | - -建议约束: - -- `month` 限制为 1-12 -- `plan_id` 必须存在 -- `location_id` 必须存在 - -建议索引: - -- `idx_plan_id` -- `idx_month` -- `idx_plan_id_month` -- `idx_location_id` - -说明: - -- 同一计划下同一月份允许出现多条明细,因此不建议对 `plan_id + month` 建唯一索引。 - -### 8.3 计划体系表 `course_plan_system` - -| 字段名 | 类型建议 | 说明 | -| --- | --- | --- | -| `id` | bigint | 主键 | -| `name` | varchar(100) | 计划体系名称 | -| `sort` | int | 排序,默认 0 | -| `status` | tinyint | 状态,默认 1 | -| `remark` | varchar(255) | 备注,可空 | -| `created_at` | datetime | 创建时间 | -| `updated_at` | datetime | 更新时间 | -| `deleted_at` | datetime | 软删除时间,可空 | - -建议约束: - -- `name` 唯一,避免重复维护相同计划体系 - -### 8.4 地点表 `course_plan_location` - -| 字段名 | 类型建议 | 说明 | -| --- | --- | --- | -| `id` | bigint | 主键 | -| `name` | varchar(100) | 地点名称 | -| `address` | varchar(255) | 详细地址,可空 | -| `sort` | int | 排序,默认 0 | -| `status` | tinyint | 状态,默认 1 | -| `remark` | varchar(255) | 备注,可空 | -| `created_at` | datetime | 创建时间 | -| `updated_at` | datetime | 更新时间 | -| `deleted_at` | datetime | 软删除时间,可空 | - -建议约束: - -- `name` 可考虑唯一,避免地点重名引发误选 - -## 9. 表关系与删除策略建议 - -关系建议如下: - -- `course_plan.plan_system_id -> course_plan_system.id` -- `course_plan.course_type_id -> course_type.id` -- `course_plan_detail.plan_id -> course_plan.id` -- `course_plan_detail.location_id -> course_plan_location.id` - -删除策略建议: - -- 删除课程计划主表时,同时删除全部明细 -- 删除计划体系前,需校验是否已被课程计划使用 -- 删除地点前,需校验是否已被课程计划明细使用 - -若后端采用软删除: - -- 主表删除时同步软删除明细 -- 基础表删除前仍需做引用检查 - -## 10. 建议的数据返回结构 - -为减少前端拼装成本,建议 `course-plan/index` 直接返回可渲染结构: - -```json -{ - "data": [ - { - "id": 1, - "year": "2026", - "plan_system_id": 1, - "plan_system_name": "年度重点计划", - "course_type_id": 2, - "course_type_name": "经营管理体系", - "course_name": "企业经营实战课", - "months": { - "1": [ - { - "detail_id": 10, - "module_name": "第一期", - "month": 1, - "location_id": 3, - "location_name": "本部校区", - "owner_name": "张老师" - } - ], - "2": [], - "3": [] - } - } - ], - "total": 1 -} -``` - -这样前端可以直接: - -- 遍历 1-12 月列 -- 读取 `row.months[month]` -- 渲染多行文本 -- 在悬浮层中使用同一份数据 - -## 11. 前端实现建议 - -### 11.1 列表页实现顺序 - -建议优先顺序: - -1. 完成年份查询与主表格静态布局 -2. 打通课程计划列表接口 -3. 完成月份列渲染 -4. 实现 `计划体系 + 课程体系` 合并单元格 -5. 接入单元格悬浮层 -6. 完成新增/编辑计划弹窗 -7. 完成计划体系和地点基础数据管理 - -### 11.2 弹窗数组区实现方式 - -建议直接参考当前项目中弹窗内 `el-table` 维护数组的方式,不建议在一期引入过于复杂的拖拽方案。原因是本需求只是“增删模块/期数条目”,不涉及复杂表单搭建。 - -建议前端表单结构: - -```js -form: { - id: "", - year: "2026", - plan_system_id: "", - course_type_id: "", - course_name: "", - details: [ - { - id: "", - module_name: "", - month: "", - location_id: "", - owner_name: "" - } - ] -} -``` - -### 11.3 保存接口提交结构 - -保存建议直接提交主表和明细数组: - -```json -{ - "id": 1, - "year": "2026", - "plan_system_id": 2, - "course_type_id": 5, - "course_name": "高层管理研修班", - "details": [ - { - "id": 11, - "module_name": "第一期", - "month": 3, - "location_id": 8, - "owner_name": "张三" - }, - { - "module_name": "", - "month": 6, - "location_id": 9, - "owner_name": "李四" - } - ] -} -``` - -后端处理建议: - -- 有 `id` 的明细按更新处理 -- 没有 `id` 的明细按新增处理 -- 编辑时前端未提交的旧明细,可视为删除,或由前端显式传删除标识;二者选其一并保持统一 - -更推荐: - -- 前端显式维护当前剩余明细列表 -- 后端保存时以 `plan_id` 为维度做差异同步 - -## 12. 风险点与注意事项 - -### 12.1 合并单元格前提 - -如果接口返回顺序不稳定,前端合并单元格会错乱,因此后端必须按统一顺序返回。 - -### 12.2 同月多条数据展示 - -单元格内多条明细换行后,高度会被撑开,建议: - -- 允许行高自适应 -- 单元格设置适当 padding -- 当数据过多时提供 `max-height` 或悬浮层查看更多 - -### 12.3 负责人字段 - -当前需求中负责人是字符串字段,落地最快;但如果后续希望与员工体系关联,建议预留扩展方案: - -- 短期先存 `owner_name` -- 中长期新增 `owner_id` - -### 12.4 地点是否复用旧表 - -如果后端确认现有预约场地类型可直接复用,则本方案中的地点表可以取消,改为复用旧接口。若复用,需要额外确认: - -- 旧数据是否都可作为课程计划地点 -- 字段含义是否匹配 -- 停用逻辑是否一致 - -## 13. 推荐一期落地范围 - -建议一期范围: - -- 课程计划主页面 -- 新增/编辑计划弹窗 -- 计划体系基础管理 -- 地点基础管理 -- 年份筛选 -- 月份表格展示 -- 单元格悬浮编辑 -- 删除计划确认 - -建议暂缓项: - -- 负责人关联员工选择器 -- 导入导出 -- 批量复制上一年度计划 -- 月份维度统计分析 - -## 14. 与现有项目实现风格的对应关系 - -本方案与现有项目风格保持一致,主要可复用如下思路: - -- 列表页骨架可参考 `src/views/course/types.vue` -- 基础配置页可参考 `src/views/courseTypeConfig/index.vue` -- 动态数组表单可参考 `src/views/dataMenu/components/addInfo.vue` -- 合并单元格可参考 `src/views/statistics/index.vue` -- 地点管理页风格可参考 `src/views/book/type.vue` - -这样可以降低前端开发成本,也能让课程计划功能在交互上与现有系统保持统一。 diff --git a/src/views/scheduleOverview/components/MemberOverview.vue b/src/views/scheduleOverview/components/MemberOverview.vue new file mode 100644 index 0000000..ea801f6 --- /dev/null +++ b/src/views/scheduleOverview/components/MemberOverview.vue @@ -0,0 +1,43 @@ + + + + diff --git a/src/views/scheduleOverview/components/MonthlyHeatmap.vue b/src/views/scheduleOverview/components/MonthlyHeatmap.vue new file mode 100644 index 0000000..a603bad --- /dev/null +++ b/src/views/scheduleOverview/components/MonthlyHeatmap.vue @@ -0,0 +1,53 @@ + + + + diff --git a/src/views/scheduleOverview/components/PlanMatrix.vue b/src/views/scheduleOverview/components/PlanMatrix.vue new file mode 100644 index 0000000..67d30c5 --- /dev/null +++ b/src/views/scheduleOverview/components/PlanMatrix.vue @@ -0,0 +1,62 @@ + + + + diff --git a/src/views/scheduleOverview/components/SummaryPanel.vue b/src/views/scheduleOverview/components/SummaryPanel.vue new file mode 100644 index 0000000..c025799 --- /dev/null +++ b/src/views/scheduleOverview/components/SummaryPanel.vue @@ -0,0 +1,26 @@ + + + + diff --git a/src/views/scheduleOverview/index.vue b/src/views/scheduleOverview/index.vue index 7348ec9..7a72695 100644 --- a/src/views/scheduleOverview/index.vue +++ b/src/views/scheduleOverview/index.vue @@ -1,5 +1,5 @@