Pārlūkot izejas kodu

Merge branch 'feature/first' of Web/ad-alliance into main

lidongsheng 1 gadu atpakaļ
vecāks
revīzija
fce9ba55a8
40 mainītis faili ar 1457 papildinājumiem un 152 dzēšanām
  1. 1 1
      index.html
  2. 16 0
      src/components/FilterHeaderLayout/index.module.css
  3. 22 0
      src/components/FilterHeaderLayout/index.tsx
  4. 13 13
      src/components/Nav/index.tsx
  5. 21 15
      src/lib/http/api.ts
  6. 4 0
      src/lib/http/index.ts
  7. 0 5
      src/pages/Index/index.tsx
  8. 92 92
      src/pages/Login/index.tsx
  9. 7 0
      src/pages/Manage/components/AdData/index.tsx
  10. 4 0
      src/pages/Manage/components/AdManage/components/ContentTable/index.module.css
  11. 95 0
      src/pages/Manage/components/AdManage/components/ContentTable/index.tsx
  12. 50 0
      src/pages/Manage/components/AdManage/components/ContentTable/types.d.ts
  13. 9 0
      src/pages/Manage/components/AdManage/components/CreateModal/index.module.css
  14. 117 0
      src/pages/Manage/components/AdManage/components/CreateModal/index.tsx
  15. 30 0
      src/pages/Manage/components/AdManage/components/CreateModal/types.d.ts
  16. 31 0
      src/pages/Manage/components/AdManage/components/HeaderFilter/index.tsx
  17. 5 0
      src/pages/Manage/components/AdManage/const.ts
  18. 115 0
      src/pages/Manage/components/AdManage/index.tsx
  19. 68 0
      src/pages/Manage/components/AdManage/request.ts
  20. 15 0
      src/pages/Manage/components/AdManage/types.d.ts
  21. 4 0
      src/pages/Manage/components/AppManage/components/ContentTable/index.module.css
  22. 107 0
      src/pages/Manage/components/AppManage/components/ContentTable/index.tsx
  23. 49 0
      src/pages/Manage/components/AppManage/components/ContentTable/types.d.ts
  24. 9 0
      src/pages/Manage/components/AppManage/components/CreateModal/index.module.css
  25. 157 0
      src/pages/Manage/components/AppManage/components/CreateModal/index.tsx
  26. 25 0
      src/pages/Manage/components/AppManage/components/CreateModal/types.d.ts
  27. 31 0
      src/pages/Manage/components/AppManage/components/HeaderFilter/index.tsx
  28. 10 0
      src/pages/Manage/components/AppManage/const.ts
  29. 103 0
      src/pages/Manage/components/AppManage/index.tsx
  30. 59 0
      src/pages/Manage/components/AppManage/request.ts
  31. 9 0
      src/pages/Manage/components/AppManage/types.d.ts
  32. 29 0
      src/pages/Manage/index.module.css
  33. 95 0
      src/pages/Manage/index.tsx
  34. 3 3
      src/router/router.ts
  35. 0 8
      src/router/routes/index.tsx
  36. 31 0
      src/router/routes/manage.tsx
  37. 0 2
      src/store/stateCreater.tsx
  38. 0 0
      types/home/index.d.ts
  39. 20 12
      types/index.d.ts
  40. 1 1
      types/lib/index.d.ts

+ 1 - 1
index.html

@@ -8,7 +8,7 @@
     <meta name="referrer" content="no-referrer" />
     <!-- icon -->
     <link rel="icon" type="" href="./assets/images/logo_icon.png" />
-    <title>广告联盟</title>
+    <title>优量圈</title>
   </head>
   <body>
     <div id="root"></div>

+ 16 - 0
src/components/FilterHeaderLayout/index.module.css

@@ -0,0 +1,16 @@
+.filter-header-layout {
+    width: 100%;
+    display: flex;
+    justify-content: space-between;
+    
+    .left {
+        margin-right: 16px;
+        /* background: wheat; */
+        flex-grow: 1;
+    }
+
+    .right {
+        /* background: antiquewhite; */
+        flex-shrink: 0;
+    }
+}

+ 22 - 0
src/components/FilterHeaderLayout/index.tsx

@@ -0,0 +1,22 @@
+import { ReactNode } from 'react'
+import styles from './index.module.css'
+
+interface Props {
+  leftComponent?: ReactNode, 
+  rightComponent?: ReactNode
+}
+
+export default function FilterHeaderLayout({ leftComponent, rightComponent }: Props) {    
+  return (
+    <>
+      <div className={styles['filter-header-layout']}>
+        <div className={styles['left']}>
+          { leftComponent }
+        </div>
+        <div className={styles['right']}>
+          { rightComponent }
+        </div>
+      </div>
+    </>
+  )
+}

+ 13 - 13
src/components/Nav/index.tsx

@@ -16,7 +16,7 @@ const { confirm } = Modal
 
 function Nav() {
   const [showModal, setShowModal] = useState(false)
-  const [current, setCurrent] = useState('/ad-manage')
+  const [current, setCurrent] = useState('/manage')
   const [userInfo, setUserInfo] = useState<UserInfoType>()
   const location = useLocation()
   const setUserState = useGlobalStateUpdate()
@@ -58,9 +58,9 @@ function Nav() {
     })
   }
 
-  function editPassword() {
-    setShowModal(true)
-  }
+  // function editPassword() {
+  //   setShowModal(true)
+  // }
 
   function formSubmit() {
     setShowModal(false)
@@ -82,13 +82,13 @@ function Nav() {
   ]
 
   const items: MenuProps['items'] = [
-    {
-      label: <p onClick={editPassword}>修改密码</p>,
-      key: '3',
-    },
-    {
-      type: 'divider',
-    },
+    // {
+    //   label: <p onClick={editPassword}>修改密码</p>,
+    //   key: '3',
+    // },
+    // {
+    //   type: 'divider',
+    // },
     {
       label: <p onClick={logout}>退出登录</p>,
       key: '4',
@@ -101,7 +101,7 @@ function Nav() {
         <div className={styles['logo-contianer']}>
           <div className={styles['logo-icon']}></div>
           <div className={styles['logo-title']}>
-            票圈 <span>|</span> 广告联盟
+            AD <span>|</span> 优量圈
           </div>
         </div>
         <div className={styles['tabs-contianer']}>
@@ -112,7 +112,7 @@ function Nav() {
             <Dropdown menu={{ items }} trigger={['click']}>
               <a className={styles['user-name']} onClick={(e) => e.preventDefault()}>
                 <Space>
-                  { userInfo?.company }
+                  { userInfo?.companyName }
                   <CaretDownOutlined />
                 </Space>
               </a>

+ 21 - 15
src/lib/http/api.ts

@@ -1,25 +1,31 @@
 // Login
-
 // 登录
-export const userLogin = `${import.meta.env.VITE_API_URL}/ad/platform/user/login`
-
-// 获取验证码
+export const userLogin = `${import.meta.env.VITE_API_URL}/ad/union/user/login`
+// 获取验证码 TODO
 export const sendVerificationCode = `${import.meta.env.VITE_API_URL}/ad/platform/user/sendVerificationCode`
-
-// 手机号登录
+// 手机号登录 TODO
 export const loginPhone = `${import.meta.env.VITE_API_URL}/ad/platform/user/login/phone`
-
-// 忘记密码
+// 忘记密码 TODO
 export const forgotPassword = `${import.meta.env.VITE_API_URL}/ad/platform/user/forgotPassword`
-
 // 登出
-export const userLogout = `${import.meta.env.VITE_API_URL}/ad/platform/user/logout`
-
+export const userLogout = `${import.meta.env.VITE_API_URL}/ad/union/user/logout`
 // 修改密码
-export const changePassword = `${import.meta.env.VITE_API_URL}/ad/platform/user/changePassword`
-
-// 获取用户信息
-export const getAgencyInfo = `${import.meta.env.VITE_API_URL}/ad/platform/user/getAgencyInfo`
+export const changePassword = `${import.meta.env.VITE_API_URL}/ad/union/user/changePassword`
+// 获取用户信息 
+export const getAgencyInfo = `${import.meta.env.VITE_API_URL}/ad/union/user/findUserDetailByLogin`
+
+// 应用管理
+export const appList = `${import.meta.env.VITE_API_URL}/ad/union/application/findByConditionAndPage`
+export const createApp = `${import.meta.env.VITE_API_URL}/ad/union/application/add`
+export const updateApp = `${import.meta.env.VITE_API_URL}/ad/union/application/updateById`
+export const deleteApp = `${import.meta.env.VITE_API_URL}/ad/union/application/deleteById`
+export const allAppList = `${import.meta.env.VITE_API_URL}/ad/union/application/findAllApprovePassApplication`
+
+// 广告管理
+export const adList = `${import.meta.env.VITE_API_URL}/ad/union/ad/findByConditionAndPage`
+export const createAd = `${import.meta.env.VITE_API_URL}/ad/union/ad/add`
+export const openAd = `${import.meta.env.VITE_API_URL}/ad/union/ad/open`
+export const closeAd = `${import.meta.env.VITE_API_URL}/ad/union/ad/close`
 
 export default {
 }

+ 4 - 0
src/lib/http/index.ts

@@ -60,6 +60,10 @@ class Request {
   post<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
     return this.instance.post(url, data, config)
   }
+
+  delete<T>(url: string, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
+    return this.instance.delete(url, config)
+  }
 }
 
 

+ 0 - 5
src/pages/Index/index.tsx

@@ -1,5 +0,0 @@
-export default function Index(){
-  return (<>
-        首页
-  </>)
-}

+ 92 - 92
src/pages/Login/index.tsx

@@ -19,7 +19,7 @@ export default function Login() {
       router.navigate(decodeURIComponent(searchParams.get('redirectUrl') as string))
     }else{
       sessionStorage.removeItem('advertiserId')
-      router.navigate('/index')
+      router.navigate('/')
     }
   }
 
@@ -41,11 +41,11 @@ export default function Login() {
       label: '账号登录',
       children: <AccountLogin activeTab={activeTab} onLogin={login} onEditPassword={editPassword}/>
     },
-    {
-      key: '2',
-      label: '短信登录',
-      children: <SMSlogin activeTab={activeTab} onLogin={login} onEditPassword={editPassword} />
-    }
+    // {
+    //   key: '2',
+    //   label: '短信登录',
+    //   children: <SMSlogin activeTab={activeTab} onLogin={login} onEditPassword={editPassword} />
+    // }
   ]
 
   return (
@@ -53,7 +53,7 @@ export default function Login() {
       <div className={styles['logo-banner']}>
         <div className={styles['logo-icon']}></div>
         <div className={styles['logo-title']}>
-          票圈 <span>|</span> 广告联盟
+          AD <span>|</span> 优量圈
         </div>
       </div>
       <div className={styles['login-body']}>
@@ -65,16 +65,16 @@ export default function Login() {
           {loginType === 'register' && <Register backToLogin={backToLogin}/>}
         </div>
       </div>
-      <div className={styles['login-footer']}>
+      {/* <div className={styles['login-footer']}>
         <p>客服电话 0731-85679198、18974809627 | 客服邮箱:piaoquankefu@piaoquantv.com</p>
         <p>ICP备案:湘B2-20180063 | <span onClick={() => window.open('https://beian.miit.gov.cn/')}>湘ICP备16013107-06号</span> | <span onClick={() => window.open('https://beian.mps.gov.cn/#/query/webSearch')}>湘公网安备:43019002001624</span></p>
-      </div>
+      </div> */}
     </div>
   )
 }
 
 // 密码登录
-function AccountLogin({ onLogin, onEditPassword, activeTab }: LoginTypeProps) {
+function AccountLogin({ onLogin, activeTab }: LoginTypeProps) {
   const [form] = Form.useForm()
 
   useEffect(() => {
@@ -122,94 +122,94 @@ function AccountLogin({ onLogin, onEditPassword, activeTab }: LoginTypeProps) {
           <Button type="primary" block onClick={login}>登录</Button>
         </Form.Item>
       </Form>
-      <p className={styles['forget-password']} onClick={onEditPassword}>忘记密码?</p>
+      {/* <p className={styles['forget-password']} onClick={onEditPassword}>忘记密码?</p> */}
     </div>
   )
 }
 
 // 短信登录
-function SMSlogin({ onLogin, onEditPassword, activeTab }: LoginTypeProps) {
-  const [smsText, setSmsText] = useState('获取验证码')
-  const [verificationDisabled, setVerificationDisabled] = useState(false)
-  const [form] = Form.useForm()
-
-  useEffect(() => {
-    form.resetFields()
-  }, [activeTab])
-
-  function login() {
-    form.validateFields().then(res => {
-      const { verificationCode, phone } = res
-      fetchLogin(phone, verificationCode)
-    })
-  }
-
-  const fetchLogin = debounce((phone, verificationCode) => {
-    sso.loginBySendCode(phone, verificationCode).then(res => {
-      res && onLogin()
-    })
-  }, 500)
-
-  function verificationCode() {
-    form.validateFields(['phone']).then(res => {
-      const { phone } = res
-      sso.sendCode(phone).then(res => {
-        res && countdown()
-      })
-    })
-  }
-
-  function countdown() {
-    let count = 59
-    setVerificationDisabled(true)
-    setSmsText(`${count--}`)
-
-    const interval = setInterval(() => {
-      setSmsText(`${count--}`)
-      if (interval && count === 0) {
-        clearInterval(interval)
-        setVerificationDisabled(false)
-        setSmsText('获取验证码')
-      }
-    }, 1000)
-  }
-
-  function validator() {
-    const phone = form.getFieldValue('phone')
-
-    if (!phone)
-      return Promise.resolve()
-
-    const phoneRegx = /^1[3456789]\d{9}$/
-
-    if (phoneRegx.test(phone))
-      return Promise.resolve()
+// function SMSlogin({ onLogin, onEditPassword, activeTab }: LoginTypeProps) {
+//   const [smsText, setSmsText] = useState('获取验证码')
+//   const [verificationDisabled, setVerificationDisabled] = useState(false)
+//   const [form] = Form.useForm()
+
+//   useEffect(() => {
+//     form.resetFields()
+//   }, [activeTab])
+
+//   function login() {
+//     form.validateFields().then(res => {
+//       const { verificationCode, phone } = res
+//       fetchLogin(phone, verificationCode)
+//     })
+//   }
+
+//   const fetchLogin = debounce((phone, verificationCode) => {
+//     sso.loginBySendCode(phone, verificationCode).then(res => {
+//       res && onLogin()
+//     })
+//   }, 500)
+
+//   function verificationCode() {
+//     form.validateFields(['phone']).then(res => {
+//       const { phone } = res
+//       sso.sendCode(phone).then(res => {
+//         res && countdown()
+//       })
+//     })
+//   }
+
+//   function countdown() {
+//     let count = 59
+//     setVerificationDisabled(true)
+//     setSmsText(`${count--}`)
+
+//     const interval = setInterval(() => {
+//       setSmsText(`${count--}`)
+//       if (interval && count === 0) {
+//         clearInterval(interval)
+//         setVerificationDisabled(false)
+//         setSmsText('获取验证码')
+//       }
+//     }, 1000)
+//   }
+
+//   function validator() {
+//     const phone = form.getFieldValue('phone')
+
+//     if (!phone)
+//       return Promise.resolve()
+
+//     const phoneRegx = /^1[3456789]\d{9}$/
+
+//     if (phoneRegx.test(phone))
+//       return Promise.resolve()
     
-    return Promise.reject('请输入合法手机号')
-  }
-
-  return (
-    <div className={styles['sms-login']}>
-      <Form form={form}>
-        <Form.Item name='phone' validateTrigger='onBlur' rules={[{ required: true, message: '请输入手机号' }, { validator }]}>
-          <Input placeholder='请输入手机号' allowClear />
-        </Form.Item>
-        <Form.Item name='verificationCode' rules={[{ required: true, message: '请输入验证码' }]}>
-          <div className={styles['msg-password']}>
-            <Input placeholder='请输入验证码' allowClear />
-            <Button type="primary" onClick={verificationCode} disabled={verificationDisabled}>
-              {smsText}
-            </Button>
-          </div>
-        </Form.Item>
-        <Form.Item name='loginBtn'>
-          <Button type="primary" block onClick={login}>登录</Button>
-        </Form.Item>
-      </Form>
-      <p className={styles['forget-password']} onClick={onEditPassword}>忘记密码?</p>
-    </div>
-  )
-}
+//     return Promise.reject('请输入合法手机号')
+//   }
+
+//   return (
+//     <div className={styles['sms-login']}>
+//       <Form form={form}>
+//         <Form.Item name='phone' validateTrigger='onBlur' rules={[{ required: true, message: '请输入手机号' }, { validator }]}>
+//           <Input placeholder='请输入手机号' allowClear />
+//         </Form.Item>
+//         <Form.Item name='verificationCode' rules={[{ required: true, message: '请输入验证码' }]}>
+//           <div className={styles['msg-password']}>
+//             <Input placeholder='请输入验证码' allowClear />
+//             <Button type="primary" onClick={verificationCode} disabled={verificationDisabled}>
+//               {smsText}
+//             </Button>
+//           </div>
+//         </Form.Item>
+//         <Form.Item name='loginBtn'>
+//           <Button type="primary" block onClick={login}>登录</Button>
+//         </Form.Item>
+//       </Form>
+//       <p className={styles['forget-password']} onClick={onEditPassword}>忘记密码?</p>
+//     </div>
+//   )
+// }
 
 // 忘记密码
 function Register({ backToLogin }: RegisterType) {

+ 7 - 0
src/pages/Manage/components/AdData/index.tsx

@@ -0,0 +1,7 @@
+export function AdData(){
+  return (
+    <>
+        广告数据
+    </>
+  )
+}

+ 4 - 0
src/pages/Manage/components/AdManage/components/ContentTable/index.module.css

@@ -0,0 +1,4 @@
+.pagination{
+    float: right;
+    margin-top: 16px;
+}

+ 95 - 0
src/pages/Manage/components/AdManage/components/ContentTable/index.tsx

@@ -0,0 +1,95 @@
+import { Table, Pagination, Switch } from 'antd'
+import { useEffect, useState } from 'react'
+import dayjs from 'dayjs'
+import { throttle } from 'lodash'
+import styles from './index.module.css'
+import { adTypes } from '../../const'
+import type { ColumnsType } from 'antd/es/table'
+import type { 
+  PropsType, RowDataType, 
+  PaginationOnChangeType, TableOnChangeType 
+} from './types'
+
+export default function ContentTable ({
+  tableData,
+  total,
+  pageNumber,
+  pageSize,
+  onTableChange,
+  onPaginationChange,
+  onSwitchStatus,
+}:PropsType) {
+  const [height, setHeight] = useState(500)
+  useEffect(()=>{
+    const otherHeight = 60 + 64 + 48 + 48 + 55 // 预留的高度
+    setHeight(window.innerHeight - otherHeight)
+    const resizeEvent =throttle((e)=>{
+      setHeight(e.target.innerHeight - otherHeight)
+    },300)
+    window.addEventListener('resize',resizeEvent)
+    return ()=> window.removeEventListener('resize',resizeEvent)
+  },[])
+ 
+  const columns:ColumnsType<RowDataType> = [
+    {
+      title: '广告名称',
+      dataIndex: 'unionAdName',
+    },
+    {
+      title: '广告ID',
+      dataIndex: 'unionAdId',
+    },
+    {
+      title: '所属应用',
+      dataIndex: 'unionAppName',
+    },
+    {
+      title: '广告类型',
+      dataIndex: 'unionAdPosition',
+      render: (v) => adTypes[v] || v
+    },
+    {
+      title: '状态',
+      dataIndex: 'status',
+      render:(v,row) => (
+        <Switch checked={v} onChange={(checked) => onSwitchStatus({checked,row}) }/>
+      )
+    },
+    {
+      title: '创建时间',
+      dataIndex: 'createTime',
+      render: (v) => dayjs(v).format('YYYY-MM-DD HH:mm:ss')
+    }
+  ]
+
+  const paginationChange:PaginationOnChangeType = (pageNumber, pageSize) => {
+    onPaginationChange?.({pageNumber, pageSize})
+  }
+
+  const tableChnage:TableOnChangeType = (_pagination, filters, sorter, extra) => {
+    onTableChange?.({filters, sorter, extra})
+  }
+
+  return (
+    <>
+      <Table 
+        rowKey='id'
+        columns={ columns } 
+        dataSource={ tableData } 
+        pagination={ false }
+        scroll={{y: height}}
+        onChange={ tableChnage }
+      />
+      <Pagination 
+        className={styles['pagination']}
+        showSizeChanger 
+        disabled={!total}
+        current={ pageNumber}
+        pageSize={pageSize}
+        total={total} 
+        showTotal={ (total) => `共${total}条记录` } 
+        onChange={ paginationChange }
+      />
+    </>
+  )
+}

+ 50 - 0
src/pages/Manage/components/AdManage/components/ContentTable/types.d.ts

@@ -0,0 +1,50 @@
+import type { FilterValue, SorterResult, TableCurrentDataSource } from 'antd/es/table/interface'
+import type { TablePaginationConfig } from 'antd/es/table'
+
+export interface PropsType {
+    tableData: RowDataType[]
+    total: number,
+    pageNumber?: number,
+    pageSize?: number,
+    onTableChange?: EventFunctionType<[TableChangeParamsType]>,
+    onPaginationChange?: EventFunctionType<[PaginationParamsType]>,
+    onSwitchStatus: EventFunctionType<[{checked:boolean,row:RowDataType}]>,
+    // onEditRow: EventFunctionType<[RowDataType]>,
+    // onDeleteRow: EventFunctionType<[RowDataType]>
+}
+
+//  表格行数据类型
+export interface RowDataType  {
+    id: number,
+    unionAdName: string, // 广告名称
+    unionAdId: string, // 广告ID
+    unionAdPosition: number, // 广告类型
+    unionAppName: number, // 所属应用名称
+    applicationId: number, // 所属应用id
+    status: number, // 状态
+    createTime: string, // 创建时间
+}
+
+//  onTableChange事件参数类型
+export interface TableChangeParamsType {
+    filters: Record<string, FilterValue| null>, 
+    sorter: SorterResult<RowDataType> | SorterResult<RowDataType>[], 
+    extra: TableCurrentDataSource<RowDataType>
+}
+
+// onPaginationChange事件参数类型
+export interface PaginationParamsType {
+    pageNumber: number,
+    pageSize: number,
+}
+
+// ant Table 的onChange事件类型
+export type TableOnChangeType = EventFunctionType<[
+    TablePaginationConfig,
+    Record<string, FilterValue| null>,
+    SorterResult<RowDataType> | SorterResult<RowDataType>[],
+    TableCurrentDataSource<RowDataType>
+]>
+
+//  ant Pagination 的onChange事件类型
+export type PaginationOnChangeType = EventFunctionType<[number, number]>

+ 9 - 0
src/pages/Manage/components/AdManage/components/CreateModal/index.module.css

@@ -0,0 +1,9 @@
+.custom-modal {
+
+    :global {
+        .ant-modal-header {
+            margin-bottom: 24px;
+        }
+    }
+
+}

+ 117 - 0
src/pages/Manage/components/AdManage/components/CreateModal/index.tsx

@@ -0,0 +1,117 @@
+import { Modal, Form, Select, Input, Radio } from 'antd'
+import { forwardRef, useImperativeHandle, useState } from 'react'
+import { adTypes } from '../../const'
+import styles from './index.module.css'
+import type { RowDataType } from '../ContentTable/types'
+import type { 
+  PropsType, Action, FormDataType, 
+  OptionsItemType, RadioOptionsType 
+} from './types'
+
+const { Item, useForm } = Form
+const adTypesOptions:RadioOptionsType = Object.entries(adTypes).map(([value,label])=>({value: +value,label}))
+const initialValues = {
+  unionAdPosition: 0, // 广告类型
+}
+
+const CreateModal = forwardRef(({ allAppList, onCreate, onUpdate }:PropsType, ref) => {
+  const [isOpen, setIsOpen] = useState(false)
+  const [action, setAction] = useState<Action>()
+  const [form] = useForm()
+  const [editRow, setEditRow] = useState<RowDataType>()
+  const isEdit = action === 'edit'
+  const title = isEdit ? '编辑广告' : '新建广告'
+  const allAppOptions:OptionsItemType[] = allAppList.map(({id, unionAppName})=>({label:unionAppName, value:id}))
+
+  useImperativeHandle(ref,()=>({
+    open: (editRow?:RowDataType) => {
+      setAction(editRow?'edit':'create')
+      setEditRow(editRow)
+      if (editRow) {
+        const {
+          applicationId, // 应用id
+          unionAdName, // 广告名称
+          unionAdPosition // 广告类型
+        } =  editRow 
+        form.setFieldsValue({
+          applicationId,
+          unionAdName,
+          unionAdPosition 
+        })
+      }
+      setIsOpen(true)
+    }
+  }))
+
+  const onOk = async () => {
+    try {
+      const res:FormDataType =  await form.validateFields()
+      switch (action) {
+        case 'create':
+          onCreate(res, ()=>{setIsOpen(false)})
+          break
+        case 'edit':
+          onUpdate({...(editRow as RowDataType), ...res},()=>{setIsOpen(false)})
+      }
+    } catch (error) { /* empty */ }
+  }
+
+  const onCancel = () => {
+    setIsOpen(false)
+  }
+
+  const afterOpenChange = (isOpen: boolean) => {
+    if (!isOpen) {
+      form.resetFields()
+    }
+  }
+
+  return (
+    <Modal
+      className={styles['custom-modal']}
+      centered={true}
+      open={isOpen}
+      title={title}
+      width={500}
+      onCancel={onCancel}
+      onOk={onOk}
+      afterOpenChange={afterOpenChange}
+    >
+      <Form
+        form={form}
+        initialValues={initialValues}
+        labelCol={{ span: 5 }}
+        wrapperCol={{ span: 18, offset: 1}}
+        colon={ false }
+        requiredMark={ false }
+      >
+        <Item
+          name='applicationId'
+          label='应用'
+          rules={[{ required: true, message: '请选择应用' }]}
+        >
+          <Select 
+            placeholder='请选择应用'
+            options={allAppOptions}
+          />
+        </Item>
+        <Item
+          name='unionAdName'
+          label='广告名称'
+          rules={[{ required: true, message: '请输入广告名称' }]}
+        >
+          <Input placeholder='请输入广告名称' showCount maxLength={ 20 } />
+        </Item>
+        <Item
+          name='unionAdPosition'
+          label='广告类型'
+          rules={[{ required: true, message: '请选择广告类型' }]}
+        >
+          <Radio.Group options={adTypesOptions} />
+        </Item>
+      </Form>
+    </Modal>
+  )
+})
+
+export default CreateModal

+ 30 - 0
src/pages/Manage/components/AdManage/components/CreateModal/types.d.ts

@@ -0,0 +1,30 @@
+import { RowDataType } from '../ContentTable/types'
+import { AllAppListItemType } from '../../types'
+
+export interface PropsType {
+    allAppList: AllAppListItemType[]
+    onCreate: EventFunctionType<[FormDataType,EventFunctionType<[]>]>,
+    onUpdate: EventFunctionType<[RowDataType,EventFunctionType<[]>]>
+}
+
+export interface FormDataType {
+    applicationId: number, // 应用id
+    unionAdName: string, // 广告名称
+    unionAdPosition: number, // 广告类型
+}
+
+export type Action = 'create' | 'edit' 
+
+export interface RefType {
+    open: EventFunctionType<[RowDataType?]>
+}
+
+// select 组件选项类型
+export interface OptionsItemType {
+    label: string,
+    value: string | number,
+    disabled?: boolean
+}
+
+// ant Radio 选项类型
+export type RadioOptionsType = string[] | number[] | Array<{ label: ReactNode; value: string|number|boolean; disabled?: boolean; }>

+ 31 - 0
src/pages/Manage/components/AdManage/components/HeaderFilter/index.tsx

@@ -0,0 +1,31 @@
+import { PlusOutlined } from '@ant-design/icons'
+import { Button } from 'antd'
+import FilterHeaderLayout from '@src/components/FilterHeaderLayout'
+import { debounce } from 'lodash'
+
+interface PropsType {
+  onCreate: EventFunctionType<[]>
+}
+
+export default function HeaderFilter({ onCreate }:PropsType) {
+
+  const createApp = debounce(() => {
+    onCreate()
+  }, 1000, {leading: true, trailing: false})
+
+  return (
+    <div style={{marginBottom:'16px'}}>
+      <FilterHeaderLayout 
+        rightComponent={
+          <>
+            <Button
+              type='primary'
+              icon={<PlusOutlined />} 
+              onClick={createApp}
+            >新建广告</Button>
+          </>
+        }
+      />
+    </div>
+  )
+}

+ 5 - 0
src/pages/Manage/components/AdManage/const.ts

@@ -0,0 +1,5 @@
+export const adTypes:MapType<string> =  {
+  0: '视频插屏广告'
+}
+
+

+ 115 - 0
src/pages/Manage/components/AdManage/index.tsx

@@ -0,0 +1,115 @@
+import { useEffect, useRef, useState } from 'react'
+import HeaderFilter from './components/HeaderFilter'
+import ContentTable from './components/ContentTable'
+import CreateModal from './components/CreateModal'
+import { useGlobalStateUpdate } from '@src/store/globalStates/loadingState'
+import { 
+  createAd as createAdRequest,
+  getAdList,
+  openAd,
+  clsoeAd,
+  getAllAppList
+} from './request'
+import type { 
+  RowDataType, 
+  PaginationParamsType, 
+} from './components/ContentTable/types'
+import type { 
+  RefType, 
+  FormDataType 
+} from './components/CreateModal/types'
+import { AllAppListItemType } from './types'
+
+
+
+export function AdManage(){
+  const [tableData, setTableData] = useState<RowDataType[]>([])
+  const [total, setTotal] = useState(0)
+  const [paginationParams, setPaginationParams] = useState<PaginationParamsType>({ pageNumber: 1, pageSize: 10})
+  const [allAppList, setAllAppList] = useState<AllAppListItemType[]>([])
+  const createModalRef = useRef<RefType>()
+  const setloading = useGlobalStateUpdate()
+
+  useEffect(()=>{
+    getAllAppList().then(res=>{
+      if (res) {
+        setAllAppList(res)
+      }
+    })
+  },[])
+
+  useEffect(()=>{
+    getTableData()   
+  },[paginationParams])
+
+  const  getTableData = async () => {
+    setloading(true)
+    
+    const {pageNumber,pageSize} = paginationParams
+    const data = await getAdList({
+      pageSize,
+      currentPage: pageNumber,
+    })
+    if (data) {
+      setTableData(data.objs)
+      setTotal(data.totalSize)
+    }
+
+    setloading(false)
+  }
+
+  const onPaginationChange =async (params: PaginationParamsType) => {
+    setPaginationParams(params)
+  }
+
+  const onCreate = () => {
+    createModalRef.current?.open()
+  }
+
+  const onSwitchStatus = async ({row, checked}:{row:RowDataType, checked:boolean}) => {
+    console.log(row, checked)
+    setloading(true)
+    const res =  checked ? await openAd(row.id) : await clsoeAd(row.id)
+    setloading(false)
+    if (res) {
+      getTableData()
+    }
+
+  }
+
+  const createAd = async (form: FormDataType, cb:EventFunctionType<[]>) => {
+    setloading(true)
+    const res = await createAdRequest(form)
+    setloading(false)
+    if (res) {
+      getTableData()
+      cb() // 关闭modal
+    }
+  }
+
+  const updateAd = (form: RowDataType,  cb:EventFunctionType<[]>) => {
+    console.log('更新',form)
+    // TODO 更新接口
+    cb()
+  }
+
+  return (
+    <>
+      <HeaderFilter onCreate={onCreate} />
+      <ContentTable 
+        total={total} 
+        pageNumber={paginationParams.pageNumber}
+        pageSize={paginationParams.pageSize}
+        tableData={tableData} 
+        onPaginationChange = {onPaginationChange}
+        onSwitchStatus={onSwitchStatus}
+      />
+      <CreateModal 
+        ref={createModalRef}
+        allAppList={allAppList}
+        onCreate={createAd}
+        onUpdate={updateAd}
+      />
+    </>
+  )
+}

+ 68 - 0
src/pages/Manage/components/AdManage/request.ts

@@ -0,0 +1,68 @@
+import { message } from 'antd'
+import http from '@src/lib/http'
+import {
+  adList,
+  createAd as createAdPath, 
+  openAd as openAdPath,
+  closeAd as closeAdPath,
+  allAppList
+} from '@src/lib/http/api'
+import type { 
+  AdListRequestParamsType, 
+  AllAppListItemType,
+} from './types'
+import type { RowDataType } from './components/ContentTable/types'
+import type { FormDataType } from './components/CreateModal/types'
+
+export const getAdList = async (
+  params: AdListRequestParamsType
+): Promise<ApiResponseData<RowDataType[]>|undefined> => {
+  try {
+    const res = await http.get(adList, {params})
+    const {data, code, msg} = res as ApiResponse<ApiResponseData<RowDataType[]>>
+    if (code===0) return data
+    message.error(msg ||'获取广告列表失败')
+  } catch (error) { /* empty */ }
+}
+
+export const createAd = async (
+  params: FormDataType
+): Promise<boolean|undefined> => {
+  try {
+    const res = await http.post(createAdPath, params)
+    const {code, msg} = res as ApiResponse<undefined>
+    if (code===0) return true
+    message.error(msg ||'创建广告失败')
+  } catch (error) { /* empty */ }
+}
+
+export const openAd = async (
+  id: number
+): Promise<boolean|undefined> => {
+  try {
+    const res = await http.get(openAdPath, {params:{id}})
+    const {code, msg} = res as ApiResponse<undefined>
+    if (code===0) return true
+    message.error(msg ||'开启广告失败')
+  } catch (error) { /* empty */ }
+}
+
+export const clsoeAd = async (
+  id: number
+): Promise<boolean|undefined> => {
+  try {
+    const res = await http.get(closeAdPath, {params:{id}})
+    const {code, msg} = res as ApiResponse<undefined>
+    if (code===0) return true
+    message.error(msg ||'关闭广告失败')
+  } catch (error) { /* empty */ }
+}
+
+export const getAllAppList = async (): Promise<AllAppListItemType[]|undefined> => {
+  try {
+    const res = await http.get(allAppList)
+    const {data, code, msg} = res as ApiResponse<AllAppListItemType[]>
+    if (code===0) return data
+    message.error(msg ||'获取全部应用列表失败')
+  } catch (error) { /* empty */ }
+}

+ 15 - 0
src/pages/Manage/components/AdManage/types.d.ts

@@ -0,0 +1,15 @@
+export interface AdListRequestParamsType {
+    currentPage: number,
+    pageSize: number,
+}
+
+export interface AllAppListItemType {
+    id: number,
+    unionAppName: string, // 应用名称
+    unionAppId: string, // 应用ID
+    thirdAppType: number, // 应用类型
+    status: number, // 状态
+    createTime: string, // 创建时间
+    thirdMpAppId?: string, // 小程序appid
+    thirdAppDomain?: string // H5链接
+}

+ 4 - 0
src/pages/Manage/components/AppManage/components/ContentTable/index.module.css

@@ -0,0 +1,4 @@
+.pagination{
+    float: right;
+    margin-top: 16px;
+}

+ 107 - 0
src/pages/Manage/components/AppManage/components/ContentTable/index.tsx

@@ -0,0 +1,107 @@
+import { useEffect, useState } from 'react'
+import { Space, Table, Popconfirm, Pagination } from 'antd'
+import { throttle } from 'lodash'
+import dayjs from 'dayjs'
+import styles from './index.module.css'
+import { appTypes, appStatus } from '../../const'
+import type { ColumnsType } from 'antd/es/table'
+import type { PropsType, RowDataType, PaginationOnChangeType, TableOnChangeType } from './types'
+
+export default function ContentTable ({
+  tableData,
+  total,
+  pageNumber,
+  pageSize,
+  onTableChange,
+  onPaginationChange,
+  onEditRow,
+  onDeleteRow
+}:PropsType) {
+  const [height, setHeight] = useState(500)
+  useEffect(()=>{
+    const otherHeight = 60 + 64 + 48 + 48 + 55 // 预留的高度
+    setHeight(window.innerHeight - otherHeight)
+    const resizeEvent =throttle((e)=>{
+      setHeight(e.target.innerHeight - otherHeight)
+    },300)
+    window.addEventListener('resize',resizeEvent)
+    return ()=> window.removeEventListener('resize',resizeEvent)
+  },[])
+
+  const columns:ColumnsType<RowDataType> = [
+    {
+      title: '应用名称',
+      dataIndex: 'unionAppName',
+    },
+    {
+      title: '应用ID',
+      dataIndex: 'unionAppId',
+    },
+    {
+      title: '应用类型',
+      dataIndex: 'thirdAppType',
+      render: (v) => appTypes[v] || v
+    },
+    {
+      title: '状态',
+      dataIndex: 'status',
+      render: (v) => appStatus[v] || v
+    },
+    {
+      title: '创建时间',
+      dataIndex: 'createTime',
+      render: (v) => dayjs(v).format('YYYY-MM-DD HH:mm:ss')
+    },
+    {
+      title: '操作',
+      key: 'action',
+      width: 120,
+      render: (_v,row) => {
+        return (
+          <Space size='middle' >
+            <a onClick={()=>{onEditRow(row)}}>编辑</a>
+            <Popconfirm 
+              title={`确定删除「${row.unionAppName}」这个应用?`}     
+              onConfirm={ ()=>{onDeleteRow(row)} }
+              okText="确认"
+              cancelText="取消"
+            >
+              <a>删除</a>
+            </Popconfirm>
+          </Space>
+        )
+      }
+    },
+  ]
+
+  const paginationChange:PaginationOnChangeType = (pageNumber, pageSize) => {
+    onPaginationChange?.({pageNumber, pageSize})
+  }
+
+  const tableChnage:TableOnChangeType = (_pagination, filters, sorter, extra) => {
+    onTableChange?.({filters, sorter, extra})
+  }
+
+  return (
+    <>
+      <Table 
+        rowKey='id'
+        columns={ columns } 
+        dataSource={ tableData } 
+        pagination={ false }
+        scroll={{y: height}}
+        onChange={ tableChnage }
+      />
+      <Pagination 
+        className={styles['pagination']}
+        showSizeChanger 
+        disabled={!total}
+        current={ pageNumber}
+        pageSize={pageSize}
+        total={total} 
+        showTotal={ (total) => `共${total}条记录` } 
+        onChange={ paginationChange }
+      />
+    </>
+  )
+}

+ 49 - 0
src/pages/Manage/components/AppManage/components/ContentTable/types.d.ts

@@ -0,0 +1,49 @@
+import type { FilterValue, SorterResult, TableCurrentDataSource } from 'antd/es/table/interface'
+import type { TablePaginationConfig } from 'antd/es/table'
+
+export interface PropsType {
+    tableData: RowDataType[]
+    total: number,
+    pageNumber?: number,
+    pageSize?: number,
+    onTableChange?: EventFunctionType<[TableChangeParamsType]>,
+    onPaginationChange?: EventFunctionType<[PaginationParamsType]>,
+    onEditRow: EventFunctionType<[RowDataType]>,
+    onDeleteRow: EventFunctionType<[RowDataType]>
+}
+
+//  表格行数据类型
+export interface RowDataType  {
+    id: number,
+    unionAppName: string, // 应用名称
+    unionAppId: string, // 应用ID
+    thirdAppType: number, // 应用类型
+    status: number, // 状态
+    createTime: string, // 创建时间
+    thirdMpAppId?: string, // 小程序appid
+    thirdAppDomain?: string // H5链接
+}
+
+//  onTableChange事件参数类型
+export interface TableChangeParamsType {
+    filters: Record<string, FilterValue| null>, 
+    sorter: SorterResult<RowDataType> | SorterResult<RowDataType>[], 
+    extra: TableCurrentDataSource<RowDataType>
+}
+
+// onPaginationChange事件参数类型
+export interface PaginationParamsType {
+    pageNumber: number,
+    pageSize: number,
+}
+
+// ant Table 的onChange事件类型
+export type TableOnChangeType = EventFunctionType<[
+    TablePaginationConfig,
+    Record<string, FilterValue| null>,
+    SorterResult<RowDataType> | SorterResult<RowDataType>[],
+    TableCurrentDataSource<RowDataType>
+]>
+
+//  ant Pagination 的onChange事件类型
+export type PaginationOnChangeType = EventFunctionType<[number, number]>

+ 9 - 0
src/pages/Manage/components/AppManage/components/CreateModal/index.module.css

@@ -0,0 +1,9 @@
+.custom-modal {
+
+    :global {
+        .ant-modal-header {
+            margin-bottom: 24px;
+        }
+    }
+
+}

+ 157 - 0
src/pages/Manage/components/AppManage/components/CreateModal/index.tsx

@@ -0,0 +1,157 @@
+import { Modal, Form, Select, Input } from 'antd'
+import type { PropsType, Action, FormDataType, OptionsItemType } from './types'
+import { forwardRef, useImperativeHandle, useState } from 'react'
+import type { RowDataType } from '../ContentTable/types'
+import { appTypes } from '../../const'
+import styles from './index.module.css'
+
+const { Item, useForm } = Form
+const appTypesOptions:OptionsItemType[] = Object.entries(appTypes).map(([value,label])=>({value: +value,label}))
+const initialValues = {
+  thirdAppType: 0, // 应用类型
+}
+
+const CreateModal = forwardRef(({ onCreate, onUpdate }:PropsType, ref) => {
+  const [isOpen, setIsOpen] = useState(false)
+  const [action, setAction] = useState<Action>()
+  const [form] = useForm()
+  const [editRow, setEditRow] = useState<RowDataType>()
+  const isEdit = action === 'edit'
+  const title = isEdit ? '编辑应用' : '新建应用'
+
+  useImperativeHandle(ref,()=>({
+    open: (editRow?:RowDataType) => {
+      setAction(editRow?'edit':'create')
+      setEditRow(editRow)
+      if (editRow) {
+        const {
+          thirdAppType, // 应用类型
+          unionAppName, // 应用名称
+          thirdMpAppId, // 小程序appid
+          thirdAppDomain // H5链接
+        } =  editRow 
+        form.setFieldsValue({
+          thirdAppType, // 应用类型
+          unionAppName, // 应用名称
+          thirdMpAppId, // 小程序appid
+          thirdAppDomain // H5链接
+        })
+      }
+      setIsOpen(true)
+    }
+  }))
+
+ 
+
+  const onOk = async () => {
+    try {
+      const res:FormDataType =  await form.validateFields()
+      switch (action) {
+        case 'create':
+          onCreate(res, ()=>{setIsOpen(false)})
+          break
+        case 'edit':
+          onUpdate({...(editRow as RowDataType), ...res},()=>{setIsOpen(false)})
+      }
+    } catch (error) { /* empty */ }
+  }
+
+  const onCancel = () => {
+    setIsOpen(false)
+  }
+
+  const afterOpenChange = (isOpen: boolean) => {
+    if (!isOpen) {
+      form.resetFields()
+    }
+  }
+
+  return (
+    <Modal
+      className={styles['custom-modal']}
+      centered={true}
+      open={isOpen}
+      title={title}
+      width={500}
+      onCancel={onCancel}
+      onOk={onOk}
+      afterOpenChange={afterOpenChange}
+    >
+      <Form
+        form={form}
+        initialValues={initialValues}
+        labelCol={{ span: 5 }}
+        wrapperCol={{ span: 18, offset: 1}}
+        colon={ false }
+        requiredMark={ false }
+      >
+        { !isEdit && <Item
+          name='thirdAppType'
+          label='应用类型'
+          rules={[{ required: true, message: '请选择应用类型' }]}
+        >
+          <Select 
+            placeholder='请选择应用类型'
+            options={appTypesOptions}
+          />
+        </Item>
+        }
+        <Item
+          name='unionAppName'
+          label='应用名称'
+          rules={[{ required: true, message: '请输入应用名称' }]}
+        >
+          <Input placeholder='请输入应用名称' showCount maxLength={ 20 } />
+        </Item>
+        {
+          !isEdit &&  <>
+            <Item
+              noStyle
+              shouldUpdate={(preV, curV) => preV.thirdAppType !== curV.thirdAppType}
+            >
+              {
+                ({getFieldValue}) => getFieldValue('thirdAppType')===0 && (
+                  <Form.Item 
+                    name='thirdMpAppId' 
+                    label='小程序appId'
+                    rules={[{ required: true, message: '请输入小程序appId' }]}
+                  >
+                    <Input placeholder='请输入小程序appId' disabled={isEdit} />
+                  </Form.Item>
+                ) 
+              }
+            </Item>
+            <Item
+              noStyle
+              shouldUpdate={(preV, curV) => preV.thirdAppType !== curV.thirdAppType}
+            >
+              {
+                ({getFieldValue}) => getFieldValue('thirdAppType')===1 && (
+                  <Form.Item 
+                    name='thirdAppDomain' 
+                    label='H5链接'
+                    rules={[
+                      () => ({
+                        validator(_, value) {
+                          if (!value) return Promise.reject(new Error('请输入H5链接'))
+                          // H5,验证网址格式
+                          const pattern = /^(https:\/\/)[^\s]+/
+                          if (!pattern.test(value)) return Promise.reject(new Error('请输入https://开头的链接'))
+                          return Promise.resolve()
+                        },
+                      })
+                    ]}
+                  >
+                    <Input placeholder='请输入H5链接'  disabled={isEdit} />
+                  </Form.Item>
+                ) 
+              }
+            </Item>
+          </>
+        }
+      </Form>
+    </Modal>
+  )
+})
+
+export default CreateModal

+ 25 - 0
src/pages/Manage/components/AppManage/components/CreateModal/types.d.ts

@@ -0,0 +1,25 @@
+import { RowDataType } from '../ContentTable/types'
+
+export interface PropsType {
+    onCreate: EventFunctionType<[FormDataType,EventFunctionType<[]>]>,
+    onUpdate: EventFunctionType<[RowDataType,EventFunctionType<[]>]>
+}
+
+export interface FormDataType {
+    thirdAppType: number, // 应用类型
+    unionAppName: string, // 应用名称
+    thirdMpAppId?: string, // 小程序appid
+    thirdAppDomain?: string // H5链接
+}
+
+export type Action = 'create' | 'edit' 
+
+export interface RefType {
+    open: EventFunctionType<[RowDataType?]>
+}
+
+// select 组件选项类型
+export interface OptionsItemType {
+    label: string,
+    value: string | number
+}

+ 31 - 0
src/pages/Manage/components/AppManage/components/HeaderFilter/index.tsx

@@ -0,0 +1,31 @@
+import { PlusOutlined } from '@ant-design/icons'
+import { Button } from 'antd'
+import FilterHeaderLayout from '@src/components/FilterHeaderLayout'
+import { debounce } from 'lodash'
+
+interface PropsType {
+  onCreate: EventFunctionType<[]>
+}
+
+export default function HeaderFilter({ onCreate }:PropsType) {
+
+  const createApp = debounce(() => {
+    onCreate()
+  }, 1000, {leading: true, trailing: false})
+
+  return (
+    <div style={{marginBottom:'16px'}}>
+      <FilterHeaderLayout 
+        rightComponent={
+          <>
+            <Button
+              type='primary'
+              icon={<PlusOutlined />} 
+              onClick={createApp}
+            >新建应用</Button>
+          </>
+        }
+      />
+    </div>
+  )
+}

+ 10 - 0
src/pages/Manage/components/AppManage/const.ts

@@ -0,0 +1,10 @@
+export const appTypes:MapType<string> =  {
+  0: '小程序',
+  1: 'H5',
+}
+
+export const appStatus:MapType<string> = {
+  0: '审核不通过',
+  1: '审核通过',
+  2: '未审核'
+}

+ 103 - 0
src/pages/Manage/components/AppManage/index.tsx

@@ -0,0 +1,103 @@
+import { useEffect, useRef, useState } from 'react'
+import HeaderFilter from './components/HeaderFilter'
+import ContentTable from './components/ContentTable'
+import CreateModal from './components/CreateModal'
+import { useGlobalStateUpdate } from '@src/store/globalStates/loadingState'
+import { 
+  getAppList, 
+  createApp as createAppRequest,
+  updateApp as updateAppRequest,
+  deleteApp as deleteAppRequest
+} from './request'
+import type{  RefType, FormDataType } from './components/CreateModal/types'
+import type { RowDataType, PaginationParamsType } from './components/ContentTable/types'
+
+export function AppManage(){
+  const [tableData, setTableData] = useState<RowDataType[]>([])
+  const [total, setTotal] = useState(0)
+  const [paginationParams, setPaginationParams] = useState<PaginationParamsType>({ pageNumber: 1, pageSize: 10})
+  const createModalRef = useRef<RefType>()
+  const setloading = useGlobalStateUpdate()
+
+  useEffect(()=>{
+    getTableData()   
+  },[paginationParams])
+
+  const  getTableData = async () => {
+    setloading(true)
+
+    const {pageNumber,pageSize} = paginationParams
+    const data = await getAppList({
+      pageSize,
+      currentPage: pageNumber,
+    })
+    if (data) {
+      setTableData(data.objs)
+      setTotal(data.totalSize)
+    }
+
+    setloading(false)
+  }
+
+  const onPaginationChange =async (params: PaginationParamsType) => {
+    setPaginationParams(params)
+  }
+
+  const onCreate = () => {
+    createModalRef.current?.open()
+  }
+
+  const editRow = (row: RowDataType) => {
+    createModalRef.current?.open(row)
+  }
+
+  const deleteApp = async (row: RowDataType) => {
+    setloading(true)
+    const res = await deleteAppRequest(row.id)
+    setloading(false)
+    if (res) {
+      getTableData()
+    }
+  }
+
+  const createApp = async (form: FormDataType, cb:EventFunctionType<[]>) => {
+    setloading(true)
+    const res = await createAppRequest(form)
+    setloading(false)
+    if (res) {
+      getTableData()
+      cb() // 关闭modal
+    }
+  }
+
+  const updateApp = async (form: RowDataType,  cb:EventFunctionType<[]>) => {
+    setloading(true)
+    const {id, unionAppName} = form
+    const res = await updateAppRequest({id, unionAppName})
+    setloading(false)
+    if (res) {
+      getTableData()
+      cb() // 关闭modal
+    }
+  }
+
+  return (
+    <>
+      <HeaderFilter onCreate={onCreate} />
+      <ContentTable 
+        total={ total } 
+        pageNumber={ paginationParams.pageNumber }
+        pageSize={ paginationParams.pageSize }
+        tableData={ tableData } 
+        onPaginationChange = { onPaginationChange }
+        onEditRow={ editRow } 
+        onDeleteRow={ deleteApp }
+      />
+      <CreateModal 
+        ref={createModalRef}
+        onCreate={createApp}
+        onUpdate={updateApp}
+      />
+    </>
+  )
+}

+ 59 - 0
src/pages/Manage/components/AppManage/request.ts

@@ -0,0 +1,59 @@
+import { message } from 'antd'
+import http from '@src/lib/http'
+import {
+  appList,
+  createApp as createAppPath, 
+  updateApp as updateAppPath, 
+  deleteApp as deleteAppPath
+} from '@src/lib/http/api'
+import type { 
+  AppListRequestParamsType, 
+  UpdateAppRequestParamsType 
+} from './types'
+import type { RowDataType } from './components/ContentTable/types'
+import type { FormDataType } from './components/CreateModal/types'
+
+export const getAppList = async (
+  params: AppListRequestParamsType
+): Promise<ApiResponseData<RowDataType[]>|undefined> => {
+  try {
+    const res = await http.get(appList, {params})
+    const {data, code, msg} = res as ApiResponse<ApiResponseData<RowDataType[]>>
+    if (code===0) return data
+    message.error(msg ||'获取应用列表失败')
+  } catch (error) { /* empty */ }
+}
+
+export const createApp = async (
+  params: FormDataType
+): Promise<boolean|undefined> => {
+  try {
+    const res = await http.post(createAppPath, params)
+    const {code, msg} = res as ApiResponse<undefined>
+    if (code===0) return true
+    message.error(msg ||'创建应用失败')
+  } catch (error) { /* empty */ }
+}
+
+export const updateApp = async (
+  params: UpdateAppRequestParamsType
+): Promise<boolean|undefined> => {
+  try {
+    const res = await http.post(updateAppPath, params)
+    const {code, msg} = res as ApiResponse<undefined>
+    if (code===0) return true
+    message.error(msg ||'修改应用失败')
+  } catch (error) { /* empty */ }
+}
+
+export const deleteApp = async (
+  id: number
+): Promise<boolean|undefined> => {
+  try {
+    const res = await http.delete(deleteAppPath, {params:{id}})
+    const {code, msg} = res as ApiResponse<undefined>
+    if (code===0) return true
+    message.error(msg ||'删除应用失败')
+  } catch (error) { /* empty */ }
+}
+

+ 9 - 0
src/pages/Manage/components/AppManage/types.d.ts

@@ -0,0 +1,9 @@
+export interface AppListRequestParamsType {
+    currentPage: number,
+    pageSize: number,
+}
+
+export interface UpdateAppRequestParamsType {
+    id: number,
+    unionAppName: string,
+}

+ 29 - 0
src/pages/Manage/index.module.css

@@ -0,0 +1,29 @@
+.container {
+    height: 100%;
+    position: relative;
+}
+
+.sider {
+    padding-top: 8px;
+    margin: 16px;
+    border-radius: 8px;
+    overflow: hidden;
+    margin-right: 0;
+
+    :global {
+        .ant-layout-sider-children {
+            overflow: auto;
+        }
+        .ant-layout-sider-trigger {
+            position: absolute;
+        }
+    }
+}
+
+.content {
+    background-color: #fff;
+    margin: 16px;
+    padding: 16px;
+    border-radius: 8px;
+    overflow: auto;
+}

+ 95 - 0
src/pages/Manage/index.tsx

@@ -0,0 +1,95 @@
+import { ReactNode, useState } from 'react'
+import { Outlet, useLocation, useNavigate } from 'react-router-dom'
+import { Layout, Menu } from 'antd'
+import {
+  // PieChartOutlined,
+  AppstoreOutlined,
+} from '@ant-design/icons'
+import styles from './index.module.css'
+
+const { Sider, Content } = Layout
+
+export default function Index(){
+  const [collapsed, setCollapsed] = useState(false)
+  const location = useLocation()
+  const navigate = useNavigate()
+  
+  const {pathname} = location
+  const defaultSelectedKeys = [pathname]
+  const defaultOpenKeys = findPath(items, pathname)
+  
+  return (<>
+    <Layout className={styles['container']}>
+      <Sider 
+        className={styles['sider']} 
+        theme='light' 
+        collapsible 
+        collapsed={collapsed} 
+        breakpoint='xl'
+        onCollapse={(v)=>setCollapsed(v)}
+        collapsedWidth={60}
+      >
+        <Menu 
+          defaultSelectedKeys={defaultSelectedKeys} 
+          defaultOpenKeys={defaultOpenKeys}
+          mode="inline" 
+          items={items} 
+          onSelect={(v)=>{navigate(v.key)}}
+        />
+      </Sider>
+      <Content className={styles['content']}>
+        <Outlet/>
+      </Content>
+    </Layout>
+  </>)
+}
+
+const items: MenuItem[] = [
+  {
+    label: '广告设置',
+    key: 'manage',
+    icon: <AppstoreOutlined />,
+    children: [
+      {
+        label: '应用列表',
+        key: '/manage/app-manage'
+      },
+      {
+        label: '广告列表',
+        key: '/manage/ad-manage',
+      }
+    ]
+  },
+  // {
+  //   label: '广告数据',
+  //   key: '/manage/ad-data',
+  //   icon: <PieChartOutlined />
+  // }
+]
+
+interface MenuItem {
+  label: string,
+  key: string,
+  icon?: ReactNode,
+  children?: MenuItem[],
+  types?: string
+}
+
+function findPath(menus:MenuItem[],findKey:string): string[]|undefined {
+  const path: string[] =  []
+
+  function dfs(items:MenuItem[]): boolean {
+    for (let i = 0; i < items.length; i++) {
+      const {key,children} = items[i]
+      path.push(key)
+      if (key===findKey) 
+        return true
+      if (children?.length && dfs(children)) 
+        return true
+      path.pop()
+    }
+    return false
+  }
+  
+  return dfs(menus) ? path : undefined
+}

+ 3 - 3
src/router/router.ts

@@ -3,7 +3,7 @@ import Home from '@src/pages/Home'
 import Login from '@src/pages/Login'
 import ErrorPage from '@src/pages/ErrorPage'
 import sso from '@src/lib/http/sso.ts'
-import Index from './routes/index'
+import Manage from './routes/manage'
 
 export default createBrowserRouter([
   {
@@ -19,12 +19,12 @@ export default createBrowserRouter([
   
       // 默认首页
       if (request.url.endsWith('/'))
-        return redirect('/index')
+        return redirect('/manage/app-manage')
       
       return null
     },
     children: [
-      Index
+      Manage
     ]
   },
   {

+ 0 - 8
src/router/routes/index.tsx

@@ -1,8 +0,0 @@
-import { RouteObject } from 'react-router-dom'
-import Index from '@src/pages/Index'
-
-const routeObject: RouteObject = {
-  path: 'index',
-  Component: Index
-}
-export default routeObject

+ 31 - 0
src/router/routes/manage.tsx

@@ -0,0 +1,31 @@
+import { RouteObject } from 'react-router-dom'
+import Manage from '@src/pages/Manage'
+
+const routeObject: RouteObject = {
+  path: 'manage',
+  Component: Manage,
+  children:[
+    {
+      path: 'app-manage',
+      lazy: async () => {
+        const {AppManage} = await import('@src/pages/Manage/components/AppManage')
+        return {Component:AppManage}
+      }
+    },
+    {
+      path: 'ad-manage',
+      lazy: async () => {
+        const {AdManage} = await import('@src/pages/Manage/components/AdManage')
+        return {Component:AdManage}
+      }
+    },
+    {
+      path: 'ad-data',
+      lazy: async () => {
+        const {AdData} = await import('@src/pages/Manage/components/AdData')
+        return {Component:AdData}
+      }
+    }
+  ]
+}
+export default routeObject

+ 0 - 2
src/store/stateCreater.tsx

@@ -1,6 +1,4 @@
 import React, { useState, createContext, useContext, PropsWithChildren } from 'react'
-// import { reactComponentProps } from ""
-
 
 // 构造器函数,用于创建全局状态管理钩子
 function createGlobalStateHook<T>(initialState?: T) {

+ 0 - 0
types/home/index.d.ts


+ 20 - 12
types/index.d.ts

@@ -1,13 +1,21 @@
 type UserInfoType = {
-  aptitudeType: number
-  company: string
-  contactPerson: string
-  createTime: string
-  email: string
-  id: number
-  isDelete: number
-  phone: string
-  picAddress: string
-  shortName: string
-  updateTime: string
-}
+  accountInfoId: number, // 账号信息主键ID
+  trafficMasterCode: string, // 流量主Code
+  phone: string, // 手机号
+  email: string, // 邮箱
+  companyInfoId: number, // 公司信息表主键ID
+  companyName: string, // 公司名称
+  contactPerson: string, // 联系人
+  bankAccountCompany: string, // 开户公司
+  companyAddress: string, // 公司所在地
+  bankName: string, // 名称银行
+  bankAccount: string // 银行账号
+}
+
+// 定义组件的事件类型
+type EventFunctionType<T extends unknown[]> = (...params: T) => void 
+
+type Keys = number | string
+interface MapType<V> {
+  [key: string|number|symbol]: V
+}

+ 1 - 1
types/lib/index.d.ts

@@ -5,7 +5,7 @@ interface ApiResponse<T> {
   success: boolean
 }
 
-interface AdResponseData<T> {
+interface ApiResponseData<T> {
   objs: T
   totalSize: number,
   totalPage?: number