| | |
| | | UPDATE sys_menu SET name = '新增健康知识', remark = '健康知识新增按钮' WHERE id = 3121; |
| | | UPDATE sys_menu SET name = '编辑健康知识', remark = '健康知识编辑按钮' WHERE id = 3122; |
| | | UPDATE sys_menu SET name = '删除健康知识', remark = '健康知识删除按钮' WHERE id = 3123; |
| | | |
| | | -- ---------------------------- |
| | | -- 新增「养生科普」菜单及按钮权限(宣传知识 6100 下,与健康知识平级) |
| | | -- 养生科普与健康知识共用 tcm_health_popularization 表,contentType=2(仅视频) |
| | | -- 主菜单 id: 6110,子按钮 6111/6112/6113 |
| | | -- 添加时间:2026-08-22 |
| | | -- ---------------------------- |
| | | INSERT INTO sys_menu (id, parent_id, name, menu_code, type, path, component, icon, sort, status, visible, permission_key, remark, deleted) |
| | | VALUES (6110, 6100, '养生科普', 'live_health_promotion_manage', 'C', '/live/promotion/health-promotion', 'Live/HealthPromotionList', NULL, 51, 1, 1, 'live:health-promotion: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 (6111, 6110, '新增养生科普', 'live_health_promotion_add', 'B', NULL, NULL, NULL, 1, 1, 1, 'live:health-promotion:add', '养生科普新增按钮', 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; |
| | | |
| | | INSERT INTO sys_menu (id, parent_id, name, menu_code, type, path, component, icon, sort, status, visible, permission_key, remark, deleted) |
| | | VALUES (6112, 6110, '编辑养生科普', 'live_health_promotion_edit', 'B', NULL, NULL, NULL, 2, 1, 1, 'live:health-promotion: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; |
| | | |
| | | INSERT INTO sys_menu (id, parent_id, name, menu_code, type, path, component, icon, sort, status, visible, permission_key, remark, deleted) |
| | | VALUES (6113, 6110, '删除养生科普', 'live_health_promotion_delete', 'B', NULL, NULL, NULL, 3, 1, 1, 'live:health-promotion:delete', '养生科普删除按钮', 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; |
| | |
| | | null, null, null, 2, "live:health-popularization:edit", "健康知识编辑按钮")); |
| | | menuIds.add(saveMenu(3123L, 3120L, "删除健康知识", "live_health_popularization_delete", "B", |
| | | null, null, null, 3, "live:health-popularization:delete", "健康知识删除按钮")); |
| | | menuIds.add(saveMenu(6110L, 6100L, "养生科普", "live_health_promotion_manage", "C", |
| | | "/live/promotion/health-promotion", "Live/HealthPromotionList", null, 51, "live:health-promotion:view", "养生科普菜单")); |
| | | menuIds.add(saveMenu(6111L, 6110L, "新增养生科普", "live_health_promotion_add", "B", |
| | | null, null, null, 1, "live:health-promotion:add", "养生科普新增按钮")); |
| | | menuIds.add(saveMenu(6112L, 6110L, "编辑养生科普", "live_health_promotion_edit", "B", |
| | | null, null, null, 2, "live:health-promotion:edit", "养生科普编辑按钮")); |
| | | menuIds.add(saveMenu(6113L, 6110L, "删除养生科普", "live_health_promotion_delete", "B", |
| | | null, null, null, 3, "live:health-promotion:delete", "养生科普删除按钮")); |
| | | menuIds.add(saveMenu(6100L, 3000L, "宣传知识", "live_promotion_root", "M", |
| | | "/live/promotion", null, null, 20, null, "宣传知识目录")); |
| | | menuIds.add(saveMenu(6010L, 6100L, "项目介绍", "live_project_introduction_manage", "C", |
| | |
| | | @Size(max = 255, message = "视频文件名长度不能超过255个字符") |
| | | private String videoFileName; |
| | | |
| | | @Size(max = 255, message = "视频介绍长度不能超过255个字符") |
| | | private String videoDescription; |
| | | |
| | | @NotNull(message = "请选择是否启用") |
| | | private Integer status; |
| | | |
| | |
| | | @TableField("video_file_name") |
| | | private String videoFileName; |
| | | |
| | | @TableField("video_description") |
| | | private String videoDescription; |
| | | |
| | | private Integer status; |
| | | |
| | | @TableField("is_home_recommend") |
| | |
| | | item.setContent(content); |
| | | item.setVideoUrl(null); |
| | | item.setVideoFileName(null); |
| | | item.setVideoDescription(null); |
| | | return; |
| | | } |
| | | |
| | |
| | | item.setContent(null); |
| | | item.setVideoUrl(videoUrl); |
| | | item.setVideoFileName(trimToNull(req.getVideoFileName())); |
| | | item.setVideoDescription(trimToNull(req.getVideoDescription())); |
| | | } |
| | | |
| | | private int validateContentType(Integer contentType) { |
| | |
| | | vo.setContent(item.getContent()); |
| | | vo.setVideoUrl(item.getVideoUrl()); |
| | | vo.setVideoFileName(item.getVideoFileName()); |
| | | vo.setVideoDescription(item.getVideoDescription()); |
| | | vo.setStatus(item.getStatus()); |
| | | vo.setIsHomeRecommend(item.getIsHomeRecommend()); |
| | | vo.setClickCount(defaultNumber(item.getClickCount())); |
| | |
| | | private String content; |
| | | private String videoUrl; |
| | | private String videoFileName; |
| | | private String videoDescription; |
| | | private Integer status; |
| | | private Integer isHomeRecommend; |
| | | private Integer clickCount; |
| | |
| | | component: './live/health-popularization/manage', |
| | | }, |
| | | { |
| | | name: '养生科普', |
| | | path: '/live/promotion/health-promotion', |
| | | access: 'live:health-promotion:view', |
| | | component: './live/health-promotion', |
| | | }, |
| | | { |
| | | name: '新增养生科普', |
| | | path: '/live/promotion/health-promotion/create', |
| | | hideInMenu: true, |
| | | access: 'live:health-promotion:add', |
| | | component: './live/health-promotion/manage', |
| | | }, |
| | | { |
| | | name: '编辑养生科普', |
| | | path: '/live/promotion/health-promotion/:id/edit', |
| | | hideInMenu: true, |
| | | access: 'live:health-promotion:edit', |
| | | component: './live/health-promotion/manage', |
| | | }, |
| | | { |
| | | name: '项目介绍', |
| | | path: '/live/promotion/project-introduction', |
| | | access: 'live:project-introduction:view', |
| | |
| | | !!permission?.['live:watch:view'] || |
| | | !!permission?.['live:expert:view'] || |
| | | !!permission?.['live:project-introduction:view'] || |
| | | !!permission?.['live:project-introduction-password:view'], |
| | | !!permission?.['live:project-introduction-password:view'] || |
| | | !!permission?.['live:health-popularization:view'] || |
| | | !!permission?.['live:health-promotion:view'], |
| | | canViewTradeFinanceModule: |
| | | !!permission?.['course:purchase-order:view'] || |
| | | !!permission?.['member:purchase-order:view'] || |
| New file |
| | |
| | | import { |
| | | deleteHealthPromotion, |
| | | getHealthPromotionPage, |
| | | } from './service'; |
| | | import { history, useAccess } from '@umijs/max'; |
| | | import { Button, Form, Image, Input, Modal, Pagination, Select, Space, Table, message } from 'antd'; |
| | | import type { ColumnsType } from 'antd/es/table'; |
| | | import React, { useEffect, useState } from 'react'; |
| | | import './style.less'; |
| | | |
| | | type SearchValues = { |
| | | keyword?: string; |
| | | status?: number; |
| | | }; |
| | | |
| | | type PromotionItem = { |
| | | id: number; |
| | | title: string; |
| | | coverImg?: string; |
| | | clickCount?: number; |
| | | sort?: number; |
| | | contentType: number; |
| | | status: number; |
| | | createdAt: string; |
| | | }; |
| | | |
| | | const contentTypeOptions = [ |
| | | { label: '图文', value: 1 }, |
| | | { label: '视频', value: 2 }, |
| | | ]; |
| | | |
| | | const statusOptions = [ |
| | | { label: '启用', value: 1 }, |
| | | { label: '禁用', value: 0 }, |
| | | ]; |
| | | |
| | | // 养生科普只有视频内容,固定 contentType=2 |
| | | const trimSearchValues = (values: SearchValues) => ({ |
| | | keyword: values.keyword?.trim() || undefined, |
| | | contentType: 2, |
| | | status: values.status, |
| | | }); |
| | | |
| | | const contentTypeText = (value?: number) => |
| | | contentTypeOptions.find((item) => item.value === value)?.label || '-'; |
| | | |
| | | const statusText = (value?: number) => (value === 1 ? '启用' : '禁用'); |
| | | |
| | | const HealthPromotionPage: React.FC = () => { |
| | | const access = useAccess(); |
| | | const [searchForm] = Form.useForm<SearchValues>(); |
| | | const [list, setList] = useState<PromotionItem[]>([]); |
| | | const [loading, setLoading] = useState(false); |
| | | const [pagination, setPagination] = useState({ page: 1, pageSize: 10, total: 0 }); |
| | | |
| | | const canView = access['live:health-promotion:view']; |
| | | const canAdd = access['live:health-promotion:add']; |
| | | const canEdit = access['live:health-promotion:edit']; |
| | | const canDelete = access['live:health-promotion:delete']; |
| | | const refreshKey = history.location.search; |
| | | |
| | | const loadList = async (page = pagination.page, pageSize = pagination.pageSize) => { |
| | | setLoading(true); |
| | | try { |
| | | const res = await getHealthPromotionPage({ |
| | | page, |
| | | pageSize, |
| | | ...trimSearchValues(searchForm.getFieldsValue()), |
| | | }); |
| | | setList(res.data.list || []); |
| | | setPagination({ |
| | | page: Number(res.data.page || page), |
| | | pageSize: Number(res.data.pageSize || pageSize), |
| | | total: Number(res.data.total || 0), |
| | | }); |
| | | } finally { |
| | | setLoading(false); |
| | | } |
| | | }; |
| | | |
| | | useEffect(() => { |
| | | if (!canView) { |
| | | history.replace('/Welcome'); |
| | | return; |
| | | } |
| | | loadList(1, 10); |
| | | }, [canView, refreshKey]); |
| | | |
| | | const handleDelete = (record: PromotionItem) => { |
| | | Modal.confirm({ |
| | | title: '确定要删除该养生科普吗?', |
| | | okText: '删除', |
| | | cancelText: '取消', |
| | | onOk: async () => { |
| | | await deleteHealthPromotion(record.id); |
| | | message.success('删除成功'); |
| | | const nextPage = list.length === 1 && pagination.page > 1 ? pagination.page - 1 : pagination.page; |
| | | await loadList(nextPage, pagination.pageSize); |
| | | }, |
| | | }); |
| | | }; |
| | | |
| | | const columns: ColumnsType<PromotionItem> = [ |
| | | { |
| | | title: '序号', |
| | | width: 72, |
| | | render: (_, __, index) => (pagination.page - 1) * pagination.pageSize + index + 1, |
| | | }, |
| | | { |
| | | title: '标题', |
| | | dataIndex: 'title', |
| | | width: 320, |
| | | render: (_, record) => ( |
| | | <div className="health-promotion-table__title"> |
| | | {record.coverImg ? ( |
| | | <Image src={record.coverImg} width={64} height={40} preview={false} /> |
| | | ) : ( |
| | | <div className="health-promotion-table__cover-placeholder"> |
| | | {record.title?.slice(0, 2) || '-'} |
| | | </div> |
| | | )} |
| | | <span>{record.title || '-'}</span> |
| | | </div> |
| | | ), |
| | | }, |
| | | { |
| | | title: '点击量', |
| | | dataIndex: 'clickCount', |
| | | width: 120, |
| | | render: (value) => value || 0, |
| | | }, |
| | | { |
| | | title: '排序', |
| | | dataIndex: 'sort', |
| | | width: 100, |
| | | render: (value) => value ?? 0, |
| | | }, |
| | | { |
| | | title: '类型', |
| | | dataIndex: 'contentType', |
| | | width: 100, |
| | | render: (value) => contentTypeText(value), |
| | | }, |
| | | { |
| | | title: '状态', |
| | | dataIndex: 'status', |
| | | width: 100, |
| | | render: (value) => statusText(value), |
| | | }, |
| | | { |
| | | title: '创建时间', |
| | | dataIndex: 'createdAt', |
| | | width: 180, |
| | | }, |
| | | { |
| | | title: '操作', |
| | | width: 180, |
| | | render: (_, record) => ( |
| | | <Space size={16} className="health-promotion-table__actions"> |
| | | {canEdit ? ( |
| | | <button |
| | | type="button" |
| | | onClick={() => history.push(`/live/promotion/health-promotion/${record.id}/edit`)} |
| | | > |
| | | 编辑 |
| | | </button> |
| | | ) : null} |
| | | {canDelete ? ( |
| | | <button type="button" onClick={() => handleDelete(record)}> |
| | | 删除 |
| | | </button> |
| | | ) : null} |
| | | </Space> |
| | | ), |
| | | }, |
| | | ]; |
| | | |
| | | return ( |
| | | <div className="health-promotion-page"> |
| | | <div className="health-promotion-page__panel"> |
| | | <div className="health-promotion-page__title">养生科普</div> |
| | | <div className="health-promotion-search-card"> |
| | | <Form form={searchForm} layout="horizontal" className="health-promotion-search-form"> |
| | | <Form.Item label="标题" name="keyword"> |
| | | <Input placeholder="请输入" allowClear /> |
| | | </Form.Item> |
| | | <Form.Item label="状态" name="status"> |
| | | <Select placeholder="全部" allowClear options={statusOptions} /> |
| | | </Form.Item> |
| | | <div className="health-promotion-search-form__actions"> |
| | | <Button |
| | | onClick={async () => { |
| | | searchForm.resetFields(); |
| | | await loadList(1, pagination.pageSize); |
| | | }} |
| | | > |
| | | 重置 |
| | | </Button> |
| | | <Button type="primary" onClick={() => loadList(1, pagination.pageSize)}> |
| | | 搜索 |
| | | </Button> |
| | | </div> |
| | | </Form> |
| | | </div> |
| | | |
| | | <div className="health-promotion-toolbar"> |
| | | {canAdd ? ( |
| | | <Button type="primary" onClick={() => history.push('/live/promotion/health-promotion/create')}> |
| | | 添加 |
| | | </Button> |
| | | ) : null} |
| | | </div> |
| | | |
| | | <div className="health-promotion-table-card"> |
| | | <Table<PromotionItem> |
| | | rowKey="id" |
| | | columns={columns} |
| | | dataSource={list} |
| | | loading={loading} |
| | | pagination={false} |
| | | scroll={{ x: 980 }} |
| | | /> |
| | | <div className="health-promotion-pagination"> |
| | | <Pagination |
| | | current={pagination.page} |
| | | pageSize={pagination.pageSize} |
| | | total={pagination.total} |
| | | showSizeChanger |
| | | showQuickJumper |
| | | showTotal={(total) => `共 ${total} 条`} |
| | | onChange={(page, pageSize) => loadList(page, pageSize)} |
| | | /> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | ); |
| | | }; |
| | | |
| | | export default HealthPromotionPage; |
| New file |
| | |
| | | import { |
| | | createHealthPromotion, |
| | | getHealthPromotionDetail, |
| | | updateHealthPromotion, |
| | | uploadHealthPromotionImage, |
| | | uploadHealthPromotionVideo, |
| | | } from './service'; |
| | | import { PlusOutlined, UploadOutlined } from '@ant-design/icons'; |
| | | import { history, useAccess } from '@umijs/max'; |
| | | import { Button, Form, Image, Input, InputNumber, Switch, Upload, message } from 'antd'; |
| | | import React, { useEffect, useMemo, useState } from 'react'; |
| | | import './style.less'; |
| | | |
| | | const HealthPromotionManagePage: React.FC = () => { |
| | | const access = useAccess(); |
| | | const [form] = Form.useForm(); |
| | | const [loading, setLoading] = useState(false); |
| | | const [submitting, setSubmitting] = useState(false); |
| | | const [uploadingCover, setUploadingCover] = useState(false); |
| | | const [uploadingVideo, setUploadingVideo] = useState(false); |
| | | const [coverImg, setCoverImg] = useState(''); |
| | | const pathname = history.location.pathname; |
| | | const isCreate = pathname.endsWith('/create'); |
| | | const listPagePath = '/live/promotion/health-promotion'; |
| | | const recordId = useMemo(() => { |
| | | if (isCreate) { |
| | | return ''; |
| | | } |
| | | const segments = pathname.split('/'); |
| | | return segments[segments.length - 2] || ''; |
| | | }, [isCreate, pathname]); |
| | | const canAccess = isCreate ? access['live:health-promotion:add'] : access['live:health-promotion:edit']; |
| | | |
| | | useEffect(() => { |
| | | if (!canAccess) { |
| | | history.replace('/Welcome'); |
| | | return; |
| | | } |
| | | const bootstrap = async () => { |
| | | setLoading(true); |
| | | try { |
| | | if (isCreate) { |
| | | setCoverImg(''); |
| | | form.setFieldsValue({ |
| | | title: '', |
| | | videoUrl: '', |
| | | videoFileName: '', |
| | | videoDescription: '', |
| | | sort: 0, |
| | | status: true, |
| | | isHomeRecommend: true, |
| | | }); |
| | | return; |
| | | } |
| | | const res = await getHealthPromotionDetail(recordId); |
| | | const detail = res.data; |
| | | setCoverImg(detail.coverImg || ''); |
| | | form.setFieldsValue({ |
| | | title: detail.title, |
| | | coverImg: detail.coverImg, |
| | | videoUrl: detail.videoUrl || '', |
| | | videoFileName: detail.videoFileName || '', |
| | | videoDescription: detail.videoDescription || '', |
| | | sort: detail.sort ?? 0, |
| | | status: detail.status === 1, |
| | | isHomeRecommend: detail.isHomeRecommend === 1, |
| | | }); |
| | | } finally { |
| | | setLoading(false); |
| | | } |
| | | }; |
| | | bootstrap(); |
| | | }, [canAccess, form, isCreate, recordId]); |
| | | |
| | | const handleSubmit = async () => { |
| | | const values = await form.validateFields(); |
| | | const payload = { |
| | | title: values.title?.trim(), |
| | | coverImg, |
| | | // 养生科普固定为视频类型 |
| | | contentType: 2, |
| | | videoUrl: values.videoUrl?.trim(), |
| | | videoFileName: values.videoFileName?.trim(), |
| | | videoDescription: values.videoDescription?.trim(), |
| | | sort: values.sort, |
| | | status: values.status ? 1 : 0, |
| | | isHomeRecommend: values.isHomeRecommend ? 1 : 0, |
| | | }; |
| | | setSubmitting(true); |
| | | try { |
| | | if (isCreate) { |
| | | await createHealthPromotion(payload); |
| | | message.success('新增成功'); |
| | | } else { |
| | | await updateHealthPromotion(recordId, payload); |
| | | message.success('保存成功'); |
| | | } |
| | | history.replace(`${listPagePath}?refresh=${Date.now()}`); |
| | | } finally { |
| | | setSubmitting(false); |
| | | } |
| | | }; |
| | | |
| | | return ( |
| | | <div className="health-promotion-page"> |
| | | <div className="health-promotion-page__panel"> |
| | | <div className="health-promotion-page__title">{isCreate ? '添加养生科普' : '编辑养生科普'}</div> |
| | | <Form form={form} layout="vertical" className="health-promotion-edit-form"> |
| | | <div className="health-promotion-edit-form__section-title">信息</div> |
| | | <div className="health-promotion-edit-form__grid"> |
| | | <Form.Item label="标题" name="title" rules={[{ required: true, message: '请输入标题' }]}> |
| | | <Input placeholder="请输入" maxLength={128} disabled={loading} /> |
| | | </Form.Item> |
| | | <Form.Item label="视频封面图" className="health-promotion-edit-form__upload" required> |
| | | <Form.Item name="coverImg" rules={[{ required: true, message: '请上传视频封面图' }]} hidden> |
| | | <Input /> |
| | | </Form.Item> |
| | | <Upload |
| | | accept="image/*" |
| | | showUploadList={false} |
| | | customRequest={async (options) => { |
| | | setUploadingCover(true); |
| | | try { |
| | | const res = await uploadHealthPromotionImage(options.file as File); |
| | | const url = res.data?.url; |
| | | if (!url) { |
| | | throw new Error('upload failed'); |
| | | } |
| | | setCoverImg(url); |
| | | form.setFieldsValue({ coverImg: url }); |
| | | options.onSuccess?.(res); |
| | | } catch (error) { |
| | | message.error('封面图上传失败'); |
| | | options.onError?.(error as Error); |
| | | } finally { |
| | | setUploadingCover(false); |
| | | } |
| | | }} |
| | | > |
| | | <div className="health-promotion-edit-form__cover-box"> |
| | | {coverImg ? ( |
| | | <Image src={coverImg} width={96} height={96} preview={false} /> |
| | | ) : uploadingCover ? ( |
| | | '上传中...' |
| | | ) : ( |
| | | <PlusOutlined /> |
| | | )} |
| | | </div> |
| | | </Upload> |
| | | </Form.Item> |
| | | <div className="health-promotion-edit-form__switch-group"> |
| | | <div className="health-promotion-edit-form__switch-item"> |
| | | <span className="health-promotion-edit-form__switch-label">是否启用</span> |
| | | <Form.Item name="status" valuePropName="checked" rules={[{ required: true, message: '请选择是否启用' }]}> |
| | | <Switch /> |
| | | </Form.Item> |
| | | </div> |
| | | <div className="health-promotion-edit-form__switch-item"> |
| | | <span className="health-promotion-edit-form__switch-label">首页推荐</span> |
| | | <Form.Item |
| | | name="isHomeRecommend" |
| | | valuePropName="checked" |
| | | rules={[{ required: true, message: '请选择是否首页推荐' }]} |
| | | > |
| | | <Switch /> |
| | | </Form.Item> |
| | | </div> |
| | | </div> |
| | | <Form.Item label="排序" name="sort" rules={[{ required: true, message: '请输入排序' }]}> |
| | | <InputNumber min={0} precision={0} placeholder="请输入" style={{ width: '100%' }} disabled={loading} /> |
| | | </Form.Item> |
| | | <Form.Item |
| | | label="视频地址" |
| | | name="videoUrl" |
| | | extra="本地视频和视频地址必须填一个" |
| | | rules={[{ required: true, message: '请填写视频地址或上传本地视频' }]} |
| | | > |
| | | <Input placeholder="请输入" disabled={loading} /> |
| | | </Form.Item> |
| | | <Form.Item |
| | | label="视频介绍" |
| | | name="videoDescription" |
| | | rules={[{ required: true, message: '请输入视频介绍' }]} |
| | | > |
| | | <Input.TextArea placeholder="请输入视频介绍" maxLength={255} rows={3} disabled={loading} /> |
| | | </Form.Item> |
| | | <Form.Item label="本地视频" extra="本地视频和视频地址必须填一个"> |
| | | <div className="health-promotion-edit-form__video-upload"> |
| | | <Upload |
| | | accept="video/*" |
| | | showUploadList={false} |
| | | customRequest={async (options) => { |
| | | setUploadingVideo(true); |
| | | try { |
| | | const res = await uploadHealthPromotionVideo(options.file as File); |
| | | const url = res.data?.url; |
| | | if (!url) { |
| | | throw new Error('upload failed'); |
| | | } |
| | | form.setFieldsValue({ |
| | | videoUrl: url, |
| | | videoFileName: res.data?.fileName || (options.file as File).name, |
| | | }); |
| | | message.success('视频上传成功'); |
| | | options.onSuccess?.(res); |
| | | } catch (error) { |
| | | message.error('视频上传失败'); |
| | | options.onError?.(error as Error); |
| | | } finally { |
| | | setUploadingVideo(false); |
| | | } |
| | | }} |
| | | > |
| | | <Button icon={<UploadOutlined />} loading={uploadingVideo}> |
| | | 上传 |
| | | </Button> |
| | | </Upload> |
| | | <span> |
| | | {form.getFieldValue('videoFileName') || |
| | | (form.getFieldValue('videoUrl') ? '已填写视频地址' : '未上传视频文件')} |
| | | </span> |
| | | </div> |
| | | <Form.Item name="videoFileName" hidden> |
| | | <Input /> |
| | | </Form.Item> |
| | | </Form.Item> |
| | | </div> |
| | | <div className="health-promotion-edit-form__footer"> |
| | | <Button onClick={() => history.push(listPagePath)}>取消</Button> |
| | | <Button type="primary" loading={submitting} onClick={handleSubmit}> |
| | | 保存 |
| | | </Button> |
| | | </div> |
| | | </Form> |
| | | </div> |
| | | </div> |
| | | ); |
| | | }; |
| | | |
| | | export default HealthPromotionManagePage; |
| New file |
| | |
| | | import { request } from '@umijs/max'; |
| | | |
| | | // 养生科普与健康知识共用后端接口(同一张表 tcm_health_popularization), |
| | | // 通过 contentType=2(视频)区分养生科普,contentType=1(图文)区分健康知识。 |
| | | |
| | | export async function getHealthPromotionPage(params?: Record<string, any>) { |
| | | return request('/health-popularizations', { |
| | | method: 'GET', |
| | | params, |
| | | }); |
| | | } |
| | | |
| | | export async function getHealthPromotionDetail(id: number | string) { |
| | | return request(`/health-popularizations/${id}`, { |
| | | method: 'GET', |
| | | }); |
| | | } |
| | | |
| | | export async function createHealthPromotion(data: Record<string, any>) { |
| | | return request('/health-popularizations', { |
| | | method: 'POST', |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | export async function updateHealthPromotion(id: number | string, data: Record<string, any>) { |
| | | return request(`/health-popularizations/${id}`, { |
| | | method: 'PUT', |
| | | data, |
| | | }); |
| | | } |
| | | |
| | | export async function deleteHealthPromotion(id: number | string) { |
| | | return request(`/health-popularizations/${id}`, { |
| | | method: 'DELETE', |
| | | }); |
| | | } |
| | | |
| | | export async function uploadHealthPromotionImage(file: File) { |
| | | const formData = new FormData(); |
| | | formData.append('file', file); |
| | | return request('/upload/image', { |
| | | method: 'POST', |
| | | data: formData, |
| | | }); |
| | | } |
| | | |
| | | export async function uploadHealthPromotionVideo(file: File) { |
| | | const formData = new FormData(); |
| | | formData.append('file', file); |
| | | return request('/upload/video', { |
| | | method: 'POST', |
| | | data: formData, |
| | | }); |
| | | } |
| New file |
| | |
| | | .health-promotion-page { |
| | | min-height: calc(100vh - 48px); |
| | | padding: 18px 20px 24px; |
| | | background: #f2f5f9; |
| | | } |
| | | |
| | | .health-promotion-page__panel { |
| | | padding: 18px 18px 24px; |
| | | border-radius: 10px; |
| | | background: #fff; |
| | | box-shadow: 0 2px 10px rgba(15, 23, 42, 0.04); |
| | | } |
| | | |
| | | .health-promotion-page__title { |
| | | margin-bottom: 16px; |
| | | color: #303133; |
| | | font-size: 20px; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .health-promotion-search-card { |
| | | padding: 16px 18px 10px; |
| | | border: 1px solid #e7edf4; |
| | | border-radius: 6px; |
| | | } |
| | | |
| | | .health-promotion-search-form { |
| | | display: grid; |
| | | grid-template-columns: repeat(4, minmax(200px, 1fr)) auto; |
| | | gap: 10px 18px; |
| | | align-items: start; |
| | | } |
| | | |
| | | .health-promotion-search-form .ant-form-item { |
| | | margin-bottom: 0; |
| | | } |
| | | |
| | | .health-promotion-search-form .ant-form-item-row { |
| | | display: grid; |
| | | grid-template-columns: 64px minmax(0, 1fr); |
| | | align-items: center; |
| | | column-gap: 12px; |
| | | } |
| | | |
| | | .health-promotion-search-form .ant-form-item-label { |
| | | padding: 0; |
| | | } |
| | | |
| | | .health-promotion-search-form__actions { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | gap: 12px; |
| | | min-width: 160px; |
| | | } |
| | | |
| | | .health-promotion-toolbar { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | margin: 16px 0 10px; |
| | | } |
| | | |
| | | .health-promotion-table-card { |
| | | overflow: hidden; |
| | | border: 1px solid #e7edf4; |
| | | border-radius: 6px; |
| | | } |
| | | |
| | | .health-promotion-table-card .ant-table-thead > tr > th { |
| | | background: #f8fafc; |
| | | color: #303133; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .health-promotion-table__title { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 12px; |
| | | } |
| | | |
| | | .health-promotion-table__cover-placeholder { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | width: 64px; |
| | | height: 40px; |
| | | border-radius: 6px; |
| | | background: #edf2f7; |
| | | color: #64748b; |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .health-promotion-table__actions { |
| | | button { |
| | | padding: 0; |
| | | border: none; |
| | | background: none; |
| | | color: #1677ff; |
| | | cursor: pointer; |
| | | } |
| | | } |
| | | |
| | | .health-promotion-pagination { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | padding: 16px 18px; |
| | | } |
| | | |
| | | .health-promotion-edit-form__section-title { |
| | | margin-bottom: 16px; |
| | | padding-left: 8px; |
| | | border-left: 3px solid #1677ff; |
| | | color: #303133; |
| | | font-size: 15px; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .health-promotion-edit-form__grid { |
| | | display: grid; |
| | | grid-template-columns: repeat(3, minmax(0, 1fr)); |
| | | gap: 0 36px; |
| | | align-items: start; |
| | | } |
| | | |
| | | .health-promotion-edit-form .ant-form-item { |
| | | margin-bottom: 18px; |
| | | } |
| | | |
| | | .health-promotion-edit-form__upload { |
| | | grid-column: span 1; |
| | | } |
| | | |
| | | .health-promotion-edit-form__cover-box { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | width: 96px; |
| | | height: 96px; |
| | | overflow: hidden; |
| | | border: 1px dashed #cbd5e1; |
| | | border-radius: 8px; |
| | | background: #f8fafc; |
| | | color: #94a3b8; |
| | | font-size: 28px; |
| | | cursor: pointer; |
| | | } |
| | | |
| | | .health-promotion-edit-form__switch-group { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 28px; |
| | | min-height: 72px; |
| | | } |
| | | |
| | | .health-promotion-edit-form__switch-item { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 10px; |
| | | } |
| | | |
| | | .health-promotion-edit-form__switch-item .ant-form-item { |
| | | margin-bottom: 0; |
| | | } |
| | | |
| | | .health-promotion-edit-form__switch-label { |
| | | color: #606266; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .health-promotion-edit-form__video-upload { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 12px; |
| | | } |
| | | |
| | | .health-promotion-edit-form__video-upload span { |
| | | color: #606266; |
| | | word-break: break-all; |
| | | } |
| | | |
| | | .health-promotion-edit-form__footer { |
| | | display: flex; |
| | | gap: 12px; |
| | | margin-top: 12px; |
| | | } |
| | | |
| | | @media (max-width: 1200px) { |
| | | .health-promotion-search-form { |
| | | grid-template-columns: repeat(2, minmax(200px, 1fr)); |
| | | } |
| | | |
| | | .health-promotion-edit-form__grid { |
| | | grid-template-columns: repeat(2, minmax(0, 1fr)); |
| | | } |
| | | } |
| | | |
| | | @media (max-width: 768px) { |
| | | .health-promotion-page { |
| | | padding: 12px; |
| | | } |
| | | |
| | | .health-promotion-page__panel { |
| | | padding: 16px; |
| | | } |
| | | |
| | | .health-promotion-search-form { |
| | | grid-template-columns: 1fr; |
| | | } |
| | | |
| | | .health-promotion-search-form__actions { |
| | | justify-content: flex-start; |
| | | } |
| | | |
| | | .health-promotion-edit-form__grid { |
| | | grid-template-columns: 1fr; |
| | | } |
| | | |
| | | .health-promotion-edit-form__switch-group, |
| | | .health-promotion-edit-form__video-upload { |
| | | flex-direction: column; |
| | | align-items: flex-start; |
| | | } |
| | | } |