|
|
@@ -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>
|