Quellcode durchsuchen

历史信息管理

Zhangbw vor 3 Monaten
Ursprung
Commit
a1b16ecf53

+ 136 - 0
src/api/stock/history/index.ts

@@ -0,0 +1,136 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+
+/**
+ * 股票池历史数据查询对象
+ */
+export interface StockPoolHistoryQuery {
+  pageNum?: number;
+  pageSize?: number;
+  stockCode?: string;
+  stockName?: string;
+  recordDate?: string;
+  startDate?: string;
+  endDate?: string;
+}
+
+/**
+ * 股票池历史数据对象
+ */
+export interface StockPoolHistoryVO {
+  id?: number;
+  recordDate?: string;
+  stockCode?: string;
+  stockName?: string;
+  changePercent?: number;
+  closePrice?: number;
+  totalAmount?: number;
+  strengthScore?: number;
+  circulationMarketValue?: number;
+  mainRisePeriod?: string;
+  recentRiseHand?: string;
+  recentLimitUp?: string;
+  dayHighestPrice?: number;
+  dayLowestPrice?: number;
+  dayAvgPrice?: number;
+  dayClosePrice?: number;
+  createTime?: string;
+  updateTime?: string;
+}
+
+/**
+ * 股票池历史数据表单对象
+ */
+export interface StockPoolHistoryForm {
+  id?: number;
+  recordDate?: string;
+  stockCode?: string;
+  stockName?: string;
+  changePercent?: number;
+  closePrice?: number;
+  totalAmount?: number;
+  strengthScore?: number;
+  circulationMarketValue?: number;
+  mainRisePeriod?: string;
+  recentRiseHand?: string;
+  recentLimitUp?: string;
+  dayHighestPrice?: number;
+  dayLowestPrice?: number;
+  dayAvgPrice?: number;
+  dayClosePrice?: number;
+}
+
+/**
+ * 查询股票池历史数据列表
+ */
+export function listStockHistory(query: StockPoolHistoryQuery): AxiosPromise<StockPoolHistoryVO[]> {
+  return request({
+    url: '/stock/history/list',
+    method: 'get',
+    params: query
+  });
+}
+
+/**
+ * 查询股票池历史数据详情
+ */
+export function getStockHistory(id: number): AxiosPromise<StockPoolHistoryVO> {
+  return request({
+    url: `/stock/history/${id}`,
+    method: 'get'
+  });
+}
+
+/**
+ * 新增股票池历史数据
+ */
+export function addStockHistory(data: StockPoolHistoryForm) {
+  return request({
+    url: '/stock/history/add',
+    method: 'post',
+    data: data
+  });
+}
+
+/**
+ * 修改股票池历史数据
+ */
+export function updateStockHistory(data: StockPoolHistoryForm) {
+  return request({
+    url: '/stock/history/edit',
+    method: 'put',
+    data: data
+  });
+}
+
+/**
+ * 删除股票池历史数据
+ */
+export function delStockHistory(ids: number | number[]) {
+  return request({
+    url: `/stock/history/${ids}`,
+    method: 'delete'
+  });
+}
+
+/**
+ * 导入股票池历史数据
+ * @param file Excel文件
+ * @param recordDate 记录日期(格式:yyyy-MM-dd)
+ * @param updateSupport 是否更新已存在数据
+ */
+export function importStockHistory(file: File, recordDate: string, updateSupport: boolean = true) {
+  const formData = new FormData();
+  formData.append('file', file);
+  formData.append('recordDate', recordDate);
+  formData.append('updateSupport', updateSupport.toString());
+  
+  return request({
+    url: '/stock/history/importData',
+    method: 'post',
+    data: formData,
+    headers: {
+      'Content-Type': 'multipart/form-data'
+    }
+  });
+}

+ 114 - 0
src/views/settings/agreement/index.vue

@@ -0,0 +1,114 @@
+<template>
+  <div class="p-2">
+    <el-tabs v-model="activeTab" type="border-card">
+      <!-- 用户协议 -->
+      <el-tab-pane label="用户协议" name="userAgreement">
+        <el-card shadow="never">
+          <template #header>
+            <div class="card-header">
+              <span>用户协议内容</span>
+              <el-button type="primary" @click="saveUserAgreement" :loading="saving">保存</el-button>
+            </div>
+          </template>
+          <div v-loading="loading">
+            <editor v-model="form.userAgreement" :min-height="400" />
+          </div>
+        </el-card>
+      </el-tab-pane>
+
+      <!-- 隐私政策 -->
+      <el-tab-pane label="隐私政策" name="privacyPolicy">
+        <el-card shadow="never">
+          <template #header>
+            <div class="card-header">
+              <span>隐私政策内容</span>
+              <el-button type="primary" @click="savePrivacyPolicy" :loading="saving">保存</el-button>
+            </div>
+          </template>
+          <div v-loading="loading">
+            <editor v-model="form.privacyPolicy" :min-height="400" />
+          </div>
+        </el-card>
+      </el-tab-pane>
+    </el-tabs>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue'
+import { ElMessage } from 'element-plus'
+import request from '@/utils/request'
+import Editor from '@/components/Editor/index.vue'
+
+const loading = ref(false)
+const saving = ref(false)
+const activeTab = ref('userAgreement')
+
+const form = ref({
+  userAgreement: '',
+  privacyPolicy: ''
+})
+
+const getConfig = async () => {
+  loading.value = true
+  try {
+    const res = await request.get('/miniapp/agreement/list')
+    if (res.code === 200 && res.data) {
+      form.value.userAgreement = res.data.userAgreement || ''
+      form.value.privacyPolicy = res.data.privacyPolicy || ''
+    }
+  } catch (e) {
+    console.error('获取配置失败', e)
+  } finally {
+    loading.value = false
+  }
+}
+
+const saveUserAgreement = async () => {
+  saving.value = true
+  try {
+    const res = await request.put('/miniapp/agreement/userAgreement', {
+      userAgreement: form.value.userAgreement
+    })
+    if (res.code === 200) {
+      ElMessage.success('用户协议保存成功')
+    } else {
+      ElMessage.error(res.msg || '保存失败')
+    }
+  } catch (e) {
+    ElMessage.error('保存失败')
+  } finally {
+    saving.value = false
+  }
+}
+
+const savePrivacyPolicy = async () => {
+  saving.value = true
+  try {
+    const res = await request.put('/miniapp/agreement/privacyPolicy', {
+      privacyPolicy: form.value.privacyPolicy
+    })
+    if (res.code === 200) {
+      ElMessage.success('隐私政策保存成功')
+    } else {
+      ElMessage.error(res.msg || '保存失败')
+    }
+  } catch (e) {
+    ElMessage.error('保存失败')
+  } finally {
+    saving.value = false
+  }
+}
+
+onMounted(() => {
+  getConfig()
+})
+</script>
+
+<style scoped>
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+</style>

+ 2 - 2
src/views/settings/paymentConfig/subscription.vue

@@ -10,12 +10,12 @@
 
       <el-form :model="form" label-width="140px" v-loading="loading">
         <el-divider content-position="left">超短池配置</el-divider>
-        <el-form-item label="订阅价格(元)">
+        <el-form-item label="打赏价格(元)">
           <el-input-number v-model="form.shortPrice" :min="0" :precision="2" :step="1" />
         </el-form-item>
 
         <el-divider content-position="left">强势池配置</el-divider>
-        <el-form-item label="订阅价格(元)">
+        <el-form-item label="打赏价格/年(元)">
           <el-input-number v-model="form.strongPrice" :min="0" :precision="2" :step="1" />
         </el-form-item>
       </el-form>

+ 54 - 2
src/views/settings/paymentConfig/wxpay.vue

@@ -49,6 +49,31 @@
             <div v-if="form.privateKeyUploaded" class="success-text">✓ 私钥已上传</div>
           </div>
         </el-form-item>
+
+        <el-divider content-position="left">微信支付公钥配置(新商户必填)</el-divider>
+        
+        <el-form-item label="公钥ID">
+          <el-input v-model="form.publicKeyId" placeholder="请输入微信支付公钥ID,如:PUB_KEY_ID_xxx" style="width: 400px" />
+          <div class="tip-text">从微信商户平台 → API安全 → 申请微信支付公钥 获取</div>
+        </el-form-item>
+        
+        <el-form-item label="微信支付公钥">
+          <div>
+            <el-upload
+              action="#"
+              :auto-upload="false"
+              :show-file-list="false"
+              accept=".pem"
+              :on-change="handlePublicKeyChange"
+            >
+              <el-button type="primary">上传公钥文件</el-button>
+            </el-upload>
+            <div class="tip-text">微信支付公钥(pub_key.pem),从微信商户平台 → API安全 下载</div>
+            <div v-if="form.publicKeyUploaded" class="success-text">✓ 公钥已上传</div>
+          </div>
+        </el-form-item>
+
+        <el-divider content-position="left">回调配置</el-divider>
         
         <el-form-item label="API回调地址">
           <el-input v-model="form.notifyUrl" placeholder="请输入支付回调地址,如:https://域名/v1/order/notify" style="width: 400px" />
@@ -67,13 +92,16 @@ const loading = ref(false)
 const saving = ref(false)
 const privateKeyFile = ref<File | null>(null)
 const certFile = ref<File | null>(null)
+const publicKeyFile = ref<File | null>(null)
 
 const form = ref({
   mchId: '',
   apiV3Key: '',
   notifyUrl: '',
   privateKeyUploaded: false,
-  certUploaded: false
+  certUploaded: false,
+  publicKeyId: '',
+  publicKeyUploaded: false
 })
 
 const handlePrivateKeyChange = (file: any) => {
@@ -86,6 +114,11 @@ const handleCertChange = (file: any) => {
   ElMessage.success('证书文件已选择,保存时将一并上传')
 }
 
+const handlePublicKeyChange = (file: any) => {
+  publicKeyFile.value = file.raw
+  ElMessage.success('公钥文件已选择,保存时将一并上传')
+}
+
 const getConfig = async () => {
   loading.value = true
   try {
@@ -96,6 +129,8 @@ const getConfig = async () => {
       form.value.notifyUrl = res.data.notifyUrl || ''
       form.value.privateKeyUploaded = res.data.privateKeyUploaded || false
       form.value.certUploaded = res.data.certUploaded || false
+      form.value.publicKeyId = res.data.publicKeyId || ''
+      form.value.publicKeyUploaded = res.data.publicKeyUploaded || false
     }
   } catch (e) {
     console.error('获取配置失败', e)
@@ -138,11 +173,28 @@ const handleSave = async () => {
       form.value.certUploaded = true
       certFile.value = null
     }
+
+    // 上传公钥
+    if (publicKeyFile.value) {
+      const formData = new FormData()
+      formData.append('file', publicKeyFile.value)
+      const uploadRes = await request.post('/miniapp/paymentConfig/uploadPublicKey', formData, {
+        headers: { 'Content-Type': 'multipart/form-data' }
+      })
+      if (uploadRes.code !== 200) {
+        ElMessage.error('公钥上传失败:' + uploadRes.msg)
+        saving.value = false
+        return
+      }
+      form.value.publicKeyUploaded = true
+      publicKeyFile.value = null
+    }
     
     const res = await request.put('/miniapp/paymentConfig/wxpay', {
       mchId: form.value.mchId,
       apiV3Key: form.value.apiV3Key,
-      notifyUrl: form.value.notifyUrl
+      notifyUrl: form.value.notifyUrl,
+      publicKeyId: form.value.publicKeyId
     })
     if (res.code === 200) {
       ElMessage.success('保存成功')

+ 311 - 0
src/views/stock/history/index.vue

@@ -0,0 +1,311 @@
+<template>
+  <div class="p-2">
+    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+      <div v-show="showSearch" class="mb-[10px]">
+        <el-card shadow="hover">
+          <el-form ref="queryFormRef" :model="queryParams" :inline="true">
+            <el-form-item label="股票代码" prop="stockCode">
+              <el-input v-model="queryParams.stockCode" placeholder="请输入股票代码" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="股票名称" prop="stockName">
+              <el-input v-model="queryParams.stockName" placeholder="请输入股票名称" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="记录日期">
+              <el-date-picker
+                v-model="dateRange"
+                type="daterange"
+                range-separator="-"
+                start-placeholder="开始日期"
+                end-placeholder="结束日期"
+                value-format="YYYY-MM-DD"
+              />
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+              <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+            </el-form-item>
+          </el-form>
+        </el-card>
+      </div>
+    </transition>
+
+    <el-card shadow="hover">
+      <template #header>
+        <el-row :gutter="10">
+          <el-col :span="1.5">
+            <el-button v-has-permi="['stock:history:import']" type="primary" plain icon="Upload" @click="handleImport">导入数据</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button v-has-permi="['stock:history:remove']" type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete">删除</el-button>
+          </el-col>
+          <right-toolbar v-model:show-search="showSearch" @query-table="getList"></right-toolbar>
+        </el-row>
+      </template>
+
+      <el-table v-loading="loading" border :data="historyList" @selection-change="handleSelectionChange">
+        <el-table-column type="selection" width="50" align="center" />
+        <el-table-column label="记录日期" align="center" prop="recordDate" width="120" />
+        <el-table-column label="股票代码" align="center" prop="stockCode" width="100" />
+        <el-table-column label="股票名称" align="center" prop="stockName" width="120" />
+        <el-table-column label="涨幅%" align="center" prop="changePercent" width="100">
+          <template #default="scope">
+            <span :class="getChangeClass(scope.row.changePercent)">{{ formatNumber(scope.row.changePercent) }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="收盘价" align="center" prop="closePrice" width="100">
+          <template #default="scope">{{ formatNumber(scope.row.closePrice) }}</template>
+        </el-table-column>
+        <el-table-column label="总金额" align="center" prop="totalAmount" width="120">
+          <template #default="scope">{{ formatNumber(scope.row.totalAmount) }}</template>
+        </el-table-column>
+        <el-table-column label="强度评分" align="center" prop="strengthScore" width="100">
+          <template #default="scope">{{ formatNumber(scope.row.strengthScore) }}</template>
+        </el-table-column>
+        <el-table-column label="流通市值" align="center" prop="circulationMarketValue" width="140">
+          <template #default="scope">{{ formatNumber(scope.row.circulationMarketValue) }}</template>
+        </el-table-column>
+        <el-table-column label="主升周期" align="center" prop="mainRisePeriod" width="100" />
+        <el-table-column label="近期涨手" align="center" prop="recentRiseHand" width="100" />
+        <el-table-column label="近期涨停" align="center" prop="recentLimitUp" width="100" />
+        <el-table-column label="当天最高价" align="center" prop="dayHighestPrice" width="110">
+          <template #default="scope">{{ formatNumber(scope.row.dayHighestPrice) }}</template>
+        </el-table-column>
+        <el-table-column label="当天最低价" align="center" prop="dayLowestPrice" width="110">
+          <template #default="scope">{{ formatNumber(scope.row.dayLowestPrice) }}</template>
+        </el-table-column>
+        <el-table-column label="当天均价" align="center" prop="dayAvgPrice" width="100">
+          <template #default="scope">{{ formatNumber(scope.row.dayAvgPrice) }}</template>
+        </el-table-column>
+        <el-table-column label="当天收盘价" align="center" prop="dayClosePrice" width="110">
+          <template #default="scope">{{ formatNumber(scope.row.dayClosePrice) }}</template>
+        </el-table-column>
+        <el-table-column label="操作" align="center" width="80" fixed="right">
+          <template #default="scope">
+            <el-tooltip content="删除" placement="top">
+              <el-button v-has-permi="['stock:history:remove']" link type="danger" icon="Delete" @click="handleDelete(scope.row)"></el-button>
+            </el-tooltip>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
+    </el-card>
+
+    <!-- 导入对话框 -->
+    <el-dialog v-model="importDialog.visible" title="导入历史数据" width="500px" append-to-body>
+      <el-form ref="importFormRef" :model="importForm" :rules="importRules" label-width="100px">
+        <el-form-item label="记录日期" prop="recordDate">
+          <el-date-picker
+            v-model="importForm.recordDate"
+            type="date"
+            placeholder="选择记录日期"
+            value-format="YYYY-MM-DD"
+            style="width: 100%"
+          />
+          <div class="text-gray-400 text-sm mt-1">此日期将作为所有导入数据的记录日期</div>
+        </el-form-item>
+        <el-form-item label="Excel文件" prop="file">
+          <el-upload
+            ref="uploadRef"
+            :auto-upload="false"
+            :limit="1"
+            accept=".xls,.xlsx"
+            :on-change="handleFileChange"
+            :on-exceed="handleExceed"
+            :file-list="fileList"
+          >
+            <el-button type="primary" icon="Upload">选择文件</el-button>
+            <template #tip>
+              <div class="el-upload__tip">只能上传 xls/xlsx 文件</div>
+            </template>
+          </el-upload>
+        </el-form-item>
+        <el-form-item label="更新策略">
+          <el-radio-group v-model="importForm.updateSupport">
+            <el-radio :label="true">更新已存在数据</el-radio>
+            <el-radio :label="false">跳过已存在数据</el-radio>
+          </el-radio-group>
+          <div class="text-gray-400 text-sm mt-1">如果同一股票代码在同一日期已有数据,选择是否更新</div>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button type="primary" @click="submitImport" :loading="importing">确 定</el-button>
+        <el-button @click="cancelImport">取 消</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup name="StockHistory" lang="ts">
+import { ref, reactive, getCurrentInstance } from 'vue';
+import { listStockHistory, delStockHistory, importStockHistory } from '@/api/stock/history';
+import type { UploadFile, UploadFiles, UploadInstance } from 'element-plus';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+const historyList = ref<any[]>([]);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref<number[]>([]);
+const multiple = ref(true);
+const total = ref(0);
+const dateRange = ref<[string, string]>();
+const importing = ref(false);
+
+const queryFormRef = ref<ElFormInstance>();
+const importFormRef = ref<ElFormInstance>();
+const uploadRef = ref<UploadInstance>();
+
+const queryParams = ref({
+  pageNum: 1,
+  pageSize: 10,
+  stockCode: '',
+  stockName: '',
+  startDate: undefined as string | undefined,
+  endDate: undefined as string | undefined
+});
+
+const importDialog = reactive({ visible: false });
+const importForm = ref({
+  recordDate: '',
+  file: null as File | null,
+  updateSupport: true
+});
+const fileList = ref<UploadFile[]>([]);
+
+const importRules: Record<string, any[]> = {
+  recordDate: [{ required: true, message: '请选择记录日期', trigger: 'change' }],
+  file: [{ required: true, message: '请选择Excel文件', trigger: 'change' }]
+};
+
+/** 查询历史数据列表 */
+const getList = async () => {
+  loading.value = true;
+  if (dateRange.value) {
+    queryParams.value.startDate = dateRange.value[0];
+    queryParams.value.endDate = dateRange.value[1];
+  } else {
+    queryParams.value.startDate = undefined;
+    queryParams.value.endDate = undefined;
+  }
+  try {
+    const res = await listStockHistory(queryParams.value);
+    historyList.value = res.rows || [];
+    total.value = res.total || 0;
+  } catch (error) {
+    console.error('获取历史数据列表失败:', error);
+    historyList.value = [];
+    total.value = 0;
+  } finally {
+    loading.value = false;
+  }
+};
+
+/** 格式化数字 */
+const formatNumber = (val: any) => (val != null ? Number(val).toFixed(2) : '-');
+
+/** 获取涨跌样式 */
+const getChangeClass = (val: any) => {
+  if (val == null) return '';
+  return val > 0 ? 'text-red-500 font-bold' : val < 0 ? 'text-green-500 font-bold' : '';
+};
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.value.pageNum = 1;
+  getList();
+};
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  dateRange.value = undefined;
+  queryFormRef.value?.resetFields();
+  handleQuery();
+};
+
+/** 多选框选中数据 */
+const handleSelectionChange = (selection: any[]) => {
+  ids.value = selection.map(item => item.id);
+  multiple.value = !selection.length;
+};
+
+/** 导入按钮操作 */
+const handleImport = () => {
+  resetImportForm();
+  importDialog.visible = true;
+};
+
+/** 文件选择变化 */
+const handleFileChange = (file: UploadFile, files: UploadFiles) => {
+  if (files.length > 0) {
+    importForm.value.file = file.raw as File;
+    fileList.value = [file];
+  }
+};
+
+/** 文件超出限制 */
+const handleExceed = () => {
+  proxy?.$modal.msgWarning('只能上传一个文件');
+};
+
+/** 提交导入 */
+const submitImport = () => {
+  importFormRef.value?.validate(async (valid: boolean) => {
+    if (valid && importForm.value.file) {
+      importing.value = true;
+      try {
+        const res = await importStockHistory(
+          importForm.value.file,
+          importForm.value.recordDate,
+          importForm.value.updateSupport
+        );
+        proxy?.$modal.msgSuccess(res.msg || '导入成功');
+        importDialog.visible = false;
+        await getList();
+      } catch (error: any) {
+        proxy?.$modal.msgError(error.msg || '导入失败');
+      } finally {
+        importing.value = false;
+      }
+    }
+  });
+};
+
+/** 取消导入 */
+const cancelImport = () => {
+  importDialog.visible = false;
+  resetImportForm();
+};
+
+/** 重置导入表单 */
+const resetImportForm = () => {
+  importForm.value = {
+    recordDate: '',
+    file: null,
+    updateSupport: true
+  };
+  fileList.value = [];
+  uploadRef.value?.clearFiles();
+  importFormRef.value?.resetFields();
+};
+
+/** 删除按钮操作 */
+const handleDelete = async (row?: any) => {
+  const historyIds = row?.id ? [row.id] : ids.value;
+  await proxy?.$modal.confirm(`是否确认删除选中的${historyIds.length}条历史数据?`);
+  await delStockHistory(historyIds);
+  proxy?.$modal.msgSuccess('删除成功');
+  await getList();
+};
+
+getList();
+</script>
+
+<style scoped>
+.text-gray-400 { color: #9ca3af; }
+.text-red-500 { color: #ef4444; }
+.text-green-500 { color: #22c55e; }
+.font-bold { font-weight: bold; }
+.text-sm { font-size: 0.875rem; }
+.mt-1 { margin-top: 0.25rem; }
+</style>