Prechádzať zdrojové kódy

feat: 更新APIKey,支持回调地址

gatsby 3 týždňov pred
rodič
commit
5c291f94eb

+ 18 - 11
frontend/src/views/module_payment/apikey/index.vue

@@ -89,6 +89,14 @@
                   min-width="180"
                   show-overflow-tooltip
                 />
+                <el-table-column
+                  v-if="contentCols.find((col) => col.prop === 'return_url')?.show"
+                  key="return_url"
+                  label="回调地址"
+                  prop="return_url"
+                  min-width="200"
+                  show-overflow-tooltip
+                />
                 <!-- <el-table-column
                   v-if="contentCols.find((col) => col.prop === 'last_used_at')?.show"
                   key="last_used_at"
@@ -109,14 +117,6 @@
                   min-width="150"
                   show-overflow-tooltip
                 />
-                <el-table-column
-                  v-if="contentCols.find((col) => col.prop === 'return_url')?.show"
-                  key="return_url"
-                  label="回调地址"
-                  prop="return_url"
-                  min-width="200"
-                  show-overflow-tooltip
-                />
                 <el-table-column
                   v-if="contentCols.find((col) => col.prop === 'created_time')?.show"
                   key="created_time"
@@ -776,7 +776,14 @@ Signature: {signature}</code></pre>
                 <h2>6. 回调通知</h2>
                 <h3>6.1 接口说明</h3>
                 <p>当转账状态发生变化时,系统会主动向商户配置的回调地址发送通知。</p>
-                <p>商户需要在 <strong>应用配置</strong> 页面设置回调地址。</p>
+                
+                <h4>回调地址配置</h4>
+                <p>系统按照以下优先级获取回调地址:</p>
+                <ol style="margin-left: 20px;">
+                  <li><strong>API Key 级别</strong>:在创建/编辑 API Key 时配置回调地址(优先级最高)</li>
+                  <li><strong>开放平台配置</strong>:在 <strong>应用配置</strong> 页面设置默认回调地址</li>
+                </ol>
+                <p style="color: #909399; margin-top: 8px;">说明:如果 API Key 已配置回调地址,则优先使用;否则使用开放平台配置中的回调地址。</p>
                 
                 <h4>通知方式</h4>
                 <ul>
@@ -1097,10 +1104,10 @@ const contentCols = reactive<
   { prop: "id", label: "ID", show: false },
   { prop: "api_key", label: "API Key", show: true },
   { prop: "status", label: "状态", show: true },
+  { prop: "return_url", label: "回调地址", show: true },
   { prop: "expired_at", label: "过期时间", show: true },
   { prop: "last_used_at", label: "最后使用时间", show: true },
   { prop: "description", label: "描述", show: true },
-  { prop: "return_url", label: "回调地址", show: true },
   { prop: "created_time", label: "创建时间", show: true },
   { prop: "operation", label: "操作", show: true },
 ]);
@@ -1236,7 +1243,7 @@ function handleConfirmApiKeyDetail() {
 }
 
 function downloadApiKeyCsv() {
-  const csvContent = `API Key,API Secret,状态,过期时间,创建时间,描述,回调地址\n"${apiKeyDetail.api_key}","${apiKeyDetail.api_secret}","${apiKeyDetail.status === '0' ? '正常' : '禁用'}","${apiKeyDetail.expired_at}","${apiKeyDetail.created_time}","${apiKeyDetail.description || ''}","${apiKeyDetail.return_url || ''}"`;
+  const csvContent = `API Key,API Secret,状态,过期时间,创建时间,描述\n"${apiKeyDetail.api_key}","${apiKeyDetail.api_secret}","${apiKeyDetail.status === '0' ? '正常' : '禁用'}","${apiKeyDetail.expired_at}","${apiKeyDetail.created_time}","${apiKeyDetail.description || ''}"`;
   const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
   const link = document.createElement('a');
   const url = URL.createObjectURL(blob);

+ 3 - 3
frontend/src/views/module_payment/employee/index.vue

@@ -1,7 +1,7 @@
 <template>
   <div v-loading="pageLoading" class="app-container" :element-loading-text="loadingText">
 
-    <el-tabs tab-position="left" style="height: 400px" class="employee-tabs">
+    <el-tabs type="card" style="height: 400px" class="employee-tabs">
       <el-tab-pane label="员工信息">
         <PageSearch ref="searchRef" :search-config="searchConfig" @query-click="handleQueryClick"
           @reset-click="handleResetClick" />
@@ -71,7 +71,7 @@
         </PageContent>
       </el-tab-pane>
 
-      <!-- <el-tab-pane label="部门信息">
+      <el-tab-pane label="部门信息">
         <PageSearch ref="deptSearchRef" :search-config="deptSearchConfig" @query-click="handleDeptQueryClick"
           @reset-click="handleDeptResetClick" />
 
@@ -140,7 +140,7 @@
             </div>
           </template>
         </PageContent>
-      </el-tab-pane> -->
+      </el-tab-pane>
     </el-tabs>
 
     <EnhancedDialog v-model="dialogVisible.visible" :title="dialogVisible.title" @close="handleCloseDialog">

+ 207 - 0
frontend/src/views/module_payment/institution/components/EmployeeSelector.vue

@@ -0,0 +1,207 @@
+<template>
+  <el-drawer
+    title="按员工选择"
+    :visible="visible"
+    :direction="'rtl'"
+    size="500px"
+    @close="handleClose"
+  >
+    <div class="employee-selector">
+      <div class="employee-selector__search">
+        <el-form :model="searchForm" inline>
+          <el-form-item label="部门">
+            <el-select v-model="searchForm.department_id" placeholder="选择部门" style="width: 120px">
+              <el-option value="" label="全部" />
+              <el-option
+                v-for="dept in departments"
+                :key="dept.value"
+                :label="dept.label"
+                :value="dept.value"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="姓名">
+            <el-input v-model="searchForm.name" placeholder="输入员工姓名" style="width: 120px" />
+          </el-form-item>
+          <el-form-item label="手机号">
+            <el-input v-model="searchForm.phone" placeholder="输入员工手机号" style="width: 120px" />
+          </el-form-item>
+          <el-form-item label="邮箱">
+            <el-input v-model="searchForm.email" placeholder="请输入邮箱" style="width: 120px" />
+          </el-form-item>
+        </el-form>
+        <div class="employee-selector__actions">
+          <el-button type="default" @click="handleReset">重置</el-button>
+          <el-button type="primary" @click="handleSearch">查询</el-button>
+        </div>
+      </div>
+
+      <div class="employee-selector__tabs">
+        <el-tabs v-model="activeTab" @tab-change="handleTabChange">
+          <el-tab-pane label="全部" name="all" />
+          <el-tab-pane label="已选 ({{ selectedEmployees.length }}人)" name="selected" />
+        </el-tabs>
+      </div>
+
+      <div class="employee-selector__table">
+        <el-table
+          :data="displayEmployees"
+          :show-header="true"
+          border
+          :max-height="300"
+        >
+          <el-table-column type="selection" width="50" />
+          <el-table-column label="员工姓名" prop="name" />
+          <el-table-column label="员工部门" prop="department" />
+          <el-table-column label="手机号" prop="phone" />
+          <el-table-column label="邮箱" prop="email" />
+        </el-table>
+      </div>
+
+      <div class="employee-selector__pagination">
+        <el-pagination
+          v-model:current-page="pagination.page_no"
+          v-model:page-size="pagination.page_size"
+          :total="pagination.total"
+          :page-sizes="[10, 20, 50]"
+          layout="prev, pager, next, jumper, ->, total, sizes"
+        />
+      </div>
+
+      <div class="employee-selector__footer">
+        <el-button @click="handleClose">取消</el-button>
+        <el-button type="primary" @click="handleConfirm">保存</el-button>
+      </div>
+    </div>
+  </el-drawer>
+</template>
+
+<script setup lang="ts">
+import { ref, computed } from "vue";
+
+interface Employee {
+  id: string;
+  name: string;
+  department: string;
+  phone: string;
+  email: string;
+}
+
+interface Props {
+  visible: boolean;
+  selectedIds: string[];
+}
+
+const props = defineProps<Props>();
+
+const emit = defineEmits<{
+  (e: "update:visible", value: boolean): void;
+  (e: "confirm", employees: Employee[]): void;
+}>();
+
+const searchForm = ref({
+  department_id: "",
+  name: "",
+  phone: "",
+  email: "",
+});
+
+const activeTab = ref("all");
+
+const pagination = ref({
+  page_no: 1,
+  page_size: 10,
+  total: 2,
+});
+
+const departments = ref([
+  { value: "dept1", label: "研发部" },
+  { value: "dept2", label: "产品部" },
+  { value: "dept3", label: "运营部" },
+]);
+
+const allEmployees = ref<Employee[]>([
+  { id: "emp1", name: "湖南钱程似锦技术服务有限公司", department: "湖南钱程似锦技术服务有限公司", phone: "-", email: "-" },
+  { id: "emp2", name: "湖南钱程似锦技术服务有限公司", department: "湖南钱程似锦技术服务有限公司", phone: "-", email: "-" },
+]);
+
+const selectedEmployees = computed(() => {
+  return allEmployees.value.filter(emp => props.selectedIds.includes(emp.id));
+});
+
+const displayEmployees = computed(() => {
+  if (activeTab.value === "selected") {
+    return selectedEmployees.value;
+  }
+  return allEmployees.value;
+});
+
+function handleClose() {
+  emit("update:visible", false);
+}
+
+function handleReset() {
+  searchForm.value = {
+    department_id: "",
+    name: "",
+    phone: "",
+    email: "",
+  };
+}
+
+function handleSearch() {
+  pagination.value.page_no = 1;
+}
+
+function handleTabChange() {
+  pagination.value.page_no = 1;
+}
+
+function handleConfirm() {
+  emit("confirm", selectedEmployees.value);
+  emit("update:visible", false);
+}
+</script>
+
+<style scoped>
+.employee-selector {
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+  height: 100%;
+}
+
+.employee-selector__search {
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
+}
+
+.employee-selector__actions {
+  display: flex;
+  gap: 8px;
+}
+
+.employee-selector__tabs {
+  background: #f5f7fa;
+  padding: 8px;
+}
+
+.employee-selector__table {
+  flex: 1;
+  overflow: hidden;
+}
+
+.employee-selector__pagination {
+  padding: 8px 0;
+  border-top: 1px solid #ebeef5;
+}
+
+.employee-selector__footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 8px;
+  padding-top: 16px;
+  border-top: 1px solid #ebeef5;
+}
+</style>

+ 376 - 81
frontend/src/views/module_payment/institution/components/InstitutionForm.vue

@@ -1,112 +1,200 @@
 <template>
   <div class="institution-form">
-    <el-form
-      ref="dataFormRef"
-      :model="formData"
-      :rules="rules"
-      label-suffix=":"
-      label-width="auto"
-      label-position="right"
-    >
+    <el-form ref="dataFormRef" :model="formData" :rules="rules" label-suffix=":" label-width="160px"
+      label-position="right">
       <el-row :gutter="20">
-        <el-col :span="12">
-          <el-form-item label="企业ID" prop="enterprise_id">
-            <el-input
-              v-model="formData.enterprise_id"
-              :placeholder="type === 'create' ? '企业ID自动填充' : '请输入企业ID'"
-              :maxlength="64"
-              :disabled="type === 'create'"
-              readonly
-            />
-          </el-form-item>
-        </el-col>
         <el-col :span="12">
           <el-form-item label="制度名称" prop="name">
-            <el-input v-model="formData.name" placeholder="请输入制度名称" :maxlength="128" />
+            <el-input v-model="formData.name" placeholder="最多50字" :maxlength="50" />
           </el-form-item>
         </el-col>
       </el-row>
 
       <el-row :gutter="20">
-        <el-col :span="12">
-          <el-form-item label="费用类型" prop="expense_type">
-            <el-select v-model="formData.expense_type" placeholder="请选择费用类型" style="width: 100%">
-              <el-option
-                v-for="item in EXPENSE_TYPE_OPTIONS"
-                :key="item.value"
-                :label="item.label"
-                :value="item.value"
-              />
-            </el-select>
+        <el-col :span="24">
+          <el-form-item label="制度生效时间" prop="effective_start_date">
+            <el-date-picker v-model="formData.effective_start_date" type="date" placeholder="开始时间" format="YYYY-MM-DD"
+              value-format="YYYY-MM-DD" />
           </el-form-item>
         </el-col>
-        <el-col :span="12">
-          <el-form-item label="是否启用" prop="effective">
-            <el-radio-group v-model="formData.effective">
-              <el-radio value="1">启用</el-radio>
-              <el-radio value="0">停用</el-radio>
-            </el-radio-group>
+        <el-col :span="24">
+          <el-form-item label="制度失效时间" prop="effective_end_date">
+            <div class="date-picker-wrapper">
+              <el-date-picker v-model="formData.effective_end_date" type="date"
+                :placeholder="formData.is_long_term ? '长期有效' : '请选择日期'" format="YYYY-MM-DD" value-format="YYYY-MM-DD"
+                :disabled="formData.is_long_term" />
+              <el-checkbox v-model="formData.is_long_term" label="长期有效" class="long-term-checkbox" />
+            </div>
           </el-form-item>
         </el-col>
       </el-row>
 
       <el-row :gutter="20">
-        <el-col :span="12">
-          <el-form-item label="生效开始时间" prop="effective_start_date">
-            <el-date-picker
-              v-model="formData.effective_start_date"
-              type="datetime"
-              placeholder="请选择生效开始时间"
-              format="YYYY-MM-DD HH:mm:ss"
-              value-format="YYYY-MM-DD HH:mm:ss"
-              style="width: 100%"
-            />
-          </el-form-item>
-        </el-col>
-        <el-col :span="12">
-          <el-form-item label="生效结束时间" prop="effective_end_date">
-            <el-date-picker
-              v-model="formData.effective_end_date"
-              type="datetime"
-              placeholder="请选择生效结束时间"
-              format="YYYY-MM-DD HH:mm:ss"
-              value-format="YYYY-MM-DD HH:mm:ss"
-              style="width: 100%"
-            />
+        <el-col :span="24">
+          <el-form-item label="额度发放" prop="grant_mode">
+            <el-radio-group v-model="formData.grant_mode">
+              <el-radio value="period">按固定周期发放</el-radio>
+              <el-radio value="manual">按手工发放</el-radio>
+            </el-radio-group>
           </el-form-item>
         </el-col>
       </el-row>
 
-      <el-row :gutter="20">
+      <el-row v-if="formData.grant_mode === 'period'" :gutter="20" class="period-config">
+        <el-col :span="24">
+          <el-form-item label="" prop="period_type">
+            <el-radio-group v-model="formData.period_type">
+              <el-radio value="daily">每日额度</el-radio>
+              <el-radio value="monthly">每月额度</el-radio>
+              <el-radio value="weekly">每周额度</el-radio>
+              <el-radio value="quarterly">每季额度</el-radio>
+              <el-radio value="yearly">每年额度</el-radio>
+            </el-radio-group>
+          </el-form-item>
+        </el-col>
         <el-col :span="12">
-          <el-form-item label="费控咨询模式" prop="consult_mode">
-            <el-select v-model="formData.consult_mode" placeholder="请选择咨询模式" style="width: 100%">
-              <el-option
-                v-for="item in CONSULT_MODE_OPTIONS"
-                :key="item.value"
-                :label="item.label"
-                :value="item.value"
-              />
-            </el-select>
+          <el-form-item label="" prop="amount">
+            <el-input-number v-model="formData.amount" controls-position="right" placeholder="输入金额" :min="0"
+              :precision="2">
+              <template #suffix>
+                <span>元</span>
+              </template>
+            </el-input-number>
           </el-form-item>
         </el-col>
+
+        <el-col :span="24" class="period-config__collapse">
+          <el-button type="text" @click="togglePeriodCollapse">
+            {{ isPeriodCollapsed ? "更多设置" : "收起" }}
+          </el-button>
+        </el-col>
+
+        <div v-show="!isPeriodCollapsed">
+          <el-col :span="24">
+            <el-form-item label="有效时间" prop="effective_time_type">
+              <el-radio-group v-model="formData.effective_time_type">
+                <el-radio value="unlimited">不限制</el-radio>
+                <el-radio value="workday">工作日/节假日</el-radio>
+                <!-- <el-radio value="custom">自定义限制</el-radio> -->
+              </el-radio-group>
+            </el-form-item>
+          </el-col>
+
+          <template v-if="formData.effective_time_type === 'workday'">
+            <el-col :span="24">
+              <el-form-item>
+                <div class="workday-config">
+                  <div class="workday-config__item">
+                    <span class="workday-config__label">工作日</span>
+                    <el-time-picker v-model="formData.workday_start_time" type="time" placeholder="请选择时间" format="HH:mm"
+                      value-format="HH:mm" />
+                    <span class="workday-config__separator">-</span>
+                    <el-select v-model="formData.workday_end_type" placeholder="选择" style="width: 80px;">
+                      <el-option label="当日" value="same_day" />
+                      <el-option label="次日" value="next_day" />
+                    </el-select>
+                    <el-time-picker v-model="formData.workday_end_time" type="time" placeholder="请选择时间" format="HH:mm"
+                      value-format="HH:mm" />
+                  </div>
+
+                  <div class="workday-config__item">
+                    <span class="workday-config__label">节假日</span>
+                    <el-time-picker v-model="formData.holiday_start_time" type="time" placeholder="请选择时间" format="HH:mm"
+                      value-format="HH:mm" />
+                    <span class="workday-config__separator">-</span>
+                    <el-select v-model="formData.holiday_end_type" placeholder="选择" style="width: 80px;">
+                      <el-option label="当日" value="same_day" />
+                      <el-option label="次日" value="next_day" />
+                    </el-select>
+                    <el-time-picker v-model="formData.holiday_end_time" type="time" placeholder="请选择时间" format="HH:mm"
+                      value-format="HH:mm" />
+                  </div>
+                </div>
+              </el-form-item>
+            </el-col>
+          </template>
+
+          <el-col :span="24">
+            <el-form-item label="单次限额" prop="single_limit">
+              <el-input-number v-model="formData.single_limit" controls-position="right" placeholder="输入金额" :min="0"
+                :precision="2">
+                <template #suffix>
+                  <span>元</span>
+                </template>
+              </el-input-number>
+            </el-form-item>
+          </el-col>
+        </div>
       </el-row>
 
       <el-row :gutter="20">
         <el-col :span="24">
-          <el-form-item label="制度描述" prop="institution_desc">
-            <el-input
-              v-model="formData.institution_desc"
-              type="textarea"
-              :rows="3"
-              :maxlength="200"
-              show-word-limit
-              placeholder="请输入制度描述"
-            />
-          </el-form-item>
+          <el-row :gutter="20">
+            <el-col :span="24">
+              <el-form-item label="适用员工" prop="applicable_scope">
+                <el-radio-group v-model="formData.applicable_scope">
+                  <el-radio value="department">按部门</el-radio>
+                  <el-radio value="employee">按员工</el-radio>
+                  <el-radio value="all">全体员工</el-radio>
+                  <!-- <el-radio value="tag">按员工标签</el-radio> -->
+                  <el-radio value="none">暂不设置</el-radio>
+                </el-radio-group>
+              </el-form-item>
+            </el-col>
+
+            <el-col v-if="formData.applicable_scope === 'department'" :span="12">
+              <el-form-item label="选择部门" prop="department_id">
+                <el-select v-model="formData.department_id" placeholder="选择部门" style="width: 100%">
+                  <el-option v-for="dept in departmentOptions" :key="dept.value" :label="dept.label"
+                    :value="dept.value" />
+                </el-select>
+                <div class="department-tip">
+                  按部门选择后,制度将在所选部门内所有员工生效,若部门中变更员工,将直接适用这条制度,需谨慎选择
+                </div>
+              </el-form-item>
+            </el-col>
+
+            <el-col v-if="formData.applicable_scope === 'employee'" :span="12">
+              <el-form-item label="选择员工" prop="employee_ids">
+                <div>
+                  <el-button type="primary" plain @click="showEmployeeSelector = true">
+                    选择员工
+                  </el-button>
+                  <div v-if="selectedEmployeeNames.length > 0" class="selected-employees">
+                    已选: {{ selectedEmployeeNames.join(", ") }}
+                  </div>
+                  <div v-else class="selected-employees warning">
+                    请完成适用员工选择
+                  </div>
+                </div>
+              </el-form-item>
+            </el-col>
+
+            <el-col v-if="formData.applicable_scope === 'tag'" :span="12">
+              <el-form-item label="选择标签" prop="tag_ids">
+                <el-select v-model="formData.tag_ids" placeholder="选择员工标签" multiple style="width: 100%">
+                  <el-option v-for="tag in tagOptions" :key="tag.value" :label="tag.label" :value="tag.value" />
+                </el-select>
+              </el-form-item>
+            </el-col>
+          </el-row>
+
+          <el-row :gutter="20">
+            <el-col :span="24">
+              <el-form-item label="是否启用" prop="effective">
+                <el-radio-group v-model="formData.effective">
+                  <el-radio value="1">启用</el-radio>
+                  <el-radio value="0">停用</el-radio>
+                </el-radio-group>
+              </el-form-item>
+            </el-col>
+          </el-row>
         </el-col>
       </el-row>
     </el-form>
+
+    <EmployeeSelector v-model:visible="showEmployeeSelector" :selected-ids="formData.employee_ids"
+      @confirm="handleEmployeeConfirm" />
   </div>
 </template>
 
@@ -118,7 +206,8 @@ import {
   InstitutionDetail,
 } from "@/api/module_payment/institution";
 import InstitutionAPI from "@/api/module_payment/institution";
-import { reactive, ref, watch } from "vue";
+import { reactive, ref, watch, computed } from "vue";
+import EmployeeSelector from "./EmployeeSelector.vue";
 
 interface Props {
   type: "create" | "update";
@@ -143,12 +232,68 @@ const initialFormData = {
   effective_start_date: undefined,
   effective_end_date: undefined,
   consult_mode: "0",
+  grant_mode: "period",
+  applicable_scope: "none",
+  is_long_term: false,
+  period_type: "monthly",
+  amount: undefined,
+  effective_time_type: "unlimited",
+  single_limit: undefined,
+  workday_start_time: undefined,
+  workday_end_type: "same_day",
+  workday_end_time: undefined,
+  holiday_start_time: undefined,
+  holiday_end_type: "same_day",
+  holiday_end_time: undefined,
+  department_id: undefined,
+  employee_ids: [] as string[],
+  tag_ids: [] as string[],
 };
 
 const formData = reactive<InstitutionForm & typeof initialFormData>(
   JSON.parse(JSON.stringify(initialFormData))
 );
 
+const expandMore = ref<string[]>([]);
+const isPeriodCollapsed = ref(false);
+
+function togglePeriodCollapse() {
+  isPeriodCollapsed.value = !isPeriodCollapsed.value;
+}
+
+const departmentOptions = ref([
+  { value: "dept1", label: "研发部" },
+  { value: "dept2", label: "产品部" },
+  { value: "dept3", label: "运营部" },
+  { value: "dept4", label: "财务部" },
+]);
+
+const employeeOptions = ref([
+  { value: "emp1", label: "张三" },
+  { value: "emp2", label: "李四" },
+  { value: "emp3", label: "王五" },
+  { value: "emp4", label: "赵六" },
+]);
+
+const tagOptions = ref([
+  { value: "tag1", label: "管理层" },
+  { value: "tag2", label: "技术骨干" },
+  { value: "tag3", label: "新员工" },
+]);
+
+const showEmployeeSelector = ref(false);
+
+const selectedEmployeeNames = computed(() => {
+  return formData.employee_ids.map(id => {
+    const emp = employeeOptions.value.find(e => e.value === id);
+    return emp ? emp.label : id;
+  });
+});
+
+function handleEmployeeConfirm(employees: any[]) {
+  formData.employee_ids = employees.map((e: any) => e.id);
+}
+
 watch(
   () => props.institutionId,
   async (newVal) => {
@@ -184,6 +329,51 @@ const rules = reactive({
   enterprise_id: [{ required: true, message: "请输入企业ID", trigger: "blur" }],
   name: [{ required: true, message: "请输入制度名称", trigger: "blur" }],
   expense_type: [{ required: true, message: "请选择费用类型", trigger: "change" }],
+  effective_start_date: [{ required: true, message: "请选择制度生效时间", trigger: "change" }],
+  effective_end_date: [{
+    required: true,
+    message: "请选择制度失效时间",
+    trigger: "change",
+    validator: (rule: any, value: any, callback: any) => {
+      if (formData.is_long_term) {
+        callback();
+      } else if (!value) {
+        callback(new Error("请选择制度失效时间"));
+      } else {
+        callback();
+      }
+    }
+  }],
+  grant_mode: [{ required: true, message: "请选择额度发放方式", trigger: "change" }],
+  period_type: [{
+    required: true,
+    message: "请选择周期类型",
+    trigger: "change",
+    validator: (rule: any, value: any, callback: any) => {
+      if (formData.grant_mode !== "period") {
+        callback();
+      } else if (!value) {
+        callback(new Error("请选择周期类型"));
+      } else {
+        callback();
+      }
+    }
+  }],
+  amount: [{
+    required: true,
+    message: "请输入额度金额",
+    trigger: "blur",
+    validator: (rule: any, value: any, callback: any) => {
+      if (formData.grant_mode !== "period") {
+        callback();
+      } else if (!value || value <= 0) {
+        callback(new Error("请输入有效的额度金额"));
+      } else {
+        callback();
+      }
+    }
+  }],
+  applicable_scope: [{ required: true, message: "请选择适用员工范围", trigger: "change" }],
 });
 
 async function submitForm() {
@@ -198,10 +388,28 @@ async function submitForm() {
       effective: formData.effective,
       institution_desc: formData.institution_desc || undefined,
       effective_start_date: formData.effective_start_date || undefined,
-      effective_end_date: formData.effective_end_date || undefined,
+      effective_end_date: formData.is_long_term ? undefined : (formData.effective_end_date || undefined),
       consult_mode: formData.consult_mode || undefined,
+      grant_mode: formData.grant_mode,
+      applicable_scope: formData.applicable_scope,
     };
 
+    if (formData.grant_mode === "period") {
+      submitData.period_type = formData.period_type;
+      submitData.amount = formData.amount;
+      submitData.effective_time_type = formData.effective_time_type;
+      submitData.single_limit = formData.single_limit || undefined;
+
+      if (formData.effective_time_type === "workday") {
+        submitData.workday_start_time = formData.workday_start_time;
+        submitData.workday_end_type = formData.workday_end_type;
+        submitData.workday_end_time = formData.workday_end_time;
+        submitData.holiday_start_time = formData.holiday_start_time;
+        submitData.holiday_end_type = formData.holiday_end_type;
+        submitData.holiday_end_time = formData.holiday_end_time;
+      }
+    }
+
     if (props.type === "create") {
       await InstitutionAPI.createInstitution(submitData);
     } else if (props.type === "update" && props.institutionId) {
@@ -222,4 +430,91 @@ defineExpose({
   submitForm,
   resetForm,
 });
-</script>
+</script>
+
+<style scoped>
+.date-picker-wrapper {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+}
+
+.long-term-checkbox {
+  flex: 1;
+}
+
+.el-collapse-item__header {
+  padding-left: 0 !important;
+  color: #409eff;
+}
+
+.el-collapse-item__header:hover {
+  color: #66b1ff;
+}
+
+.period-config {
+  padding: 16px;
+  background: #fafafa;
+  margin-bottom: 16px;
+  border-radius: 4px;
+}
+
+.period-config__collapse {
+  text-align: left;
+  padding-top: 8px;
+}
+
+.workday-config {
+  padding: 16px;
+  background: #fff;
+  border-radius: 4px;
+  margin-top: 8px;
+}
+
+.workday-config__item {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  margin-bottom: 12px;
+}
+
+.workday-config__item:last-child {
+  margin-bottom: 0;
+}
+
+.workday-config__label {
+  width: 60px;
+  font-weight: 500;
+}
+
+.workday-config__separator {
+  color: #999;
+}
+
+.department-tip {
+  margin-top: 8px;
+  padding: 8px 12px;
+  background: #fffbe6;
+  border: 1px solid #ffe58f;
+  border-radius: 4px;
+  font-size: 12px;
+  color: #d48806;
+  line-height: 1.5;
+}
+
+.selected-employees {
+  margin-top: 8px;
+  padding: 6px 12px;
+  background: #f0f5ff;
+  border: 1px solid #b3d1ff;
+  border-radius: 4px;
+  font-size: 12px;
+  color: #1e88e5;
+}
+
+.selected-employees.warning {
+  background: #fff2f0;
+  border-color: #ffccc7;
+  color: #ff4d4f;
+}
+</style>

+ 65 - 31
frontend/src/views/module_payment/institution/index.vue

@@ -18,17 +18,19 @@
     <!-- 搜索栏和场景选择 -->
     <div class="search-bar">
       <div class="search-bar__scene">
-        <span class="search-bar__label">场景:</span>
-        <el-radio-group v-model="sceneValue" class="scene-radio-group">
-          <el-radio
-            v-for="scene in currentScenes"
-            :key="scene.value"
-            :label="scene.value"
-          >
-            {{ scene.label }}
-          </el-radio>
-        </el-radio-group>
-      </div>
+          <span class="search-bar__label">场景:</span>
+          <div class="scene-card-group">
+            <el-tag
+              v-for="scene in currentScenes"
+              :key="scene.value"
+              :class="{ 'scene-card--active': sceneValue === scene.value }"
+              class="scene-card"
+              @click="handleSceneChange(scene.value)"
+            >
+              {{ scene.label }}
+            </el-tag>
+          </div>
+        </div>
       <div class="search-bar__right">
         <el-input
           v-model="searchName"
@@ -219,20 +221,20 @@ const route = useRoute();
 
 // 分类标签与对应场景映射
 const categoryTabs = [
-  { key: "meal", label: "餐饮", icon: "UtensilsCrossed", scenes: [{ label: "差旅餐饮", value: "business_meal" }, { label: "员工餐补", value: "staff_meal" }, { label: "团建聚餐", value: "team_dinner" }] },
-  { key: "hotel", label: "酒店", icon: "Building2", scenes: [{ label: "商务出差", value: "business_trip" }, { label: "会议住宿", value: "meeting_hotel" }, { label: "培训住宿", value: "training_hotel" }] },
-  { key: "flight", label: "机票", icon: "Plane", scenes: [{ label: "国内出差", value: "domestic_flight" }, { label: "国际出差", value: "international_flight" }, { label: "紧急出差", value: "urgent_flight" }] },
-  { key: "train", label: "火车票", icon: "Train", scenes: [{ label: "省内出差", value: "provincial_train" }, { label: "跨省出差", value: "interprovincial_train" }, { label: "通勤", value: "commute_train" }] },
-  { key: "bus", label: "公交", icon: "Bus", scenes: [{ label: "市内通勤", value: "city_bus" }, { label: "郊区出行", value: "suburb_bus" }] },
-  { key: "subway", label: "地铁", icon: "Metro", scenes: [{ label: "日常通勤", value: "daily_subway" }, { label: "加班补贴", value: "overtime_subway" }] },
-  { key: "car", label: "用车", icon: "Car", scenes: [{ label: "公务用车", value: "official_car" }, { label: "网约车", value: "ride_hailing" }, { label: "自驾补贴", value: "self_drive" }] },
-  { key: "service", label: "服务", icon: "Headphones", scenes: [{ label: "咨询服务", value: "consult_service" }, { label: "外包服务", value: "outsourcing" }] },
-  { key: "shopping", label: "商城", icon: "ShoppingCart", scenes: [{ label: "办公用品", value: "office_supplies" }, { label: "劳保用品", value: "labor_protection" }, { label: "员工福利", value: "employee_welfare" }] },
-  { key: "express", label: "快递", icon: "Truck", scenes: [{ label: "日常快递", value: "daily_express" }, { label: "大件物流", value: "bulk_logistics" }] },
-  { key: "gas", label: "加油", icon: "Fuel", scenes: [{ label: "公务车加油", value: "official_gas" }, { label: "私家车补贴", value: "private_gas" }] },
-  { key: "medical", label: "医疗", icon: "Stethoscope", scenes: [{ label: "体检", value: "medical_checkup" }, { label: "门诊报销", value: "outpatient" }, { label: "住院报销", value: "hospitalization" }] },
-  { key: "default", label: "默认", icon: "FileText", scenes: [{ label: "通用", value: "common" }, { label: "其他", value: "other" }] },
-  { key: "advertising", label: "电商广告充值", icon: "Monitor", scenes: [{ label: "平台推广", value: "platform_promo" }, { label: "品牌广告", value: "brand_ad" }, { label: "促销活动", value: "promotion" }] },
+  // { key: "meal", label: "餐饮", icon: "UtensilsCrossed", scenes: [{ label: "差旅餐饮", value: "business_meal" }, { label: "员工餐补", value: "staff_meal" }, { label: "团建聚餐", value: "team_dinner" }] },
+  // { key: "hotel", label: "酒店", icon: "Building2", scenes: [{ label: "商务出差", value: "business_trip" }, { label: "会议住宿", value: "meeting_hotel" }, { label: "培训住宿", value: "training_hotel" }] },
+  // { key: "flight", label: "机票", icon: "Plane", scenes: [{ label: "国内出差", value: "domestic_flight" }, { label: "国际出差", value: "international_flight" }, { label: "紧急出差", value: "urgent_flight" }] },
+  // { key: "train", label: "火车票", icon: "Train", scenes: [{ label: "省内出差", value: "provincial_train" }, { label: "跨省出差", value: "interprovincial_train" }, { label: "通勤", value: "commute_train" }] },
+  // { key: "bus", label: "公交", icon: "Bus", scenes: [{ label: "市内通勤", value: "city_bus" }, { label: "郊区出行", value: "suburb_bus" }] },
+  // { key: "subway", label: "地铁", icon: "Metro", scenes: [{ label: "日常通勤", value: "daily_subway" }, { label: "加班补贴", value: "overtime_subway" }] },
+  // { key: "car", label: "用车", icon: "Car", scenes: [{ label: "公务用车", value: "official_car" }, { label: "网约车", value: "ride_hailing" }, { label: "自驾补贴", value: "self_drive" }] },
+  // { key: "service", label: "服务", icon: "Headphones", scenes: [{ label: "咨询服务", value: "consult_service" }, { label: "外包服务", value: "outsourcing" }] },
+  // { key: "shopping", label: "商城", icon: "ShoppingCart", scenes: [{ label: "办公用品", value: "office_supplies" }, { label: "劳保用品", value: "labor_protection" }, { label: "员工福利", value: "employee_welfare" }] },
+  // { key: "express", label: "快递", icon: "Truck", scenes: [{ label: "日常快递", value: "daily_express" }, { label: "大件物流", value: "bulk_logistics" }] },
+  // { key: "gas", label: "加油", icon: "Fuel", scenes: [{ label: "公务车加油", value: "official_gas" }, { label: "私家车补贴", value: "private_gas" }] },
+  // { key: "medical", label: "医疗", icon: "Stethoscope", scenes: [{ label: "体检", value: "medical_checkup" }, { label: "门诊报销", value: "outpatient" }, { label: "住院报销", value: "hospitalization" }] },
+  // { key: "advertising", label: "电商广告充值", icon: "Monitor", scenes: [{ label: "平台推广", value: "platform_promo" }, { label: "品牌广告", value: "brand_ad" }, { label: "促销活动", value: "promotion" }] },
+  { key: "default", label: "默认", icon: "Box", scenes: [{ label: "通用", value: "common" }] },
 ];
 
 // 当前激活的分类
@@ -244,14 +246,13 @@ const currentScenes = computed(() => {
   return category?.scenes || [{ label: "通用", value: "common" }];
 });
 
-// 场景值
-const sceneValue = ref("");
+// 场景值 - 默认选中第一个
+const sceneValue = ref(currentScenes.value[0]?.value || "");
 
 // 搜索名称
 const searchName = ref("");
 
-const { searchRef, contentRef, handleQueryClick, handleResetClick, refreshList } =
-  useCrudList();
+const { searchRef, contentRef, handleQueryClick, handleResetClick, refreshList } = useCrudList();
 const formRef = ref();
 
 const { pageLoading, loadingText, execute: loadingExecute } = useLoadingAction();
@@ -368,6 +369,11 @@ function handleCategoryChange(categoryKey: string) {
   refreshList();
 }
 
+function handleSceneChange(sceneValueStr: string) {
+  sceneValue.value = sceneValueStr;
+  refreshList();
+}
+
 function handleSearch() {
   refreshList();
 }
@@ -429,8 +435,36 @@ function handleMore(row: any) {
   width: 200px;
 }
 
-.scene-radio-group {
+.scene-card-group {
   display: flex;
-  gap: 16px;
+  gap: 8px;
+  flex-wrap: wrap;
+}
+
+.scene-card {
+  padding: 6px 16px;
+  border-radius: 4px;
+  cursor: pointer;
+  transition: all 0.3s ease;
+  border: 1px solid #d9d9d9;
+  background: #fff;
+  color: #666;
+}
+
+.scene-card:hover {
+  border-color: #409eff;
+  color: #409eff;
+}
+
+.scene-card--active {
+  background: #409eff;
+  border-color: #409eff;
+  color: #fff;
+}
+
+.scene-card--active:hover {
+  background: #66b1ff;
+  border-color: #66b1ff;
+  color: #fff;
 }
 </style>