From 37f52097847f9025589c79034be2454432c55433 Mon Sep 17 00:00:00 2001
From: xyc <jc_xiong@hotmail.com>
Date: 星期五, 02 十月 2026 10:12:27 +0800
Subject: [PATCH] docs(交接): 记录关机前开发服务已停止

---
 traffic-audit-web/src/views/SystemConfig.vue |  295 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 295 insertions(+), 0 deletions(-)

diff --git a/traffic-audit-web/src/views/SystemConfig.vue b/traffic-audit-web/src/views/SystemConfig.vue
new file mode 100644
index 0000000..f9df604
--- /dev/null
+++ b/traffic-audit-web/src/views/SystemConfig.vue
@@ -0,0 +1,295 @@
+<template>
+  <el-container direction="vertical">
+    <app-header title="浜ら�氱粺璁℃姤琛ㄥ鏍哥郴缁� - 绯荤粺閰嶇疆"></app-header>
+    <el-container>
+      <el-aside width="200px">
+        <el-menu ref="sideMenu" router :default-active="$route.path" @select="onMenuSelect">
+          <el-menu-item index="/dashboard">宸ヤ綔鍙�</el-menu-item>
+          <el-menu-item index="/import">鏁版嵁瀵煎叆</el-menu-item>
+          <el-menu-item index="/data">鏁版嵁鏌ョ湅</el-menu-item>
+          <el-menu-item index="/explain">浼佷笟瑙i噴</el-menu-item>
+          <el-menu-item index="/audit">瀹℃牳缁撴灉</el-menu-item>
+          <el-menu-item index="/report">鎶ヨ〃鐢熸垚</el-menu-item>
+          <el-submenu index="/special-stats">
+            <template slot="title">涓撻」缁熻</template>
+            <el-menu-item index="/holiday-stat">鑺傚亣鏃�</el-menu-item>
+          </el-submenu>
+          <el-menu-item index="/rules">瑙勫垯绠$悊</el-menu-item>
+          <el-menu-item index="/users">鐢ㄦ埛绠$悊</el-menu-item>
+          <el-menu-item v-if="operatorIsAdmin" index="/system-config">绯荤粺閰嶇疆</el-menu-item>
+        </el-menu>
+      </el-aside>
+      <el-main>
+        <div class="page-scroll">
+          <h3>绯荤粺閰嶇疆</h3>
+          <el-tabs v-model="activeTab" @tab-click="loadActiveTab">
+            <el-tab-pane label="鑺傚亣鏃ョ鐞�" name="holidays">
+              <div class="toolbar">
+                <el-select v-model="holidayQuery.year" clearable placeholder="骞翠唤" style="width:130px" @change="loadHolidays">
+                  <el-option v-for="year in years" :key="year" :label="year + '骞�'" :value="year"></el-option>
+                </el-select>
+                <el-select v-model="holidayQuery.holidayType" clearable placeholder="鑺傚亣鏃ュ垎绫�" style="width:150px" @change="loadHolidays">
+                  <el-option v-for="item in holidayTypeOptions" :key="item.value" :label="item.label" :value="item.value"></el-option>
+                </el-select>
+                <el-button type="primary" icon="el-icon-search" @click="loadHolidays">鏌ヨ</el-button>
+                <el-button type="success" icon="el-icon-plus" @click="openHolidayDialog()">鏂板鑺傚亣鏃�</el-button>
+              </div>
+              <el-table :data="holidays" border stripe size="small" v-loading="loadingHolidays" style="width:100%">
+                <el-table-column prop="year" label="骞翠唤" width="100" align="center"></el-table-column>
+                <el-table-column label="鑺傚亣鏃ュ垎绫�" width="140">
+                  <template slot-scope="scope">{{ labelOf(holidayTypeOptions, scope.row.holidayType) }}</template>
+                </el-table-column>
+                <el-table-column prop="holidayDate" label="鏃ユ湡" width="150" align="center"></el-table-column>
+                <el-table-column prop="dayIndex" label="绗嚑鏃�" width="100" align="center"></el-table-column>
+                <el-table-column label="鎿嶄綔" width="150" align="center">
+                  <template slot-scope="scope">
+                    <el-button type="text" size="mini" @click="openHolidayDialog(scope.row)">缂栬緫</el-button>
+                    <el-button type="text" size="mini" class="danger-text" @click="removeHoliday(scope.row)">鍒犻櫎</el-button>
+                  </template>
+                </el-table-column>
+              </el-table>
+            </el-tab-pane>
+
+            <el-tab-pane label="瀹㈣繍绯绘暟绠$悊" name="coefficients">
+              <div class="toolbar">
+                <el-select v-model="coefficientQuery.year" clearable placeholder="骞翠唤" style="width:130px" @change="loadCoefficients">
+                  <el-option v-for="year in years" :key="year" :label="year + '骞�'" :value="year"></el-option>
+                </el-select>
+                <el-button type="primary" icon="el-icon-search" @click="loadCoefficients">鏌ヨ</el-button>
+                <el-button type="success" icon="el-icon-plus" @click="openCoefficientDialog()">鏂板瀹㈣繍绯绘暟</el-button>
+              </div>
+              <el-table :data="coefficients" border stripe size="small" v-loading="loadingCoefficients" style="width:100%">
+                <el-table-column prop="year" label="骞翠唤" width="120" align="center"></el-table-column>
+                <el-table-column label="鏃ユ湡鍒嗙被" width="160">
+                  <template slot-scope="scope">{{ labelOf(dateCategoryOptions, scope.row.dateCategory) }}</template>
+                </el-table-column>
+                <el-table-column label="瀹㈣繍绯绘暟" width="160" align="center">
+                  <template slot-scope="scope">{{ formatCoefficient(scope.row.coefficient) }}</template>
+                </el-table-column>
+                <el-table-column label="鎿嶄綔" width="150" align="center">
+                  <template slot-scope="scope">
+                    <el-button type="text" size="mini" @click="openCoefficientDialog(scope.row)">缂栬緫</el-button>
+                    <el-button type="text" size="mini" class="danger-text" @click="removeCoefficient(scope.row)">鍒犻櫎</el-button>
+                  </template>
+                </el-table-column>
+              </el-table>
+            </el-tab-pane>
+          </el-tabs>
+        </div>
+
+        <el-dialog :title="holidayForm.id ? '缂栬緫鑺傚亣鏃�' : '鏂板鑺傚亣鏃�'" :visible.sync="holidayDialog" width="520px" v-dialog-drag>
+          <el-form ref="holidayForm" :model="holidayForm" :rules="holidayRules" label-width="110px">
+            <el-form-item label="骞翠唤" prop="year">
+              <el-select v-model="holidayForm.year" style="width:100%" @change="onHolidayYearChange">
+                <el-option v-for="year in years" :key="year" :label="year + '骞�'" :value="year"></el-option>
+              </el-select>
+            </el-form-item>
+            <el-form-item label="鑺傚亣鏃ュ垎绫�" prop="holidayType">
+              <el-select v-model="holidayForm.holidayType" style="width:100%">
+                <el-option v-for="item in holidayTypeOptions" :key="item.value" :label="item.label" :value="item.value"></el-option>
+              </el-select>
+            </el-form-item>
+            <el-form-item label="鏃ユ湡" prop="holidayDate">
+              <el-date-picker v-model="holidayForm.holidayDate" type="date" value-format="yyyy-MM-dd"
+                              style="width:100%" placeholder="閫夋嫨鏃ユ湡"></el-date-picker>
+            </el-form-item>
+            <el-form-item label="绗嚑鏃�" prop="dayIndex">
+              <el-input-number v-model="holidayForm.dayIndex" :min="1" :max="31" style="width:100%"></el-input-number>
+            </el-form-item>
+          </el-form>
+          <span slot="footer">
+            <el-button @click="holidayDialog = false">鍙栨秷</el-button>
+            <el-button type="primary" :loading="saving" @click="saveHoliday">淇濆瓨</el-button>
+          </span>
+        </el-dialog>
+
+        <el-dialog :title="coefficientForm.id ? '缂栬緫瀹㈣繍绯绘暟' : '鏂板瀹㈣繍绯绘暟'" :visible.sync="coefficientDialog" width="520px" v-dialog-drag>
+          <el-form ref="coefficientForm" :model="coefficientForm" :rules="coefficientRules" label-width="110px">
+            <el-form-item label="骞翠唤" prop="year">
+              <el-select v-model="coefficientForm.year" style="width:100%">
+                <el-option v-for="year in years" :key="year" :label="year + '骞�'" :value="year"></el-option>
+              </el-select>
+            </el-form-item>
+            <el-form-item label="鏃ユ湡鍒嗙被" prop="dateCategory">
+              <el-select v-model="coefficientForm.dateCategory" style="width:100%">
+                <el-option v-for="item in dateCategoryOptions" :key="item.value" :label="item.label" :value="item.value"></el-option>
+              </el-select>
+            </el-form-item>
+            <el-form-item label="瀹㈣繍绯绘暟" prop="coefficient">
+              <el-input-number v-model="coefficientForm.coefficient" :min="0.01" :precision="4" :step="0.1" style="width:100%"></el-input-number>
+            </el-form-item>
+          </el-form>
+          <span slot="footer">
+            <el-button @click="coefficientDialog = false">鍙栨秷</el-button>
+            <el-button type="primary" :loading="saving" @click="saveCoefficient">淇濆瓨</el-button>
+          </span>
+        </el-dialog>
+      </el-main>
+    </el-container>
+  </el-container>
+</template>
+
+<script>
+import api from '@/api'
+import AppHeader from '@/components/AppHeader.vue'
+
+export default {
+  components: { AppHeader },
+  name: 'SystemConfig',
+  data() {
+    return {
+      activeTab: 'holidays',
+      years: [2024, 2025, 2026, 2027, 2028, 2029, 2030],
+      holidayTypeOptions: [],
+      dateCategoryOptions: [],
+      holidayQuery: { year: 2026, holidayType: 'NATIONAL_DAY' },
+      coefficientQuery: { year: 2026 },
+      holidays: [],
+      coefficients: [],
+      loadingHolidays: false,
+      loadingCoefficients: false,
+      saving: false,
+      holidayDialog: false,
+      coefficientDialog: false,
+      holidayForm: { id: null, year: 2026, holidayType: 'NATIONAL_DAY', holidayDate: '', dayIndex: 1 },
+      coefficientForm: { id: null, year: 2026, dateCategory: 'HOLIDAY', coefficient: 2.4 },
+      holidayRules: {
+        year: [{ required: true, message: '璇烽�夋嫨骞翠唤', trigger: 'change' }],
+        holidayType: [{ required: true, message: '璇烽�夋嫨鑺傚亣鏃ュ垎绫�', trigger: 'change' }],
+        holidayDate: [{ required: true, message: '璇烽�夋嫨鏃ユ湡', trigger: 'change' }],
+        dayIndex: [{ required: true, message: '璇疯緭鍏ョ鍑犳棩', trigger: 'change' }]
+      },
+      coefficientRules: {
+        year: [{ required: true, message: '璇烽�夋嫨骞翠唤', trigger: 'change' }],
+        dateCategory: [{ required: true, message: '璇烽�夋嫨鏃ユ湡鍒嗙被', trigger: 'change' }],
+        coefficient: [{ required: true, message: '璇疯緭鍏ュ杩愮郴鏁�', trigger: 'change' }]
+      }
+    }
+  },
+  mounted() {
+    this.loadOptions()
+    this.loadHolidays()
+    this.loadCoefficients()
+  },
+  methods: {
+    loadOptions() {
+      api.get('/holiday/config/options').then(res => {
+        const data = res.data || {}
+        this.holidayTypeOptions = this.mapOptions(data.holidayTypes)
+        this.dateCategoryOptions = this.mapOptions(data.dateCategories)
+      })
+    },
+    mapOptions(map) {
+      if (!map) return []
+      return Object.keys(map).map(key => ({ value: key, label: map[key] }))
+    },
+    labelOf(options, value) {
+      const item = (options || []).find(x => x.value === value)
+      return item ? item.label : value
+    },
+    loadActiveTab() {
+      if (this.activeTab === 'holidays') this.loadHolidays()
+      else this.loadCoefficients()
+    },
+    loadHolidays() {
+      this.loadingHolidays = true
+      api.get('/holiday/config/holidays', { params: this.holidayQuery })
+        .then(res => { this.holidays = res.data || [] })
+        .catch(e => { if (!e || !e._toast) this.$message.error('鑺傚亣鏃ュ姞杞藉け璐�') })
+        .finally(() => { this.loadingHolidays = false })
+    },
+    loadCoefficients() {
+      this.loadingCoefficients = true
+      api.get('/holiday/config/coefficients', { params: this.coefficientQuery })
+        .then(res => { this.coefficients = res.data || [] })
+        .catch(e => { if (!e || !e._toast) this.$message.error('瀹㈣繍绯绘暟鍔犺浇澶辫触') })
+        .finally(() => { this.loadingCoefficients = false })
+    },
+    openHolidayDialog(row) {
+      this.holidayDialog = true
+      this.$nextTick(() => {
+        this.holidayForm = row ? Object.assign({}, row) : { id: null, year: 2026, holidayType: 'NATIONAL_DAY', holidayDate: '', dayIndex: 1 }
+        if (this.$refs.holidayForm) this.$refs.holidayForm.clearValidate()
+      })
+    },
+    onHolidayYearChange(year) {
+      if (this.holidayForm.holidayDate && Number(this.holidayForm.holidayDate.slice(0, 4)) !== year) {
+        this.holidayForm.holidayDate = ''
+      }
+    },
+    saveHoliday() {
+      this.$refs.holidayForm.validate(valid => {
+        if (!valid) return
+        this.saving = true
+        api.post('/holiday/config/holidays/save', this.holidayForm)
+          .then(res => {
+            if (res.code !== 200) throw new Error(res.message || '淇濆瓨澶辫触')
+            this.$message.success('淇濆瓨鎴愬姛')
+            this.holidayDialog = false
+            this.loadHolidays()
+          })
+          .catch(err => this.showError(err, '淇濆瓨澶辫触'))
+          .finally(() => { this.saving = false })
+      })
+    },
+    removeHoliday(row) {
+      this.$confirm('纭畾鍒犻櫎璇ヨ妭鍋囨棩鏃ユ湡鍚楋紵', '鍒犻櫎纭', { type: 'warning' })
+        .then(() => api.post('/holiday/config/holidays/delete', null, { params: { id: row.id } }))
+        .then(res => {
+          if (res.code !== 200) throw new Error(res.message || '鍒犻櫎澶辫触')
+          this.$message.success('宸插垹闄�')
+          this.loadHolidays()
+        })
+        .catch(err => { if (err !== 'cancel') this.showError(err, '鍒犻櫎澶辫触') })
+    },
+    openCoefficientDialog(row) {
+      this.coefficientDialog = true
+      this.$nextTick(() => {
+        this.coefficientForm = row ? Object.assign({}, row) : { id: null, year: 2026, dateCategory: 'HOLIDAY', coefficient: 2.4 }
+        if (this.$refs.coefficientForm) this.$refs.coefficientForm.clearValidate()
+      })
+    },
+    saveCoefficient() {
+      this.$refs.coefficientForm.validate(valid => {
+        if (!valid) return
+        this.saving = true
+        api.post('/holiday/config/coefficients/save', this.coefficientForm)
+          .then(res => {
+            if (res.code !== 200) throw new Error(res.message || '淇濆瓨澶辫触')
+            this.$message.success('淇濆瓨鎴愬姛')
+            this.coefficientDialog = false
+            this.loadCoefficients()
+          })
+          .catch(err => this.showError(err, '淇濆瓨澶辫触'))
+          .finally(() => { this.saving = false })
+      })
+    },
+    removeCoefficient(row) {
+      this.$confirm('纭畾鍒犻櫎璇ュ杩愮郴鏁板悧锛�', '鍒犻櫎纭', { type: 'warning' })
+        .then(() => api.post('/holiday/config/coefficients/delete', null, { params: { id: row.id } }))
+        .then(res => {
+          if (res.code !== 200) throw new Error(res.message || '鍒犻櫎澶辫触')
+          this.$message.success('宸插垹闄�')
+          this.loadCoefficients()
+        })
+        .catch(err => { if (err !== 'cancel') this.showError(err, '鍒犻櫎澶辫触') })
+    },
+    formatCoefficient(value) {
+      if (value === null || value === undefined || value === '') return '鈥�'
+      const number = Number(value)
+      if (!Number.isFinite(number)) return value
+      return number.toFixed(15).replace(/0+$/, '').replace(/\.$/, '')
+    },
+    showError(err, fallback) {
+      if (err && err._toast) return
+      const data = err && err.response && err.response.data
+      this.$message.error((data && data.message) || (err && err.message) || fallback)
+    }
+  }
+}
+</script>
+
+<style scoped>
+.toolbar { margin-bottom: 12px; display: flex; gap: 8px; align-items: center; }
+.danger-text { color: #f56c6c; }
+</style>

--
Gitblit v1.9.1