调试接口
This commit is contained in:
parent
0b684a8419
commit
36fe597d86
Binary file not shown.
|
|
@ -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() {
|
|||
<Route path="/payment" element={<Payment />} />
|
||||
<Route path="/member-valid" element={<MemberValid />} />
|
||||
<Route path="/member-rules" element={<MemberRules />} />
|
||||
<Route path="/invoice" element={<Invoice />} />
|
||||
</Routes>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
147
src/api/auth.ts
147
src/api/auth.ts
|
|
@ -78,43 +78,100 @@ export const EducationLevelLabel: Record<EducationLevel, string> = {
|
|||
[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, string> = {
|
||||
[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, string> = {
|
||||
[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<PersonalMemberType, string> = {
|
|||
|
||||
/** 单位/组织类型 */
|
||||
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, string> = {
|
||||
[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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ export {
|
|||
getMemberStatus,
|
||||
getMemberById,
|
||||
renewMember,
|
||||
submitInvoiceTitle,
|
||||
} from './member'
|
||||
|
||||
export {
|
||||
|
|
|
|||
|
|
@ -50,6 +50,14 @@ export interface RenewParams {
|
|||
duration?: number
|
||||
}
|
||||
|
||||
/** 发票抬头提交参数 (接口体) */
|
||||
export interface InvoiceTitleDTO {
|
||||
/** 发票抬头 (个人姓名 或 单位全称) */
|
||||
invoiceTitle?: string
|
||||
/** 纳税人识别号 (单位发票填写) */
|
||||
invoiceTaxNo?: string
|
||||
}
|
||||
|
||||
/** 获取当前登录会员的信息 */
|
||||
export const getCurrentMember = () => request.get<MemberInfo>('/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<boolean>(`/members/${memberId}/invoice-title`, data)
|
||||
|
|
|
|||
|
|
@ -64,6 +64,44 @@ export interface PayResult {
|
|||
export const createAlipayOrder = (data: CreateOrderParams) =>
|
||||
request.post<CreateOrderResult>('/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<RechargeAlipayH5OrderResult>(
|
||||
'/recharges/alipay/h5/orders',
|
||||
data,
|
||||
)
|
||||
|
||||
/** 查询订单支付结果(用于轮询) */
|
||||
export const queryPayResult = (orderId: string) =>
|
||||
request.get<PayResult>(`/payment/result/${orderId}`)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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<InvoiceFormValues>()
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [invoiceType, setInvoiceType] = useState<InvoiceType>(
|
||||
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 (
|
||||
<div className="invoice-page">
|
||||
<div className="invoice-header">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<ArrowLeftOutlined />}
|
||||
onClick={() => navigate(-1)}
|
||||
className="back-btn"
|
||||
/>
|
||||
<h1 className="invoice-title">填写发票信息</h1>
|
||||
<span className="header-spacer" />
|
||||
</div>
|
||||
|
||||
<div className="invoice-container">
|
||||
<Form<InvoiceFormValues>
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleSubmit}
|
||||
initialValues={{ invoiceType: InvoiceType.PERSONAL }}
|
||||
className="invoice-form"
|
||||
>
|
||||
<Form.Item
|
||||
label="发票类型"
|
||||
name="invoiceType"
|
||||
rules={[{ required: true, message: '请选择发票类型' }]}
|
||||
>
|
||||
<Radio.Group
|
||||
onChange={(e) => handleTypeChange(e.target.value)}
|
||||
className="invoice-type-group"
|
||||
>
|
||||
<Radio.Button value={InvoiceType.PERSONAL}>个人发票</Radio.Button>
|
||||
<Radio.Button value={InvoiceType.UNIT}>单位发票</Radio.Button>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
{/* 个人发票: 个人抬头 */}
|
||||
{invoiceType === InvoiceType.PERSONAL && (
|
||||
<Form.Item
|
||||
label="个人抬头"
|
||||
name="personalTitle"
|
||||
rules={[
|
||||
{ required: true, message: '请输入个人抬头' },
|
||||
{ max: 50, message: '最多 50 个字符' },
|
||||
]}
|
||||
>
|
||||
<Input placeholder="请输入个人抬头(如本人姓名)" allowClear />
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{/* 单位发票: 单位全称 + 税号 */}
|
||||
{invoiceType === InvoiceType.UNIT && (
|
||||
<>
|
||||
<Form.Item
|
||||
label="单位全称"
|
||||
name="unitName"
|
||||
rules={[
|
||||
{ required: true, message: '请输入单位全称' },
|
||||
{ max: 100, message: '最多 100 个字符' },
|
||||
]}
|
||||
>
|
||||
<Input placeholder="请输入单位全称(发票抬头)" allowClear />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="税号"
|
||||
name="taxNumber"
|
||||
rules={[
|
||||
{ required: true, message: '请输入纳税人识别号' },
|
||||
{
|
||||
pattern: /^[A-Z0-9]{15,20}$/,
|
||||
message: '税号为 15-20 位数字或大写字母',
|
||||
},
|
||||
]}
|
||||
normalize={(v: string) => (v ? v.toUpperCase() : v)}
|
||||
>
|
||||
<Input
|
||||
placeholder="请输入纳税人识别号(15-20 位)"
|
||||
maxLength={20}
|
||||
allowClear
|
||||
/>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Form.Item className="submit-item">
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
block
|
||||
size="large"
|
||||
loading={submitting}
|
||||
className="submit-btn"
|
||||
>
|
||||
提交
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Invoice
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
/* 移动端适配 */
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="member-rules-page">
|
||||
|
|
@ -86,68 +232,61 @@ const MemberRules = () => {
|
|||
className="back-btn"
|
||||
/>
|
||||
<h1 className="rules-title">会员管理办法</h1>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={handleReload}
|
||||
className="reload-btn"
|
||||
aria-label="重新加载"
|
||||
/>
|
||||
<span className="header-spacer" />
|
||||
</div>
|
||||
|
||||
<div className="rules-toolbar">
|
||||
<span className="rules-toolbar-tip">
|
||||
如预览异常,请尝试
|
||||
<a className="toolbar-link" onClick={handleOpenInBrowser}>
|
||||
在浏览器打开
|
||||
</a>
|
||||
或
|
||||
<a className="toolbar-link" onClick={handleDownload}>
|
||||
下载查看
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<div className="rules-doc">
|
||||
{/* 公文头 */}
|
||||
<div className="doc-banner">
|
||||
<div className="banner-line">金华市中医药学会文件</div>
|
||||
<div className="banner-no">金中会〔2022〕9 号</div>
|
||||
</div>
|
||||
|
||||
<div className="rules-viewer">
|
||||
{iframeError ? (
|
||||
<div className="rules-fallback">
|
||||
<div className="fallback-icon">📄</div>
|
||||
<p className="fallback-title">无法在线预览</p>
|
||||
<p className="fallback-desc">
|
||||
当前环境暂不支持 PDF 在线预览,请点击下方按钮在浏览器中打开或下载查看。
|
||||
</p>
|
||||
<div className="fallback-actions">
|
||||
<Button type="primary" onClick={handleOpenInBrowser}>
|
||||
在浏览器打开
|
||||
</Button>
|
||||
<Button icon={<DownloadOutlined />} onClick={handleDownload}>
|
||||
下载 PDF
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<iframe
|
||||
key={iframeKey}
|
||||
className="rules-iframe"
|
||||
src={previewUrl}
|
||||
title="会员管理办法"
|
||||
onError={() => setIframeError(true)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{/* 决议 */}
|
||||
<h2 className="doc-decision-title">
|
||||
关于通过《金华市中医药学会会员管理办法》
|
||||
<br />
|
||||
的决议
|
||||
</h2>
|
||||
<p className="doc-paragraph">
|
||||
经金华市中医药学会第七次会员代表大会审议,决定通过《金华市中医药学会会员管理办法》,现予发布。
|
||||
</p>
|
||||
<p className="doc-paragraph">附件:《金华市中医药学会会员管理办法》</p>
|
||||
<div className="doc-sign">
|
||||
<div>金华市中医药学会</div>
|
||||
<div>2022 年 6 月 25 日</div>
|
||||
</div>
|
||||
|
||||
<div className="rules-actions">
|
||||
<Button block onClick={handleOpenInBrowser}>
|
||||
在浏览器打开
|
||||
</Button>
|
||||
<Button
|
||||
block
|
||||
type="primary"
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={handleDownload}
|
||||
>
|
||||
下载 PDF
|
||||
</Button>
|
||||
<div className="doc-divider" />
|
||||
|
||||
{/* 正文标题 */}
|
||||
<h2 className="doc-title">金华市中医药学会会员管理办法</h2>
|
||||
|
||||
{/* 章节正文 */}
|
||||
{CHAPTERS.map((chapter) => (
|
||||
<section key={chapter.no} className="doc-chapter">
|
||||
<h3 className="chapter-title">
|
||||
{chapter.no} {chapter.title}
|
||||
</h3>
|
||||
{chapter.articles.map((article) => (
|
||||
<div key={article.no} className="doc-article">
|
||||
<p className="article-text">
|
||||
<span className="article-no">{article.no} </span>
|
||||
{article.text}
|
||||
</p>
|
||||
{article.items && (
|
||||
<ul className="article-items">
|
||||
{article.items.map((item, idx) => (
|
||||
<li key={idx}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
))}
|
||||
|
||||
<div className="doc-footer-tip">— 全文完 —</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
radial-gradient(circle at 15% 10%, rgba(139, 38, 50, 0.12) 0%, transparent 45%),
|
||||
radial-gradient(circle at 85% 20%, rgba(193, 124, 74, 0.14) 0%, transparent 50%),
|
||||
radial-gradient(circle at 50% 100%, rgba(139, 38, 50, 0.10) 0%, transparent 55%),
|
||||
linear-gradient(180deg, #fbf6ee 0%, #f5ebd8 100%);
|
||||
var(--page-bg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
|
|
@ -39,7 +39,7 @@
|
|||
.header {
|
||||
padding: 48px 20px 32px;
|
||||
text-align: center;
|
||||
color: #5d1a22;
|
||||
color: var(--brand-color-dark);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
|
@ -63,7 +63,7 @@
|
|||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
letter-spacing: 4px;
|
||||
color: #5d1a22;
|
||||
color: var(--brand-color-dark);
|
||||
text-shadow: 0 1px 2px rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
|
|
@ -84,13 +84,13 @@
|
|||
}
|
||||
|
||||
.login-card {
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
background: var(--surface-bg);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 32px 24px;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
box-shadow: var(--shadow-elevated);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
|
|
@ -107,16 +107,10 @@
|
|||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.login-card.personal::before {
|
||||
background: #1890ff;
|
||||
}
|
||||
|
||||
.login-card.company::before {
|
||||
background: #52c41a;
|
||||
}
|
||||
|
||||
.login-card.personal::before,
|
||||
.login-card.company::before,
|
||||
.login-card.rules::before {
|
||||
background: #8b2632;
|
||||
background: var(--brand-color);
|
||||
}
|
||||
|
||||
.login-card:hover {
|
||||
|
|
@ -143,16 +137,10 @@
|
|||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.login-card.personal .card-icon {
|
||||
background: linear-gradient(135deg, #e6f7ff 0%, #bae7ff 100%);
|
||||
}
|
||||
|
||||
.login-card.company .card-icon {
|
||||
background: linear-gradient(135deg, #f6ffed 0%, #d9f7be 100%);
|
||||
}
|
||||
|
||||
.login-card.personal .card-icon,
|
||||
.login-card.company .card-icon,
|
||||
.login-card.rules .card-icon {
|
||||
background: linear-gradient(135deg, #fbeaec 0%, #f3c8ce 100%);
|
||||
background: linear-gradient(135deg, var(--brand-color-light) 0%, #f3c8ce 100%);
|
||||
}
|
||||
|
||||
.card-content {
|
||||
|
|
@ -166,7 +154,7 @@
|
|||
.card-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
line-height: 1.3;
|
||||
word-break: break-word;
|
||||
|
|
@ -174,7 +162,7 @@
|
|||
|
||||
.card-desc {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
color: var(--text-secondary);
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
word-break: break-word;
|
||||
|
|
@ -212,7 +200,7 @@
|
|||
.contact-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #5d1a22;
|
||||
color: var(--brand-color-dark);
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
|
|
@ -237,18 +225,18 @@
|
|||
.contact-label {
|
||||
flex-shrink: 0;
|
||||
width: 72px;
|
||||
color: #999;
|
||||
color: var(--text-tertiary);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.contact-value {
|
||||
flex: 1;
|
||||
color: #333;
|
||||
color: var(--text-primary);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.contact-phone {
|
||||
color: #8b2632;
|
||||
color: var(--brand-color);
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
letter-spacing: 0.5px;
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ const MemberService = () => {
|
|||
<div className="login-card personal" onClick={handlePersonalLogin}>
|
||||
<div className="card-icon">
|
||||
<svg viewBox="0 0 1024 1024" width="64" height="64">
|
||||
<path d="M512 512c119.3 0 216-96.7 216-216S631.3 80 512 80 296 176.7 296 296s96.7 216 216 216zm0 64c-154.6 0-280 125.4-280 280h560c0-154.6-125.4-280-280-280z" fill="#1890ff"/>
|
||||
<path d="M512 512c119.3 0 216-96.7 216-216S631.3 80 512 80 296 176.7 296 296s96.7 216 216 216zm0 64c-154.6 0-280 125.4-280 280h560c0-154.6-125.4-280-280-280z" fill="#8b2632"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="card-content">
|
||||
|
|
@ -45,7 +45,7 @@ const MemberService = () => {
|
|||
<div className="login-card company" onClick={handleCompanyLogin}>
|
||||
<div className="card-icon">
|
||||
<svg viewBox="0 0 1024 1024" width="64" height="64">
|
||||
<path d="M160 960V64h704v896H160zm64-64h576V128H224v768zm128-512h320v64H352v-64zm0 160h320v64H352v-64zm0-320h320v64H352v-64z" fill="#52c41a"/>
|
||||
<path d="M160 960V64h704v896H160zm64-64h576V128H224v768zm128-512h320v64H352v-64zm0 160h320v64H352v-64zm0-320h320v64H352v-64z" fill="#8b2632"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="card-content">
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
.member-valid-page {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(180deg, #f0f9ff 0%, #f5f5f5 100%);
|
||||
background: var(--page-bg);
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
|
|
@ -20,7 +20,7 @@
|
|||
|
||||
.success-header .ant-result-icon .anticon {
|
||||
font-size: 64px;
|
||||
color: #52c41a;
|
||||
color: var(--brand-color);
|
||||
}
|
||||
|
||||
.success-header .ant-result-title {
|
||||
|
|
@ -44,10 +44,10 @@
|
|||
|
||||
.member-info-card {
|
||||
width: 100%;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-elevated);
|
||||
margin-bottom: 20px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
background: var(--brand-gradient);
|
||||
}
|
||||
|
||||
.member-info-card .ant-card-body {
|
||||
|
|
@ -118,21 +118,44 @@
|
|||
.detail-value.amount {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #ff4d4f;
|
||||
color: var(--danger-color);
|
||||
}
|
||||
|
||||
.back-home-btn {
|
||||
height: 52px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--brand-gradient) !important;
|
||||
color: var(--text-on-brand) !important;
|
||||
border: none !important;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.back-home-btn:hover {
|
||||
background: linear-gradient(135deg, #5568d3 0%, #6a3f91 100%);
|
||||
.back-home-btn:hover,
|
||||
.back-home-btn:focus {
|
||||
background: var(--brand-gradient-hover) !important;
|
||||
color: var(--text-on-brand) !important;
|
||||
}
|
||||
|
||||
.invoice-btn,
|
||||
.renew-btn {
|
||||
height: 52px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--brand-gradient) !important;
|
||||
color: var(--text-on-brand) !important;
|
||||
border: none !important;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.invoice-btn:hover,
|
||||
.invoice-btn:focus,
|
||||
.renew-btn:hover,
|
||||
.renew-btn:focus {
|
||||
background: var(--brand-gradient-hover) !important;
|
||||
color: var(--text-on-brand) !important;
|
||||
}
|
||||
|
||||
/* 移动端适配 */
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useNavigate, useLocation } from 'react-router-dom'
|
||||
import { Button, Card, Result } from 'antd'
|
||||
import { CheckCircleOutlined } from '@ant-design/icons'
|
||||
import { getUserId, getMemberNo } from '../utils/request'
|
||||
import './MemberValid.css'
|
||||
|
||||
const MemberValid = () => {
|
||||
|
|
@ -8,9 +9,12 @@ const MemberValid = () => {
|
|||
const location = useLocation()
|
||||
const memberData = location.state || {}
|
||||
|
||||
// userId/memberNo 优先取路由透传,其次取 localStorage (刷新页面后仍能展示)
|
||||
const {
|
||||
memberId = 'MEM20260510001',
|
||||
memberType = '普通会员',
|
||||
userId = getUserId(),
|
||||
memberNo = getMemberNo(),
|
||||
memberId = '',
|
||||
memberType = '个人会员',
|
||||
amount = 99.00,
|
||||
validPeriod = '1年'
|
||||
} = memberData
|
||||
|
|
@ -32,6 +36,24 @@ const MemberValid = () => {
|
|||
navigate('/')
|
||||
}
|
||||
|
||||
const handleInvoice = () => {
|
||||
navigate('/invoice', { state: { userId, memberNo, memberId, amount } })
|
||||
}
|
||||
|
||||
const handleRenew = () => {
|
||||
navigate('/payment', {
|
||||
state: {
|
||||
userId,
|
||||
memberNo,
|
||||
memberId,
|
||||
memberType,
|
||||
amount,
|
||||
validPeriod,
|
||||
isRenew: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="member-valid-page">
|
||||
<div className="success-header">
|
||||
|
|
@ -53,8 +75,8 @@ const MemberValid = () => {
|
|||
</svg>
|
||||
</div>
|
||||
<div className="badge-text">
|
||||
<div className="member-id-label">会员ID</div>
|
||||
<div className="member-id-value">{memberId}</div>
|
||||
<div className="member-id-label">会员编号</div>
|
||||
<div className="member-id-value">{memberNo || '--'}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -82,6 +104,26 @@ const MemberValid = () => {
|
|||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 续费 */}
|
||||
<Button
|
||||
className="renew-btn"
|
||||
onClick={handleRenew}
|
||||
block
|
||||
size="large"
|
||||
>
|
||||
续费
|
||||
</Button>
|
||||
|
||||
{/* 填写发票信息 */}
|
||||
<Button
|
||||
className="invoice-btn"
|
||||
onClick={handleInvoice}
|
||||
block
|
||||
size="large"
|
||||
>
|
||||
填写发票信息
|
||||
</Button>
|
||||
|
||||
{/* 返回首页 */}
|
||||
<Button
|
||||
type="primary"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
.payment-page {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
background: var(--page-bg);
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
|
|
@ -92,6 +92,9 @@
|
|||
font-size: 14px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
text-align: right;
|
||||
word-break: break-all;
|
||||
max-width: 70%;
|
||||
}
|
||||
|
||||
.amount-item {
|
||||
|
|
@ -101,21 +104,21 @@
|
|||
.amount-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #ff4d4f;
|
||||
color: var(--danger-color);
|
||||
}
|
||||
|
||||
.payment-method {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
background: #e6f4ff;
|
||||
border-radius: 8px;
|
||||
border: 2px solid #1677FF;
|
||||
background: var(--brand-color-light);
|
||||
border-radius: var(--radius-sm);
|
||||
border: 2px solid var(--brand-color);
|
||||
}
|
||||
|
||||
.alipay-method {
|
||||
background: #e6f4ff;
|
||||
border-color: #1677FF;
|
||||
background: var(--brand-color-light);
|
||||
border-color: var(--brand-color);
|
||||
}
|
||||
|
||||
.method-icon {
|
||||
|
|
@ -153,20 +156,22 @@
|
|||
height: 52px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
margin-top: 8px;
|
||||
background: #1677FF;
|
||||
border-color: #1677FF;
|
||||
background: var(--brand-gradient) !important;
|
||||
border: none !important;
|
||||
color: var(--text-on-brand) !important;
|
||||
}
|
||||
|
||||
.pay-btn:hover {
|
||||
background: #0958d9 !important;
|
||||
border-color: #0958d9 !important;
|
||||
.pay-btn:hover,
|
||||
.pay-btn:focus {
|
||||
background: var(--brand-gradient-hover) !important;
|
||||
color: var(--text-on-brand) !important;
|
||||
}
|
||||
|
||||
.alipay-btn {
|
||||
background: #1677FF;
|
||||
border-color: #1677FF;
|
||||
background: var(--brand-gradient) !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.payment-tips {
|
||||
|
|
@ -230,27 +235,6 @@
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
.alipay-link-box {
|
||||
background: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.alipay-link-label {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.alipay-link-value {
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
word-break: break-all;
|
||||
line-height: 1.5;
|
||||
font-family: -apple-system, BlinkMacSystemFont, monospace;
|
||||
}
|
||||
|
||||
.alipay-action-btn {
|
||||
height: 44px;
|
||||
border-radius: 8px;
|
||||
|
|
@ -259,41 +243,23 @@
|
|||
}
|
||||
|
||||
.alipay-action-btn.primary {
|
||||
background: #1677FF;
|
||||
border-color: #1677FF;
|
||||
background: var(--brand-gradient) !important;
|
||||
border: none !important;
|
||||
color: var(--text-on-brand) !important;
|
||||
}
|
||||
|
||||
.alipay-action-btn.primary:hover {
|
||||
background: #0958d9 !important;
|
||||
border-color: #0958d9 !important;
|
||||
}
|
||||
|
||||
.alipay-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 8px 0 4px 0;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.alipay-divider::before,
|
||||
.alipay-divider::after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.alipay-divider span {
|
||||
padding: 0 12px;
|
||||
.alipay-action-btn.primary:hover,
|
||||
.alipay-action-btn.primary:focus {
|
||||
background: var(--brand-gradient-hover) !important;
|
||||
color: var(--text-on-brand) !important;
|
||||
}
|
||||
|
||||
.alipay-done-btn {
|
||||
color: #1677FF;
|
||||
color: var(--brand-color);
|
||||
font-size: 14px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.alipay-done-btn:hover {
|
||||
color: #0958d9 !important;
|
||||
color: var(--brand-color-dark) !important;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,48 +1,88 @@
|
|||
import { useState } from 'react'
|
||||
import { useNavigate, useLocation } from 'react-router-dom'
|
||||
import { Button, Card, message, Modal } from 'antd'
|
||||
import { ArrowLeftOutlined, CopyOutlined, GlobalOutlined } from '@ant-design/icons'
|
||||
import { ArrowLeftOutlined, GlobalOutlined } from '@ant-design/icons'
|
||||
import {
|
||||
createRechargeAlipayH5Order,
|
||||
type RechargeAlipayH5OrderResult,
|
||||
} from '../api/payment'
|
||||
import './Payment.css'
|
||||
|
||||
/**
|
||||
* 测试阶段调起支付宝用的金额(元)
|
||||
* TODO: 上线前改回 orderInfo.amount
|
||||
*/
|
||||
const PAY_AMOUNT = 0.01
|
||||
|
||||
/** 从后端不同形态返回中提取支付跳转 URL */
|
||||
const extractPayUrl = (res: RechargeAlipayH5OrderResult): string => {
|
||||
if (!res) return ''
|
||||
if (typeof res === 'string') return res
|
||||
return (
|
||||
res.payUrl ||
|
||||
res.payURL ||
|
||||
res.url ||
|
||||
res.body ||
|
||||
res.form ||
|
||||
''
|
||||
)
|
||||
}
|
||||
|
||||
const Payment = () => {
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [modalVisible, setModalVisible] = useState(false)
|
||||
/** 后端返回的支付宝跳转 URL(点击支付后才拿到) */
|
||||
const [alipayUrl, setAlipayUrl] = useState<string>('')
|
||||
|
||||
// 从路由状态获取订单信息(支持从登录页或注册页跳转)
|
||||
const stateData = location.state || {}
|
||||
|
||||
// 订单信息
|
||||
const orderInfo = {
|
||||
orderId: 'ORD' + Date.now(),
|
||||
memberId: stateData.memberId || '',
|
||||
memberType: stateData.memberType || '普通会员',
|
||||
amount: stateData.amount || 99.00,
|
||||
amount: stateData.amount || 99.0,
|
||||
validPeriod: stateData.validPeriod || '1年',
|
||||
isRenew: stateData.isRenew || false // 是否是续费
|
||||
isRenew: stateData.isRenew || false, // 是否是续费
|
||||
}
|
||||
|
||||
// 构建支付宝支付链接(实际项目中应由后端返回)
|
||||
const buildAlipayUrl = () => {
|
||||
const params = new URLSearchParams({
|
||||
orderId: orderInfo.orderId,
|
||||
amount: orderInfo.amount.toFixed(2),
|
||||
subject: orderInfo.isRenew ? '会员续费' : `${orderInfo.memberType}开通`
|
||||
})
|
||||
// 模拟支付宝支付链接,实际应替换为后端返回的真实地址
|
||||
return `https://qr.alipay.com/pay?${params.toString()}`
|
||||
}
|
||||
// 订单标题
|
||||
const orderSubject = orderInfo.isRenew
|
||||
? '会员续费'
|
||||
: `${orderInfo.memberType}开通`
|
||||
|
||||
const alipayUrl = buildAlipayUrl()
|
||||
|
||||
// 点击支付宝支付按钮,弹出引导用户在浏览器中打开支付页面
|
||||
const handlePayment = () => {
|
||||
setModalVisible(true)
|
||||
// 点击支付宝支付按钮: 调后端创建订单,拿到支付链接后弹出引导弹窗
|
||||
const handlePayment = async () => {
|
||||
if (loading) return
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await createRechargeAlipayH5Order({
|
||||
amount: PAY_AMOUNT, // 测试阶段 0.01,上线前换回 orderInfo.amount
|
||||
subject: orderSubject,
|
||||
})
|
||||
const url = extractPayUrl(res)
|
||||
if (!url) {
|
||||
message.error('获取支付链接失败,请重试')
|
||||
return
|
||||
}
|
||||
setAlipayUrl(url)
|
||||
setModalVisible(true)
|
||||
} catch (e) {
|
||||
// request 拦截器已统一提示错误
|
||||
console.error('[createRechargeAlipayH5Order]', e)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 在浏览器中打开支付页面
|
||||
const handleOpenInBrowser = () => {
|
||||
if (!alipayUrl) {
|
||||
message.warning('支付链接为空,请重新发起支付')
|
||||
return
|
||||
}
|
||||
setLoading(true)
|
||||
try {
|
||||
const newWindow = window.open(alipayUrl, '_blank')
|
||||
|
|
@ -58,28 +98,6 @@ const Payment = () => {
|
|||
}
|
||||
}
|
||||
|
||||
// 复制支付链接
|
||||
const handleCopyLink = async () => {
|
||||
try {
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
await navigator.clipboard.writeText(alipayUrl)
|
||||
} else {
|
||||
// 兼容性处理
|
||||
const textarea = document.createElement('textarea')
|
||||
textarea.value = alipayUrl
|
||||
textarea.style.position = 'fixed'
|
||||
textarea.style.opacity = '0'
|
||||
document.body.appendChild(textarea)
|
||||
textarea.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(textarea)
|
||||
}
|
||||
message.success('支付链接已复制,请在浏览器中粘贴打开')
|
||||
} catch (error) {
|
||||
message.error('复制失败,请手动复制链接')
|
||||
}
|
||||
}
|
||||
|
||||
// 用户在外部完成支付后,点击"我已完成支付"
|
||||
const handlePaymentDone = () => {
|
||||
setModalVisible(false)
|
||||
|
|
@ -115,16 +133,6 @@ const Payment = () => {
|
|||
{/* 订单信息 */}
|
||||
<Card title="订单信息" className="payment-card" bordered={false}>
|
||||
<div className="order-info">
|
||||
<div className="order-item">
|
||||
<span className="order-label">订单编号</span>
|
||||
<span className="order-value">{orderInfo.orderId}</span>
|
||||
</div>
|
||||
{orderInfo.memberId && (
|
||||
<div className="order-item">
|
||||
<span className="order-label">会员ID</span>
|
||||
<span className="order-value">{orderInfo.memberId}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="order-item">
|
||||
<span className="order-label">会员类型</span>
|
||||
<span className="order-value">{orderInfo.memberType}</span>
|
||||
|
|
@ -135,7 +143,7 @@ const Payment = () => {
|
|||
</div>
|
||||
<div className="order-item amount-item">
|
||||
<span className="order-label">{orderInfo.isRenew ? '续费金额' : '应付金额'}</span>
|
||||
<span className="order-value amount-value">¥{orderInfo.amount.toFixed(2)}</span>
|
||||
<span className="order-value amount-value">¥{PAY_AMOUNT.toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
|
@ -165,10 +173,11 @@ const Payment = () => {
|
|||
type="primary"
|
||||
className="pay-btn alipay-btn"
|
||||
onClick={handlePayment}
|
||||
loading={loading}
|
||||
block
|
||||
size="large"
|
||||
>
|
||||
支付宝支付 ¥{orderInfo.amount.toFixed(2)}
|
||||
支付宝支付 ¥{PAY_AMOUNT.toFixed(2)}
|
||||
</Button>
|
||||
|
||||
<p className="payment-tips">
|
||||
|
|
@ -191,11 +200,6 @@ const Payment = () => {
|
|||
点击下方按钮,将在浏览器中打开支付宝支付页面,完成支付后请返回本页面。
|
||||
</p>
|
||||
|
||||
<div className="alipay-link-box">
|
||||
<div className="alipay-link-label">支付链接</div>
|
||||
<div className="alipay-link-value">{alipayUrl}</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<GlobalOutlined />}
|
||||
|
|
@ -208,20 +212,6 @@ const Payment = () => {
|
|||
在浏览器中打开支付页面
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
icon={<CopyOutlined />}
|
||||
className="alipay-action-btn"
|
||||
onClick={handleCopyLink}
|
||||
block
|
||||
size="large"
|
||||
>
|
||||
复制支付链接
|
||||
</Button>
|
||||
|
||||
<div className="alipay-divider">
|
||||
<span>支付完成后</span>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="link"
|
||||
className="alipay-done-btn"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom'
|
|||
import { Form, Input, Button, message, Card } from 'antd'
|
||||
import { ArrowLeftOutlined, UserOutlined, LockOutlined } from '@ant-design/icons'
|
||||
import { loginPersonal } from '../api/auth'
|
||||
import { setToken } from '../utils/request'
|
||||
import { setToken, setUserId, setMemberNo } from '../utils/request'
|
||||
import './Login.css'
|
||||
|
||||
const PersonalLogin = () => {
|
||||
|
|
@ -26,41 +26,45 @@ const PersonalLogin = () => {
|
|||
setToken(accessToken)
|
||||
}
|
||||
|
||||
// TODO: 待 yapi 返回字段明确后,调用 /member/status 查询会员状态决定跳转
|
||||
// 当前调试阶段,使用 mock 状态检查逻辑
|
||||
const memberStatus = {
|
||||
isPaid: result?.isPaid ?? false,
|
||||
isExpired: result?.isExpired ?? false,
|
||||
memberId: (result?.memberId as string) || 'MEM20260510001',
|
||||
memberType: '普通会员',
|
||||
// 保存后端返回的 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: 99.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,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
@ -96,7 +100,8 @@ const PersonalLogin = () => {
|
|||
onFinish={onFinish}
|
||||
size="large"
|
||||
initialValues={{
|
||||
phone: '13800138000',
|
||||
// 与个人会员注册默认字段保持一致,方便联调调试
|
||||
phone: '13800000001',
|
||||
password: '123456'
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import {
|
|||
PoliticalStatusLabel,
|
||||
EducationLevel,
|
||||
EducationLevelLabel,
|
||||
ProfessionalTitle,
|
||||
ProfessionalTitleLabel,
|
||||
RegionOrUnit,
|
||||
RegionOrUnitLabel,
|
||||
PersonalMemberType,
|
||||
|
|
@ -31,19 +33,19 @@ const enumToOptions = <T extends string>(labelMap: Record<T, string>) =>
|
|||
const genderOptions = enumToOptions(GenderLabel)
|
||||
const politicalStatusOptions = enumToOptions(PoliticalStatusLabel)
|
||||
const educationOptions = enumToOptions(EducationLevelLabel)
|
||||
const titleOptions = enumToOptions(ProfessionalTitleLabel)
|
||||
const regionOptions = enumToOptions(RegionOrUnitLabel)
|
||||
const memberTypeOptions = enumToOptions(PersonalMemberTypeLabel)
|
||||
|
||||
// 地区(协会会长、秘书长场景使用,沿用原有省内地区)
|
||||
// 地区(地区中医药学/协会会长、秘书长场景使用,金华市内区县)
|
||||
const cityOptions = [
|
||||
'杭州市', '宁波市', '湖州市', '嘉兴市', '绍兴市', '舟山市',
|
||||
'温州市', '台州市', '丽水市', '金华市', '衢州市',
|
||||
'婺城区', '金东区', '开发区', '义乌市', '兰溪市', '东阳市',
|
||||
'永康市', '浦江县', '武义县', '磐安县',
|
||||
]
|
||||
|
||||
/** 表单字段类型: 包含 PersonalRegisterParams + UI 辅助字段 */
|
||||
type FormValues = PersonalRegisterParams & {
|
||||
confirmPassword?: string
|
||||
branchCommittee?: string
|
||||
cityAssociation?: string
|
||||
}
|
||||
|
||||
|
|
@ -63,12 +65,17 @@ const PersonalRegister = () => {
|
|||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
confirmPassword,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
branchCommittee,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
cityAssociation,
|
||||
...payload
|
||||
} = values
|
||||
|
||||
// 非专科分会委员不传 specialCommitteeMemberType
|
||||
if (
|
||||
payload.memberType !== PersonalMemberType.SPECIAL_COMMITTEE_MEMBER
|
||||
) {
|
||||
delete payload.specialCommitteeMemberType
|
||||
}
|
||||
|
||||
// 调用真实注册接口
|
||||
// 后端约定: success === true 表示成功, 返回值是 data 字段内容
|
||||
// 业务失败会被拦截器统一 reject + message.error,走 catch
|
||||
|
|
@ -81,23 +88,11 @@ const PersonalRegister = () => {
|
|||
setToken(accessToken)
|
||||
}
|
||||
|
||||
message.success('注册成功!')
|
||||
message.success('注册成功,请登录!')
|
||||
|
||||
// 跳转到支付页面
|
||||
// 注册成功后跳转到个人会员登录页,由用户主动登录
|
||||
setTimeout(() => {
|
||||
navigate('/payment', {
|
||||
state: {
|
||||
memberId: result?.memberId,
|
||||
phone: payload.phone,
|
||||
memberType: PersonalMemberTypeLabel[
|
||||
(payload.memberType as PersonalMemberType) ||
|
||||
PersonalMemberType.ORDINARY_MEMBER
|
||||
],
|
||||
amount: 99.0,
|
||||
validPeriod: '1年',
|
||||
isRenew: false,
|
||||
},
|
||||
})
|
||||
navigate('/personal-login')
|
||||
}, 500)
|
||||
} catch (error) {
|
||||
// 请求拦截器已统一 message.error
|
||||
|
|
@ -110,7 +105,7 @@ const PersonalRegister = () => {
|
|||
const handleMemberTypeChange = (value: PersonalMemberType) => {
|
||||
setMemberType(value)
|
||||
if (value !== PersonalMemberType.SPECIAL_COMMITTEE_MEMBER) {
|
||||
form.setFieldValue('branchCommittee', undefined)
|
||||
form.setFieldValue('specialCommitteeMemberType', undefined)
|
||||
}
|
||||
if (value !== PersonalMemberType.REGIONAL_ASSOCIATION_LEADER) {
|
||||
form.setFieldValue('cityAssociation', undefined)
|
||||
|
|
@ -141,18 +136,19 @@ const PersonalRegister = () => {
|
|||
name: '张三',
|
||||
phone: '13800000001',
|
||||
email: 'demo@example.com',
|
||||
zipCode: '310000',
|
||||
zipCode: '321000',
|
||||
gender: Gender.MALE,
|
||||
identityCard: '330101199001011234',
|
||||
identityCard: '330701199001011234',
|
||||
politicalStatus: PoliticalStatus.CPC_MEMBER,
|
||||
educationLevel: EducationLevel.DOCTORAL,
|
||||
title: '主任医师',
|
||||
title: ProfessionalTitle.CHIEF_PHYSICIAN,
|
||||
position: '科室负责人',
|
||||
regionOrUnit: RegionOrUnit.HANGZHOU,
|
||||
unitName: '浙江省中医院',
|
||||
unitAddress: '杭州市西湖区邮电路54号',
|
||||
regionOrUnit: RegionOrUnit.JINHUA_TCM_HOSPITAL,
|
||||
unitName: '金华市中医医院',
|
||||
unitAddress: '金华市婺城区陕西路439号',
|
||||
// 会员类型
|
||||
memberType: PersonalMemberType.SPECIAL_COMMITTEE_MEMBER,
|
||||
specialCommitteeMemberType: '内科分会',
|
||||
// 密码
|
||||
password: '123456',
|
||||
confirmPassword: '123456',
|
||||
|
|
@ -246,9 +242,19 @@ const PersonalRegister = () => {
|
|||
<Form.Item
|
||||
label="职称"
|
||||
name="title"
|
||||
rules={[{ max: 50, message: '职称最长 50 个字符' }]}
|
||||
>
|
||||
<Input placeholder="请输入职称" maxLength={50} />
|
||||
<Select
|
||||
placeholder="请选择职称"
|
||||
showSearch
|
||||
optionFilterProp="children"
|
||||
allowClear
|
||||
>
|
||||
{titleOptions.map((opt) => (
|
||||
<Option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
|
|
@ -335,11 +341,11 @@ const PersonalRegister = () => {
|
|||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
{/* 专科分会委员 - 条件显示(UI辅助字段,接口暂不传) */}
|
||||
{/* 专科分会委员 - 条件显示(对接 specialCommitteeMemberType) */}
|
||||
{memberType === PersonalMemberType.SPECIAL_COMMITTEE_MEMBER && (
|
||||
<Form.Item
|
||||
label="专科分会"
|
||||
name="branchCommittee"
|
||||
name="specialCommitteeMemberType"
|
||||
rules={[{ required: true, message: '请选择或输入专科分会' }]}
|
||||
>
|
||||
<Select
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
.register-page {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
background: var(--page-bg);
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
|
|
@ -47,7 +47,7 @@
|
|||
}
|
||||
|
||||
.form-card .ant-card-head {
|
||||
border-bottom: 2px solid #667eea;
|
||||
border-bottom: 2px solid var(--brand-color);
|
||||
padding: 0 24px;
|
||||
min-height: 48px;
|
||||
}
|
||||
|
|
@ -107,20 +107,29 @@
|
|||
}
|
||||
|
||||
.login-link {
|
||||
color: #1890ff;
|
||||
color: var(--brand-color);
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.login-link:hover {
|
||||
color: #40a9ff;
|
||||
color: var(--brand-color-dark);
|
||||
}
|
||||
|
||||
.register-submit-btn {
|
||||
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;
|
||||
}
|
||||
|
||||
.register-submit-btn:hover,
|
||||
.register-submit-btn:focus {
|
||||
background: var(--brand-gradient-hover) !important;
|
||||
color: var(--text-on-brand) !important;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,10 @@ import type { ApiResponse } from '../api/types'
|
|||
|
||||
/** 本地存储 token 的 key (存放后端返回的 accessToken) */
|
||||
export const TOKEN_KEY = 'member_login_token'
|
||||
/** 本地存储 userId 的 key (存放后端登录返回的 userId) */
|
||||
export const USER_ID_KEY = 'member_login_user_id'
|
||||
/** 本地存储 memberNo 的 key (后端登录返回的会员编号) */
|
||||
export const MEMBER_NO_KEY = 'member_login_member_no'
|
||||
|
||||
/** 获取本地 token (accessToken) */
|
||||
export const getToken = (): string => localStorage.getItem(TOKEN_KEY) || ''
|
||||
|
|
@ -19,6 +23,31 @@ export const setToken = (token: string): void => {
|
|||
/** 清除本地 token */
|
||||
export const clearToken = (): void => {
|
||||
localStorage.removeItem(TOKEN_KEY)
|
||||
localStorage.removeItem(USER_ID_KEY)
|
||||
localStorage.removeItem(MEMBER_NO_KEY)
|
||||
}
|
||||
|
||||
/** 获取本地 userId */
|
||||
export const getUserId = (): string => localStorage.getItem(USER_ID_KEY) || ''
|
||||
/** 保存后端返回的 userId 到本地 */
|
||||
export const setUserId = (userId: string): void => {
|
||||
localStorage.setItem(USER_ID_KEY, userId)
|
||||
}
|
||||
/** 清除本地 userId */
|
||||
export const clearUserId = (): void => {
|
||||
localStorage.removeItem(USER_ID_KEY)
|
||||
}
|
||||
|
||||
/** 获取本地 memberNo (会员编号) */
|
||||
export const getMemberNo = (): string =>
|
||||
localStorage.getItem(MEMBER_NO_KEY) || ''
|
||||
/** 保存后端返回的 memberNo 到本地 */
|
||||
export const setMemberNo = (memberNo: string): void => {
|
||||
localStorage.setItem(MEMBER_NO_KEY, memberNo)
|
||||
}
|
||||
/** 清除本地 memberNo */
|
||||
export const clearMemberNo = (): void => {
|
||||
localStorage.removeItem(MEMBER_NO_KEY)
|
||||
}
|
||||
|
||||
/** 创建 axios 实例 */
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/auth.ts","./src/api/index.ts","./src/api/member.ts","./src/api/payment.ts","./src/api/types.ts","./src/pages/companylogin.tsx","./src/pages/companyregister.tsx","./src/pages/memberrules.tsx","./src/pages/memberservice.tsx","./src/pages/membervalid.tsx","./src/pages/payment.tsx","./src/pages/personallogin.tsx","./src/pages/personalregister.tsx","./src/utils/request.ts"],"version":"5.9.3"}
|
||||
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/auth.ts","./src/api/index.ts","./src/api/member.ts","./src/api/payment.ts","./src/api/types.ts","./src/pages/companylogin.tsx","./src/pages/companyregister.tsx","./src/pages/invoice.tsx","./src/pages/memberrules.tsx","./src/pages/memberservice.tsx","./src/pages/membervalid.tsx","./src/pages/payment.tsx","./src/pages/personallogin.tsx","./src/pages/personalregister.tsx","./src/utils/request.ts"],"version":"5.9.3"}
|
||||
Loading…
Reference in New Issue