diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 00000000..209afe67 Binary files /dev/null and b/.DS_Store differ diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 00000000..398edec1 Binary files /dev/null and b/src/.DS_Store differ diff --git a/src/App.tsx b/src/App.tsx index b9036c75..370f3d80 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,6 +7,7 @@ import CompanyRegister from './pages/CompanyRegister' import Payment from './pages/Payment' import MemberValid from './pages/MemberValid' import MemberRules from './pages/MemberRules' +import Invoice from './pages/Invoice' function App() { return ( @@ -19,6 +20,7 @@ function App() { } /> } /> } /> + } /> ) } diff --git a/src/api/auth.ts b/src/api/auth.ts index 713ce22b..7d2d4ed9 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -78,43 +78,100 @@ export const EducationLevelLabel: Record = { [EducationLevel.OTHER]: '其他', } -/** 所属地区/单位 */ +/** 职称 */ +export enum ProfessionalTitle { + CHIEF_PHYSICIAN = 'CHIEF_PHYSICIAN', + DEPUTY_CHIEF_PHYSICIAN = 'DEPUTY_CHIEF_PHYSICIAN', + ATTENDING_PHYSICIAN = 'ATTENDING_PHYSICIAN', + LICENSED_PHYSICIAN = 'LICENSED_PHYSICIAN', + ASSISTANT_PHYSICIAN = 'ASSISTANT_PHYSICIAN', + CHIEF_PHARMACIST = 'CHIEF_PHARMACIST', + DEPUTY_CHIEF_PHARMACIST = 'DEPUTY_CHIEF_PHARMACIST', + SUPERVISOR_PHARMACIST = 'SUPERVISOR_PHARMACIST', + LICENSED_PHARMACIST = 'LICENSED_PHARMACIST', + CHIEF_NURSE = 'CHIEF_NURSE', + DEPUTY_CHIEF_NURSE = 'DEPUTY_CHIEF_NURSE', + SUPERVISOR_NURSE = 'SUPERVISOR_NURSE', + LICENSED_NURSE = 'LICENSED_NURSE', + RESEARCHER = 'RESEARCHER', + DEPUTY_RESEARCHER = 'DEPUTY_RESEARCHER', + ASSISTANT_RESEARCHER = 'ASSISTANT_RESEARCHER', + CHIEF_TECHNICIAN = 'CHIEF_TECHNICIAN', + DEPUTY_CHIEF_TECHNICIAN = 'DEPUTY_CHIEF_TECHNICIAN', + SUPERVISOR_TECHNICIAN = 'SUPERVISOR_TECHNICIAN', + PROFESSOR = 'PROFESSOR', + ASSOCIATE_PROFESSOR = 'ASSOCIATE_PROFESSOR', + LECTURER = 'LECTURER', + OTHER_SENIOR_TITLE = 'OTHER_SENIOR_TITLE', + OTHER_DEPUTY_SENIOR_TITLE = 'OTHER_DEPUTY_SENIOR_TITLE', + OTHER_INTERMEDIATE_TITLE = 'OTHER_INTERMEDIATE_TITLE', + OTHER_JUNIOR_TITLE = 'OTHER_JUNIOR_TITLE', +} + +export const ProfessionalTitleLabel: Record = { + [ProfessionalTitle.CHIEF_PHYSICIAN]: '主任(中)医师', + [ProfessionalTitle.DEPUTY_CHIEF_PHYSICIAN]: '副主任(中)医师', + [ProfessionalTitle.ATTENDING_PHYSICIAN]: '主治(中)医师', + [ProfessionalTitle.LICENSED_PHYSICIAN]: '执业(中)医师', + [ProfessionalTitle.ASSISTANT_PHYSICIAN]: '助理(中)医师', + [ProfessionalTitle.CHIEF_PHARMACIST]: '主任(中)药师', + [ProfessionalTitle.DEPUTY_CHIEF_PHARMACIST]: '副主任(中)药师', + [ProfessionalTitle.SUPERVISOR_PHARMACIST]: '主管(中)药师', + [ProfessionalTitle.LICENSED_PHARMACIST]: '执业(中)药师', + [ProfessionalTitle.CHIEF_NURSE]: '主任护师', + [ProfessionalTitle.DEPUTY_CHIEF_NURSE]: '副主任护师', + [ProfessionalTitle.SUPERVISOR_NURSE]: '主管护师', + [ProfessionalTitle.LICENSED_NURSE]: '执业护师', + [ProfessionalTitle.RESEARCHER]: '研究员', + [ProfessionalTitle.DEPUTY_RESEARCHER]: '副研究员', + [ProfessionalTitle.ASSISTANT_RESEARCHER]: '助理研究员', + [ProfessionalTitle.CHIEF_TECHNICIAN]: '主任技师', + [ProfessionalTitle.DEPUTY_CHIEF_TECHNICIAN]: '副主任技师', + [ProfessionalTitle.SUPERVISOR_TECHNICIAN]: '主管技师', + [ProfessionalTitle.PROFESSOR]: '教授', + [ProfessionalTitle.ASSOCIATE_PROFESSOR]: '副教授', + [ProfessionalTitle.LECTURER]: '讲师', + [ProfessionalTitle.OTHER_SENIOR_TITLE]: '其他高级职称', + [ProfessionalTitle.OTHER_DEPUTY_SENIOR_TITLE]: '其他副高职称', + [ProfessionalTitle.OTHER_INTERMEDIATE_TITLE]: '其他中级职称', + [ProfessionalTitle.OTHER_JUNIOR_TITLE]: '其他初级职称', +} + +/** 所属地区/单位 (金华市内区县与医院) */ export enum RegionOrUnit { - HANGZHOU = 'HANGZHOU', - NINGBO = 'NINGBO', - HUZHOU = 'HUZHOU', - JIAXING = 'JIAXING', - SHAOXING = 'SHAOXING', - JINHUA = 'JINHUA', - QUZHOU = 'QUZHOU', - ZHOUSHAN = 'ZHOUSHAN', - TAIZHOU = 'TAIZHOU', - LISHUI = 'LISHUI', - ZJCMU = 'ZJCMU', - ZJTDH = 'ZJTDH', - ZJTCMH = 'ZJTCMH', - ZJXH = 'ZJXH', - ZJZS = 'ZJZS', - OTHER = 'OTHER', + JINHUA_TCM_HOSPITAL = 'JINHUA_TCM_HOSPITAL', + JINHUA_CENTRAL_HOSPITAL = 'JINHUA_CENTRAL_HOSPITAL', + JINHUA_PEOPLE_HOSPITAL = 'JINHUA_PEOPLE_HOSPITAL', + JINHUA_SECOND_HOSPITAL = 'JINHUA_SECOND_HOSPITAL', + JINHUA_FIFTH_HOSPITAL = 'JINHUA_FIFTH_HOSPITAL', + WUCHENG_DISTRICT = 'WUCHENG_DISTRICT', + JINDONG_DISTRICT = 'JINDONG_DISTRICT', + DEVELOPMENT_ZONE = 'DEVELOPMENT_ZONE', + YIWU_CITY = 'YIWU_CITY', + LANXI_CITY = 'LANXI_CITY', + DONGYANG_CITY = 'DONGYANG_CITY', + YONGKANG_CITY = 'YONGKANG_CITY', + PUJIANG_COUNTY = 'PUJIANG_COUNTY', + WUYI_COUNTY = 'WUYI_COUNTY', + PANAN_COUNTY = 'PANAN_COUNTY', } export const RegionOrUnitLabel: Record = { - [RegionOrUnit.HANGZHOU]: '杭州', - [RegionOrUnit.NINGBO]: '宁波', - [RegionOrUnit.HUZHOU]: '湖州', - [RegionOrUnit.JIAXING]: '嘉兴', - [RegionOrUnit.SHAOXING]: '绍兴', - [RegionOrUnit.JINHUA]: '金华', - [RegionOrUnit.QUZHOU]: '衢州', - [RegionOrUnit.ZHOUSHAN]: '舟山', - [RegionOrUnit.TAIZHOU]: '台州', - [RegionOrUnit.LISHUI]: '丽水', - [RegionOrUnit.ZJCMU]: '浙江中医药大学', - [RegionOrUnit.ZJTDH]: '浙江省立同德医院', - [RegionOrUnit.ZJTCMH]: '浙江省中医院', - [RegionOrUnit.ZJXH]: '浙江省新华医院', - [RegionOrUnit.ZJZS]: '浙江省中山医院', - [RegionOrUnit.OTHER]: '其他', + [RegionOrUnit.JINHUA_TCM_HOSPITAL]: '金华市中医医院', + [RegionOrUnit.JINHUA_CENTRAL_HOSPITAL]: '金华市中心医院', + [RegionOrUnit.JINHUA_PEOPLE_HOSPITAL]: '金华市人民医院', + [RegionOrUnit.JINHUA_SECOND_HOSPITAL]: '金华市第二医院', + [RegionOrUnit.JINHUA_FIFTH_HOSPITAL]: '金华市第五医院', + [RegionOrUnit.WUCHENG_DISTRICT]: '婺城区', + [RegionOrUnit.JINDONG_DISTRICT]: '金东区', + [RegionOrUnit.DEVELOPMENT_ZONE]: '开发区', + [RegionOrUnit.YIWU_CITY]: '义乌市', + [RegionOrUnit.LANXI_CITY]: '兰溪市', + [RegionOrUnit.DONGYANG_CITY]: '东阳市', + [RegionOrUnit.YONGKANG_CITY]: '永康市', + [RegionOrUnit.PUJIANG_COUNTY]: '浦江县', + [RegionOrUnit.WUYI_COUNTY]: '武义县', + [RegionOrUnit.PANAN_COUNTY]: '磐安县', } /** 会员类型 (个人会员注册专用) */ @@ -132,12 +189,8 @@ export const PersonalMemberTypeLabel: Record = { /** 单位/组织类型 */ export enum UnitOrOrgType { - /** 高等院校 */ - COLLEGE = 'COLLEGE', - /** 三级医疗机构 */ - TERTIARY_MEDICAL = 'TERTIARY_MEDICAL', - /** 二级及以下医疗机构 */ - SECONDARY_AND_BELOW_MEDICAL = 'SECONDARY_AND_BELOW_MEDICAL', + /** 事业单位 (含医院、高校、科研院所等) */ + PUBLIC_INSTITUTION = 'PUBLIC_INSTITUTION', /** 企业 */ ENTERPRISE = 'ENTERPRISE', /** 其他 */ @@ -145,9 +198,7 @@ export enum UnitOrOrgType { } export const UnitOrOrgTypeLabel: Record = { - [UnitOrOrgType.COLLEGE]: '高等院校', - [UnitOrOrgType.TERTIARY_MEDICAL]: '三级医疗机构', - [UnitOrOrgType.SECONDARY_AND_BELOW_MEDICAL]: '二级及以下医疗机构', + [UnitOrOrgType.PUBLIC_INSTITUTION]: '事业单位', [UnitOrOrgType.ENTERPRISE]: '企业', [UnitOrOrgType.OTHER]: '其他', } @@ -176,11 +227,15 @@ export interface LoginResult { userId?: string /** 会员ID */ memberId?: string + /** 会员编号 (后端主会员业务编号,会员权益页展示及发票接口路径参数均使用此值) */ + memberNo?: string /** 账号类型 */ accountType?: AccountType /** 用户昵称/单位名称 */ nickname?: string - /** 是否已支付 */ + /** 是否已充值/开通会员 (后端主字段,用于登录后跳转判断) */ + isVip?: boolean + /** 是否已支付 (旧字段,保留兼容) */ isPaid?: boolean /** 是否已过期 */ isExpired?: boolean @@ -206,8 +261,8 @@ export interface PersonalRegisterParams { politicalStatus?: PoliticalStatus /** 学历 */ educationLevel?: EducationLevel - /** 职称 */ - title?: string + /** 职称 (枚举) */ + title?: ProfessionalTitle /** 职务 */ position?: string /** 所属地区/单位 */ @@ -218,6 +273,8 @@ export interface PersonalRegisterParams { unitAddress?: string /** 会员类型 */ memberType?: PersonalMemberType + /** 专科分会委员类型 (memberType = SPECIAL_COMMITTEE_MEMBER 时填写) */ + specialCommitteeMemberType?: string /** 密码 */ password?: string } diff --git a/src/api/index.ts b/src/api/index.ts index 291715b7..94ea2a2d 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -41,6 +41,7 @@ export { getMemberStatus, getMemberById, renewMember, + submitInvoiceTitle, } from './member' export { diff --git a/src/api/member.ts b/src/api/member.ts index 09644e08..b9f2f3b7 100644 --- a/src/api/member.ts +++ b/src/api/member.ts @@ -50,6 +50,14 @@ export interface RenewParams { duration?: number } +/** 发票抬头提交参数 (接口体) */ +export interface InvoiceTitleDTO { + /** 发票抬头 (个人姓名 或 单位全称) */ + invoiceTitle?: string + /** 纳税人识别号 (单位发票填写) */ + invoiceTaxNo?: string +} + /** 获取当前登录会员的信息 */ export const getCurrentMember = () => request.get('/member/current') @@ -63,3 +71,15 @@ export const getMemberById = (memberId: string) => /** 申请续费,返回新订单ID */ export const renewMember = (data: RenewParams) => request.post<{ orderId: string; amount: number }>('/member/renew', data) + +/** + * 提交会员发票抬头 + * @url PUT /api/v1/wechat/official-account/members/{memberId}/invoice-title + * @param memberId 路径参数(后端命名 memberId,实际传入登录接口返回的 userId) + * @param data { invoiceTitle, invoiceTaxNo } + * @returns boolean (data 字段,true 表示提交成功) + */ +export const submitInvoiceTitle = ( + memberId: string, + data: InvoiceTitleDTO, +) => request.put(`/members/${memberId}/invoice-title`, data) diff --git a/src/api/payment.ts b/src/api/payment.ts index e6cd39c6..8ba577a8 100644 --- a/src/api/payment.ts +++ b/src/api/payment.ts @@ -64,6 +64,44 @@ export interface PayResult { export const createAlipayOrder = (data: CreateOrderParams) => request.post('/payment/alipay/create', data) +/** 创建支付宝充值 H5 订单参数 */ +export interface RechargeCreateDTO { + /** 充值金额,示例值(99) */ + amount: number + /** 订单标题,示例值(会员充值) */ + subject?: string +} + +/** + * 创建支付宝充值 H5 订单返回 + * 后端返回的形态可能是: + * - 字符串: 直接是支付宝表单 HTML 或跳转 URL + * - 对象: { payUrl?, body?, form?, orderId?, ... } + * 这里用宽松类型,前端容错取支付链接 + */ +export type RechargeAlipayH5OrderResult = + | string + | { + payUrl?: string + payURL?: string + url?: string + body?: string + form?: string + orderId?: string + tradeNo?: string + [key: string]: unknown + } + +/** + * 创建支付宝充值 H5 订单 + * POST /api/v1/wechat/official-account/recharges/alipay/h5/orders + */ +export const createRechargeAlipayH5Order = (data: RechargeCreateDTO) => + request.post( + '/recharges/alipay/h5/orders', + data, + ) + /** 查询订单支付结果(用于轮询) */ export const queryPayResult = (orderId: string) => request.get(`/payment/result/${orderId}`) diff --git a/src/index.css b/src/index.css index af2a80c3..b9d173a2 100644 --- a/src/index.css +++ b/src/index.css @@ -1,3 +1,40 @@ +/** + * 全站设计令牌(Design Tokens) + * 所有页面/按钮/卡片/链接 统一引用以下变量,确保品牌一致性 + */ +:root { + /* 品牌主色 - 酒红(中医药学会主题色) */ + --brand-color: #8b2632; + --brand-color-dark: #5d1a22; + --brand-color-light: #fbeaec; + --brand-gradient: linear-gradient(135deg, #8b2632 0%, #5d1a22 100%); + --brand-gradient-hover: linear-gradient(135deg, #a32a39 0%, #6f1f29 100%); + + /* 页面背景 */ + --page-bg: linear-gradient(180deg, #fbf6ee 0%, #f5ebd8 100%); + --page-bg-flat: #fbf6ee; + --surface-bg: #ffffff; + + /* 文字 */ + --text-primary: #333333; + --text-secondary: #666666; + --text-tertiary: #999999; + --text-on-brand: #ffffff; + + /* 提示 */ + --warning-bg: #fff7ed; + --warning-border: #fde0c4; + --warning-text: #ad6800; + --danger-color: #ff4d4f; + + /* 圆角/阴影 */ + --radius-sm: 8px; + --radius-md: 12px; + --radius-lg: 16px; + --shadow-card: 0 4px 12px rgba(139, 38, 50, 0.08); + --shadow-elevated: 0 8px 24px rgba(139, 38, 50, 0.12); +} + * { margin: 0; padding: 0; @@ -7,12 +44,14 @@ html, body { width: 100%; height: 100%; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', - 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', - sans-serif; + font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", + "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Helvetica, Arial, + "Segoe UI", Roboto, sans-serif; + font-size: 14px; + color: var(--text-primary); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; - background-color: #f5f5f5; + background: var(--page-bg-flat); overflow-x: hidden; } diff --git a/src/pages/CompanyLogin.tsx b/src/pages/CompanyLogin.tsx index fff747af..48c0fccd 100644 --- a/src/pages/CompanyLogin.tsx +++ b/src/pages/CompanyLogin.tsx @@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom' import { Form, Input, Button, message, Card } from 'antd' import { ArrowLeftOutlined, BankOutlined, LockOutlined } from '@ant-design/icons' import { loginCompany } from '../api/auth' -import { setToken } from '../utils/request' +import { setToken, setUserId, setMemberNo } from '../utils/request' import './CompanyLogin.css' const CompanyLogin = () => { @@ -27,41 +27,45 @@ const CompanyLogin = () => { setToken(accessToken) } - // TODO: 待 yapi 返回字段明确后,调用 /member/status 查询会员状态决定跳转 - // 当前调试阶段,使用 mock 状态检查逻辑 - const memberStatus = { - isPaid: result?.isPaid ?? false, - isExpired: result?.isExpired ?? false, - memberId: (result?.memberId as string) || 'COMP20260510001', + // 保存后端返回的 userId (会员权益页面需要展示) + const userId = (result?.userId as string) || '' + if (userId) { + setUserId(userId) + } + + // 保存后端返回的 memberNo (会员编号,发票接口路径参数也使用) + const memberNo = (result?.memberNo as string) || '' + if (memberNo) { + setMemberNo(memberNo) + } + + // 根据后端返回的 isVip 判断会员状态 + // - isVip = true: 已充值会员 -> 会员权益页(/member-valid) + // - isVip = false/未返回: 未充值 -> 支付页(/payment) + const isVip = result?.isVip === true + const memberInfo = { + userId, + memberNo, + memberId: (result?.memberId as string) || '', memberType: '单位会员', amount: 999.0, validPeriod: '1年', - expireDate: '2027-05-10', } message.success('登录成功!') - // 根据会员状态跳转 setTimeout(() => { - if (!memberStatus.isPaid || memberStatus.isExpired) { - // 未支付或已过期,跳转至待支付页面 - navigate('/payment', { - state: { - memberId: memberStatus.memberId, - memberType: memberStatus.memberType, - amount: memberStatus.amount, - validPeriod: memberStatus.validPeriod, - isRenew: memberStatus.isExpired, - }, + if (isVip) { + // 已充值 -> 会员权益页 + navigate('/member-valid', { + state: memberInfo, }) } else { - // 会员有效,跳转至会员有效页面 - navigate('/member-valid', { + // 未充值 -> 支付页 + navigate('/payment', { state: { - memberId: memberStatus.memberId, - memberType: memberStatus.memberType, - amount: memberStatus.amount, - validPeriod: memberStatus.validPeriod, + ...memberInfo, + isRenew: false, }, }) } diff --git a/src/pages/CompanyRegister.tsx b/src/pages/CompanyRegister.tsx index cd98febf..c26db218 100644 --- a/src/pages/CompanyRegister.tsx +++ b/src/pages/CompanyRegister.tsx @@ -46,20 +46,11 @@ const CompanyRegister = () => { setToken(accessToken) } - message.success('注册成功!') + message.success('注册成功,请登录!') - // 跳转到支付页面 + // 注册成功后跳转到单位会员登录页,由用户主动登录 setTimeout(() => { - navigate('/payment', { - state: { - memberId: result?.memberId, - phone: payload.phone, - memberType: '单位会员', - amount: 999.0, - validPeriod: '1年', - isRenew: false, - }, - }) + navigate('/company-login') }, 500) } catch (error) { // 请求拦截器已统一 message.error diff --git a/src/pages/Invoice.css b/src/pages/Invoice.css new file mode 100644 index 00000000..0f37563b --- /dev/null +++ b/src/pages/Invoice.css @@ -0,0 +1,152 @@ +.invoice-page { + width: 100%; + min-height: 100vh; + background: var(--page-bg); + display: flex; + flex-direction: column; +} + +/* 顶部 Header */ +.invoice-header { + position: sticky; + top: 0; + z-index: 10; + display: flex; + align-items: center; + padding: 12px 16px; + background: var(--brand-gradient); + color: var(--text-on-brand); + box-shadow: 0 2px 8px rgba(139, 38, 50, 0.18); +} + +.invoice-header .back-btn { + width: 36px; + height: 36px; + color: #fff !important; + font-size: 18px; + border: none; + background: transparent !important; +} + +.invoice-header .back-btn:hover { + background: rgba(255, 255, 255, 0.12) !important; +} + +.invoice-title { + flex: 1; + margin: 0 8px; + text-align: center; + font-size: 18px; + font-weight: 600; + letter-spacing: 2px; + color: #fff; +} + +.header-spacer { + width: 36px; + height: 36px; +} + +/* 表单容器 */ +.invoice-container { + flex: 1; + padding: 20px 16px; +} + +.invoice-form { + background: var(--surface-bg); + border-radius: var(--radius-md); + padding: 20px 16px; + box-shadow: var(--shadow-card); +} + +.invoice-form .ant-form-item-label > label { + font-weight: 500; + color: #333; +} + +/* 发票类型按钮组 */ +.invoice-type-group { + display: flex !important; + width: 100%; +} + +.invoice-type-group .ant-radio-button-wrapper { + flex: 1; + text-align: center; + height: 44px; + line-height: 42px; + font-size: 15px; +} + +.invoice-type-group + .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled) { + color: var(--brand-color) !important; + border-color: var(--brand-color) !important; + background: var(--brand-color-light) !important; +} + +.invoice-type-group + .ant-radio-button-wrapper-checked:not( + .ant-radio-button-wrapper-disabled + )::before { + background-color: var(--brand-color) !important; +} + +.invoice-type-group .ant-radio-button-wrapper:hover { + color: var(--brand-color) !important; +} + +/* 输入框 */ +.invoice-form .ant-input, +.invoice-form .ant-input-affix-wrapper { + height: 44px; + font-size: 15px; + border-radius: 8px; +} + +.invoice-form .ant-input-affix-wrapper .ant-input { + height: auto; +} + +/* 无需发票提示 */ +.invoice-tip { + background: var(--warning-bg); + border: 1px solid var(--warning-border); + color: var(--warning-text); + border-radius: var(--radius-sm); + padding: 12px 14px; + font-size: 13px; + line-height: 1.6; + margin-bottom: 24px; +} + +/* 提交按钮 */ +.submit-item { + margin-top: 8px; + margin-bottom: 0; +} + +.submit-btn { + height: 48px !important; + font-size: 16px !important; + font-weight: 600; + border-radius: var(--radius-sm) !important; + background: var(--brand-gradient) !important; + border: none !important; + color: var(--text-on-brand) !important; + letter-spacing: 4px; +} + +.submit-btn:hover, +.submit-btn:focus { + background: var(--brand-gradient-hover) !important; + color: var(--text-on-brand) !important; +} + +/* iPhone 安全区 */ +@supports (padding: max(0px)) { + .invoice-container { + padding-bottom: max(20px, env(safe-area-inset-bottom)); + } +} diff --git a/src/pages/Invoice.tsx b/src/pages/Invoice.tsx new file mode 100644 index 00000000..17ab40b4 --- /dev/null +++ b/src/pages/Invoice.tsx @@ -0,0 +1,189 @@ +import { useState } from 'react' +import { useNavigate, useLocation } from 'react-router-dom' +import { Button, Form, Input, Radio, message } from 'antd' +import { ArrowLeftOutlined } from '@ant-design/icons' +import { submitInvoiceTitle } from '../api/member' +import { getUserId } from '../utils/request' +import './Invoice.css' + +/** 发票类型枚举 */ +export enum InvoiceType { + /** 个人发票 */ + PERSONAL = 'PERSONAL', + /** 单位发票 */ + UNIT = 'UNIT', +} + +/** 发票表单字段 */ +interface InvoiceFormValues { + invoiceType: InvoiceType + /** 个人抬头(invoiceType=PERSONAL) */ + personalTitle?: string + /** 单位全称(invoiceType=UNIT) */ + unitName?: string + /** 税号(invoiceType=UNIT) */ + taxNumber?: string +} + +const Invoice = () => { + const navigate = useNavigate() + const location = useLocation() + // 发票接口路径参数 {memberId} 实际使用 userId 填充 + // 优先读取路由透传,其次读取 localStorage 兑底 + const stateData = (location.state || {}) as { userId?: string } + const userId = stateData.userId || getUserId() + + const [form] = Form.useForm() + const [submitting, setSubmitting] = useState(false) + const [invoiceType, setInvoiceType] = useState( + InvoiceType.PERSONAL + ) + + /** 切换发票类型, 清空已不再需要的字段 */ + const handleTypeChange = (value: InvoiceType) => { + setInvoiceType(value) + if (value === InvoiceType.PERSONAL) { + form.setFieldsValue({ unitName: undefined, taxNumber: undefined }) + } else if (value === InvoiceType.UNIT) { + form.setFieldsValue({ personalTitle: undefined }) + } + } + + const handleSubmit = async (values: InvoiceFormValues) => { + if (!userId) { + message.error('未获取到用户信息,请重新登录后重试') + return + } + + // 根据发票类型组装接口请求体 + // - 个人发票: invoiceTitle = personalTitle, invoiceTaxNo 不传 + // - 单位发票: invoiceTitle = unitName, invoiceTaxNo = taxNumber + const payload = + values.invoiceType === InvoiceType.PERSONAL + ? { invoiceTitle: values.personalTitle } + : { + invoiceTitle: values.unitName, + invoiceTaxNo: values.taxNumber, + } + + setSubmitting(true) + try { + const ok = await submitInvoiceTitle(userId, payload) + console.log('[Invoice submit]', { userId, payload, ok }) + // 后端约定: data 字段为 boolean,true 表示提交成功 + if (ok === false) { + message.error('提交失败,请稍后重试') + return + } + message.success('发票信息提交成功!') + setTimeout(() => navigate(-1), 600) + } catch (e) { + // 请求拦截器已统一 message.error,这里不重复提示 + console.error('[Invoice submit error]', e) + } finally { + setSubmitting(false) + } + } + + return ( +
+
+
+ +
+ + form={form} + layout="vertical" + onFinish={handleSubmit} + initialValues={{ invoiceType: InvoiceType.PERSONAL }} + className="invoice-form" + > + + handleTypeChange(e.target.value)} + className="invoice-type-group" + > + 个人发票 + 单位发票 + + + + {/* 个人发票: 个人抬头 */} + {invoiceType === InvoiceType.PERSONAL && ( + + + + )} + + {/* 单位发票: 单位全称 + 税号 */} + {invoiceType === InvoiceType.UNIT && ( + <> + + + + (v ? v.toUpperCase() : v)} + > + + + + )} + + + + + +
+
+ ) +} + +export default Invoice diff --git a/src/pages/Login.css b/src/pages/Login.css index 542ca536..0e6de6e1 100644 --- a/src/pages/Login.css +++ b/src/pages/Login.css @@ -1,7 +1,7 @@ .login-page { width: 100%; min-height: 100vh; - background: #f5f5f5; + background: var(--page-bg); } .login-header { @@ -79,22 +79,34 @@ height: 48px; font-size: 16px; font-weight: 600; - border-radius: 8px; + border-radius: var(--radius-sm); margin-top: 8px; + background: var(--brand-gradient) !important; + border: none !important; + color: var(--text-on-brand) !important; +} + +.login-btn:hover, +.login-btn:focus { + background: var(--brand-gradient-hover) !important; + color: var(--text-on-brand) !important; } .register-btn { height: 44px; font-size: 15px; font-weight: 600; - border-radius: 8px; - border-color: #667eea; - color: #667eea; + border-radius: var(--radius-sm); + border-color: var(--brand-color) !important; + color: var(--brand-color) !important; + background: var(--surface-bg) !important; } -.register-btn:hover { - border-color: #764ba2; - color: #764ba2; +.register-btn:hover, +.register-btn:focus { + border-color: var(--brand-color-dark) !important; + color: var(--brand-color-dark) !important; + background: var(--brand-color-light) !important; } /* 移动端适配 */ diff --git a/src/pages/MemberRules.css b/src/pages/MemberRules.css index 1fbb5843..6f8534dd 100644 --- a/src/pages/MemberRules.css +++ b/src/pages/MemberRules.css @@ -1,7 +1,7 @@ .member-rules-page { width: 100%; min-height: 100vh; - background: #f5f5f5; + background: var(--page-bg); display: flex; flex-direction: column; } @@ -14,13 +14,12 @@ display: flex; align-items: center; padding: 12px 16px; - background: linear-gradient(135deg, #8b2632 0%, #5d1a22 100%); - color: #fff; + background: var(--brand-gradient); + color: var(--text-on-brand); box-shadow: 0 2px 8px rgba(139, 38, 50, 0.18); } -.rules-header .back-btn, -.rules-header .reload-btn { +.rules-header .back-btn { width: 36px; height: 36px; color: #fff !important; @@ -29,8 +28,7 @@ background: transparent !important; } -.rules-header .back-btn:hover, -.rules-header .reload-btn:hover { +.rules-header .back-btn:hover { background: rgba(255, 255, 255, 0.12) !important; } @@ -44,136 +42,206 @@ color: #fff; } -/* 提示工具栏 */ -.rules-toolbar { - padding: 10px 16px; - background: #fff7ed; - border-bottom: 1px solid #fde0c4; - font-size: 12px; - color: #ad6800; - text-align: center; +.header-spacer { + width: 36px; + height: 36px; } -.rules-toolbar-tip { - display: inline-flex; - flex-wrap: wrap; - justify-content: center; - gap: 4px; -} - -.toolbar-link { - color: #8b2632; - font-weight: 600; - cursor: pointer; - text-decoration: underline; -} - -.toolbar-link:hover { - opacity: 0.85; -} - -/* PDF 预览容器 */ -.rules-viewer { +/* 文档主体 */ +.rules-doc { flex: 1; - margin: 12px; - background: #fff; - border-radius: 8px; - box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08); - overflow: hidden; - min-height: calc(100vh - 220px); - display: flex; -} - -.rules-iframe { width: 100%; - height: 100%; - min-height: calc(100vh - 220px); - border: none; - background: #fff; -} - -/* 兜底视图 */ -.rules-fallback { - flex: 1; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - padding: 48px 24px; - text-align: center; -} - -.fallback-icon { - font-size: 64px; - margin-bottom: 16px; - opacity: 0.55; -} - -.fallback-title { - font-size: 18px; - font-weight: 600; - color: #333; - margin: 0 0 8px; -} - -.fallback-desc { - font-size: 14px; - color: #888; - margin: 0 0 24px; - line-height: 1.6; - max-width: 320px; -} - -.fallback-actions { - display: flex; - gap: 12px; - flex-wrap: wrap; - justify-content: center; -} - -/* 底部操作 */ -.rules-actions { - display: flex; - gap: 12px; - padding: 12px 16px; - background: #fff; - border-top: 1px solid #f0f0f0; - position: sticky; - bottom: 0; -} - -.rules-actions .ant-btn { - height: 44px; + max-width: 760px; + margin: 16px auto; + padding: 28px 22px 40px; + background: var(--surface-bg); + border-radius: var(--radius-md); + box-shadow: var(--shadow-card); font-size: 15px; - border-radius: 8px; + line-height: 1.85; + color: var(--text-primary); + font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", + "Hiragino Sans GB", "Microsoft YaHei", "STSong", "Songti SC", serif; } -.rules-actions .ant-btn-primary { - background: #8b2632; - border-color: #8b2632; +/* 公文头(标题 + 文号) */ +.doc-banner { + text-align: center; + padding-bottom: 14px; + border-bottom: 2.5px solid var(--brand-color); + margin-bottom: 24px; } -.rules-actions .ant-btn-primary:hover { - background: #a32a39 !important; - border-color: #a32a39 !important; +.banner-line { + font-size: 24px; + font-weight: 700; + color: var(--brand-color); + letter-spacing: 4px; + line-height: 1.4; } -/* iPhone 安全区域 */ +.banner-no { + font-size: 14px; + color: var(--brand-color-dark); + margin-top: 6px; + letter-spacing: 1px; +} + +/* 决议标题 */ +.doc-decision-title { + font-size: 18px; + font-weight: 700; + color: var(--text-primary); + text-align: center; + line-height: 1.6; + margin: 24px 0 18px; + letter-spacing: 1px; +} + +/* 普通段落 */ +.doc-paragraph { + margin: 0 0 12px 0; + text-indent: 2em; + color: var(--text-primary); +} + +/* 落款 */ +.doc-sign { + margin: 24px 0 8px; + text-align: right; + line-height: 1.9; + font-size: 15px; + color: var(--text-primary); + padding-right: 12px; +} + +/* 分隔线 */ +.doc-divider { + height: 1px; + background: linear-gradient( + 90deg, + transparent 0%, + rgba(139, 38, 50, 0.25) 50%, + transparent 100% + ); + margin: 32px 0; +} + +/* 正文大标题 */ +.doc-title { + font-size: 22px; + font-weight: 700; + color: var(--brand-color-dark); + text-align: center; + letter-spacing: 3px; + margin: 0 0 24px; + line-height: 1.5; +} + +/* 章节 */ +.doc-chapter { + margin-bottom: 18px; +} + +.chapter-title { + font-size: 17px; + font-weight: 700; + color: var(--brand-color); + text-align: center; + letter-spacing: 2px; + margin: 22px 0 14px; + line-height: 1.5; +} + +/* 条款 */ +.doc-article { + margin-bottom: 10px; +} + +.article-text { + margin: 0; + text-indent: 2em; + color: var(--text-primary); +} + +.article-no { + font-weight: 600; + color: var(--brand-color-dark); + margin-right: 2px; +} + +/* 子条款列表 */ +.article-items { + list-style: none; + padding: 4px 0 4px 2em; + margin: 4px 0 8px; +} + +.article-items li { + padding: 2px 0; + color: var(--text-primary); + text-indent: 0; +} + +/* 文末 */ +.doc-footer-tip { + margin-top: 32px; + text-align: center; + font-size: 13px; + color: var(--text-tertiary); + letter-spacing: 4px; +} + +/* iPhone 安全区 */ @supports (padding: max(0px)) { - .rules-actions { - padding-bottom: max(12px, env(safe-area-inset-bottom)); + .rules-doc { + margin-bottom: max(16px, env(safe-area-inset-bottom)); } } -/* 大屏适配 */ -@media screen and (min-width: 768px) { - .rules-viewer { - margin: 16px auto; - max-width: 960px; - width: calc(100% - 32px); +/* 移动端窄屏 */ +@media screen and (max-width: 375px) { + .rules-doc { + margin: 12px; + padding: 22px 16px 32px; + font-size: 14px; } - .rules-title { + .banner-line { font-size: 20px; + letter-spacing: 2px; + } + + .doc-title { + font-size: 19px; + letter-spacing: 2px; + } + + .chapter-title { + font-size: 16px; + } + + .doc-decision-title { + font-size: 16px; + } +} + +/* 大屏 */ +@media screen and (min-width: 768px) { + .rules-doc { + padding: 40px 56px 56px; + font-size: 16px; + } + + .banner-line { + font-size: 28px; + } + + .doc-title { + font-size: 26px; + } + + .chapter-title { + font-size: 19px; } } diff --git a/src/pages/MemberRules.tsx b/src/pages/MemberRules.tsx index 15c7b321..cdb6eb56 100644 --- a/src/pages/MemberRules.tsx +++ b/src/pages/MemberRules.tsx @@ -1,80 +1,226 @@ -import { useEffect, useMemo, useState } from 'react' import { useNavigate } from 'react-router-dom' -import { Button, message } from 'antd' -import { - ArrowLeftOutlined, - DownloadOutlined, - ReloadOutlined, -} from '@ant-design/icons' +import { Button } from 'antd' +import { ArrowLeftOutlined } from '@ant-design/icons' import './MemberRules.css' -/** - * 会员管理办法 PDF 文件路径 - * - 文件需放置在 public/member-rules.pdf 下,部署后通过 /member-rules.pdf 访问 - * - 支持通过 url query 覆盖,便于调试: /member-rules?src=https://xxx.pdf - */ -const DEFAULT_PDF_URL = '/member-rules.pdf' +/** 章节数据结构 */ +interface Article { + /** 条款编号(用于显示) */ + no: string + /** 条款标题/正文(可含子条款) */ + text: string + /** 子条款 */ + items?: string[] +} + +interface Chapter { + /** 章节编号 */ + no: string + /** 章节标题 */ + title: string + articles: Article[] +} + +const CHAPTERS: Chapter[] = [ + { + no: '第一章', + title: '总则', + articles: [ + { + no: '第一条', + text: '为规范金华市中医药学会(以下简称"学会")会员管理,根据《金华市中医药学会章程》的有关规定,结合实际情况,特制定本管理办法。', + }, + { no: '第二条', text: '学会会员的入会、退会和日常活动的管理,适用本办法。' }, + { no: '第三条', text: '学会会员设个人会员和单位会员。' }, + { + no: '第四条', + text: '学会会员管理,遵循严格审批、入会自愿、退会自由的原则。', + }, + { + no: '第五条', + text: '学会会员遵守国家法律法规和《金华市中医药学会章程》的规定,积极参加学会活动,维护学会名誉,为促进中医药科技进步和健康中国作贡献。', + }, + ], + }, + { + no: '第二章', + title: '个人会员', + articles: [ + { + no: '第六条', + text: '申请加入本会的个人会员,必须具备下列条件:', + items: [ + '(一)自愿加入本会,拥护本会章程;', + '(二)积极参加本会组织的各项活动;', + '(三)在中医药医疗、教学、科研、生产、经营等部门取得中级或以上技术职称者;', + '(四)获得执业中医(药)师,从事中医药工作三年以上并有一定的学术水平、工作经验和一技之长者;', + '(五)西医学习中医以及其他科技人员,从事中医药工作三年以上并有一定成绩者;', + '(六)从事中医药管理工作者;', + '(七)中医药事业爱好者。', + ], + }, + { + no: '第七条', + text: '个人会员入会程序', + items: [ + '(一)填写入会申请表(线上注册),申请入会;', + '(二)缴纳会费;', + '(三)经金华市中医药学会审核通过。', + ], + }, + { + no: '第八条', + text: '个人会员享有以下权利', + items: [ + '(一)本会的选举权、被选举权和表决权;', + '(二)获得本会服务的优先权;', + '(三)对本会工作的批评建议权和监督权;', + '(四)优先参加本会和专科分会组织的各类学术活动及基层培训;', + '(五)优先取得本会有关学术资料和会议信息;', + '(六)优先取得本会的科研成果、科技奖励等各类奖项的申报权;', + '(七)免费获得本会编辑出版的学术资料、内部刊物和学术活动计划;', + '(八)优先推荐专科分会委员、青年委员候选人;', + '(九)入会自愿,退会自由。', + ], + }, + { + no: '第九条', + text: '个人会员应履行以下义务', + items: [ + '(一)遵守本会章程,执行本会的决议、决定;', + '(二)维护本会合法权益;', + '(三)按规定缴纳会费;', + '(四)完成本会交办的工作;', + '(五)向本会反映情况,提供有关资料;', + '(六)参加本会组织的各项学术、咨询、义诊和科普活动。', + ], + }, + ], + }, + { + no: '第三章', + title: '单位会员', + articles: [ + { + no: '第十条', + text: '凡具有一定数量中级以上职称的科技人员的中医药医疗、教育、科研、药品生产以及健康服务等相关领域的企、事业单位和依法登记的社会组织,承认本会章程,愿意参加本会活动,支持本会工作,均可成为本会单位会员。', + }, + { + no: '第十一条', + text: '单位会员入会程序', + items: [ + '(一)填写入会申请表(线上注册),申请入会;', + '(二)缴纳会费;', + '(三)经金华市中医药学会审核通过;', + '(四)授予单位会员证书。', + ], + }, + { + no: '第十二条', + text: '单位会员享有的权利', + items: [ + '(一)优先参加本会组织的各类学术活动;', + '(二)免费获得本会编辑出版的学术资料、科普杂志、每年度工作总结和学术活动计划;', + '(三)优先取得本会的科研成果、科技奖励等各类奖项的申报权、相关信息技术咨询服务;', + '(四)优先参加学会和专科分会组织的基层培训、帮扶等;', + '(五)对本会工作提出意见和建议;', + '(六)入会自愿,退会自由。', + ], + }, + { + no: '第十三条', + text: '单位会员应履行以下义务', + items: [ + '(一)遵守本会章程,执行本会的决议、决定;', + '(二)维护本会合法权益;', + '(三)按规定缴纳会费;', + '(四)承担本会委托的工作,协助开展有关的学术和科普活动;', + '(五)积极支持鼓励本单位技术人员加入本会并参加本会组织的各项学术、义诊、科普活动和有关社会公益活动;', + '(六)选派 1 名联系人负责与本会联系(若单位法人代表或联系人等信息变更,应及时与本会取得联系以修改信息)。', + ], + }, + ], + }, + { + no: '第四章', + title: '会费管理', + articles: [ + { + no: '第十四条', + text: '会员缴纳的会费,是确保本会正常运转的主要经费来源。按时缴纳会费,是每个会员应尽的基本义务。', + }, + { + no: '第十五条', + text: '会费缴纳标准', + items: [ + '(一)个人会员会费标准:每人每年 30 元,五年一交(五年到期后应主动续交会费);', + '(二)单位会员会费标准:事业单位每年 1000 元,企业单位及其他机构每年 2000 元。', + ], + }, + { + no: '第十六条', + text: '根据国家规定,会员可在自愿的前提下,向本会提供赞助。', + }, + { + no: '第十七条', + text: '单位会员应在当年度完成会费缴纳,新入会单位会员应于正式入会后 1 个月之内缴纳会费。', + }, + { no: '第十八条', text: '会费收缴工作由学会财务具体负责。' }, + { no: '第十九条', text: '收取会费,严格使用财政部门的正规票据。' }, + ], + }, + { + no: '第五章', + title: '会费用途', + articles: [ + { + no: '第二十条', + text: '会费应本着"取之于会员,用之于会员"的原则管理使用。主要使用范围是:', + items: [ + '(一)为会员提供信息和服务的支出;', + '(二)召开会员代表大会、理事会、常务理事会等会议的支出;', + '(三)举办和参与旨在推动学会事业发展的宣传推广、学术研讨等活动的支出;', + '(四)本会办公经费等支出;', + '(五)奖励有学术成就和突出贡献的会员的支出;', + '(六)符合本会宗旨的其它支出。', + ], + }, + ], + }, + { + no: '第六章', + title: '取消会员资格的规定和退会程序', + articles: [ + { + no: '第二十一条', + text: '取消会员资格的规定和退会程序', + items: [ + '(一)会员退会应书面通知本会,且单位会员需交回会员证书;', + '(二)会员退会和除名,已缴纳的会费不予退还;', + '(三)会员 1 年不缴纳会费或不履行会员义务的,视为自动退会;', + '(四)会员从事违反本会章程和有损本会声誉的一切活动,未经本会允许,擅自以本会名义从事商业活动,经组织工作委员会讨论决定,取消其会员资格。被剥夺政治权利者,其会员资格自动取消。', + ], + }, + ], + }, + { + no: '第七章', + title: '附则', + articles: [ + { + no: '第二十二条', + text: '本办法的制订和修改,须经会员代表大会表决通过有效。', + }, + { + no: '第二十三条', + text: '本办法经 2022 年 6 月 25 日第七次会员代表大会表决通过。', + }, + ], + }, +] const MemberRules = () => { const navigate = useNavigate() - const [iframeKey, setIframeKey] = useState(0) - const [iframeError, setIframeError] = useState(false) - - /** 从 URL 参数中读取自定义 PDF 地址(可选) */ - const pdfUrl = useMemo(() => { - const params = new URLSearchParams(window.location.search) - return params.get('src') || DEFAULT_PDF_URL - }, []) - - /** 嵌入预览的 URL: 隐藏工具栏 + 适应宽度 */ - const previewUrl = useMemo(() => { - // PDF 锚点参数: toolbar=1 显示工具栏, view=FitH 横向铺满 - return `${pdfUrl}#toolbar=1&navpanes=0&view=FitH` - }, [pdfUrl]) - - /** 检测 PDF 是否能正常加载(简单 HEAD 请求) */ - useEffect(() => { - let cancelled = false - fetch(pdfUrl, { method: 'HEAD' }) - .then((res) => { - if (cancelled) return - if (!res.ok) { - setIframeError(true) - } - }) - .catch(() => { - if (!cancelled) setIframeError(true) - }) - return () => { - cancelled = true - } - }, [pdfUrl]) - - /** 在浏览器新标签打开 PDF */ - const handleOpenInBrowser = () => { - window.open(pdfUrl, '_blank', 'noopener,noreferrer') - } - - /** 下载 PDF */ - const handleDownload = () => { - try { - const a = document.createElement('a') - a.href = pdfUrl - a.download = '金华市中医药学会会员管理办法.pdf' - document.body.appendChild(a) - a.click() - document.body.removeChild(a) - } catch (e) { - console.error('[下载失败]', e) - message.error('下载失败,请尝试在浏览器中打开') - } - } - - /** 重新加载 iframe */ - const handleReload = () => { - setIframeError(false) - setIframeKey((k) => k + 1) - } return (
@@ -86,68 +232,61 @@ const MemberRules = () => { className="back-btn" />

会员管理办法

-
-
- - 如预览异常,请尝试 - - 在浏览器打开 - - 或 - - 下载查看 - - -
+
+ {/* 公文头 */} +
+
金华市中医药学会文件
+
金中会〔2022〕9 号
+
-
- {iframeError ? ( -
-
📄
-

无法在线预览

-

- 当前环境暂不支持 PDF 在线预览,请点击下方按钮在浏览器中打开或下载查看。 -

-
- - -
-
- ) : ( -