index.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516
  1. <template>
  2. <div v-loading="pageLoading" class="app-container" :element-loading-text="loadingText">
  3. <!-- 顶部分类标签栏 -->
  4. <div class="category-tabs">
  5. <div class="category-tabs__nav">
  6. <el-button
  7. v-for="tab in categoryTabs"
  8. :key="tab.key"
  9. :type="activeCategory === tab.key ? 'primary' : 'default'"
  10. :icon="tab.icon"
  11. @click="handleCategoryChange(tab.key)"
  12. >
  13. {{ tab.label }}
  14. </el-button>
  15. </div>
  16. </div>
  17. <!-- 搜索栏和场景选择 -->
  18. <div class="search-bar">
  19. <div class="search-bar__scene">
  20. <span class="search-bar__label">场景:</span>
  21. <div class="scene-card-group">
  22. <el-tag
  23. v-for="scene in currentScenes"
  24. :key="scene.value"
  25. :class="{ 'scene-card--active': sceneValue === scene.value }"
  26. class="scene-card"
  27. @click="handleSceneChange(scene.value)"
  28. >
  29. {{ scene.label }}
  30. </el-tag>
  31. </div>
  32. </div>
  33. <div class="search-bar__right">
  34. <el-input
  35. v-model="searchName"
  36. placeholder="制度名称搜索"
  37. class="search-bar__input"
  38. @keyup.enter="handleSearch"
  39. />
  40. <el-button type="primary" @click="handleSearch">搜索</el-button>
  41. <el-button type="success" @click="handleOpenDialog('create')">新增制度</el-button>
  42. </div>
  43. </div>
  44. <PageContent ref="contentRef" :content-config="contentConfig">
  45. <template #toolbar="{ toolbarRight, onToolbar, removeIds, cols }">
  46. <div class="data-table__toolbar--right">
  47. <CrudToolbarRight :buttons="toolbarRight" :cols="cols" :on-toolbar="onToolbar" />
  48. </div>
  49. </template>
  50. <template #table="{ data, loading, tableRef, onSelectionChange }">
  51. <div class="data-table__content">
  52. <el-table
  53. :ref="tableRef as any"
  54. v-loading="loading"
  55. :data="data"
  56. height="100%"
  57. border
  58. @selection-change="onSelectionChange"
  59. >
  60. <template #empty>
  61. <el-empty :image-size="80" description="暂无数据" />
  62. </template>
  63. <el-table-column
  64. v-if="contentCols.find((col) => col.prop === 'selection')?.show"
  65. type="selection"
  66. min-width="55"
  67. align="center"
  68. />
  69. <el-table-column
  70. v-if="contentCols.find((col) => col.prop === 'institution_name')?.show"
  71. key="institution_name"
  72. label="制度名称"
  73. prop="institution_name"
  74. min-width="150"
  75. show-overflow-tooltip
  76. />
  77. <el-table-column
  78. v-if="contentCols.find((col) => col.prop === 'valid_period')?.show"
  79. key="valid_period"
  80. label="制度有效期"
  81. min-width="150"
  82. show-overflow-tooltip
  83. >
  84. <template #default="scope">
  85. {{ (scope.row.effective_start_date || '').substring(0, 10) || '-' }}
  86. ~
  87. {{ (scope.row.effective_end_date || '').substring(0, 10) || '长期有效' }}
  88. </template>
  89. </el-table-column>
  90. <el-table-column
  91. v-if="contentCols.find((col) => col.prop === 'expense_type')?.show"
  92. key="expense_type"
  93. label="费用类型"
  94. prop="expense_type"
  95. min-width="100"
  96. >
  97. <template #default="scope">
  98. {{ EXPENSE_TYPE_LABEL[scope.row.expense_type] || scope.row.expense_type }}
  99. </template>
  100. </el-table-column>
  101. <el-table-column
  102. v-if="contentCols.find((col) => col.prop === 'status')?.show"
  103. key="status"
  104. label="制度状态"
  105. prop="status"
  106. min-width="100"
  107. >
  108. <template #default="scope">
  109. <el-tag :type="STATUS_TAG_TYPE[scope.row.status]">
  110. {{ STATUS_LABEL[scope.row.status] || scope.row.status }}
  111. </el-tag>
  112. </template>
  113. </el-table-column>
  114. <el-table-column
  115. v-if="contentCols.find((col) => col.prop === 'updated_time')?.show"
  116. key="updated_time"
  117. label="修改时间"
  118. prop="updated_time"
  119. min-width="160"
  120. sortable
  121. />
  122. <el-table-column
  123. v-if="contentCols.find((col) => col.prop === 'operation')?.show"
  124. fixed="right"
  125. label="操作"
  126. align="center"
  127. min-width="240"
  128. >
  129. <template #default="scope">
  130. <el-button
  131. v-hasPerm="['module_payment:expense:institution:detail']"
  132. type="text"
  133. size="small"
  134. @click="handleOpenDialog('detail', scope.row.institution_id)"
  135. >
  136. 详情
  137. </el-button>
  138. <el-button
  139. v-hasPerm="['module_payment:expense:institution:modify']"
  140. type="text"
  141. size="small"
  142. @click="handleOpenDialog('update', scope.row.institution_id)"
  143. >
  144. 编辑
  145. </el-button>
  146. <el-button
  147. v-hasPerm="['module_payment:expense:institution:scope:modify']"
  148. type="text"
  149. size="small"
  150. @click="handleToggleEffective(scope.row)"
  151. >
  152. {{ scope.row.effective === "1" ? "停用" : "启用" }}
  153. </el-button>
  154. <el-button
  155. v-hasPerm="['module_payment:expense:institution:scope:modify']"
  156. type="text"
  157. size="small"
  158. @click="handleOpenScopeDialog(scope.row.institution_id)"
  159. >
  160. 成员管理
  161. </el-button>
  162. <el-button
  163. v-hasPerm="['module_payment:expense:institution:delete']"
  164. type="text"
  165. size="small"
  166. @click="handleDelete(scope.row)"
  167. >
  168. 删除
  169. </el-button>
  170. </template>
  171. </el-table-column>
  172. </el-table>
  173. </div>
  174. </template>
  175. </PageContent>
  176. <EnhancedDialog
  177. v-model="dialogVisible.visible"
  178. :title="dialogVisible.title"
  179. width="1000px"
  180. @close="handleCloseDialog"
  181. >
  182. <template v-if="dialogVisible.type === 'detail'">
  183. <InstitutionDetail :institution-id="currentInstitutionId" />
  184. </template>
  185. <template v-else>
  186. <InstitutionForm
  187. ref="formRef"
  188. :key="formKey"
  189. :type="dialogVisible.type"
  190. :institution-id="currentInstitutionId"
  191. :enterprise-id="currentEnterpriseId"
  192. @success="handleFormSuccess"
  193. />
  194. </template>
  195. <template #footer>
  196. <div class="dialog-footer">
  197. <el-button v-if="dialogVisible.type !== 'detail'" type="primary" @click="handleSubmit">
  198. 确定
  199. </el-button>
  200. <el-button v-else type="primary" @click="handleCloseDialog">确定</el-button>
  201. <el-button @click="handleCloseDialog">取消</el-button>
  202. </div>
  203. </template>
  204. </EnhancedDialog>
  205. <ScopeDialog
  206. v-model="scopeDialogVisible"
  207. :institution-id="currentInstitutionId"
  208. :enterprise-id="currentEnterpriseId"
  209. />
  210. </div>
  211. </template>
  212. <script setup lang="ts">
  213. defineOptions({
  214. name: "Institution",
  215. inheritAttrs: false,
  216. });
  217. import InstitutionAPI, {
  218. InstitutionPageQuery,
  219. STATUS_TAG_TYPE,
  220. STATUS_LABEL,
  221. EXPENSE_TYPE_LABEL,
  222. } from "@/api/module_payment/institution";
  223. import CrudToolbarRight from "@/components/CURD/CrudToolbarRight.vue";
  224. import PageContent from "@/components/CURD/PageContent.vue";
  225. import EnhancedDialog from "@/components/CURD/EnhancedDialog.vue";
  226. import InstitutionForm from "./components/InstitutionForm.vue";
  227. import InstitutionDetail from "./components/InstitutionDetail.vue";
  228. import ScopeDialog from "./components/ScopeDialog.vue";
  229. import type { IContentConfig } from "@/components/CURD/types";
  230. import { useCrudList } from "@/components/CURD/useCrudList";
  231. import { useLoadingAction } from "@/composables/useLoadingAction";
  232. import { useRoute } from "vue-router";
  233. import { ElMessage } from "element-plus";
  234. import { ref, reactive, computed } from "vue";
  235. import { useEnterpriseStore } from "@/store/modules/enterprise.store";
  236. const route = useRoute();
  237. const enterpriseStore = useEnterpriseStore();
  238. // 分类标签与对应场景映射
  239. const categoryTabs = [
  240. // { key: "meal", label: "餐饮", icon: "UtensilsCrossed", scenes: [{ label: "差旅餐饮", value: "business_meal" }, { label: "员工餐补", value: "staff_meal" }, { label: "团建聚餐", value: "team_dinner" }] },
  241. // { key: "hotel", label: "酒店", icon: "Building2", scenes: [{ label: "商务出差", value: "business_trip" }, { label: "会议住宿", value: "meeting_hotel" }, { label: "培训住宿", value: "training_hotel" }] },
  242. // { key: "flight", label: "机票", icon: "Plane", scenes: [{ label: "国内出差", value: "domestic_flight" }, { label: "国际出差", value: "international_flight" }, { label: "紧急出差", value: "urgent_flight" }] },
  243. // { key: "train", label: "火车票", icon: "Train", scenes: [{ label: "省内出差", value: "provincial_train" }, { label: "跨省出差", value: "interprovincial_train" }, { label: "通勤", value: "commute_train" }] },
  244. // { key: "bus", label: "公交", icon: "Bus", scenes: [{ label: "市内通勤", value: "city_bus" }, { label: "郊区出行", value: "suburb_bus" }] },
  245. // { key: "subway", label: "地铁", icon: "Metro", scenes: [{ label: "日常通勤", value: "daily_subway" }, { label: "加班补贴", value: "overtime_subway" }] },
  246. // { key: "car", label: "用车", icon: "Car", scenes: [{ label: "公务用车", value: "official_car" }, { label: "网约车", value: "ride_hailing" }, { label: "自驾补贴", value: "self_drive" }] },
  247. // { key: "service", label: "服务", icon: "Headphones", scenes: [{ label: "咨询服务", value: "consult_service" }, { label: "外包服务", value: "outsourcing" }] },
  248. // { key: "shopping", label: "商城", icon: "ShoppingCart", scenes: [{ label: "办公用品", value: "office_supplies" }, { label: "劳保用品", value: "labor_protection" }, { label: "员工福利", value: "employee_welfare" }] },
  249. // { key: "express", label: "快递", icon: "Truck", scenes: [{ label: "日常快递", value: "daily_express" }, { label: "大件物流", value: "bulk_logistics" }] },
  250. // { key: "gas", label: "加油", icon: "Fuel", scenes: [{ label: "公务车加油", value: "official_gas" }, { label: "私家车补贴", value: "private_gas" }] },
  251. // { key: "medical", label: "医疗", icon: "Stethoscope", scenes: [{ label: "体检", value: "medical_checkup" }, { label: "门诊报销", value: "outpatient" }, { label: "住院报销", value: "hospitalization" }] },
  252. // { key: "advertising", label: "电商广告充值", icon: "Monitor", scenes: [{ label: "平台推广", value: "platform_promo" }, { label: "品牌广告", value: "brand_ad" }, { label: "促销活动", value: "promotion" }] },
  253. { key: "DEFAULT", label: "默认", icon: "Box", scenes: [{ label: "通用", value: "common" }] },
  254. ];
  255. // 当前激活的分类
  256. const activeCategory = ref("DEFAULT");
  257. // 当前分类对应的场景列表
  258. const currentScenes = computed(() => {
  259. const category = categoryTabs.find(tab => tab.key === activeCategory.value);
  260. return category?.scenes || [{ label: "通用", value: "common" }];
  261. });
  262. // 场景值 - 默认选中第一个
  263. const sceneValue = ref(currentScenes.value[0]?.value || "");
  264. // 搜索名称
  265. const searchName = ref("");
  266. const { searchRef, contentRef, handleQueryClick, handleResetClick, refreshList } = useCrudList();
  267. const formRef = ref();
  268. const { pageLoading, loadingText, execute: loadingExecute } = useLoadingAction();
  269. const currentEnterpriseId = computed(() => enterpriseStore.getCurrentEnterprise?.enterprise_id);
  270. const contentCols = reactive<
  271. Array<{
  272. prop?: string;
  273. label?: string;
  274. show?: boolean;
  275. }>
  276. >([
  277. { prop: "selection", label: "选择框", show: false },
  278. { prop: "index", label: "序号", show: true },
  279. { prop: "institution_name", label: "制度名称", show: true },
  280. { prop: "valid_period", label: "制度有效期", show: true },
  281. { prop: "expense_type", label: "费用类型", show: true },
  282. { prop: "status", label: "制度状态", show: true },
  283. { prop: "updated_time", label: "修改时间", show: true },
  284. { prop: "operation", label: "操作", show: true },
  285. ]);
  286. const contentConfig = reactive<IContentConfig<InstitutionPageQuery>>({
  287. permPrefix: "module_payment:institution",
  288. pk: "institution_id",
  289. cols: contentCols as IContentConfig["cols"],
  290. hideColumnFilter: false,
  291. toolbar: [],
  292. defaultToolbar: ["refresh", "filter"],
  293. pagination: true,
  294. initialParams: computed(() => ({
  295. enterprise_id: currentEnterpriseId.value,
  296. })) as Record<string, unknown>,
  297. indexAction: async (params) => {
  298. const query: InstitutionPageQuery = {
  299. page_no: params.page_no,
  300. page_size: params.page_size,
  301. };
  302. if (params.enterprise_id) query.enterprise_id = params.enterprise_id;
  303. const res = await InstitutionAPI.listInstitution(query);
  304. return {
  305. list: res.data.data?.items || [],
  306. total: res.data.data?.total || 0,
  307. };
  308. },
  309. });
  310. const dialogVisible = reactive({
  311. title: "",
  312. visible: false,
  313. type: "create" as "create" | "update" | "detail",
  314. });
  315. const currentInstitutionId = ref<string>();
  316. const formKey = ref(0);
  317. const scopeDialogVisible = ref(false);
  318. function handleOpenDialog(type: "create" | "update" | "detail", institutionId?: string) {
  319. dialogVisible.type = type;
  320. currentInstitutionId.value = institutionId;
  321. if (type === "create") {
  322. dialogVisible.title = "创建费控制度";
  323. } else if (type === "update") {
  324. dialogVisible.title = "编辑费控制度";
  325. } else {
  326. dialogVisible.title = "费控制度详情";
  327. }
  328. if (type === "update") formKey.value++;
  329. dialogVisible.visible = true;
  330. }
  331. function handleOpenScopeDialog(institutionId?: string) {
  332. if (!institutionId) {
  333. ElMessage.warning("制度ID不存在");
  334. return;
  335. }
  336. currentInstitutionId.value = institutionId;
  337. scopeDialogVisible.value = true;
  338. }
  339. async function handleCloseDialog() {
  340. dialogVisible.visible = false;
  341. }
  342. function handleSubmit() {
  343. formRef.value?.submitForm();
  344. }
  345. function handleFormSuccess() {
  346. dialogVisible.visible = false;
  347. refreshList();
  348. }
  349. async function handleDelete(row: any) {
  350. const institutionId = row.institution_id;
  351. const enterpriseId = row.enterprise_id || currentEnterpriseId.value;
  352. if (!institutionId) return;
  353. if (!enterpriseId) {
  354. ElMessage.warning("企业ID不存在,无法删除");
  355. return;
  356. }
  357. await loadingExecute({
  358. confirmMessage: "确认删除该费控制度?",
  359. confirmTitle: "警告",
  360. confirmType: "warning",
  361. loadingText: "正在删除...",
  362. action: () => InstitutionAPI.deleteInstitution(institutionId, enterpriseId),
  363. onSuccess: () => {
  364. ElMessage.success("删除成功");
  365. refreshList();
  366. },
  367. });
  368. }
  369. async function handleToggleEffective(row: any) {
  370. const institutionId = row.institution_id;
  371. const enterpriseId = row.enterprise_id || currentEnterpriseId.value;
  372. if (!institutionId || !enterpriseId) {
  373. ElMessage.warning("制度ID或企业ID不存在");
  374. return;
  375. }
  376. const newEffective = row.effective === "1" ? "0" : "1";
  377. const actionText = newEffective === "1" ? "启用" : "停用";
  378. await loadingExecute({
  379. confirmMessage: `确认${actionText}该制度?`,
  380. confirmTitle: "提示",
  381. confirmType: "warning",
  382. loadingText: `正在${actionText}...`,
  383. action: () => InstitutionAPI.modifyEffective(institutionId, enterpriseId, newEffective),
  384. onSuccess: () => {
  385. ElMessage.success(`${actionText}成功`);
  386. refreshList();
  387. },
  388. });
  389. }
  390. function handleCategoryChange(categoryKey: string) {
  391. activeCategory.value = categoryKey;
  392. // 切换分类后,重置场景为当前分类的第一个场景
  393. const category = categoryTabs.find(tab => tab.key === categoryKey);
  394. sceneValue.value = category?.scenes?.[0]?.value || "";
  395. refreshList();
  396. }
  397. function handleSceneChange(sceneValueStr: string) {
  398. sceneValue.value = sceneValueStr;
  399. refreshList();
  400. }
  401. function handleSearch() {
  402. refreshList();
  403. }
  404. </script>
  405. <style scoped>
  406. .category-tabs {
  407. margin-bottom: 16px;
  408. padding: 16px;
  409. background: #fff;
  410. border-radius: 8px;
  411. }
  412. .category-tabs__nav {
  413. display: flex;
  414. gap: 8px;
  415. flex-wrap: wrap;
  416. }
  417. .search-bar {
  418. display: flex;
  419. justify-content: space-between;
  420. align-items: center;
  421. margin-bottom: 16px;
  422. padding: 16px;
  423. background: #fff;
  424. border-radius: 8px;
  425. }
  426. .search-bar__scene {
  427. display: flex;
  428. align-items: center;
  429. gap: 8px;
  430. }
  431. .search-bar__label {
  432. font-weight: 500;
  433. }
  434. .search-bar__right {
  435. display: flex;
  436. align-items: center;
  437. gap: 8px;
  438. }
  439. .search-bar__input {
  440. width: 200px;
  441. }
  442. .scene-card-group {
  443. display: flex;
  444. gap: 8px;
  445. flex-wrap: wrap;
  446. }
  447. .scene-card {
  448. padding: 6px 16px;
  449. border-radius: 4px;
  450. cursor: pointer;
  451. transition: all 0.3s ease;
  452. border: 1px solid #d9d9d9;
  453. background: #fff;
  454. color: #666;
  455. }
  456. .scene-card:hover {
  457. border-color: #409eff;
  458. color: #409eff;
  459. }
  460. .scene-card--active {
  461. background: #409eff;
  462. border-color: #409eff;
  463. color: #fff;
  464. }
  465. .scene-card--active:hover {
  466. background: #66b1ff;
  467. border-color: #66b1ff;
  468. color: #fff;
  469. }
  470. </style>