index.vue 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795
  1. <template>
  2. <div class="project-inventory-page">
  3. <!-- 查询表单 -->
  4. <div class="query-form-container">
  5. <div class="query-form-basic">
  6. <el-form class="query-form-fields" :inline="true" :model="queryForm" size="small">
  7. <el-form-item label="项目名称">
  8. <el-input v-model="queryForm.projectName" clearable placeholder="请输入" style="width: 180px" />
  9. </el-form-item>
  10. <el-form-item label="项目状态">
  11. <el-select v-model="queryForm.projectStatus" clearable placeholder="请选择" style="width: 150px">
  12. <el-option v-for="item in projectStatusOptions" :key="item.key" :label="item.value" :value="item.key" />
  13. </el-select>
  14. </el-form-item>
  15. <el-form-item label="计划交付时间">
  16. <el-date-picker
  17. v-model="queryForm.planDeliveryTimeRange"
  18. end-placeholder="结束日期"
  19. range-separator="至"
  20. start-placeholder="开始日期"
  21. style="width: 260px"
  22. type="daterange"
  23. value-format="yyyy-MM-dd" />
  24. </el-form-item>
  25. </el-form>
  26. <div class="query-form-actions">
  27. <el-button icon="el-icon-search" type="primary" @click="handleSearch">查询</el-button>
  28. <el-button icon="el-icon-refresh-right" @click="handleReset">重置</el-button>
  29. <el-button type="text" @click="showAdvanced = !showAdvanced">
  30. {{ showAdvanced ? '收起' : '高级筛选' }}
  31. <i :class="showAdvanced ? 'el-icon-arrow-up' : 'el-icon-arrow-down'" />
  32. </el-button>
  33. <el-divider direction="vertical" />
  34. <el-button icon="el-icon-download" type="success" @click="handleExport">导出</el-button>
  35. </div>
  36. </div>
  37. <div v-show="showAdvanced" class="query-form-advanced">
  38. <el-form :inline="true" :model="queryForm" size="small">
  39. <el-form-item label="合同编号">
  40. <el-input v-model="queryForm.contractNo" clearable placeholder="请输入" style="width: 150px" />
  41. </el-form-item>
  42. <el-form-item label="产品线">
  43. <el-select v-model="queryForm.productLine" clearable placeholder="请选择" style="width: 130px">
  44. <el-option v-for="item in productLineOptions" :key="item.key" :label="item.value" :value="item.key" />
  45. </el-select>
  46. </el-form-item>
  47. </el-form>
  48. </div>
  49. </div>
  50. <!-- 主体内容 -->
  51. <div class="main-content">
  52. <!-- 左侧人员筛选 -->
  53. <div :class="['person-sidebar', { collapsed: sidebarCollapsed }]">
  54. <div class="sidebar-header">
  55. <div v-if="!sidebarCollapsed" class="sidebar-title-group">
  56. <span class="sidebar-title">人员筛选</span>
  57. </div>
  58. <div class="sidebar-actions">
  59. <button
  60. class="collapse-trigger"
  61. :title="sidebarCollapsed ? '展开' : '折叠'"
  62. type="button"
  63. @click="toggleSidebar">
  64. <i :class="sidebarCollapsed ? 'el-icon-d-arrow-right' : 'el-icon-d-arrow-left'" />
  65. </button>
  66. </div>
  67. </div>
  68. <div v-if="sidebarCollapsed" class="sidebar-collapsed-label">人员列表</div>
  69. <template v-if="!sidebarCollapsed">
  70. <!-- 页签切换 -->
  71. <div class="person-tabs">
  72. <div :class="['person-tab', { active: personTab === 'delivery' }]" @click="switchPersonTab('delivery')">
  73. 交付
  74. </div>
  75. <div :class="['person-tab', { active: personTab === 'operation' }]" @click="switchPersonTab('operation')">
  76. 运维
  77. </div>
  78. </div>
  79. <!-- 人员搜索 -->
  80. <el-input
  81. v-model="personSearch"
  82. class="person-search"
  83. clearable
  84. placeholder="搜索人员姓名"
  85. prefix-icon="el-icon-search"
  86. size="small" />
  87. <!-- 人员列表 -->
  88. <div class="person-list">
  89. <div
  90. v-if="allPersonOption"
  91. :class="['person-item', 'person-item--all', { active: selectedPerson === allPersonOption.userId }]"
  92. @click="selectPerson(allPersonOption.userId)">
  93. <div>
  94. <div class="person-name">{{ allPersonOption.userName }}</div>
  95. <div class="person-desc">查看全部项目</div>
  96. </div>
  97. </div>
  98. <div
  99. v-for="person in filteredPersonList"
  100. :key="person.userId"
  101. :class="['person-card', { active: selectedPerson === person.userId }]"
  102. @click="selectPerson(person.userId)">
  103. <div class="person-card-info">
  104. <span class="person-name">{{ person.nickName || person.userName }}</span>
  105. <span class="person-role">{{ person.roleName }}</span>
  106. </div>
  107. <div v-if="person.deptName" class="person-dept">{{ person.deptName }}</div>
  108. <i v-if="selectedPerson === person.userId" class="el-icon-check person-card-check" />
  109. </div>
  110. </div>
  111. </template>
  112. </div>
  113. <!-- 右侧数据表格 -->
  114. <div class="table-container">
  115. <div class="table-scroll-wrapper">
  116. <el-table
  117. v-loading="loading"
  118. border
  119. class="project-inventory-table"
  120. :data="tableData"
  121. :fit="false"
  122. height="100%"
  123. stripe
  124. style="width: 100%">
  125. <el-table-column align="center" type="index" width="50" />
  126. <el-table-column label="合同编号" prop="contractNo" show-overflow-tooltip width="120" />
  127. <el-table-column label="项目名称" min-width="200" prop="projectName" show-overflow-tooltip />
  128. <el-table-column label="产品线" prop="productLine" width="120">
  129. <template slot-scope="{ row }">
  130. <el-tag effect="plain" size="small" type="primary">
  131. {{ getProductLineLabel(row.productLine) }}
  132. </el-tag>
  133. </template>
  134. </el-table-column>
  135. <el-table-column label="项目状态" prop="projectStatus" width="100">
  136. <template slot-scope="{ row }">
  137. <el-tag size="small" :type="getProjectStatusType(row.projectStatus)">
  138. {{ getProjectStatusLabel(row.projectStatus) }}
  139. </el-tag>
  140. </template>
  141. </el-table-column>
  142. <el-table-column label="交付节点" prop="deliveryNode" width="120">
  143. <template slot-scope="{ row }">
  144. {{ getDeliveryNodeLabel(row.deliveryNode) }}
  145. </template>
  146. </el-table-column>
  147. <el-table-column label="交付负责人" prop="deliveryUserName" show-overflow-tooltip width="120" />
  148. <el-table-column label="销售负责人" prop="salesUserName" show-overflow-tooltip width="120" />
  149. <el-table-column label="计划交付时间" prop="planDeliveryTime" width="120">
  150. <template slot-scope="{ row }">
  151. {{ formatDate(row.planDeliveryTime) }}
  152. </template>
  153. </el-table-column>
  154. <el-table-column label="计划验收时间" prop="planAcceptTime" width="120">
  155. <template slot-scope="{ row }">
  156. {{ formatDate(row.planAcceptTime) }}
  157. </template>
  158. </el-table-column>
  159. <el-table-column align="right" header-align="center" label="合同金额" prop="contractAmount" width="150">
  160. <template slot-scope="{ row }">
  161. <span class="amount-text">{{ formatAmount(row.contractAmount) }}</span>
  162. </template>
  163. </el-table-column>
  164. <el-table-column align="right" header-align="center" label="回款金额" prop="collectedAmount" width="150">
  165. <template slot-scope="{ row }">
  166. <span :class="['amount-text', { 'text-success': row.collectedAmount >= row.contractAmount }]">
  167. {{ formatAmount(row.collectedAmount) }}
  168. </span>
  169. </template>
  170. </el-table-column>
  171. <el-table-column label="客户名称" min-width="180" prop="custName" show-overflow-tooltip />
  172. <el-table-column align="center" fixed="right" header-align="center" label="操作" width="100">
  173. <template slot-scope="{ row }">
  174. <el-button size="mini" type="text" @click="handleViewDetail(row)">查看</el-button>
  175. </template>
  176. </el-table-column>
  177. </el-table>
  178. </div>
  179. <!-- 分页 -->
  180. <div class="pagination-wrapper">
  181. <el-pagination
  182. background
  183. :current-page.sync="queryForm.pageNum"
  184. layout="total, sizes, prev, pager, next, jumper"
  185. :page-size.sync="queryForm.pageSize"
  186. :page-sizes="[10, 20, 50, 100]"
  187. :total="total"
  188. @current-change="handleCurrentChange"
  189. @size-change="handleSizeChange" />
  190. </div>
  191. </div>
  192. </div>
  193. <!-- 产品详情浮窗 -->
  194. <el-dialog
  195. :title="`产品清单 - ${currentProject ? currentProject.projectName : ''}`"
  196. :visible.sync="productDialogVisible"
  197. width="800px">
  198. <el-table border :data="productList" stripe>
  199. <el-table-column align="center" type="index" width="50" />
  200. <el-table-column label="产品型号" prop="prodCode" show-overflow-tooltip width="150" />
  201. <el-table-column label="产品名称" min-width="180" prop="prodName" show-overflow-tooltip />
  202. <el-table-column label="产品类别" prop="prodClass" show-overflow-tooltip width="120" />
  203. <el-table-column align="center" label="数量" prop="prodNum" width="80" />
  204. <el-table-column align="right" header-align="center" label="成交价格" prop="tranPrice" width="120">
  205. <template slot-scope="{ row }">
  206. {{ formatAmount(row.tranPrice) }}
  207. </template>
  208. </el-table-column>
  209. <el-table-column align="right" header-align="center" label="合同总价" prop="contractPrive" width="120">
  210. <template slot-scope="{ row }">
  211. {{ formatAmount(row.contractPrive) }}
  212. </template>
  213. </el-table-column>
  214. <el-table-column align="center" label="维保期" prop="maintTerm" width="100">
  215. <template slot-scope="{ row }">{{ row.maintTerm }}个月</template>
  216. </el-table-column>
  217. <el-table-column label="备注" min-width="150" prop="remark" show-overflow-tooltip />
  218. </el-table>
  219. </el-dialog>
  220. </div>
  221. </template>
  222. <script>
  223. import projectInventoryApi from '@/api/devops/projectInventory'
  224. import { parseTime } from '@/utils'
  225. import to from 'await-to-js'
  226. import { projectStatusTagTypes, getTagType } from '@/config/devopsTagTypes'
  227. export default {
  228. name: 'ProjectInventory',
  229. data() {
  230. return {
  231. loading: false,
  232. showAdvanced: false,
  233. sidebarCollapsed: false,
  234. productDialogVisible: false,
  235. currentProject: null,
  236. productList: [],
  237. queryForm: {
  238. pageNum: 1,
  239. pageSize: 20,
  240. contractNo: '',
  241. projectName: '',
  242. productLine: '',
  243. projectStatus: '',
  244. deliveryUserId: null,
  245. opsManagerUserId: null,
  246. salesUserId: null,
  247. planDeliveryTimeRange: [],
  248. },
  249. personSearch: '',
  250. personTab: 'delivery',
  251. selectedPerson: 'all',
  252. allPersonOption: { userId: 'all', userName: '全部人员', nickName: '全部人员' },
  253. personList: [],
  254. tableData: [],
  255. total: 0,
  256. productLineOptions: [],
  257. projectStatusOptions: [],
  258. deliveryNodeOptions: [],
  259. }
  260. },
  261. computed: {
  262. filteredPersonList() {
  263. if (!this.personSearch) return this.personList
  264. const search = this.personSearch.toLowerCase()
  265. return this.personList.filter(
  266. (p) =>
  267. (p.userName && p.userName.toLowerCase().includes(search)) ||
  268. (p.nickName && p.nickName.toLowerCase().includes(search))
  269. )
  270. },
  271. },
  272. created() {
  273. this.initDicts()
  274. this.loadPersonList()
  275. this.loadData()
  276. },
  277. methods: {
  278. getTagType,
  279. initDicts() {
  280. Promise.all([
  281. this.getDicts('sys_product_line'),
  282. this.getDicts('delivery_project_status'),
  283. this.getDicts('delivery_node'),
  284. ])
  285. .then(([productLine, projectStatus, deliveryNode]) => {
  286. this.productLineOptions = productLine.data.values || []
  287. this.projectStatusOptions = projectStatus.data.values || []
  288. this.deliveryNodeOptions = deliveryNode.data.values || []
  289. })
  290. .catch((err) => console.log(err))
  291. },
  292. loadPersonList() {
  293. projectInventoryApi.getProjectManagers('all').then((res) => {
  294. if (res.code === 0 || res.code === 200) {
  295. this.personList = res.data || []
  296. }
  297. })
  298. },
  299. loadData() {
  300. this.loading = true
  301. const params = { ...this.queryForm }
  302. if (params.planDeliveryTimeRange && params.planDeliveryTimeRange.length === 2) {
  303. params.planDeliveryTimeStart = params.planDeliveryTimeRange[0]
  304. params.planDeliveryTimeEnd = params.planDeliveryTimeRange[1]
  305. }
  306. delete params.planDeliveryTimeRange
  307. // 根据页签类型设置对应的人员ID过滤
  308. if (this.selectedPerson !== 'all') {
  309. if (this.personTab === 'delivery') {
  310. params.deliveryUserId = parseInt(this.selectedPerson)
  311. } else if (this.personTab === 'operation') {
  312. params.opsManagerUserId = parseInt(this.selectedPerson)
  313. }
  314. }
  315. projectInventoryApi
  316. .getList(params)
  317. .then((res) => {
  318. if (res.code === 0 || res.code === 200) {
  319. this.tableData = res.data?.list || []
  320. this.total = res.data?.total || 0
  321. }
  322. })
  323. .finally(() => {
  324. this.loading = false
  325. })
  326. },
  327. handleSearch() {
  328. this.queryForm.pageNum = 1
  329. this.loadData()
  330. },
  331. handleReset() {
  332. this.queryForm = {
  333. pageNum: 1,
  334. pageSize: 20,
  335. contractNo: '',
  336. projectName: '',
  337. productLine: '',
  338. projectStatus: '',
  339. deliveryUserId: null,
  340. opsManagerUserId: null,
  341. salesUserId: null,
  342. planDeliveryTimeRange: [],
  343. }
  344. this.selectedPerson = 'all'
  345. this.loadData()
  346. },
  347. handleCurrentChange(val) {
  348. this.queryForm.pageNum = val
  349. this.loadData()
  350. },
  351. handleSizeChange(val) {
  352. this.queryForm.pageSize = val
  353. this.queryForm.pageNum = 1
  354. this.loadData()
  355. },
  356. toggleSidebar() {
  357. this.sidebarCollapsed = !this.sidebarCollapsed
  358. },
  359. switchPersonTab(tab) {
  360. this.personTab = tab
  361. this.selectedPerson = 'all'
  362. this.queryForm.pageNum = 1
  363. this.loadData()
  364. },
  365. selectPerson(userId) {
  366. this.selectedPerson = userId
  367. this.queryForm.pageNum = 1
  368. this.loadData()
  369. },
  370. handleViewDetail(row) {
  371. if (row.contractId) {
  372. this.currentProject = row
  373. projectInventoryApi.getContractProducts(row.contractId).then((res) => {
  374. if (res.code === 0 || res.code === 200) {
  375. this.productList = res.data || []
  376. this.productDialogVisible = true
  377. }
  378. })
  379. }
  380. },
  381. async handleExport() {
  382. const params = {
  383. contractNo: this.queryForm.contractNo,
  384. projectName: this.queryForm.projectName,
  385. productLine: this.queryForm.productLine,
  386. projectStatus: this.queryForm.projectStatus,
  387. }
  388. // 根据页签类型设置对应的人员ID过滤
  389. if (this.selectedPerson !== 'all') {
  390. if (this.personTab === 'delivery') {
  391. params.deliveryUserId = parseInt(this.selectedPerson)
  392. } else if (this.personTab === 'operation') {
  393. params.opsManagerUserId = parseInt(this.selectedPerson)
  394. }
  395. }
  396. if (this.queryForm.planDeliveryTimeRange && this.queryForm.planDeliveryTimeRange.length === 2) {
  397. params.planDeliveryTimeStart = this.queryForm.planDeliveryTimeRange[0]
  398. params.planDeliveryTimeEnd = this.queryForm.planDeliveryTimeRange[1]
  399. }
  400. const [err, res] = await to(projectInventoryApi.exportList(params))
  401. if (err) {
  402. this.$message.error('导出失败')
  403. return
  404. }
  405. if (res.data?.content) {
  406. try {
  407. const binaryString = window.atob(res.data.content)
  408. const len = binaryString.length
  409. const bytes = new Uint8Array(len)
  410. for (let i = 0; i < len; i++) {
  411. bytes[i] = binaryString.charCodeAt(i)
  412. }
  413. const blob = new Blob([bytes], {
  414. type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
  415. })
  416. const link = document.createElement('a')
  417. const url = window.URL.createObjectURL(blob)
  418. link.href = url
  419. link.download = `项目清单_${new Date().getTime()}.xlsx`
  420. document.body.appendChild(link)
  421. link.click()
  422. document.body.removeChild(link)
  423. window.URL.revokeObjectURL(url)
  424. this.$message.success('导出成功')
  425. } catch (e) {
  426. console.error('下载失败', e)
  427. this.$message.error('文件下载失败')
  428. }
  429. } else {
  430. this.$message.warning('没有可导出的数据')
  431. }
  432. },
  433. formatDate(time) {
  434. return time ? parseTime(time, '{y}-{m}-{d}') : '-'
  435. },
  436. formatAmount(amount) {
  437. if (amount === null || amount === undefined) return '-'
  438. return '¥' + parseFloat(amount).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
  439. },
  440. getProductLineLabel(value) {
  441. const label = this.selectDictLabel(this.productLineOptions, value)
  442. return label || value || '-'
  443. },
  444. getProjectStatusLabel(value) {
  445. const label = this.selectDictLabel(this.projectStatusOptions, value)
  446. return label || value || '-'
  447. },
  448. getProjectStatusType(value) {
  449. return getTagType(projectStatusTagTypes, value, 'info')
  450. },
  451. getDeliveryNodeLabel(value) {
  452. const label = this.selectDictLabel(this.deliveryNodeOptions, value)
  453. return label || value || '-'
  454. },
  455. },
  456. }
  457. </script>
  458. <style lang="scss" scoped>
  459. .project-inventory-page {
  460. box-sizing: border-box;
  461. display: flex;
  462. flex-direction: column;
  463. min-height: 0;
  464. height: calc(100vh - 122px);
  465. padding: 4px;
  466. background: #f5f7fa;
  467. overflow: hidden;
  468. .query-form-container {
  469. flex-shrink: 0;
  470. margin-bottom: 4px;
  471. padding: 8px 12px;
  472. background: #fff;
  473. border-radius: 6px;
  474. box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
  475. .query-form-basic {
  476. display: flex;
  477. align-items: flex-start;
  478. justify-content: space-between;
  479. gap: 16px;
  480. .query-form-fields {
  481. flex: 1;
  482. min-width: 0;
  483. }
  484. .query-form-actions {
  485. display: flex;
  486. align-items: center;
  487. justify-content: flex-end;
  488. flex-shrink: 0;
  489. min-height: 32px;
  490. white-space: nowrap;
  491. }
  492. ::v-deep .el-form {
  493. display: flex;
  494. flex-wrap: wrap;
  495. align-items: center;
  496. row-gap: 2px;
  497. }
  498. ::v-deep .el-form-item {
  499. margin-right: 20px;
  500. margin-bottom: 2px;
  501. }
  502. ::v-deep .el-form-item__label {
  503. padding-right: 8px;
  504. font-size: 13px;
  505. color: #606266;
  506. }
  507. ::v-deep .el-divider--vertical {
  508. margin: 0 12px;
  509. }
  510. }
  511. .query-form-advanced {
  512. margin-top: 8px;
  513. padding-top: 8px;
  514. border-top: 1px dashed #e4e7ed;
  515. ::v-deep .el-form {
  516. display: flex;
  517. flex-wrap: wrap;
  518. align-items: center;
  519. row-gap: 2px;
  520. }
  521. ::v-deep .el-form-item {
  522. margin-right: 20px;
  523. margin-bottom: 2px;
  524. }
  525. ::v-deep .el-form-item__label {
  526. padding-right: 8px;
  527. font-size: 13px;
  528. color: #606266;
  529. }
  530. }
  531. }
  532. .main-content {
  533. flex: 1;
  534. display: flex;
  535. gap: 4px;
  536. overflow: hidden;
  537. min-height: 0;
  538. .person-sidebar {
  539. display: flex;
  540. flex-direction: column;
  541. width: 280px;
  542. min-height: 0;
  543. background: #fff;
  544. border-radius: 4px;
  545. padding: 8px;
  546. flex-shrink: 0;
  547. box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
  548. transition: width 0.2s ease, padding 0.2s ease;
  549. overflow: hidden;
  550. &.collapsed {
  551. width: 44px;
  552. padding: 8px 4px;
  553. align-items: center;
  554. }
  555. .sidebar-header {
  556. display: flex;
  557. justify-content: space-between;
  558. align-items: center;
  559. margin-bottom: 6px;
  560. .sidebar-title-group {
  561. display: flex;
  562. align-items: center;
  563. gap: 6px;
  564. .sidebar-title {
  565. font-weight: 500;
  566. font-size: 14px;
  567. color: #303133;
  568. }
  569. }
  570. .sidebar-actions {
  571. .collapse-trigger {
  572. background: none;
  573. border: none;
  574. cursor: pointer;
  575. color: #909399;
  576. padding: 4px;
  577. &:hover {
  578. color: #409eff;
  579. }
  580. }
  581. }
  582. }
  583. .sidebar-collapsed-label {
  584. writing-mode: vertical-rl;
  585. text-orientation: mixed;
  586. padding: 16px 4px;
  587. font-size: 14px;
  588. font-weight: 500;
  589. color: #606266;
  590. text-align: center;
  591. }
  592. .person-tabs {
  593. display: flex;
  594. gap: 8px;
  595. margin-bottom: 8px;
  596. .person-tab {
  597. flex: 1;
  598. text-align: center;
  599. padding: 8px 12px;
  600. border-radius: 4px;
  601. background: #f5f7fa;
  602. cursor: pointer;
  603. font-size: 14px;
  604. color: #606266;
  605. transition: all 0.2s ease;
  606. &:hover {
  607. background: #ecf5ff;
  608. color: #409eff;
  609. }
  610. &.active {
  611. background: #409eff;
  612. color: #fff;
  613. }
  614. }
  615. }
  616. .person-search {
  617. margin-bottom: 8px;
  618. }
  619. .person-list {
  620. flex: 1;
  621. overflow-y: auto;
  622. padding-right: 2px;
  623. .person-item--all {
  624. display: flex;
  625. justify-content: space-between;
  626. align-items: center;
  627. padding: 12px;
  628. margin-bottom: 8px;
  629. border-radius: 4px;
  630. background: #f5f7fa;
  631. cursor: pointer;
  632. &:hover,
  633. &.active {
  634. background: #ecf5ff;
  635. }
  636. .person-name {
  637. font-weight: 600;
  638. font-size: 14px;
  639. color: #303133;
  640. }
  641. .person-desc {
  642. font-size: 12px;
  643. color: #909399;
  644. margin-top: 4px;
  645. }
  646. }
  647. .person-card {
  648. position: relative;
  649. padding: 12px;
  650. border-radius: 4px;
  651. cursor: pointer;
  652. margin-bottom: 8px;
  653. border: 1px solid #ebeef5;
  654. &:hover {
  655. border-color: #409eff;
  656. background: #f5f7fa;
  657. }
  658. &.active {
  659. border-color: #409eff;
  660. background: #ecf5ff;
  661. }
  662. .person-card-info {
  663. display: flex;
  664. align-items: center;
  665. gap: 8px;
  666. margin-bottom: 4px;
  667. .person-name {
  668. font-weight: 500;
  669. font-size: 14px;
  670. color: #303133;
  671. }
  672. .person-role {
  673. font-size: 11px;
  674. color: #409eff;
  675. background: #ecf5ff;
  676. padding: 2px 6px;
  677. border-radius: 2px;
  678. }
  679. }
  680. .person-dept {
  681. font-size: 12px;
  682. color: #909399;
  683. }
  684. .person-card-check {
  685. position: absolute;
  686. top: 8px;
  687. right: 8px;
  688. color: #409eff;
  689. }
  690. }
  691. }
  692. }
  693. .table-container {
  694. flex: 1;
  695. display: flex;
  696. flex-direction: column;
  697. min-width: 0;
  698. min-height: 0;
  699. padding: 6px 8px;
  700. overflow: hidden;
  701. background: #fff;
  702. border-radius: 4px;
  703. box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
  704. .table-scroll-wrapper {
  705. display: flex;
  706. flex: 1;
  707. min-width: 0;
  708. min-height: 0;
  709. overflow: hidden;
  710. }
  711. .pagination-wrapper {
  712. margin-top: auto;
  713. display: flex;
  714. justify-content: flex-end;
  715. padding-top: 4px;
  716. ::v-deep .el-pagination {
  717. padding: 0;
  718. line-height: 32px;
  719. }
  720. }
  721. }
  722. }
  723. .amount-text {
  724. font-family: 'Roboto Mono', monospace;
  725. font-weight: 500;
  726. &.text-success {
  727. color: #67c23a;
  728. }
  729. }
  730. .project-inventory-table {
  731. flex: 1;
  732. min-width: 0;
  733. }
  734. }
  735. </style>