index.vue 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831
  1. <!--
  2. * @Author: wanglj wanglijie@dashoo.cn
  3. * @Date: 2025-03-11 18:02:10
  4. * @LastEditors: wanglj wanglijie@dashoo.cn
  5. * @LastEditTime: 2025-03-19 18:23:43
  6. * @FilePath: \vant-demo-master\vant\vue3-ts\src\view\login\index.vue
  7. * @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
  8. -->
  9. <template>
  10. <div class="register">
  11. <div class="form">
  12. <van-row class="pl10 pr10 pt10">
  13. <van-button type="success" @click="changeType('10')">
  14. 注册课题组成员
  15. </van-button>
  16. <van-button type="primary" @click="changeType('20')">
  17. 注册课题组负责人
  18. </van-button>
  19. </van-row>
  20. <van-steps :active="state.active" class="pl20 pr20 pt10">
  21. <van-step>登录信息</van-step>
  22. <van-step>个人信息</van-step>
  23. <van-step v-if="state.form.registerType == '20'">项目信息</van-step>
  24. </van-steps>
  25. <van-form ref="loginInfoRef" v-show="state.active == 0" required="auto">
  26. <van-cell-group inset>
  27. <van-field v-model="state.form.userName" label="登录账号" placeholder="登录账号" :rules="[{ required: true, message: '请填写登录账号' },
  28. {validator: checkUserNameExists, message: '账号不可用'}]" />
  29. <van-field v-model="state.form.password" :type="state.showPassword ? 'text' : 'password'" label="密码" placeholder="密码"
  30. :right-icon="state.showPassword ? 'eye-o' : 'closed-eye'"
  31. @click-right-icon="state.showPassword = !state.showPassword"
  32. :rules="[{ required: true, message: '密码不能为空'}, {validator: checkPassword, message: '密码不合法'}]" />
  33. <van-field v-model="state.form.confirmPassword" :type="state.showConfirmPassword ? 'text' : 'password'" label="确认密码" placeholder="确认密码"
  34. :right-icon="state.showConfirmPassword ? 'eye-o' : 'closed-eye'"
  35. @click-right-icon="state.showConfirmPassword = !state.showConfirmPassword"
  36. :rules="[{ required: true, validator: confirmPasswordSame, message: '两次输入的密码不一致' }]" />
  37. </van-cell-group>
  38. </van-form>
  39. <van-form ref="personInfoRef" v-show="state.active == 1" required="auto">
  40. <van-cell-group inset>
  41. <van-field v-model="state.form.nickName" label="姓名" placeholder="姓名"
  42. :rules="[{ required: true, message: '请填写姓名' }]" />
  43. <van-field label="性别" :rules="[{ required: false }]">
  44. <template #input>
  45. <van-radio-group v-model="state.form.sex" label="性别" placeholder="性别" direction="horizontal">
  46. <van-radio v-for="item in userSexList" :name="item.dictValue" :key="item.dictValue">
  47. {{ item.dictLabel }}
  48. </van-radio>
  49. </van-radio-group>
  50. </template>
  51. </van-field>
  52. <van-field>
  53. <template #label>
  54. <span>用户类型</span>
  55. <el-tooltip class="box-item" effect="dark" placement="top" append-to="body">
  56. <template #content>
  57. <div v-for="item in UserTypeTooltip" :key="item">
  58. {{ item }}
  59. </div>
  60. </template>
  61. <el-icon style="margin-left: 10px;">
  62. <QuestionFilled />
  63. </el-icon>
  64. </el-tooltip>
  65. </template>
  66. <template #input>
  67. <van-radio-group v-model="state.form.userType" label="用户类型" placeholder="用户类型" direction="horizontal">
  68. <van-radio v-for="item in userTypeList" :name="item.dictValue" :key="item.dictValue">
  69. {{ item.dictLabel }}
  70. </van-radio>
  71. </van-radio-group>
  72. </template>
  73. </van-field>
  74. <van-field v-model="state.form.deptName" is-link readonly label="组织部门" placeholder="组织部门"
  75. @click="showDeptPickerHandler" :rules="[{ required: true, message: '请选择组织部门' }]" />
  76. <van-field v-if="needInputDeptInfo" v-model="state.form.deptDescribe" label="部门描述" placeholder="部门描述"
  77. :rules="[{ required: true, message: '请填写部门描述' }]" />
  78. <van-field v-model="state.form.phone" type="tel" label="手机号" placeholder="手机号"
  79. @blur="checkUserNamePhoneExists('phone')" :rules="[{ required: true, message: '请填写手机号' }]" />
  80. <van-field v-model="state.form.email" label="邮箱" placeholder="邮箱"
  81. :rules="[{ required: true, message: '请填写邮箱' }]" />
  82. <van-field label="证件类型" :rules="[{ required: true, message: '请选择证件类型' }]">
  83. <template #input>
  84. <van-radio-group disabled v-model="state.form.idType" label="证件类型" placeholder="证件类型"
  85. direction="horizontal">
  86. <van-radio v-for="item in userCertList" :name="item.dictValue" :key="item.id">
  87. {{ item.dictLabel }}
  88. </van-radio>
  89. </van-radio-group>
  90. </template>
  91. </van-field>
  92. <van-field v-model="state.form.idCode" label="证件号" placeholder="证件号"
  93. :rules="[{ required: true, message: '请填写证件号' }]" />
  94. <van-field v-model="state.form.projectGroupName" v-if="state.form.registerType === '20'" label="课题组"
  95. placeholder="课题组" :rules="[{ required: true, message: '请填写课题组' }]" />
  96. <template v-else>
  97. <van-field :disabled="!state.form.deptId" v-model="state.form.projectGroupName" is-link readonly label="课题组"
  98. placeholder="课题组" :rules="[{ required: true, message: '请填写课题组' }]" @click="showPjtPicker = true" />
  99. </template>
  100. </van-cell-group>
  101. </van-form>
  102. <van-form ref="projectInfoRef" v-show="state.active == 2" required="auto">
  103. <van-cell-group inset>
  104. <div class="project-list-container">
  105. <van-button type="primary" block @click="addProject" class="add-project-btn">
  106. 新增项目
  107. </van-button>
  108. <div v-if="state.form.projectList.length === 0" class="empty-project-tip">
  109. 请至少添加一个项目
  110. </div>
  111. <div v-for="(item, index) in state.form.projectList" :key="index" class="project-card">
  112. <div class="project-card-header">
  113. <span class="project-card-title">项目 {{ index + 1 }}</span>
  114. <van-button
  115. v-if="state.form.projectList.length > 1"
  116. size="mini"
  117. type="danger"
  118. plain
  119. @click="delProjectItem(index)"
  120. class="delete-btn">
  121. 删除
  122. </van-button>
  123. </div>
  124. <div class="project-card-body">
  125. <van-field
  126. v-model="item.projectName"
  127. label="项目名称"
  128. placeholder="请输入项目名称"
  129. :rules="[{ required: true, message: '请输入项目名称' }]"
  130. class="project-field">
  131. </van-field>
  132. <van-field
  133. v-model="item.projectTypeName"
  134. is-link
  135. readonly
  136. label="项目类型"
  137. placeholder="请选择项目类型"
  138. @click="openPjtType(item, index)"
  139. :rules="[{ required: true, message: '请选择项目类型' }]"
  140. class="project-field">
  141. </van-field>
  142. <van-field
  143. v-model="item.projectSource"
  144. label="项目来源"
  145. placeholder="请输入项目来源"
  146. :rules="[{ required: true, message: '请输入项目来源' }]"
  147. class="project-field">
  148. </van-field>
  149. </div>
  150. </div>
  151. </div>
  152. </van-cell-group>
  153. </van-form>
  154. </div>
  155. <footer>
  156. <van-button v-if="state.active > 0" @click="preStep">
  157. 上一步
  158. </van-button>
  159. <van-button v-if="state.active < (state.form.registerType == '10' ? 1 : 2)" type="primary" class="ml10"
  160. @click="nextStep">
  161. 下一步
  162. </van-button>
  163. <van-button v-if="
  164. (state.form.registerType == '10' && state.active === 1) ||
  165. (state.form.registerType == '20' && state.active === 2)
  166. " @click="onRegister" type="primary" class="ml10">
  167. 提交
  168. </van-button>
  169. </footer>
  170. <!-- 部门选择器 -->
  171. <van-popup v-model:show="showDeptPicker" position="bottom">
  172. <div class="cascade-picker">
  173. <div class="cascade-header">
  174. <span>请选择部门</span>
  175. <van-icon name="cross" @click="showDeptPicker = false" />
  176. </div>
  177. <div class="cascade-content">
  178. <!-- 第一级 -->
  179. <div class="cascade-column">
  180. <div v-for="item in cascadeData.level1" :key="item.id" class="cascade-item"
  181. :class="{ active: selectedLevel1?.id === item.id }" @click="selectLevel1(item)">
  182. <span>{{ item.deptName }}{{ item.children?.length ? `(${item.children.length})` : '' }}</span>
  183. <van-icon v-if="item.children?.length" name="arrow" />
  184. </div>
  185. </div>
  186. <!-- 第二级 -->
  187. <div v-if="cascadeData.level2.length" class="cascade-column">
  188. <div v-for="item in cascadeData.level2" :key="item.id" class="cascade-item"
  189. :class="{ active: selectedLevel2?.id === item.id }" @click="selectLevel2(item)">
  190. <span>{{ item.deptName }}{{ item.children?.length ? `(${item.children.length})` : '' }}</span>
  191. <van-icon v-if="item.children?.length" name="arrow" />
  192. </div>
  193. </div>
  194. <!-- 第三级 -->
  195. <div v-if="cascadeData.level3.length" class="cascade-column">
  196. <div v-for="item in cascadeData.level3" :key="item.id" class="cascade-item"
  197. :class="{ active: selectedLevel3?.id === item.id }" @click="selectLevel3(item)">
  198. <span>{{ item.deptName }}</span>
  199. </div>
  200. </div>
  201. </div>
  202. <div class="cascade-footer">
  203. <van-button @click="showDeptPicker = false">取消</van-button>
  204. <van-button type="primary" @click="confirmCascadeSelection">
  205. 确定
  206. </van-button>
  207. </div>
  208. </div>
  209. </van-popup>
  210. <!-- 课题组选择器 -->
  211. <van-popup v-model:show="showPjtPicker" position="bottom">
  212. <van-picker :columns="pjtList" :columns-field-names="{ text: 'pgName', value: 'id' }" @confirm="onPjtPicker"
  213. @cancel="showPjtPicker = false" />
  214. </van-popup>
  215. <!-- 所在时间 -->
  216. <van-popup v-model:show="showPjtDatePicker" position="bottom">
  217. <van-picker-group title="预约日期" :tabs="['开始日期', '结束日期']" next-step-text="下一步" @confirm="onPjtDatePicker">
  218. <van-date-picker v-model="state.form.startDate" />
  219. <van-date-picker v-model="state.form.endDate" />
  220. </van-picker-group>
  221. </van-popup>
  222. <!-- 项目类型 -->
  223. <van-popup v-model:show="showPjtTypePicker" position="bottom">
  224. <van-picker :columns="pjtTypeList" :columns-field-names="{ text: 'dictLabel', value: 'dictValue' }"
  225. @confirm="onPjtTypePicker" @cancel="showPjtTypePicker = false" />
  226. </van-popup>
  227. <van-notify v-model:show="show" type="danger">
  228. <span>操作失败</span>
  229. </van-notify>
  230. </div>
  231. </template>
  232. <script name="register" lang="ts" setup>
  233. import { onMounted, reactive, ref, watch } from 'vue'
  234. import to from 'await-to-js'
  235. import { useLoginApi } from '/@/api/login/index'
  236. import crypto from 'sm-crypto'
  237. import { useDictApi } from '/@/api/system/dict'
  238. import { useProApi } from '/@/api/project'
  239. import { useDeptApi } from '/@/api/system/dept'
  240. import { useRouter, useRoute } from 'vue-router'
  241. import { showNotify } from 'vant'
  242. import { UserTypeTooltip } from '/@/constants/pageConstants'
  243. import { isPasswordValid } from '/@/utils/stringUtils'
  244. const sm3 = crypto.sm3
  245. const loginApi = useLoginApi()
  246. const router = useRouter()
  247. const dictApi = useDictApi()
  248. const proApi = useProApi()
  249. const deptApi = useDeptApi()
  250. const loginInfoRef = ref()
  251. const personInfoRef = ref()
  252. const projectInfoRef = ref()
  253. const showDeptPicker = ref(false)
  254. const showPjtPicker = ref(false)
  255. const showPjtDatePicker = ref(false)
  256. const showPjtTypePicker = ref(false)
  257. const show = ref(false)
  258. const pjtTypeIndex = ref(-1)
  259. const needInputDeptInfo = ref(false)
  260. // 级联选择器相关数据
  261. const cascadeData = ref({
  262. level1: <any[]>[],
  263. level2: <any[]>[],
  264. level3: <any[]>[],
  265. })
  266. const selectedLevel1 = ref<any>(null)
  267. const selectedLevel2 = ref<any>(null)
  268. const selectedLevel3 = ref<any>(null)
  269. const userTypeList = ref(<RowDicDataType[]>[])
  270. const userSexList = ref(<RowDicDataType[]>[])
  271. const userCertList = ref(<RowDicDataType[]>[])
  272. const deptData = ref(<any[]>[])
  273. const pjtList = ref(<any[]>[])
  274. const pjtTypeList = ref(<any[]>[])
  275. const state = reactive({
  276. active: 0,
  277. showPassword: false,
  278. showConfirmPassword: false,
  279. loading: {
  280. signIn: false,
  281. },
  282. form: {
  283. id: 0,
  284. userName: '', // 账户名称
  285. nickName: '', // 用户姓名
  286. userType: '10', // 关联角色
  287. deptId: null,
  288. deptName: '', // 单位名称
  289. phone: '', // 手机号
  290. email: '', // 邮箱
  291. sex: '30', // 性别
  292. password: '', // 账户密码
  293. confirmPassword: '',
  294. status: '10', // 用户状态
  295. describe: '', // 用户描述
  296. avatar: '',
  297. idType: '', //证件类型
  298. idCode: '', // 证件号
  299. personnelType: '', // 人员类型
  300. projectId: null,
  301. projectName: '',
  302. projectDate: '',
  303. startDate: [],
  304. endDate: [],
  305. registerType: '10',
  306. applyPg: {},
  307. projectGroupName: '',
  308. projectGroupId: null,
  309. projectList: <any[]>[{
  310. projectName: '',
  311. projectType: '',
  312. projectTypeName: '',
  313. projectSource: '',
  314. }],
  315. unitName: '',
  316. deptDescribe: '',
  317. },
  318. })
  319. const deptDataBackup = ref(<any[]>[])
  320. const getDicts = () => {
  321. Promise.all([
  322. dictApi.getDictDataByType('sys_user_type'),
  323. dictApi.getDictDataByType('sys_com_sex'),
  324. dictApi.getDictDataByType('sys_user_certificate'),
  325. deptApi.getDeptTree(),
  326. proApi.getProjectGroupListForApp({ noPage: true }),
  327. dictApi.getDictDataByType('sci_pjt_level'),
  328. ]).then(([type, sex, cert, dept, pjt, pjtType]) => {
  329. userTypeList.value = type.data.values || []
  330. userSexList.value = sex.data.values || []
  331. userCertList.value = cert.data.values || []
  332. deptDataBackup.value = dept.data || []
  333. deptData.value = dept.data || []
  334. pjtList.value = pjt.data.list || []
  335. pjtTypeList.value = pjtType.data.values || []
  336. // 初始化级联数据
  337. initCascadeData()
  338. })
  339. }
  340. const checkUserNamePhoneExists = async (type: 'userName' | 'phone') => {
  341. let resquest = loginApi.checkUserNamePhoneExists({ userName: state.form.userName, phone: '' })
  342. if (type === 'phone') {
  343. resquest = loginApi.checkUserNamePhoneExists({ userName: '', phone: state.form.phone })
  344. }
  345. await to(resquest)
  346. }
  347. const checkUserNameExists = (value: string) => {
  348. if (!value) {
  349. return "请输入账号"
  350. }
  351. return loginApi.checkUserNamePhoneExists({ userName: value, phone: '' })
  352. .then(res => {
  353. return true
  354. })
  355. .catch(() => {
  356. return "账号不合法"
  357. })
  358. }
  359. const renderDeptData = () => {
  360. let daptTree = deptDataBackup.value
  361. if (state.form.userType === '10') {
  362. daptTree = daptTree.filter((item) => item.id === 100001)
  363. }
  364. if (state.form.userType === '15') {
  365. daptTree = daptTree.filter((item) => item.id !== 1000220)
  366. }
  367. if (state.form.userType === '20') {
  368. daptTree = daptTree.filter((item) => item.id === 1000220)
  369. }
  370. return daptTree
  371. }
  372. const deptIncludesProjectGroup = async (deptId: number) => {
  373. const [err, res]: ToResponse = await to(
  374. proApi.getProjectGroupList({
  375. pgOrg: deptId,
  376. pgName: '',
  377. pageNum: 1,
  378. pageSize: 1000,
  379. }),
  380. )
  381. if (err) return
  382. pjtList.value = res?.data.list || []
  383. }
  384. const preStep = () => {
  385. state.active--
  386. }
  387. const validateProjectList = () => {
  388. // 校验至少有一个项目
  389. if (!state.form.projectList || state.form.projectList.length === 0) {
  390. showNotify({
  391. type: 'danger',
  392. message: '请至少添加一个项目',
  393. })
  394. return false
  395. }
  396. // 校验每个项目的必填字段
  397. for (let i = 0; i < state.form.projectList.length; i++) {
  398. const item = state.form.projectList[i]
  399. if (!item.projectName || !item.projectName.trim()) {
  400. showNotify({
  401. type: 'danger',
  402. message: `项目 ${i + 1} 的项目名称不能为空`,
  403. })
  404. return false
  405. }
  406. if (!item.projectType || !item.projectTypeName) {
  407. showNotify({
  408. type: 'danger',
  409. message: `项目 ${i + 1} 的项目类型不能为空`,
  410. })
  411. return false
  412. }
  413. if (!item.projectSource || !item.projectSource.trim()) {
  414. showNotify({
  415. type: 'danger',
  416. message: `项目 ${i + 1} 的项目来源不能为空`,
  417. })
  418. return false
  419. }
  420. }
  421. return true
  422. }
  423. const nextStep = async () => {
  424. if (state.active < 3) {
  425. let form = loginInfoRef.value
  426. if (state.active == 1) {
  427. form = personInfoRef.value
  428. } else if (state.active == 2) {
  429. form = projectInfoRef.value
  430. // 校验项目列表
  431. if (!validateProjectList()) {
  432. return
  433. }
  434. // 校验表单字段
  435. const [err] = await to(form.validate())
  436. if (err) return
  437. state.active++
  438. return
  439. }
  440. const [err, res] = await to(form.validate())
  441. if (err) return
  442. state.active++
  443. }
  444. }
  445. // const checkPassword = (value: any) => {
  446. // let checkResult = isPasswordValid(value)
  447. // if (!checkResult.isPassed) {
  448. // return checkResult.errorMsg
  449. // }
  450. // }
  451. const checkPassword = (value: string) => {
  452. if (!value) {
  453. return "请输入密码"
  454. }
  455. return loginApi.validatePassword({ password: value })
  456. .then(res => {
  457. return true
  458. })
  459. .catch((err) => {
  460. return err.message || "密码不合法"
  461. })
  462. }
  463. const confirmPasswordSame = (value: any) => {
  464. if (!value) {
  465. return false
  466. } else if (value !== state.form.password) {
  467. return false
  468. } else {
  469. return true
  470. }
  471. }
  472. watch(
  473. () => state.form.userType,
  474. (newVal) => {
  475. if (newVal === '20') {
  476. state.form.idType = '30'
  477. needInputDeptInfo.value = true
  478. } else {
  479. state.form.idType = '20'
  480. state.form.deptId = null
  481. state.form.deptName = ''
  482. needInputDeptInfo.value = false
  483. }
  484. },
  485. { immediate: true },
  486. )
  487. const changeType = (val: string) => {
  488. state.form.registerType = val
  489. if (val === '10' && state.active == 2) {
  490. state.active = 1
  491. }
  492. }
  493. const showDeptPickerHandler = () => {
  494. initCascadeData()
  495. showDeptPicker.value = true
  496. }
  497. const onPjtPicker = ({ selectedOptions }: { selectedOptions: any[] }) => {
  498. showPjtPicker.value = false
  499. state.form.projectGroupName = selectedOptions[0].pgName
  500. state.form.projectGroupId = selectedOptions[0].id
  501. }
  502. const onPjtDatePicker = () => {
  503. showPjtDatePicker.value = false
  504. state.form.projectDate = `${state.form.startDate.join('-')}至${state.form.endDate.join('-')}`
  505. }
  506. const addProject = () => {
  507. state.form.projectList.push({
  508. projectName: '',
  509. projectType: '',
  510. projectTypeName: '',
  511. projectSource: '',
  512. })
  513. }
  514. const delProjectItem = (idx: number) => {
  515. if (state.form.projectList.length > 1) {
  516. state.form.projectList.splice(idx, 1)
  517. } else {
  518. showNotify({
  519. type: 'danger',
  520. message: '至少需要保留一个项目',
  521. })
  522. }
  523. }
  524. const openPjtType = (row: any, idx: number) => {
  525. pjtTypeIndex.value = idx
  526. showPjtTypePicker.value = true
  527. }
  528. const onPjtTypePicker = ({ selectedOptions }: { selectedOptions: any[] }) => {
  529. showPjtTypePicker.value = false
  530. state.form.projectList[pjtTypeIndex.value].projectType = selectedOptions[0].dictValue
  531. state.form.projectList[pjtTypeIndex.value].projectTypeName = selectedOptions[0].dictLabel
  532. }
  533. watch(
  534. () => state.form.deptId,
  535. (newVal) => {
  536. if (newVal) {
  537. deptIncludesProjectGroup(newVal)
  538. }
  539. },
  540. )
  541. // 级联选择器方法
  542. const initCascadeData = () => {
  543. cascadeData.value.level1 = renderDeptData()
  544. cascadeData.value.level2 = []
  545. cascadeData.value.level3 = []
  546. selectedLevel1.value = null
  547. selectedLevel2.value = null
  548. selectedLevel3.value = null
  549. }
  550. const selectLevel1 = (item: any) => {
  551. selectedLevel1.value = item
  552. selectedLevel2.value = null
  553. selectedLevel3.value = null
  554. if (item.children && item.children.length > 0) {
  555. cascadeData.value.level2 = item.children
  556. cascadeData.value.level3 = []
  557. } else {
  558. cascadeData.value.level2 = []
  559. cascadeData.value.level3 = []
  560. }
  561. }
  562. const selectLevel2 = (item: any) => {
  563. selectedLevel2.value = item
  564. selectedLevel3.value = null
  565. if (item.children && item.children.length > 0) {
  566. cascadeData.value.level3 = item.children
  567. } else {
  568. cascadeData.value.level3 = []
  569. }
  570. }
  571. const selectLevel3 = (item: any) => {
  572. selectedLevel3.value = item
  573. }
  574. const confirmCascadeSelection = () => {
  575. const selectedItem = selectedLevel3.value || selectedLevel2.value || selectedLevel1.value
  576. if (selectedItem) {
  577. state.form.deptName = selectedItem.deptName
  578. state.form.deptId = selectedItem.id
  579. showDeptPicker.value = false
  580. }
  581. }
  582. const onRegister = async () => {
  583. const form = state.form.registerType == '10' ? personInfoRef.value : personInfoRef.value
  584. const [error] = await to(form.validate())
  585. if (error) return
  586. // 如果是课题组负责人,需要校验项目信息
  587. if (state.form.registerType === '20') {
  588. if (!validateProjectList()) {
  589. return
  590. }
  591. }
  592. const params = JSON.parse(JSON.stringify(state.form))
  593. params.password = sm3(params.password)
  594. delete params.confirmPassword
  595. params.startDate = params.startDate.join('-')
  596. params.endDate = params.endDate.join('-')
  597. delete params.projectDate
  598. const [err] = await to(loginApi.register(params))
  599. if (err) {
  600. // show.value = true
  601. // setTimeout(() => {
  602. // show.value = false
  603. // }, 2000)
  604. return
  605. }
  606. showNotify({
  607. type: 'success',
  608. message:
  609. state.form.registerType === '20'
  610. ? '你已经注册为课题组负责人,等待系统管理员审核通过'
  611. : '你已经注册为课题组成员,等待课题组负责人审核通过',
  612. })
  613. router.push('/login')
  614. }
  615. onMounted(async () => {
  616. getDicts()
  617. })
  618. </script>
  619. <style lang="scss" scoped>
  620. .register {
  621. display: flex;
  622. flex-direction: column;
  623. height: calc(100% - 20px);
  624. .van-row .van-button {
  625. flex: 1;
  626. &+.van-button {
  627. margin-left: 10px;
  628. }
  629. }
  630. .form {
  631. flex: 1;
  632. overflow-y: auto;
  633. }
  634. footer {
  635. padding: 0 10px 10px;
  636. flex: 0 0 40px;
  637. display: flex;
  638. justify-content: flex-end;
  639. }
  640. }
  641. .cascade-picker {
  642. background: #fff;
  643. border-radius: 8px 8px 0 0;
  644. overflow: hidden;
  645. .cascade-header {
  646. display: flex;
  647. justify-content: space-between;
  648. align-items: center;
  649. padding: 16px;
  650. border-bottom: 1px solid #f5f5f5;
  651. font-size: 16px;
  652. font-weight: 500;
  653. .van-icon {
  654. font-size: 18px;
  655. color: #969799;
  656. cursor: pointer;
  657. }
  658. }
  659. .cascade-content {
  660. display: flex;
  661. height: 300px;
  662. .cascade-column {
  663. flex: 1;
  664. border-right: 1px solid #f5f5f5;
  665. overflow-y: auto;
  666. &:last-child {
  667. border-right: none;
  668. }
  669. .cascade-item {
  670. display: flex;
  671. justify-content: space-between;
  672. align-items: center;
  673. padding: 12px 16px;
  674. cursor: pointer;
  675. transition: background-color 0.2s;
  676. &:hover {
  677. background-color: #f7f8fa;
  678. }
  679. &.active {
  680. background-color: #e8f3ff;
  681. color: #1989fa;
  682. }
  683. span {
  684. flex: 1;
  685. font-size: 14px;
  686. }
  687. .van-icon {
  688. font-size: 12px;
  689. color: #c8c9cc;
  690. }
  691. }
  692. }
  693. }
  694. .cascade-footer {
  695. display: flex;
  696. padding: 12px 16px;
  697. border-top: 1px solid #f5f5f5;
  698. gap: 12px;
  699. .van-button {
  700. flex: 1;
  701. }
  702. }
  703. }
  704. .project-list-container {
  705. padding: 12px;
  706. .add-project-btn {
  707. margin-bottom: 12px;
  708. }
  709. .empty-project-tip {
  710. padding: 20px;
  711. text-align: center;
  712. color: #969799;
  713. font-size: 14px;
  714. background-color: #f7f8fa;
  715. border-radius: 8px;
  716. margin-bottom: 16px;
  717. }
  718. .project-card {
  719. background: #fff;
  720. border: 1px solid #ebedf0;
  721. border-radius: 8px;
  722. margin-bottom: 16px;
  723. overflow: hidden;
  724. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
  725. transition: box-shadow 0.3s;
  726. &:hover {
  727. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  728. }
  729. .project-card-header {
  730. display: flex;
  731. justify-content: space-between;
  732. align-items: center;
  733. padding: 12px 16px;
  734. background: linear-gradient(135deg, #1989fa 0%, #0050b3 100%);
  735. color: #fff;
  736. .project-card-title {
  737. font-size: 16px;
  738. font-weight: 500;
  739. }
  740. .delete-btn {
  741. background: rgba(255, 255, 255, 0.2);
  742. border-color: rgba(255, 255, 255, 0.3);
  743. color: #fff;
  744. &:hover {
  745. background: rgba(255, 255, 255, 0.3);
  746. }
  747. }
  748. }
  749. .project-card-body {
  750. padding: 12px 0;
  751. .project-field {
  752. margin-bottom: 8px;
  753. &:last-child {
  754. margin-bottom: 0;
  755. }
  756. }
  757. }
  758. }
  759. }
  760. </style>