index.vue 30 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189
  1. <template>
  2. <view class="detail-page">
  3. <!-- 顶部状态栏 -->
  4. <view class="header-section">
  5. <view class="header-content">
  6. <view class="title-group">
  7. <text class="main-title">黑名单企业监控</text>
  8. <text class="sub-title">监控异常经营企业</text>
  9. </view>
  10. <view class="stat-box">
  11. <text class="stat-num">{{ rows?.length || 0 }}</text>
  12. <text class="stat-unit">家</text>
  13. </view>
  14. </view>
  15. <view class="header-toolbar">
  16. <view class="filter-tabs">
  17. <view
  18. class="tab-item"
  19. v-for="(tab, index) in timeFilters"
  20. :key="index"
  21. :class="{ active: currentFilter === tab.value }"
  22. @click="switchFilter(tab.value)">
  23. {{ tab.label }}
  24. </view>
  25. </view>
  26. <view class="filter-wrap">
  27. <view class="selector" @click.stop="toggleProvinceDropdown">
  28. <text class="selector-text">{{ selectedProvince || "全部省份" }}</text>
  29. <text class="selector-arrow" :class="{ open: dropdownProvinceOpen }"></text>
  30. </view>
  31. </view>
  32. </view>
  33. <!-- 背景装饰 -->
  34. <view class="header-circle circle-1"></view>
  35. <view class="header-circle circle-2"></view>
  36. </view>
  37. <scroll-view class="list-scroll" scroll-y="true" refresher-enabled :refresher-triggered="isRefreshing" @refresherrefresh="onRefresh">
  38. <view class="list-container">
  39. <view class="date-group" v-for="groupItem in groupedRows" :key="groupItem.date">
  40. <view class="date-group-card">
  41. <view class="group-left">
  42. <view
  43. class="group-select-btn"
  44. :class="{
  45. selected: isGroupFullySelected(groupItem),
  46. partial: isGroupPartiallySelected(groupItem),
  47. }"
  48. @click.stop="toggleGroupSelection(groupItem)">
  49. <text v-if="isGroupFullySelected(groupItem)">✓</text>
  50. <text v-else-if="isGroupPartiallySelected(groupItem)">-</text>
  51. </view>
  52. <text class="date-label">{{ groupItem.date }}</text>
  53. </view>
  54. </view>
  55. <view class="card-item" v-for="(item, index) in groupItem.list" :key="item.__selectId || index" @click="toDetail(item)">
  56. <view class="card-header">
  57. <view class="header-left">
  58. <view class="company-select-btn" :class="{ selected: isCompanySelected(item) }" @click.stop="toggleCompanySelection(item)">
  59. <text v-if="isCompanySelected(item)">✓</text>
  60. </view>
  61. <text class="company-name">{{ item.queryCustomer }}</text>
  62. <!-- <text
  63. class="level-tag"
  64. :class="getLevelClass(item.customerLevel)"
  65. >
  66. {{ item.customerLevel }}
  67. </text> -->
  68. </view>
  69. <view class="header-right">
  70. <text class="status-tag">黑名单</text>
  71. </view>
  72. </view>
  73. <view class="card-body">
  74. <view class="code-row">
  75. <text class="label">信用代码:</text>
  76. <text class="value code-text">{{ item.socialCreditCode }}</text>
  77. </view>
  78. <view class="info-grid">
  79. <view class="info-item">
  80. <text class="label">省份</text>
  81. <text class="value">{{ item.province }}</text>
  82. </view>
  83. <!-- <view class="info-item">
  84. <text class="label">责任经理</text>
  85. <text class="value">{{ item.responsibleManager }}</text>
  86. </view>
  87. <view class="info-item">
  88. <text class="label">客户性质</text>
  89. <text class="value">{{ item.customerNature }}</text>
  90. </view> -->
  91. </view>
  92. </view>
  93. </view>
  94. </view>
  95. <view v-if="!loading && rows.length === 0" class="empty-data">
  96. <EmptyView text="无相关数据" />
  97. </view>
  98. </view>
  99. </scroll-view>
  100. <view class="footer-btn-area">
  101. <button class="action-btn export-btn" @click="handleExport">导出至邮箱</button>
  102. </view>
  103. <!-- 蒙层 -->
  104. <view class="report-export-create-modal-mask" v-if="emailModalOpen" @click="closeEmailModal" @touchmove.stop.prevent></view>
  105. <!-- 邮箱导出弹窗 -->
  106. <view
  107. class="report-export-create-modal report-export-create-email-modal"
  108. :class="{ 'report-export-create-modal--open': emailModalOpen }"
  109. v-if="emailModalOpen">
  110. <view class="report-export-create-modal-title">
  111. <text>发送至邮箱</text>
  112. <view class="report-export-create-modal-close" @click.stop="closeEmailModal">×</view>
  113. </view>
  114. <view class="report-export-create-modal-body" :style="{ padding: '40rpx' }">
  115. <up-input
  116. v-model="emailForm.email"
  117. border="none"
  118. :customStyle="{
  119. backgroundColor: '#ebf3fb',
  120. height: '80rpx',
  121. paddingLeft: '20rpx',
  122. }"
  123. placeholder="请填写邮箱"
  124. @input="emailError = false"
  125. disabled>
  126. <template #suffix>
  127. <view style="margin-right: 20rpx; color: #666">@999.com.cn</view>
  128. </template>
  129. </up-input>
  130. <text v-if="emailError" class="report-export-error-text">请输入邮箱</text>
  131. </view>
  132. <view
  133. class="report-export-create-modal-footer"
  134. :style="{
  135. padding: '20rpx 40rpx 40rpx',
  136. display: 'flex',
  137. 'justify-content': 'space-between',
  138. }">
  139. <view class="report-export-create-modal-btn cancel-btn" @click.stop="closeEmailModal">取消</view>
  140. <view class="report-export-create-modal-btn confirm-btn" @click.stop="handleSendEmail">发送</view>
  141. </view>
  142. </view>
  143. <!-- 独立的下拉菜单层 -->
  144. <view class="dropdown-layer" v-if="dropdownProvinceOpen" @click.stop>
  145. <view class="dropdown">
  146. <view class="dropdown-search-bar">
  147. <input
  148. class="dropdown-search-input"
  149. v-model="provinceSearchText"
  150. @input="onProvinceSearch"
  151. placeholder="搜索省份..."
  152. placeholder-style="color: #999" />
  153. </view>
  154. <scroll-view scroll-y="true" class="dropdown-scroll-view">
  155. <view
  156. class="dropdown-item"
  157. v-for="(p, i) in filteredProvinceList"
  158. :key="p || i"
  159. :class="{
  160. active: p === selectedProvince || (!selectedProvince && i === 0),
  161. }"
  162. @click.stop="selectProvince(p)">
  163. {{ p || "全部省份" }}
  164. <text v-if="p === selectedProvince || (!selectedProvince && i === 0)" class="check-mark">✓</text>
  165. </view>
  166. <view v-if="filteredProvinceList.length === 0" class="dropdown-empty">暂无数据</view>
  167. </scroll-view>
  168. </view>
  169. </view>
  170. </view>
  171. <BottomScrollTip :text="'下滑动查看更多内容'" :bottom="'140rpx'" />
  172. </template>
  173. <script>
  174. import EmptyView from "../../../wigets/empty.vue";
  175. import BottomScrollTip from "../../../wigets/BottomScrollTip.vue";
  176. import request from "../../../request/index.js";
  177. import { formatDate, guid } from "../../../utils/utils.js";
  178. export default {
  179. components: {
  180. EmptyView,
  181. BottomScrollTip,
  182. },
  183. data() {
  184. return {
  185. isRefreshing: false,
  186. loading: false,
  187. rows: [],
  188. allRows: [],
  189. dropdownProvinceOpen: false,
  190. provinceSearchText: "",
  191. provinceList: [""],
  192. selectedProvince: "",
  193. currentFilter: "all",
  194. timeFilters: [
  195. { label: "全部", value: "all" },
  196. { label: "近7天", value: "7" },
  197. { label: "近15天", value: "15" },
  198. { label: "近30天", value: "30" },
  199. ],
  200. emailModalOpen: false,
  201. emailForm: {
  202. email: "",
  203. },
  204. emailError: false,
  205. // 仅用于展示/校验(不影响请求逻辑)
  206. currentFromDate: "",
  207. currentToDate: "",
  208. // 多选导出选择状态:存 socialCreditCode(或其他唯一字段)的集合
  209. selectedCompanyKeys: [],
  210. };
  211. },
  212. created() {
  213. this.resetFetch();
  214. this.getProviceList();
  215. const userEmail = uni.getStorageSync("traceCode_useremail");
  216. this.emailForm.email = userEmail;
  217. },
  218. methods: {
  219. formatDate,
  220. getLevelClass(level) {
  221. if (level === "VIP") return "tag-vip";
  222. if (level === "二级") return "tag-l2";
  223. if (level === "三级") return "tag-l3";
  224. return "tag-default";
  225. },
  226. switchFilter(value) {
  227. if (this.currentFilter === value) return;
  228. this.currentFilter = value;
  229. this.resetFetch();
  230. },
  231. // 用于选择的唯一 key(优先 socialCreditCode)
  232. getCompanyKey(item = {}) {
  233. return String(item.__selectId || item.socialCreditCode || item.id || item.queryCustomer || "");
  234. },
  235. /** 将 YYYY-MM-DD 加一天,用于邮件接口 statisticsDate */
  236. addOneDayFromDateStr(dateStr) {
  237. if (!dateStr) return "";
  238. const parts = String(dateStr).trim().split("-");
  239. if (parts.length !== 3) return dateStr;
  240. const y = +parts[0];
  241. const m = +parts[1];
  242. const d = +parts[2];
  243. if (!y || !m || !d) return dateStr;
  244. const dt = new Date(y, m - 1, d);
  245. dt.setDate(dt.getDate() + 1);
  246. return this.formatDate(dt, "YYYY-MM-DD");
  247. },
  248. isCompanySelected(item) {
  249. const key = this.getCompanyKey(item);
  250. if (!key) return false;
  251. return this.selectedCompanyKeys.includes(key);
  252. },
  253. toggleCompanySelection(item) {
  254. const key = this.getCompanyKey(item);
  255. if (!key) return;
  256. const has = this.selectedCompanyKeys.includes(key);
  257. if (has) {
  258. this.selectedCompanyKeys = this.selectedCompanyKeys.filter((k) => k !== key);
  259. } else {
  260. this.selectedCompanyKeys = [...this.selectedCompanyKeys, key];
  261. }
  262. },
  263. isGroupFullySelected(groupItem = {}) {
  264. const list = Array.isArray(groupItem.list) ? groupItem.list : [];
  265. if (!list.length) return false;
  266. return list.every((it) => this.isCompanySelected(it));
  267. },
  268. isGroupPartiallySelected(groupItem = {}) {
  269. const list = Array.isArray(groupItem.list) ? groupItem.list : [];
  270. if (!list.length) return false;
  271. const selectedCount = list.filter((it) => this.isCompanySelected(it)).length;
  272. return selectedCount > 0 && selectedCount < list.length;
  273. },
  274. toggleGroupSelection(groupItem = {}) {
  275. const list = Array.isArray(groupItem.list) ? groupItem.list : [];
  276. if (!list.length) return;
  277. const groupKeys = list.map((it) => this.getCompanyKey(it)).filter(Boolean);
  278. if (!groupKeys.length) return;
  279. const allSelected = groupKeys.every((k) => this.selectedCompanyKeys.includes(k));
  280. if (allSelected) {
  281. this.selectedCompanyKeys = this.selectedCompanyKeys.filter((k) => !groupKeys.includes(k));
  282. } else {
  283. const merged = new Set([...this.selectedCompanyKeys, ...groupKeys]);
  284. this.selectedCompanyKeys = Array.from(merged);
  285. }
  286. },
  287. getProviceList() {
  288. request("/common/getProviceList", {
  289. path: "traceabilityReport/pages/blacklist/index.vue",
  290. }).then((res) => {
  291. if (res.code == 200) {
  292. const _data = res.data || [];
  293. this.provinceList = ["", ..._data.map((item) => item.regionName)];
  294. }
  295. });
  296. },
  297. fetchList() {
  298. if (this.loading) return;
  299. this.loading = true;
  300. const today = new Date();
  301. const toDateStr = this.formatDate(today, "YYYY-MM-DD");
  302. let fromDateStr = "";
  303. if (this.currentFilter === "all") {
  304. const twoMonthsAgo = new Date();
  305. twoMonthsAgo.setMonth(today.getMonth() - 2);
  306. fromDateStr = this.formatDate(twoMonthsAgo, "YYYY-MM-DD");
  307. } else {
  308. const days = parseInt(this.currentFilter);
  309. const pastDate = new Date();
  310. pastDate.setDate(today.getDate() - days);
  311. fromDateStr = this.formatDate(pastDate, "YYYY-MM-DD");
  312. }
  313. request("/blacklist-report/get-export-company-data", {
  314. fromDate: fromDateStr,
  315. toDate: toDateStr,
  316. path: "traceabilityReport/pages/blacklist/index.vue",
  317. }).then((res) => {
  318. if (res.code == 200) {
  319. this.allRows = (res.data || []).map((it) => ({
  320. ...it,
  321. __selectId: it?.__selectId || guid("blk_"),
  322. }));
  323. this.applyFilter();
  324. }
  325. this.loading = false;
  326. this.isRefreshing = false;
  327. });
  328. },
  329. async onRefresh() {
  330. this.isRefreshing = true;
  331. this.fetchList();
  332. },
  333. resetFetch() {
  334. this.rows = [];
  335. this.selectedCompanyKeys = [];
  336. this.fetchList();
  337. },
  338. toDetail(item) {
  339. uni.navigateTo({
  340. url: `/traceCodePackages/traceabilityReport/pages/blacklist/detail/index?id=${item.socialCreditCode}&dateStr=${item.billTime}&name=${encodeURIComponent(item.queryCustomer)}&province=${encodeURIComponent(item.province || "")}&customerNature=${encodeURIComponent(item.customerNature || "")}&customerLevel=${encodeURIComponent(item.customerLevel || "")}&responsibleManager=${encodeURIComponent(item.responsibleManager || "")}`,
  341. });
  342. },
  343. handleHistory() {
  344. uni.navigateTo({
  345. url: "/traceCodePackages/traceabilityReport/pages/blacklist/history/index",
  346. });
  347. },
  348. handleExport() {
  349. if (this.selectedCompanyKeys?.length === 0) {
  350. uni.showToast({
  351. title: "请选择要导出的企业",
  352. icon: "none",
  353. });
  354. return;
  355. }
  356. this.emailModalOpen = true;
  357. this.emailError = false;
  358. },
  359. closeEmailModal() {
  360. this.emailModalOpen = false;
  361. },
  362. handleSendEmail() {
  363. if (!this.emailForm.email) {
  364. this.emailError = true;
  365. return;
  366. }
  367. const selectedTaskIds = this.getSelectedTaskIds();
  368. if (!selectedTaskIds.length) {
  369. uni.showToast({ title: "未找到可导出的任务", icon: "none" });
  370. return;
  371. }
  372. uni.showLoading({ title: "发送中..." });
  373. const doSingleSend = async () => {
  374. const keys = new Set(this.selectedCompanyKeys || []);
  375. const selectedRows = (this.rows || []).filter((r) => keys.has(this.getCompanyKey(r)));
  376. const emailDTOList = selectedRows.map((item) => ({
  377. companyNameList: item.queryCustomer.split(","),
  378. statisticsDate: this.addOneDayFromDateStr(item.billTime),
  379. taskId: item.taskId,
  380. }));
  381. return request(
  382. "/api/email/send",
  383. {
  384. emailDTOList,
  385. type: 0,
  386. uid: uni.getStorageSync("userInfo").ldap,
  387. emailAddress: this.emailForm.email + "@999.com.cn",
  388. path: "traceabilityReport/pages/blacklist/index.vue",
  389. },
  390. "post",
  391. );
  392. };
  393. const run = async () => {
  394. // try {
  395. // // 先尝试一次性传多个 taskId(后端若支持)
  396. // const res = await request("/report/sendemail", {
  397. // taskId: selectedTaskIds,
  398. // emailAddress: this.emailForm.email + "@999.com.cn",
  399. // path: "traceabilityReport/pages/blacklist/index.vue",
  400. // });
  401. // if (res?.code == 200) {
  402. // uni.hideLoading();
  403. // uni.showToast({ title: "发送成功", icon: "success" });
  404. // this.closeEmailModal();
  405. // return;
  406. // }
  407. // } catch (e) {
  408. // // ignore, fallback below
  409. // }
  410. // 不支持 combinedTask:逐个发送
  411. // for (let i = 0; i < selectedTaskIds.length; i++) {
  412. // const taskId = selectedTaskIds[i];
  413. // const r = await doSingleSend(taskId);
  414. // if (r?.code != 200) {
  415. // uni.hideLoading();
  416. // uni.showToast({
  417. // title: r?.msg || "发送失败",
  418. // icon: "none",
  419. // });
  420. // return;
  421. // }
  422. // }
  423. const r = await doSingleSend();
  424. if (r?.code != 200) {
  425. uni.hideLoading();
  426. uni.showToast({
  427. title: r?.msg || "发送失败",
  428. icon: "none",
  429. });
  430. return;
  431. }
  432. uni.hideLoading();
  433. uni.showToast({ title: "发送成功", icon: "success" });
  434. this.closeEmailModal();
  435. };
  436. run();
  437. },
  438. getSelectedTaskIds() {
  439. const keys = new Set(this.selectedCompanyKeys || []);
  440. const selectedRows = (this.rows || []).filter((r) => keys.has(this.getCompanyKey(r)));
  441. const ids = selectedRows.map((r) => r?.taskId).filter(Boolean);
  442. return Array.from(new Set(ids));
  443. },
  444. applyFilter() {
  445. if (!this.selectedProvince) {
  446. this.rows = [...this.allRows];
  447. } else {
  448. this.rows = this.allRows.filter((item) => item.customerProvinceName === this.selectedProvince);
  449. }
  450. this.totalCount = this.rows.length;
  451. // 切换省份/刷新数据后,清空已选企业,避免“跨筛选脏选中”
  452. this.selectedCompanyKeys = [];
  453. },
  454. toggleProvinceDropdown() {
  455. this.dropdownProvinceOpen = !this.dropdownProvinceOpen;
  456. },
  457. closeProvinceDropdown() {
  458. this.dropdownProvinceOpen = false;
  459. },
  460. selectProvince(province) {
  461. this.selectedProvince = province || "";
  462. this.dropdownProvinceOpen = false;
  463. this.applyFilter();
  464. },
  465. },
  466. computed: {
  467. filteredProvinceList() {
  468. if (!this.provinceSearchText) {
  469. return this.provinceList;
  470. }
  471. return this.provinceList.filter((p) => p?.toLowerCase()?.includes(this.provinceSearchText.toLowerCase()));
  472. },
  473. groupedRows() {
  474. const groups = {};
  475. (this.rows || []).forEach((item) => {
  476. const date = item?.billTime || "未知日期";
  477. if (!groups[date]) groups[date] = [];
  478. groups[date].push(item);
  479. });
  480. return Object.keys(groups)
  481. .sort((a, b) => new Date(String(b).replace(/-/g, "/")).getTime() - new Date(String(a).replace(/-/g, "/")).getTime())
  482. .map((date) => ({
  483. date,
  484. list: groups[date],
  485. }));
  486. },
  487. },
  488. };
  489. </script>
  490. <style scoped>
  491. .detail-page {
  492. display: flex;
  493. flex-direction: column;
  494. height: calc(100vh - 116rpx - env(safe-area-inset-bottom));
  495. box-sizing: border-box;
  496. background: #f5f7fa;
  497. }
  498. /* Header Section */
  499. .header-section {
  500. background: linear-gradient(135deg, #2b32b2 0%, #1488cc 100%);
  501. padding: 40rpx 40rpx 80rpx;
  502. /* Extra padding at bottom for overlap effect */
  503. position: relative;
  504. z-index: 20;
  505. color: #fff;
  506. border-bottom-left-radius: 40rpx;
  507. border-bottom-right-radius: 40rpx;
  508. overflow: hidden;
  509. box-shadow: 0 10rpx 30rpx rgba(20, 136, 204, 0.2);
  510. }
  511. .header-content {
  512. display: flex;
  513. justify-content: space-between;
  514. align-items: center;
  515. position: relative;
  516. z-index: 2;
  517. margin-bottom: 24rpx;
  518. }
  519. .title-group {
  520. display: flex;
  521. flex-direction: column;
  522. }
  523. .main-title {
  524. font-size: 40rpx;
  525. font-weight: bold;
  526. margin-bottom: 8rpx;
  527. letter-spacing: 2rpx;
  528. }
  529. .sub-title {
  530. font-size: 24rpx;
  531. opacity: 0.8;
  532. }
  533. .stat-box {
  534. display: flex;
  535. align-items: baseline;
  536. }
  537. .stat-num {
  538. font-size: 56rpx;
  539. font-weight: bold;
  540. margin-right: 8rpx;
  541. font-family: "DINAlternate-Bold", sans-serif;
  542. }
  543. .stat-unit {
  544. font-size: 24rpx;
  545. opacity: 0.8;
  546. }
  547. .header-toolbar {
  548. display: flex;
  549. justify-content: space-between;
  550. align-items: center;
  551. position: relative;
  552. z-index: 3;
  553. gap: 20rpx;
  554. }
  555. .update-tip {
  556. display: inline-flex;
  557. align-items: center;
  558. background: rgba(255, 255, 255, 0.15);
  559. padding: 8rpx 20rpx;
  560. border-radius: 30rpx;
  561. font-size: 22rpx;
  562. backdrop-filter: blur(10px);
  563. }
  564. .tip-icon {
  565. margin-right: 8rpx;
  566. font-size: 20rpx;
  567. }
  568. /* Filter Styles */
  569. .filter-wrap {
  570. position: relative;
  571. }
  572. .selector {
  573. display: flex;
  574. align-items: center;
  575. color: #fff;
  576. font-size: 26rpx;
  577. font-weight: 500;
  578. padding: 8rpx 20rpx;
  579. background: rgba(255, 255, 255, 0.2);
  580. border-radius: 30rpx;
  581. border: 1rpx solid rgba(255, 255, 255, 0.3);
  582. }
  583. .selector-text {
  584. max-width: 200rpx;
  585. overflow: hidden;
  586. white-space: nowrap;
  587. text-overflow: ellipsis;
  588. }
  589. .selector-arrow {
  590. margin-left: 10rpx;
  591. width: 0;
  592. height: 0;
  593. border-left: 8rpx solid transparent;
  594. border-right: 8rpx solid transparent;
  595. border-top: 10rpx solid #fff;
  596. transition: transform 0.3s;
  597. }
  598. .selector-arrow.open {
  599. transform: rotate(180deg);
  600. }
  601. /* Dropdown Layer (Absolute on top of everything) */
  602. .dropdown-layer {
  603. position: absolute;
  604. top: 300rpx;
  605. /* Adjust based on header layout */
  606. right: 40rpx;
  607. z-index: 999;
  608. }
  609. /* Header date filter tabs */
  610. .filter-tabs {
  611. display: flex;
  612. background: rgba(255, 255, 255, 0.2);
  613. border-radius: 16rpx;
  614. padding: 6rpx 6rpx;
  615. z-index: 30;
  616. position: relative;
  617. flex: 1;
  618. max-width: 420rpx;
  619. }
  620. .tab-item {
  621. flex: 1;
  622. text-align: center;
  623. font-size: 24rpx;
  624. padding: 10rpx 0;
  625. border-radius: 12rpx;
  626. color: rgba(255, 255, 255, 0.8);
  627. transition: all 0.3s;
  628. }
  629. .tab-item.active {
  630. background: #fff;
  631. color: #2b32b2;
  632. font-weight: 600;
  633. box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
  634. }
  635. /* Date-group (history style) */
  636. .date-group {
  637. margin-bottom: 40rpx;
  638. }
  639. .date-group-card {
  640. display: flex;
  641. align-items: center;
  642. justify-content: flex-start;
  643. background: #fff;
  644. padding: 20rpx 30rpx;
  645. border-radius: 16rpx;
  646. margin-bottom: 20rpx;
  647. box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.03);
  648. border-left: 8rpx solid #1488cc;
  649. }
  650. .group-left {
  651. display: flex;
  652. align-items: center;
  653. }
  654. .date-label {
  655. font-size: 30rpx;
  656. font-weight: 600;
  657. color: #333;
  658. }
  659. .group-right {
  660. flex-shrink: 0;
  661. }
  662. .group-select-btn {
  663. width: 44rpx;
  664. height: 44rpx;
  665. border-radius: 50%;
  666. border: 2rpx solid #1488cc;
  667. color: #1488cc;
  668. display: flex;
  669. align-items: center;
  670. justify-content: center;
  671. font-size: 26rpx;
  672. background: rgba(20, 136, 204, 0.06);
  673. margin-right: 18rpx;
  674. }
  675. .group-select-btn.selected {
  676. background: #1488cc;
  677. color: #fff;
  678. }
  679. .group-select-btn.partial {
  680. background: rgba(20, 136, 204, 0.15);
  681. }
  682. .company-select-btn {
  683. width: 44rpx;
  684. height: 44rpx;
  685. border-radius: 50%;
  686. border: 2rpx solid #d0d7e2;
  687. color: #d0d7e2;
  688. display: flex;
  689. align-items: center;
  690. justify-content: center;
  691. margin-right: 14rpx;
  692. font-size: 26rpx;
  693. background: #fff;
  694. }
  695. .company-select-btn.selected {
  696. border-color: #1488cc;
  697. color: #1488cc;
  698. background: rgba(20, 136, 204, 0.08);
  699. }
  700. .dropdown {
  701. width: 360rpx;
  702. background: #fff;
  703. border-radius: 12rpx;
  704. box-shadow: 0 8rpx 30rpx rgba(0, 0, 0, 0.15);
  705. overflow: hidden;
  706. }
  707. .dropdown-search-bar {
  708. padding: 16rpx;
  709. border-bottom: 1rpx solid #f0f0f0;
  710. }
  711. .dropdown-search-input {
  712. background: #f5f7fa;
  713. height: 64rpx;
  714. border-radius: 32rpx;
  715. padding: 0 24rpx;
  716. font-size: 26rpx;
  717. }
  718. .dropdown-scroll-view {
  719. max-height: 400rpx;
  720. }
  721. .dropdown-item {
  722. padding: 20rpx 30rpx;
  723. font-size: 28rpx;
  724. color: #333;
  725. display: flex;
  726. justify-content: space-between;
  727. align-items: center;
  728. transition: background 0.2s;
  729. }
  730. .dropdown-item:active {
  731. background: #f5f7fa;
  732. }
  733. .dropdown-item.active {
  734. color: #1890ff;
  735. font-weight: 500;
  736. background: #e6f7ff;
  737. }
  738. .check-mark {
  739. font-size: 24rpx;
  740. }
  741. .dropdown-empty {
  742. padding: 40rpx;
  743. text-align: center;
  744. color: #999;
  745. font-size: 26rpx;
  746. }
  747. /* Header Decor Circles */
  748. .header-circle {
  749. position: absolute;
  750. border-radius: 50%;
  751. background: rgba(255, 255, 255, 0.05);
  752. }
  753. .circle-1 {
  754. width: 300rpx;
  755. height: 300rpx;
  756. top: -100rpx;
  757. right: -50rpx;
  758. }
  759. .circle-2 {
  760. width: 200rpx;
  761. height: 200rpx;
  762. bottom: -50rpx;
  763. left: -50rpx;
  764. }
  765. /* List Scroll */
  766. .list-scroll {
  767. flex: 1;
  768. height: 0;
  769. padding: 0 24rpx;
  770. /* Remove vertical padding from scroll container */
  771. box-sizing: border-box;
  772. margin-top: -50rpx;
  773. /* Negative margin to pull list up */
  774. position: relative;
  775. z-index: 21;
  776. }
  777. .list-container {
  778. padding-top: 10rpx;
  779. padding-bottom: calc(200rpx + env(safe-area-inset-bottom));
  780. }
  781. .card-item {
  782. background: #fff;
  783. border-radius: 20rpx;
  784. padding: 30rpx;
  785. margin-bottom: 24rpx;
  786. box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.05);
  787. transition: transform 0.2s;
  788. }
  789. .card-item:active {
  790. transform: scale(0.99);
  791. }
  792. .card-header {
  793. display: flex;
  794. justify-content: space-between;
  795. align-items: flex-start;
  796. margin-bottom: 24rpx;
  797. }
  798. .header-left {
  799. flex: 1;
  800. display: flex;
  801. align-items: center;
  802. margin-right: 20rpx;
  803. }
  804. .index-num {
  805. font-size: 24rpx;
  806. color: #999;
  807. margin-right: 12rpx;
  808. font-family: monospace;
  809. }
  810. .company-name {
  811. font-size: 32rpx;
  812. font-weight: 600;
  813. color: #096dd9;
  814. line-height: 1.4;
  815. text-decoration: underline;
  816. }
  817. .header-right {
  818. flex-shrink: 0;
  819. }
  820. .status-tag {
  821. font-size: 20rpx;
  822. padding: 6rpx 16rpx;
  823. border-radius: 20rpx;
  824. background: #fff1f0;
  825. color: #ff4d4f;
  826. font-weight: 600;
  827. border: 1rpx solid rgba(255, 77, 79, 0.2);
  828. }
  829. .card-body {
  830. background: #f9fbfd;
  831. border-radius: 12rpx;
  832. padding: 24rpx;
  833. }
  834. .code-row {
  835. display: flex;
  836. align-items: center;
  837. margin-bottom: 20rpx;
  838. padding-bottom: 20rpx;
  839. border-bottom: 1rpx solid #edf0f5;
  840. }
  841. .code-row:last-child {
  842. margin-bottom: 0;
  843. padding-bottom: 0;
  844. border-bottom: none;
  845. }
  846. .code-text {
  847. font-family: monospace;
  848. color: #333 !important;
  849. font-weight: 600 !important;
  850. letter-spacing: 1rpx;
  851. }
  852. .info-grid {
  853. display: flex;
  854. flex-wrap: wrap;
  855. gap: 24rpx;
  856. }
  857. .info-item {
  858. display: flex;
  859. align-items: center;
  860. width: 45%;
  861. }
  862. .info-item .label,
  863. .code-row .label {
  864. font-size: 24rpx;
  865. color: #999;
  866. margin-right: 12rpx;
  867. }
  868. .info-item .value,
  869. .code-row .value {
  870. font-size: 26rpx;
  871. color: #666;
  872. font-weight: 500;
  873. }
  874. /* Loading & Footer */
  875. .loading-more {
  876. display: flex;
  877. justify-content: center;
  878. align-items: center;
  879. padding: 30rpx 0;
  880. }
  881. .loading-text {
  882. font-size: 24rpx;
  883. color: #999;
  884. }
  885. .loading-icon {
  886. width: 32rpx;
  887. height: 32rpx;
  888. margin-right: 12rpx;
  889. animation: spin 1s linear infinite;
  890. }
  891. .empty-data {
  892. padding-top: 120rpx;
  893. }
  894. .no-more {
  895. text-align: center;
  896. color: #ccc;
  897. font-size: 24rpx;
  898. padding: 30rpx 0;
  899. }
  900. @keyframes spin {
  901. from {
  902. transform: rotate(0deg);
  903. }
  904. to {
  905. transform: rotate(360deg);
  906. }
  907. }
  908. .footer-btn-area {
  909. padding: 24rpx 32rpx calc(24rpx + env(safe-area-inset-bottom));
  910. background: #fff;
  911. box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.08);
  912. position: fixed;
  913. z-index: 100;
  914. bottom: 0;
  915. width: 100%;
  916. box-sizing: border-box;
  917. display: flex;
  918. gap: 24rpx;
  919. }
  920. .action-btn {
  921. flex: 1;
  922. height: 88rpx;
  923. line-height: 88rpx;
  924. border-radius: 44rpx;
  925. font-size: 30rpx;
  926. font-weight: 600;
  927. text-align: center;
  928. border: none;
  929. transition: all 0.3s;
  930. }
  931. .action-btn::after {
  932. border: none;
  933. }
  934. .action-btn:active {
  935. transform: scale(0.96);
  936. }
  937. .history-btn {
  938. background: #fff;
  939. color: #1890ff;
  940. border: 2rpx solid #1890ff;
  941. box-shadow: 0 4rpx 12rpx rgba(24, 144, 255, 0.1);
  942. }
  943. .export-btn {
  944. background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
  945. color: #fff;
  946. box-shadow: 0 6rpx 16rpx rgba(24, 144, 255, 0.35);
  947. }
  948. /* 弹窗样式 */
  949. .report-export-create-modal-mask {
  950. position: fixed;
  951. top: 0;
  952. left: 0;
  953. right: 0;
  954. bottom: 0;
  955. background: rgba(0, 0, 0, 0.6);
  956. z-index: 999;
  957. backdrop-filter: blur(2px);
  958. }
  959. .report-export-create-modal {
  960. position: fixed;
  961. z-index: 1000;
  962. background: #fff;
  963. transition: all 0.3s ease;
  964. }
  965. .report-export-create-email-modal {
  966. top: 50%;
  967. left: 50%;
  968. transform: translate(-50%, -50%);
  969. width: 620rpx;
  970. border-radius: 24rpx;
  971. opacity: 0;
  972. visibility: hidden;
  973. overflow: hidden;
  974. }
  975. .report-export-create-email-modal.report-export-create-modal--open {
  976. opacity: 1;
  977. visibility: visible;
  978. }
  979. .report-export-create-modal-title {
  980. padding: 36rpx 32rpx;
  981. font-size: 36rpx;
  982. color: #333;
  983. text-align: center;
  984. font-weight: 600;
  985. border-bottom: 1rpx solid #f0f0f0;
  986. position: relative;
  987. }
  988. .report-export-create-modal-close {
  989. position: absolute;
  990. right: 32rpx;
  991. top: 50%;
  992. transform: translateY(-50%);
  993. font-size: 44rpx;
  994. color: #999;
  995. line-height: 1;
  996. padding: 10rpx;
  997. }
  998. .report-export-create-modal-body {
  999. padding: 48rpx 40rpx 32rpx;
  1000. }
  1001. .report-export-error-text {
  1002. font-size: 24rpx;
  1003. color: #ff4d4f;
  1004. margin-top: 12rpx;
  1005. display: block;
  1006. }
  1007. .report-export-create-modal-footer {
  1008. padding: 0 40rpx 48rpx;
  1009. display: flex;
  1010. justify-content: space-between;
  1011. gap: 24rpx;
  1012. }
  1013. .report-export-create-modal-btn {
  1014. height: 88rpx;
  1015. line-height: 88rpx;
  1016. border-radius: 44rpx;
  1017. text-align: center;
  1018. font-size: 30rpx;
  1019. font-weight: 600;
  1020. flex: 1;
  1021. margin: 0;
  1022. }
  1023. .cancel-btn {
  1024. background: #f5f7fa;
  1025. color: #666;
  1026. border: 1rpx solid #e4e7ed;
  1027. }
  1028. .confirm-btn {
  1029. background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
  1030. color: #fff;
  1031. box-shadow: 0 6rpx 16rpx rgba(24, 144, 255, 0.3);
  1032. }
  1033. .level-tag {
  1034. font-size: 20rpx;
  1035. padding: 4rpx 12rpx;
  1036. border-radius: 8rpx;
  1037. white-space: nowrap;
  1038. line-height: 1.2;
  1039. }
  1040. .tag-vip {
  1041. background: linear-gradient(135deg, #e6f7ff, #bae7ff);
  1042. color: #096dd9;
  1043. }
  1044. .tag-l2 {
  1045. background: linear-gradient(135deg, #f6ffed, #d9f7be);
  1046. color: #389e0d;
  1047. }
  1048. .tag-l3 {
  1049. background: linear-gradient(135deg, #fff7e6, #ffe7ba);
  1050. color: #d46b08;
  1051. }
  1052. .tag-default {
  1053. background: #f5f5f5;
  1054. color: #999;
  1055. }
  1056. </style>