| | |
| | | INSERT INTO sys_menu (id, parent_id, name, menu_code, type, sort, status, visible, permission_key, remark, deleted) |
| | | VALUES (5045, 2118, '编辑', 'member_clinic_edit', 'B', 1, 1, 1, 'member:clinic:edit', '编辑诊所按钮', 0) |
| | | ON DUPLICATE KEY UPDATE permission_key = VALUES(permission_key), deleted = 0; |
| | | |
| | | -- 微赞直播间单例设置:仅保留 Banner 图片地址和直播间地址两个业务字段 |
| | | CREATE TABLE IF NOT EXISTS tcm_live_room_setting ( |
| | | banner_image_url VARCHAR(500) NOT NULL COMMENT 'Banner图片地址', |
| | | live_room_url VARCHAR(500) NULL COMMENT '直播间地址' |
| | | ) COMMENT='微赞直播间设置'; |
| | | |
| | | INSERT INTO sys_menu (id, parent_id, name, menu_code, type, path, component, icon, sort, status, visible, permission_key, remark, deleted) |
| | | VALUES (6000, 3050, '微赞直播间设置', 'live_room_setting', 'C', '/live/course/live-room-setting', 'Live/LiveRoomSetting', NULL, 18, 1, 1, 'live:room-setting:view', '微赞直播间设置菜单', 0) |
| | | ON DUPLICATE KEY UPDATE parent_id = VALUES(parent_id), name = VALUES(name), menu_code = VALUES(menu_code), type = VALUES(type), path = VALUES(path), component = VALUES(component), sort = VALUES(sort), status = VALUES(status), visible = VALUES(visible), permission_key = VALUES(permission_key), remark = VALUES(remark), deleted = 0; |
| | | |
| | | INSERT INTO sys_menu (id, parent_id, name, menu_code, type, path, component, icon, sort, status, visible, permission_key, remark, deleted) |
| | | VALUES (6001, 6000, '编辑微赞直播间设置', 'live_room_setting_edit', 'B', NULL, NULL, NULL, 1, 1, 1, 'live:room-setting:edit', '微赞直播间设置编辑权限', 0) |
| | | ON DUPLICATE KEY UPDATE parent_id = VALUES(parent_id), name = VALUES(name), menu_code = VALUES(menu_code), type = VALUES(type), sort = VALUES(sort), status = VALUES(status), visible = VALUES(visible), permission_key = VALUES(permission_key), remark = VALUES(remark), deleted = 0; |
| New file |
| | |
| | | package com.tcm.training.admin.app.home.controller; |
| | | |
| | | import com.tcm.training.admin.common.ApiResponse; |
| | | import com.tcm.training.admin.live.service.LiveRoomSettingService; |
| | | import com.tcm.training.admin.live.vo.LiveRoomSettingVO; |
| | | import org.springframework.web.bind.annotation.GetMapping; |
| | | import org.springframework.web.bind.annotation.RequestMapping; |
| | | import org.springframework.web.bind.annotation.RestController; |
| | | |
| | | @RestController |
| | | @RequestMapping({"/api/app/home", "/api/v1/app/home"}) |
| | | public class AppLiveRoomSettingController { |
| | | private final LiveRoomSettingService service; |
| | | |
| | | public AppLiveRoomSettingController(LiveRoomSettingService service) { |
| | | this.service = service; |
| | | } |
| | | |
| | | @GetMapping("/live-room-setting") |
| | | public ApiResponse<LiveRoomSettingVO> current() { |
| | | return ApiResponse.ok(service.current()); |
| | | } |
| | | } |
| | |
| | | null, null, null, 16, "live:session:delete", "直播场次删除按钮")); |
| | | menuIds.add(saveMenu(3077L, 3060L, "推送开播提醒", "live_session_push_notification", "B", |
| | | null, null, null, 17, "live:session:push-notification", "直播场次推送提醒按钮")); |
| | | menuIds.add(saveMenu(3080L, 3050L, "直播预约记录", "live_reserve_view", "C", |
| | | menuIds.add(saveMenu(6000L, 3050L, "微赞直播间设置", "live_room_setting", "C", |
| | | "/live/course/live-room-setting", "Live/LiveRoomSetting", null, 18, "live:room-setting:view", "微赞直播间设置菜单")); |
| | | menuIds.add(saveMenu(6001L, 6000L, "编辑微赞直播间设置", "live_room_setting_edit", "B", |
| | | null, null, null, 1, "live:room-setting:edit", "微赞直播间设置编辑按钮")); menuIds.add(saveMenu(3080L, 3050L, "直播预约记录", "live_reserve_view", "C", |
| | | "/live/course/reserve-records", "Live/ReserveRecords", null, 20, "live:reserve:view", "直播预约记录菜单")); |
| | | menuIds.add(saveMenu(3081L, 3050L, "直播观看记录", "live_watch_view", "C", |
| | | "/live/course/watch-records", "Live/WatchRecords", null, 21, "live:watch:view", "直播观看记录菜单")); |
| New file |
| | |
| | | package com.tcm.training.admin.live.controller; |
| | | |
| | | import com.tcm.training.admin.common.ApiResponse; |
| | | import com.tcm.training.admin.live.dto.LiveRoomSettingSaveRequest; |
| | | import com.tcm.training.admin.live.service.LiveRoomSettingService; |
| | | import com.tcm.training.admin.live.vo.LiveRoomSettingVO; |
| | | import org.springframework.validation.annotation.Validated; |
| | | import org.springframework.web.bind.annotation.GetMapping; |
| | | import org.springframework.web.bind.annotation.PutMapping; |
| | | import org.springframework.web.bind.annotation.RequestBody; |
| | | import org.springframework.web.bind.annotation.RequestMapping; |
| | | import org.springframework.web.bind.annotation.RestController; |
| | | |
| | | import javax.validation.Valid; |
| | | |
| | | @Validated |
| | | @RestController |
| | | @RequestMapping({"/api/v1", "/api"}) |
| | | public class LiveRoomSettingController { |
| | | private final LiveRoomSettingService service; |
| | | |
| | | public LiveRoomSettingController(LiveRoomSettingService service) { |
| | | this.service = service; |
| | | } |
| | | |
| | | @GetMapping("/live-room-settings/current") |
| | | public ApiResponse<LiveRoomSettingVO> current() { |
| | | return ApiResponse.ok(service.current()); |
| | | } |
| | | |
| | | @PutMapping("/live-room-settings/current") |
| | | public ApiResponse<LiveRoomSettingVO> save(@Valid @RequestBody LiveRoomSettingSaveRequest request) { |
| | | return ApiResponse.ok(service.save(request)); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.tcm.training.admin.live.dto; |
| | | |
| | | import lombok.Getter; |
| | | import lombok.NoArgsConstructor; |
| | | import lombok.Setter; |
| | | |
| | | import javax.validation.constraints.NotBlank; |
| | | import javax.validation.constraints.Size; |
| | | |
| | | @Getter |
| | | @Setter |
| | | @NoArgsConstructor |
| | | public class LiveRoomSettingSaveRequest { |
| | | @NotBlank(message = "请上传直播间 Banner 图片") |
| | | @Size(max = 500, message = "Banner 图片地址不能超过500个字符") |
| | | private String bannerImageUrl; |
| | | |
| | | @Size(max = 500, message = "直播间地址不能超过500个字符") |
| | | private String liveRoomUrl; |
| | | } |
| New file |
| | |
| | | package com.tcm.training.admin.live.entity; |
| | | |
| | | import com.baomidou.mybatisplus.annotation.TableField; |
| | | import com.baomidou.mybatisplus.annotation.TableName; |
| | | import lombok.Getter; |
| | | import lombok.NoArgsConstructor; |
| | | import lombok.Setter; |
| | | |
| | | @TableName("tcm_live_room_setting") |
| | | @Getter |
| | | @Setter |
| | | @NoArgsConstructor |
| | | public class LiveRoomSetting { |
| | | @TableField("banner_image_url") |
| | | private String bannerImageUrl; |
| | | |
| | | @TableField("live_room_url") |
| | | private String liveRoomUrl; |
| | | } |
| New file |
| | |
| | | package com.tcm.training.admin.live.mapper; |
| | | |
| | | import com.tcm.training.admin.live.entity.LiveRoomSetting; |
| | | import org.apache.ibatis.annotations.Insert; |
| | | import org.apache.ibatis.annotations.Mapper; |
| | | import org.apache.ibatis.annotations.Select; |
| | | import org.apache.ibatis.annotations.Update; |
| | | |
| | | @Mapper |
| | | public interface LiveRoomSettingMapper { |
| | | @Select("SELECT banner_image_url, live_room_url FROM tcm_live_room_setting LIMIT 1") |
| | | LiveRoomSetting selectCurrent(); |
| | | |
| | | @Insert("INSERT INTO tcm_live_room_setting (banner_image_url, live_room_url) VALUES (#{bannerImageUrl}, #{liveRoomUrl})") |
| | | int insertCurrent(LiveRoomSetting setting); |
| | | |
| | | @Update("UPDATE tcm_live_room_setting SET banner_image_url = #{bannerImageUrl}, live_room_url = #{liveRoomUrl}") |
| | | int updateCurrent(LiveRoomSetting setting); |
| | | } |
| New file |
| | |
| | | package com.tcm.training.admin.live.service; |
| | | |
| | | import com.tcm.training.admin.live.dto.LiveRoomSettingSaveRequest; |
| | | import com.tcm.training.admin.live.entity.LiveRoomSetting; |
| | | import com.tcm.training.admin.live.mapper.LiveRoomSettingMapper; |
| | | import com.tcm.training.admin.live.vo.LiveRoomSettingVO; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | |
| | | @Service |
| | | public class LiveRoomSettingService { |
| | | private final LiveRoomSettingMapper mapper; |
| | | |
| | | public LiveRoomSettingService(LiveRoomSettingMapper mapper) { |
| | | this.mapper = mapper; |
| | | } |
| | | |
| | | public LiveRoomSettingVO current() { |
| | | return toVO(mapper.selectCurrent()); |
| | | } |
| | | |
| | | @Transactional |
| | | public LiveRoomSettingVO save(LiveRoomSettingSaveRequest request) { |
| | | LiveRoomSetting setting = mapper.selectCurrent(); |
| | | if (setting == null) { |
| | | setting = new LiveRoomSetting(); |
| | | fill(setting, request); |
| | | mapper.insertCurrent(setting); |
| | | } else { |
| | | fill(setting, request); |
| | | mapper.updateCurrent(setting); |
| | | } |
| | | return toVO(setting); |
| | | } |
| | | |
| | | private void fill(LiveRoomSetting setting, LiveRoomSettingSaveRequest request) { |
| | | setting.setBannerImageUrl(request.getBannerImageUrl().trim()); |
| | | String liveRoomUrl = request.getLiveRoomUrl(); |
| | | setting.setLiveRoomUrl(liveRoomUrl == null || liveRoomUrl.trim().isEmpty() ? null : liveRoomUrl.trim()); |
| | | } |
| | | |
| | | private LiveRoomSettingVO toVO(LiveRoomSetting setting) { |
| | | if (setting == null) { |
| | | return null; |
| | | } |
| | | LiveRoomSettingVO vo = new LiveRoomSettingVO(); |
| | | vo.setBannerImageUrl(setting.getBannerImageUrl()); |
| | | vo.setLiveRoomUrl(setting.getLiveRoomUrl()); |
| | | return vo; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.tcm.training.admin.live.vo; |
| | | |
| | | import lombok.Getter; |
| | | import lombok.NoArgsConstructor; |
| | | import lombok.Setter; |
| | | |
| | | @Getter |
| | | @Setter |
| | | @NoArgsConstructor |
| | | public class LiveRoomSettingVO { |
| | | private String bannerImageUrl; |
| | | private String liveRoomUrl; |
| | | } |
| | |
| | | component: './live/broadcast/edit', |
| | | }, |
| | | { |
| | | name: '微赞直播间设置', |
| | | path: '/live/course/live-room-setting', |
| | | access: 'live:room-setting:view', |
| | | component: './live/live-room-setting', |
| | | }, { |
| | | name: '直播预约记录', |
| | | path: '/live/course/reserve-records', |
| | | access: 'live:reserve:view', |
| New file |
| | |
| | | import { PlusOutlined } from '@ant-design/icons'; |
| | | import { history, useAccess } from '@umijs/max'; |
| | | import { Button, Form, Image, Input, Spin, Upload, message } from 'antd'; |
| | | import React, { useEffect, useState } from 'react'; |
| | | import { getLiveRoomSetting, saveLiveRoomSetting, uploadLiveRoomBanner } from './service'; |
| | | import './style.less'; |
| | | |
| | | type FormValues = { bannerImageUrl?: string; liveRoomUrl?: string }; |
| | | |
| | | const LiveRoomSettingPage: React.FC = () => { |
| | | const access = useAccess(); |
| | | const [form] = Form.useForm<FormValues>(); |
| | | const [loading, setLoading] = useState(false); |
| | | const [submitting, setSubmitting] = useState(false); |
| | | const [uploading, setUploading] = useState(false); |
| | | const canView = access['live:room-setting:view']; |
| | | const canEdit = access['live:room-setting:edit']; |
| | | const bannerImageUrl = Form.useWatch('bannerImageUrl', form); |
| | | |
| | | const loadCurrent = async () => { |
| | | setLoading(true); |
| | | try { |
| | | const res = await getLiveRoomSetting(); |
| | | form.setFieldsValue({ |
| | | bannerImageUrl: res.data?.bannerImageUrl || '', |
| | | liveRoomUrl: res.data?.liveRoomUrl || '', |
| | | }); |
| | | } catch (error) { |
| | | message.error((error as Error)?.message || '直播间设置加载失败'); |
| | | } finally { |
| | | setLoading(false); |
| | | } |
| | | }; |
| | | |
| | | useEffect(() => { |
| | | if (!canView) { |
| | | history.replace('/Welcome'); |
| | | return; |
| | | } |
| | | loadCurrent(); |
| | | }, [canView]); |
| | | |
| | | const handleSubmit = async () => { |
| | | if (!canEdit || submitting) return; |
| | | const values = await form.validateFields(); |
| | | setSubmitting(true); |
| | | try { |
| | | await saveLiveRoomSetting({ |
| | | bannerImageUrl: values.bannerImageUrl!, |
| | | liveRoomUrl: values.liveRoomUrl?.trim() || undefined, |
| | | }); |
| | | message.success('保存成功'); |
| | | await loadCurrent(); |
| | | } catch (error) { |
| | | message.error((error as Error)?.message || '保存失败,请稍后重试'); |
| | | } finally { |
| | | setSubmitting(false); |
| | | } |
| | | }; |
| | | |
| | | return ( |
| | | <div className="live-room-setting-page"> |
| | | <div className="live-room-setting-card"> |
| | | <div className="live-room-setting-title">微赞直播间设置</div> |
| | | <Spin spinning={loading}> |
| | | <Form form={form} layout="vertical" disabled={loading || !canEdit} className="live-room-setting-form"> |
| | | <Form.Item label="Banner图片" name="bannerImageUrl" rules={[{ required: true, message: '请上传Banner图片' }]}> |
| | | <Input type="hidden" /> |
| | | </Form.Item> |
| | | <div className="live-room-setting-upload-row"> |
| | | {bannerImageUrl ? <Image src={bannerImageUrl} className="live-room-setting-preview" /> : null} |
| | | <Upload |
| | | accept="image/*" |
| | | showUploadList={false} |
| | | customRequest={async (options) => { |
| | | setUploading(true); |
| | | try { |
| | | const res = await uploadLiveRoomBanner(options.file as File); |
| | | const url = res.data?.url; |
| | | if (!url) throw new Error('上传失败'); |
| | | form.setFieldValue('bannerImageUrl', url); |
| | | await form.validateFields(['bannerImageUrl']); |
| | | options.onSuccess?.(res); |
| | | } catch (error) { |
| | | message.error((error as Error)?.message || '图片上传失败'); |
| | | options.onError?.(error as Error); |
| | | } finally { |
| | | setUploading(false); |
| | | } |
| | | }} |
| | | > |
| | | <Button icon={<PlusOutlined />} loading={uploading}>上传图片</Button> |
| | | </Upload> |
| | | <span className="live-room-setting-tip">建议尺寸 750 × 320px,支持 JPG、PNG</span> |
| | | </div> |
| | | <Form.Item label="直播间地址" name="liveRoomUrl" rules={[{ type: 'url', message: '请输入正确的直播间地址' }, { max: 500 }]}> |
| | | <Input placeholder="请输入微赞直播间地址(可不填)" allowClear /> |
| | | </Form.Item> |
| | | <Button type="primary" loading={submitting} disabled={!canEdit} onClick={handleSubmit}>保存</Button> |
| | | </Form> |
| | | </Spin> |
| | | </div> |
| | | </div> |
| | | ); |
| | | }; |
| | | |
| | | export default LiveRoomSettingPage; |
| New file |
| | |
| | | import { request } from '@umijs/max'; |
| | | |
| | | export async function getLiveRoomSetting() { |
| | | return request('/live-room-settings/current', { method: 'GET' }); |
| | | } |
| | | |
| | | export async function saveLiveRoomSetting(data: { bannerImageUrl: string; liveRoomUrl?: string }) { |
| | | return request('/live-room-settings/current', { method: 'PUT', data }); |
| | | } |
| | | |
| | | export async function uploadLiveRoomBanner(file: File) { |
| | | const formData = new FormData(); |
| | | formData.append('file', file); |
| | | return request('/upload/image', { method: 'POST', data: formData }); |
| | | } |
| New file |
| | |
| | | .live-room-setting-page { min-height: calc(100vh - 48px); padding: 20px; background: #f2f5f9; } |
| | | .live-room-setting-card { min-height: 620px; padding: 24px; border-radius: 10px; background: #fff; box-shadow: 0 2px 10px rgba(15, 23, 42, .04); } |
| | | .live-room-setting-title { margin-bottom: 28px; color: #303133; font-size: 18px; font-weight: 500; } |
| | | .live-room-setting-form { width: 620px; } |
| | | .live-room-setting-upload-row { display: flex; align-items: center; gap: 16px; margin: -14px 0 28px; } |
| | | .live-room-setting-preview { width: 300px; height: 128px; border-radius: 8px; object-fit: cover; border: 1px solid #ebeef5; } |
| | | .live-room-setting-tip { color: #909399; font-size: 13px; } |
| | | @media (max-width: 800px) { .live-room-setting-form { width: 100%; } .live-room-setting-upload-row { align-items: flex-start; flex-direction: column; } } |
| | |
| | | <view class="quick-entry-row"> |
| | | <view class="quick-entry-card" hover-class="quick-entry-card--pressed" hover-stay-time="80" |
| | | @tap="goWeiZanLive"> |
| | | <image src="/static/image/home/weizan-live.png" mode="aspectFill" class="quick-entry-card__img"></image> |
| | | <image :src="weiZanLiveBanner || '/static/image/home/weizan-live.png'" mode="aspectFill" class="quick-entry-card__img"></image> |
| | | </view> |
| | | <view class="quick-entry-card" hover-class="quick-entry-card--pressed" hover-stay-time="80" |
| | | @tap="goOperatorTraining"> |
| | |
| | | <script setup lang="ts"> |
| | | import { computed, ref, onMounted } from "vue"; |
| | | import { onPullDownRefresh, onShow } from "@dcloudio/uni-app"; |
| | | import { getBannerList, getHomeFeaturedContent, getHomeLiveCourseList } from "./service"; |
| | | import { getBannerList, getHomeFeaturedContent, getHomeLiveCourseList, getLiveRoomSetting } from "./service"; |
| | | |
| | | interface HomeBannerItem { |
| | | id : number | string; |
| | |
| | | const systemInfo = uni.getSystemInfoSync(); |
| | | return String(systemInfo.platform || '').toLowerCase() === 'ios'; |
| | | }); |
| | | const weiZanLiveH5Url = 'https://www.vzan.com'; |
| | | const weiZanLiveH5Url = ref(''); |
| | | const weiZanLiveBanner = ref(''); |
| | | |
| | | const operatorTrainingLiveCourse = computed(() => { |
| | | const list = liveCourseList.value; |
| | |
| | | /** |
| | | * 请求首页顶部 banner 列表。 |
| | | */ |
| | | const loadLiveRoomSetting = async () => { |
| | | try { |
| | | const res = await getLiveRoomSetting(); |
| | | if (res?.code === 0 && res.data) { |
| | | weiZanLiveBanner.value = res.data.bannerImageUrl || ''; |
| | | weiZanLiveH5Url.value = res.data.liveRoomUrl || ''; |
| | | } |
| | | } catch { |
| | | weiZanLiveBanner.value = ''; |
| | | weiZanLiveH5Url.value = ''; |
| | | } |
| | | }; |
| | | const loadBannerList = async () => { |
| | | try { |
| | | const res = await getBannerList(); |
| | |
| | | } |
| | | |
| | | /** |
| | | * 跳转微赞直播 H5 页面(后端接口暂未提供,先使用静态链接)。 |
| | | * 跳转后台配置的微赞直播 H5 页面。 |
| | | */ |
| | | const goWeiZanLive = () => { |
| | | if (!weiZanLiveH5Url.value) { |
| | | uni.showToast({ title: '直播间地址暂未配置', icon: 'none' }); |
| | | return; |
| | | } |
| | | uni.navigateTo({ |
| | | url: `/pages/webview/webview?url=${encodeURIComponent(weiZanLiveH5Url)}&title=${encodeURIComponent('微赞直播')}` |
| | | url: `/pages/webview/webview?url=${encodeURIComponent(weiZanLiveH5Url.value)}&title=${encodeURIComponent('微赞直播')}` |
| | | }); |
| | | }; |
| | | |
| | |
| | | try { |
| | | await Promise.allSettled([ |
| | | loadBannerList(), |
| | | loadLiveRoomSetting(), |
| | | loadHomeLiveCourseList(), |
| | | loadFeaturedContent() |
| | | ]); |
| | |
| | | export const getNoticeList = (params) => { |
| | | return request.get(`/index/getNoticeList`, params, 1) |
| | | } |
| | | /** |
| | | * 获取 APP 首页微赞直播间配置。 |
| | | */ |
| | | export const getLiveRoomSetting = () => { |
| | | return publicGet('/api/app/home/live-room-setting') |
| | | } |