Zhangbw 3 месяцев назад
Родитель
Сommit
635a32e0cd

+ 32 - 0
src/api/miniapp/user/index.ts

@@ -21,6 +21,38 @@ export function getMiniappUser(id: number) {
   });
 }
 
+/**
+ * 新增小程序用户
+ */
+export function addMiniappUser(data: any) {
+  return request({
+    url: '/miniapp/user',
+    method: 'post',
+    data
+  });
+}
+
+/**
+ * 修改小程序用户
+ */
+export function updateMiniappUser(data: any) {
+  return request({
+    url: '/miniapp/user',
+    method: 'put',
+    data
+  });
+}
+
+/**
+ * 删除小程序用户
+ */
+export function delMiniappUser(id: number) {
+  return request({
+    url: `/miniapp/user/${id}`,
+    method: 'delete'
+  });
+}
+
 /**
  * 修改用户状态
  */

+ 180 - 0
src/views/miniapp/order/index.vue

@@ -0,0 +1,180 @@
+<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="orderNo">
+              <el-input v-model="queryParams.orderNo" placeholder="请输入订单号" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="用户昵称" prop="nickname">
+              <el-input v-model="queryParams.nickname" placeholder="请输入用户昵称" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="手机号" prop="phone">
+              <el-input v-model="queryParams.phone" placeholder="请输入手机号" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="池类型" prop="poolType">
+              <el-select v-model="queryParams.poolType" placeholder="请选择池类型" clearable>
+                <el-option label="超短池" :value="1" />
+                <el-option label="强势池" :value="2" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="订单状态" prop="orderStatus">
+              <el-select v-model="queryParams.orderStatus" placeholder="请选择状态" clearable>
+                <el-option label="待支付" :value="0" />
+                <el-option label="已支付" :value="1" />
+                <el-option label="已取消" :value="2" />
+                <el-option label="已关闭" :value="3" />
+              </el-select>
+            </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="['miniapp:order:export']" type="warning" plain icon="Download" @click="handleExport">导出</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button type="info" plain icon="Refresh" @click="handleRefresh" :loading="refreshing">刷新</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="dataList" stripe>
+        <el-table-column label="订单号" align="center" prop="orderNo" width="200" :show-overflow-tooltip="true" />
+        <el-table-column label="用户昵称" align="center" prop="nickname" :show-overflow-tooltip="true" />
+        <el-table-column label="手机号" align="center" prop="phone" width="120" />
+        <el-table-column label="池类型" align="center" width="100">
+          <template #default="scope">
+            <el-tag v-if="scope.row.poolType === 1" type="warning">超短池</el-tag>
+            <el-tag v-else type="success">强势池</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="订单金额" align="center" prop="amount" width="100">
+          <template #default="scope">
+            <span class="text-red-500">¥{{ scope.row.amount }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="订单状态" align="center" width="100">
+          <template #default="scope">
+            <el-tag v-if="scope.row.orderStatus === 0" type="warning">待支付</el-tag>
+            <el-tag v-else-if="scope.row.orderStatus === 1" type="success">已支付</el-tag>
+            <el-tag v-else-if="scope.row.orderStatus === 2" type="info">已取消</el-tag>
+            <el-tag v-else type="danger">已关闭</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="微信交易号" align="center" prop="transactionId" width="200" :show-overflow-tooltip="true" />
+        <el-table-column label="支付时间" align="center" prop="payTime" width="170" />
+        <el-table-column label="创建时间" align="center" prop="createTime" width="170" />
+        <el-table-column label="操作" align="center" width="80">
+          <template #default="scope">
+            <el-tooltip content="删除" placement="top">
+              <el-button v-has-permi="['miniapp:order: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>
+  </div>
+</template>
+
+<script setup name="MiniappOrder" lang="ts">
+import { ref, onMounted, getCurrentInstance } from 'vue';
+import request from '@/utils/request';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+const dataList = ref<any[]>([]);
+const loading = ref(true);
+const refreshing = ref(false);
+const showSearch = ref(true);
+const total = ref(0);
+
+const queryFormRef = ref<ElFormInstance>();
+
+const queryParams = ref({
+  pageNum: 1,
+  pageSize: 10,
+  orderNo: '',
+  nickname: '',
+  phone: '',
+  poolType: undefined as number | undefined,
+  orderStatus: undefined as number | undefined
+});
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true;
+  try {
+    const res = await request.get('/miniapp/order/list', { params: queryParams.value });
+    dataList.value = res.rows || [];
+    total.value = res.total || 0;
+  } catch (error) {
+    console.error('获取订单列表失败:', error);
+  } finally {
+    loading.value = false;
+  }
+};
+
+/** 手动刷新 */
+const handleRefresh = async () => {
+  refreshing.value = true;
+  try {
+    await getList();
+  } finally {
+    refreshing.value = false;
+  }
+};
+
+/** 搜索 */
+const handleQuery = () => {
+  queryParams.value.pageNum = 1;
+  getList();
+};
+
+/** 重置 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields();
+  queryParams.value.poolType = undefined;
+  queryParams.value.orderStatus = undefined;
+  handleQuery();
+};
+
+/** 删除 */
+const handleDelete = async (row: any) => {
+  try {
+    await proxy?.$modal.confirm(`确认要删除订单"${row.orderNo}"吗?`);
+    await request.delete(`/miniapp/order/${row.id}`);
+    proxy?.$modal.msgSuccess('删除成功');
+    getList();
+  } catch {
+    // 取消操作
+  }
+};
+
+/** 导出 */
+const handleExport = () => {
+  proxy?.download('miniapp/order/export', { ...queryParams.value }, `order_${new Date().getTime()}.xlsx`);
+};
+
+onMounted(() => {
+  getList();
+});
+</script>

+ 227 - 0
src/views/miniapp/subscription/index.vue

@@ -0,0 +1,227 @@
+<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="nickname">
+              <el-input v-model="queryParams.nickname" placeholder="请输入用户昵称" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="手机号" prop="phone">
+              <el-input v-model="queryParams.phone" placeholder="请输入手机号" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="池类型" prop="poolType">
+              <el-select v-model="queryParams.poolType" placeholder="请选择池类型" clearable>
+                <el-option label="超短池" :value="1" />
+                <el-option label="强势池" :value="2" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="状态" prop="status">
+              <el-select v-model="queryParams.status" placeholder="请选择状态" clearable>
+                <el-option label="生效中" :value="1" />
+                <el-option label="已过期" :value="0" />
+              </el-select>
+            </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="['miniapp:subscription:export']" type="warning" plain icon="Download" @click="handleExport">导出</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button type="info" plain icon="Refresh" @click="handleRefresh" :loading="refreshing">刷新</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="dataList" stripe>
+        <el-table-column label="ID" align="center" prop="id" width="80" />
+        <el-table-column label="用户昵称" align="center" prop="nickname" :show-overflow-tooltip="true" />
+        <el-table-column label="手机号" align="center" prop="phone" />
+        <el-table-column label="池类型" align="center" width="100">
+          <template #default="scope">
+            <el-tag v-if="scope.row.poolType === 1" type="warning">超短池</el-tag>
+            <el-tag v-else type="success">强势池</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="订阅金额" align="center" prop="amount" width="100">
+          <template #default="scope">
+            <span class="text-red-500">¥{{ scope.row.amount }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="开始时间" align="center" prop="startTime" width="170" />
+        <el-table-column label="到期时间" align="center" prop="expireTime" width="170" />
+        <el-table-column label="状态" align="center" width="100">
+          <template #default="scope">
+            <el-tag v-if="scope.row.status === 1" type="success">生效中</el-tag>
+            <el-tag v-else type="info">已过期</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="创建时间" align="center" prop="createTime" width="170" />
+        <el-table-column label="操作" align="center" width="100">
+          <template #default="scope">
+            <el-tooltip content="编辑" placement="top">
+              <el-button v-has-permi="['miniapp:subscription:edit']" link type="primary" icon="Edit" @click="handleEdit(scope.row)"></el-button>
+            </el-tooltip>
+            <el-tooltip content="删除" placement="top">
+              <el-button v-has-permi="['miniapp:subscription: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="editDialog.visible" title="编辑订阅" width="500px" append-to-body>
+      <el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
+        <el-form-item label="到期时间" prop="expireTime">
+          <el-date-picker v-model="form.expireTime" type="datetime" placeholder="选择到期时间" style="width: 100%" />
+        </el-form-item>
+        <el-form-item label="状态" prop="status">
+          <el-select v-model="form.status" placeholder="请选择状态">
+            <el-option label="生效中" :value="1" />
+            <el-option label="已过期" :value="0" />
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="editDialog.visible = false">取消</el-button>
+        <el-button type="primary" @click="submitForm">确定</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup name="MiniappSubscription" lang="ts">
+import { ref, onMounted, getCurrentInstance, reactive } from 'vue';
+import request from '@/utils/request';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+const dataList = ref<any[]>([]);
+const loading = ref(true);
+const refreshing = ref(false);
+const showSearch = ref(true);
+const total = ref(0);
+
+const queryFormRef = ref<ElFormInstance>();
+const formRef = ref<ElFormInstance>();
+
+const queryParams = ref({
+  pageNum: 1,
+  pageSize: 10,
+  nickname: '',
+  phone: '',
+  poolType: undefined as number | undefined,
+  status: undefined as number | undefined
+});
+
+const editDialog = ref({ visible: false });
+
+const form = ref<any>({
+  id: undefined,
+  expireTime: '',
+  status: 1
+});
+
+const rules = reactive({
+  expireTime: [{ required: true, message: '请选择到期时间', trigger: 'change' }],
+  status: [{ required: true, message: '请选择状态', trigger: 'change' }]
+});
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true;
+  try {
+    const res = await request.get('/miniapp/subscription/list', { params: queryParams.value });
+    dataList.value = res.rows || [];
+    total.value = res.total || 0;
+  } catch (error) {
+    console.error('获取订阅列表失败:', error);
+  } finally {
+    loading.value = false;
+  }
+};
+
+/** 手动刷新 */
+const handleRefresh = async () => {
+  refreshing.value = true;
+  try {
+    await getList();
+  } finally {
+    refreshing.value = false;
+  }
+};
+
+/** 搜索 */
+const handleQuery = () => {
+  queryParams.value.pageNum = 1;
+  getList();
+};
+
+/** 重置 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields();
+  queryParams.value.poolType = undefined;
+  queryParams.value.status = undefined;
+  handleQuery();
+};
+
+/** 编辑 */
+const handleEdit = (row: any) => {
+  form.value = {
+    id: row.id,
+    expireTime: row.expireTime,
+    status: row.status
+  };
+  editDialog.value.visible = true;
+};
+
+/** 提交表单 */
+const submitForm = async () => {
+  await formRef.value?.validate();
+  await request.put('/miniapp/subscription', form.value);
+  proxy?.$modal.msgSuccess('修改成功');
+  editDialog.value.visible = false;
+  getList();
+};
+
+/** 删除 */
+const handleDelete = async (row: any) => {
+  try {
+    await proxy?.$modal.confirm(`确认要删除该订阅记录吗?`);
+    await request.delete(`/miniapp/subscription/${row.id}`);
+    proxy?.$modal.msgSuccess('删除成功');
+    getList();
+  } catch {
+    // 取消操作
+  }
+};
+
+/** 导出 */
+const handleExport = () => {
+  proxy?.download('miniapp/subscription/export', { ...queryParams.value }, `subscription_${new Date().getTime()}.xlsx`);
+};
+
+onMounted(() => {
+  getList();
+});
+</script>

+ 249 - 23
src/views/miniapp/user/index.vue

@@ -14,6 +14,7 @@
               <el-select v-model="queryParams.status" placeholder="用户状态" clearable>
                 <el-option label="正常" :value="0" />
                 <el-option label="禁用" :value="1" />
+                <el-option label="管理员" :value="2" />
               </el-select>
             </el-form-item>
             <el-form-item>
@@ -28,36 +29,71 @@
     <el-card shadow="hover">
       <template #header>
         <el-row :gutter="10">
+          <el-col :span="1.5">
+            <el-button v-has-permi="['miniapp:user:edit']" type="success" plain icon="Edit" :disabled="single" @click="handleUpdate(selectedRow)">修改</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button v-has-permi="['miniapp:user:remove']" type="danger" plain icon="Delete" :disabled="multiple" @click="handleBatchDelete">删除</el-button>
+          </el-col>
           <el-col :span="1.5">
             <el-button v-has-permi="['miniapp:user:export']" type="warning" plain icon="Download" @click="handleExport">导出</el-button>
           </el-col>
+          <el-col :span="1.5">
+            <el-button type="info" plain icon="Refresh" @click="handleRefresh" :loading="refreshing">刷新</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="userList">
-        <el-table-column label="用户ID" align="center" prop="id" width="80" />
-        <el-table-column label="头像" align="center" width="80">
-          <template #default="scope">
-            <el-avatar :size="40" :src="scope.row.avatar || defaultAvatar" />
-          </template>
-        </el-table-column>
+      <el-table v-loading="loading" border :data="userList" stripe @selection-change="handleSelectionChange">
+        <el-table-column type="selection" width="55" align="center" />
+        <el-table-column label="ID" align="center" prop="id" />
         <el-table-column label="昵称" align="center" prop="nickname" :show-overflow-tooltip="true" />
-        <el-table-column label="手机号" align="center" prop="phone" width="120" />
-        <el-table-column label="状态" align="center" width="100">
+        <el-table-column label="手机号" align="center" prop="phone" />
+        <el-table-column label="状态" align="center">
           <template #default="scope">
-            <el-switch
-              v-model="scope.row.status"
-              :active-value="0"
-              :inactive-value="1"
-              @change="handleStatusChange(scope.row)"
-            />
+            <el-tag v-if="scope.row.status === 2" type="warning" size="small">管理员</el-tag>
+            <el-tag v-else-if="scope.row.status === 0" type="success" size="small">正常</el-tag>
+            <el-tag v-else type="danger" size="small">禁用</el-tag>
           </template>
         </el-table-column>
-        <el-table-column label="注册时间" align="center" prop="createTime" width="180" />
-        <el-table-column label="操作" align="center" width="150">
+        <el-table-column label="注册时间" align="center" prop="createTime" />
+        <el-table-column label="操作" align="center" width="280">
           <template #default="scope">
-            <el-button v-has-permi="['miniapp:user:query']" link type="primary" icon="View" @click="handleView(scope.row)">详情</el-button>
+            <el-tooltip content="修改" placement="top">
+              <el-button v-has-permi="['miniapp:user:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
+            </el-tooltip>
+            <el-tooltip content="删除" placement="top">
+              <el-button v-has-permi="['miniapp:user:remove']" link type="danger" icon="Delete" @click="handleDelete(scope.row)"></el-button>
+            </el-tooltip>
+            <el-button 
+              v-if="scope.row.status === 0" 
+              v-has-permi="['miniapp:user:edit']" 
+              link 
+              type="danger" 
+              @click="handleDisable(scope.row)"
+            >禁用</el-button>
+            <el-button 
+              v-if="scope.row.status === 1" 
+              v-has-permi="['miniapp:user:edit']" 
+              link 
+              type="success" 
+              @click="handleEnable(scope.row)"
+            >启用</el-button>
+            <el-button 
+              v-if="scope.row.status === 0" 
+              v-has-permi="['miniapp:user:edit']" 
+              link 
+              type="warning" 
+              @click="handleSetAdmin(scope.row)"
+            >设为管理员</el-button>
+            <el-button 
+              v-if="scope.row.status === 2" 
+              v-has-permi="['miniapp:user:edit']" 
+              link 
+              type="info" 
+              @click="handleCancelAdmin(scope.row)"
+            >取消管理员</el-button>
           </template>
         </el-table-column>
       </el-table>
@@ -79,29 +115,57 @@
         <el-descriptions-item label="手机号">{{ currentUser.phone }}</el-descriptions-item>
         <el-descriptions-item label="OpenID">{{ currentUser.openid }}</el-descriptions-item>
         <el-descriptions-item label="状态">
-          <el-tag :type="currentUser.status === 0 ? 'success' : 'danger'">
-            {{ currentUser.status === 0 ? '正常' : '禁用' }}
+          <el-tag :type="currentUser.status === 0 ? 'success' : (currentUser.status === 2 ? 'warning' : 'danger')">
+            {{ currentUser.status === 0 ? '正常' : (currentUser.status === 2 ? '管理员' : '禁用') }}
           </el-tag>
         </el-descriptions-item>
         <el-descriptions-item label="注册时间">{{ currentUser.createTime }}</el-descriptions-item>
       </el-descriptions>
     </el-dialog>
+
+    <!-- 修改对话框 -->
+    <el-dialog v-model="formDialog.visible" title="修改用户" width="500px" append-to-body>
+      <el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
+        <el-form-item label="昵称" prop="nickname">
+          <el-input v-model="form.nickname" placeholder="请输入昵称" />
+        </el-form-item>
+        <el-form-item label="手机号" prop="phone">
+          <el-input v-model="form.phone" placeholder="请输入手机号" maxlength="11" />
+        </el-form-item>
+        <el-form-item label="状态" prop="status">
+          <el-select v-model="form.status" placeholder="请选择状态">
+            <el-option label="正常" :value="0" />
+            <el-option label="禁用" :value="1" />
+            <el-option label="管理员" :value="2" />
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="formDialog.visible = false">取消</el-button>
+        <el-button type="primary" @click="submitForm">确定</el-button>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
 <script setup name="MiniappUser" lang="ts">
-import { ref, onMounted, getCurrentInstance } from 'vue';
-import { listMiniappUser, getMiniappUser, changeUserStatus } from '@/api/miniapp/user';
+import { ref, onMounted, getCurrentInstance, reactive } from 'vue';
+import { listMiniappUser, getMiniappUser, updateMiniappUser, delMiniappUser, changeUserStatus } from '@/api/miniapp/user';
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 
-const defaultAvatar = 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png';
 const userList = ref<any[]>([]);
 const loading = ref(true);
+const refreshing = ref(false);
 const showSearch = ref(true);
 const total = ref(0);
+const ids = ref<number[]>([]);
+const single = ref(true);
+const multiple = ref(true);
+const selectedRow = ref<any>({});
 
 const queryFormRef = ref<ElFormInstance>();
+const formRef = ref<ElFormInstance>();
 
 const queryParams = ref({
   pageNum: 1,
@@ -115,6 +179,27 @@ const detailDialog = ref({
   visible: false
 });
 
+const formDialog = ref({
+  visible: false,
+  title: ''
+});
+
+const form = ref<any>({
+  id: undefined,
+  nickname: '',
+  phone: '',
+  status: 0
+});
+
+const rules = reactive({
+  nickname: [{ required: true, message: '昵称不能为空', trigger: 'blur' }],
+  phone: [
+    { required: true, message: '手机号不能为空', trigger: 'blur' },
+    { pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
+  ],
+  status: [{ required: true, message: '请选择状态', trigger: 'change' }]
+});
+
 const currentUser = ref<any>({});
 
 /** 查询用户列表 */
@@ -134,6 +219,16 @@ const getList = async () => {
   }
 };
 
+/** 手动刷新 */
+const handleRefresh = async () => {
+  refreshing.value = true;
+  try {
+    await getList();
+  } finally {
+    refreshing.value = false;
+  }
+};
+
 /** 搜索按钮操作 */
 const handleQuery = () => {
   queryParams.value.pageNum = 1;
@@ -147,6 +242,75 @@ const resetQuery = () => {
   handleQuery();
 };
 
+/** 多选框选中数据 */
+const handleSelectionChange = (selection: any[]) => {
+  ids.value = selection.map(item => item.id);
+  single.value = selection.length !== 1;
+  multiple.value = !selection.length;
+  if (selection.length === 1) {
+    selectedRow.value = selection[0];
+  }
+};
+
+/** 批量删除 */
+const handleBatchDelete = async () => {
+  if (ids.value.length === 0) {
+    proxy?.$modal.msgWarning('请选择要删除的用户');
+    return;
+  }
+  try {
+    await proxy?.$modal.confirm(`确认要删除选中的${ids.value.length}个用户吗?`);
+    for (const id of ids.value) {
+      await delMiniappUser(id);
+    }
+    proxy?.$modal.msgSuccess('删除成功');
+    getList();
+  } catch {
+    // 取消操作
+  }
+};
+
+/** 重置表单 */
+const resetForm = () => {
+  form.value = {
+    id: undefined,
+    nickname: '',
+    phone: '',
+    status: 0
+  };
+  formRef.value?.resetFields();
+};
+
+/** 修改按钮操作 */
+const handleUpdate = async (row: any) => {
+  resetForm();
+  const res = await getMiniappUser(row.id);
+  form.value = { ...res.data };
+  formDialog.value.visible = true;
+  formDialog.value.title = '修改用户';
+};
+
+/** 提交表单 */
+const submitForm = async () => {
+  await formRef.value?.validate();
+  await updateMiniappUser(form.value);
+  proxy?.$modal.msgSuccess('修改成功');
+  formDialog.value.visible = false;
+  getList();
+};
+
+/** 删除按钮操作 */
+const handleDelete = async (row: any) => {
+  try {
+    await proxy?.$modal.confirm(`确认要删除用户"${row.nickname}"吗?`);
+    await delMiniappUser(row.id);
+    proxy?.$modal.msgSuccess('删除成功');
+    getList();
+  } catch {
+    // 取消操作
+  }
+};
+
 /** 用户状态修改 */
 const handleStatusChange = async (row: any) => {
   const text = row.status === 0 ? '启用' : '禁用';
@@ -159,6 +323,68 @@ const handleStatusChange = async (row: any) => {
   }
 };
 
+/** 切换用户状态(启用/禁用) */
+const handleToggleStatus = async (row: any) => {
+  const newStatus = row.status === 0 ? 1 : 0;
+  const text = newStatus === 0 ? '启用' : '禁用';
+  try {
+    await proxy?.$modal.confirm(`确认要${text}用户"${row.nickname}"吗?`);
+    await changeUserStatus(row.id, newStatus);
+    proxy?.$modal.msgSuccess(`${text}成功`);
+    getList();
+  } catch {
+    // 取消操作
+  }
+};
+
+/** 设为管理员 */
+const handleSetAdmin = async (row: any) => {
+  try {
+    await proxy?.$modal.confirm(`确认要将用户"${row.nickname}"设为管理员吗?`);
+    await changeUserStatus(row.id, 2);
+    proxy?.$modal.msgSuccess('设置成功');
+    getList();
+  } catch {
+    // 取消操作
+  }
+};
+
+/** 取消管理员 */
+const handleCancelAdmin = async (row: any) => {
+  try {
+    await proxy?.$modal.confirm(`确认要取消用户"${row.nickname}"的管理员权限吗?`);
+    await changeUserStatus(row.id, 0);
+    proxy?.$modal.msgSuccess('取消成功');
+    getList();
+  } catch {
+    // 取消操作
+  }
+};
+
+/** 禁用用户 */
+const handleDisable = async (row: any) => {
+  try {
+    await proxy?.$modal.confirm(`确认要禁用用户"${row.nickname}"吗?禁用后该用户将无法使用小程序。`);
+    await changeUserStatus(row.id, 1);
+    proxy?.$modal.msgSuccess('禁用成功');
+    getList();
+  } catch {
+    // 取消操作
+  }
+};
+
+/** 启用用户 */
+const handleEnable = async (row: any) => {
+  try {
+    await proxy?.$modal.confirm(`确认要启用用户"${row.nickname}"吗?`);
+    await changeUserStatus(row.id, 0);
+    proxy?.$modal.msgSuccess('启用成功');
+    getList();
+  } catch {
+    // 取消操作
+  }
+};
+
 /** 查看详情 */
 const handleView = async (row: any) => {
   const res = await getMiniappUser(row.id);

+ 173 - 0
src/views/settings/paymentConfig/index.vue

@@ -0,0 +1,173 @@
+<template>
+  <div class="p-2">
+    <el-card shadow="hover">
+      <template #header>
+        <div class="card-header">
+          <span>支付配置</span>
+          <el-button type="primary" @click="handleSave" :loading="saving">保存配置</el-button>
+        </div>
+      </template>
+
+      <el-form :model="form" label-width="140px" v-loading="loading">
+        <!-- 微信支付商户配置 -->
+        <el-divider content-position="left">微信小程序支付</el-divider>
+        
+        <el-form-item label="商户号ID">
+          <el-input v-model="form.mchId" placeholder="请输入微信支付商户号(MCHID)" style="width: 400px" />
+        </el-form-item>
+        
+        <el-form-item label="商户号API密钥">
+          <el-input v-model="form.apiV3Key" placeholder="请输入微信支付商户API密钥(APIv3密钥)" 
+                    type="password" show-password style="width: 400px" />
+        </el-form-item>
+        
+        <el-form-item label="商户API证书序列号">
+          <el-input v-model="form.mchSerialNo" placeholder="请输入商户API证书序列号" style="width: 400px" />
+        </el-form-item>
+        
+        <el-form-item label="商户私钥">
+          <div>
+            <el-upload
+              action="#"
+              :auto-upload="false"
+              :show-file-list="false"
+              accept=".pem"
+              :on-change="handlePrivateKeyChange"
+            >
+              <el-button type="primary">上传私钥文件</el-button>
+            </el-upload>
+            <div class="tip-text">微信支付证书私钥(apiclient_key.pem),前往微信商家平台下载</div>
+            <div v-if="form.privateKeyUploaded" class="success-text">✓ 私钥已上传</div>
+          </div>
+        </el-form-item>
+        
+        <el-form-item label="API回调地址">
+          <el-input v-model="form.notifyUrl" placeholder="请输入支付回调地址,如:https://域名/v1/order/notify" style="width: 400px" />
+        </el-form-item>
+
+        <!-- 订阅价格配置 -->
+        <el-divider content-position="left">超短池配置</el-divider>
+        <el-form-item label="订阅价格(元)">
+          <el-input-number v-model="form.shortPrice" :min="0" :precision="2" :step="1" />
+          <span class="ml-2 tip-text">有效期:当日24:00</span>
+        </el-form-item>
+
+        <el-divider content-position="left">强势池配置</el-divider>
+        <el-form-item label="订阅价格(元)">
+          <el-input-number v-model="form.strongPrice" :min="0" :precision="2" :step="1" />
+          <span class="ml-2 tip-text">有效期:1年</span>
+        </el-form-item>
+      </el-form>
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue'
+import { ElMessage } from 'element-plus'
+import request from '@/utils/request'
+
+const loading = ref(false)
+const saving = ref(false)
+const privateKeyFile = ref<File | null>(null)
+
+const form = ref({
+  mchId: '',
+  apiV3Key: '',
+  mchSerialNo: '',
+  notifyUrl: '',
+  shortPrice: 18,
+  strongPrice: 998,
+  privateKeyUploaded: false
+})
+
+// 处理私钥文件选择
+const handlePrivateKeyChange = (file: any) => {
+  privateKeyFile.value = file.raw
+  ElMessage.success('私钥文件已选择,保存时将一并上传')
+}
+
+// 获取配置
+const getConfig = async () => {
+  loading.value = true
+  try {
+    const res = await request.get('/miniapp/paymentConfig/list')
+    if (res.code === 200 && res.data) {
+      form.value.mchId = res.data.mchId || ''
+      form.value.apiV3Key = res.data.apiV3Key || ''
+      form.value.mchSerialNo = res.data.mchSerialNo || ''
+      form.value.notifyUrl = res.data.notifyUrl || ''
+      form.value.shortPrice = res.data.shortPrice || 18
+      form.value.strongPrice = res.data.strongPrice || 998
+      form.value.privateKeyUploaded = res.data.privateKeyUploaded || false
+    }
+  } catch (e) {
+    console.error('获取配置失败', e)
+  } finally {
+    loading.value = false
+  }
+}
+
+// 保存配置
+const handleSave = async () => {
+  saving.value = true
+  try {
+    // 如果有私钥文件,先上传
+    if (privateKeyFile.value) {
+      const formData = new FormData()
+      formData.append('file', privateKeyFile.value)
+      const uploadRes = await request.post('/miniapp/paymentConfig/uploadPrivateKey', formData, {
+        headers: { 'Content-Type': 'multipart/form-data' }
+      })
+      if (uploadRes.code !== 200) {
+        ElMessage.error('私钥上传失败:' + uploadRes.msg)
+        saving.value = false
+        return
+      }
+      form.value.privateKeyUploaded = true
+      privateKeyFile.value = null
+    }
+    
+    // 保存其他配置
+    const res = await request.put('/miniapp/paymentConfig', {
+      mchId: form.value.mchId,
+      apiV3Key: form.value.apiV3Key,
+      mchSerialNo: form.value.mchSerialNo,
+      notifyUrl: form.value.notifyUrl,
+      shortPrice: form.value.shortPrice,
+      strongPrice: form.value.strongPrice
+    })
+    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;
+}
+.tip-text {
+  color: #909399;
+  font-size: 12px;
+  margin-top: 4px;
+}
+.success-text {
+  color: #67c23a;
+  font-size: 12px;
+  margin-top: 4px;
+}
+</style>

+ 89 - 0
src/views/settings/paymentConfig/subscription.vue

@@ -0,0 +1,89 @@
+<template>
+  <div class="p-2">
+    <el-card shadow="hover">
+      <template #header>
+        <div class="card-header">
+          <span>订阅价格配置</span>
+          <el-button type="primary" @click="handleSave" :loading="saving">保存配置</el-button>
+        </div>
+      </template>
+
+      <el-form :model="form" label-width="140px" v-loading="loading">
+        <el-divider content-position="left">超短池配置</el-divider>
+        <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-input-number v-model="form.strongPrice" :min="0" :precision="2" :step="1" />
+        </el-form-item>
+      </el-form>
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue'
+import { ElMessage } from 'element-plus'
+import request from '@/utils/request'
+
+const loading = ref(false)
+const saving = ref(false)
+
+const form = ref({
+  shortPrice: 1,
+  strongPrice: 100
+})
+
+const getConfig = async () => {
+  loading.value = true
+  try {
+    const res = await request.get('/miniapp/paymentConfig/list')
+    if (res.code === 200 && res.data) {
+      form.value.shortPrice = res.data.shortPrice || 1
+      form.value.strongPrice = res.data.strongPrice || 100
+    }
+  } catch (e) {
+    console.error('获取配置失败', e)
+  } finally {
+    loading.value = false
+  }
+}
+
+const handleSave = async () => {
+  saving.value = true
+  try {
+    const res = await request.put('/miniapp/paymentConfig/subscription', {
+      shortPrice: form.value.shortPrice,
+      strongPrice: form.value.strongPrice
+    })
+    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;
+}
+.tip-text {
+  color: #909399;
+  font-size: 12px;
+  margin-top: 4px;
+}
+</style>

+ 180 - 0
src/views/settings/paymentConfig/wxpay.vue

@@ -0,0 +1,180 @@
+<template>
+  <div class="p-2">
+    <el-card shadow="hover">
+      <template #header>
+        <div class="card-header">
+          <span>微信小程序支付配置</span>
+          <el-button type="primary" @click="handleSave" :loading="saving">保存配置</el-button>
+        </div>
+      </template>
+
+      <el-form :model="form" label-width="140px" v-loading="loading">
+        <el-form-item label="商户号ID">
+          <el-input v-model="form.mchId" placeholder="请输入微信支付商户号(MCHID)" style="width: 400px" />
+        </el-form-item>
+        
+        <el-form-item label="商户号API密钥">
+          <el-input v-model="form.apiV3Key" placeholder="请输入微信支付商户API密钥(APIv3密钥)" 
+                    type="password" show-password style="width: 400px" />
+        </el-form-item>
+
+        <el-form-item label="支付证书">
+          <div>
+            <el-upload
+              action="#"
+              :auto-upload="false"
+              :show-file-list="false"
+              accept=".pem"
+              :on-change="handleCertChange"
+            >
+              <el-button type="primary">上传证书文件</el-button>
+            </el-upload>
+            <div class="tip-text">微信支付证书(apiclient_cert.pem),前往微信商家平台下载</div>
+            <div v-if="form.certUploaded" class="success-text">✓ 证书已上传</div>
+          </div>
+        </el-form-item>
+        
+        <el-form-item label="商户私钥">
+          <div>
+            <el-upload
+              action="#"
+              :auto-upload="false"
+              :show-file-list="false"
+              accept=".pem"
+              :on-change="handlePrivateKeyChange"
+            >
+              <el-button type="primary">上传私钥文件</el-button>
+            </el-upload>
+            <div class="tip-text">微信支付证书私钥(apiclient_key.pem),前往微信商家平台下载</div>
+            <div v-if="form.privateKeyUploaded" class="success-text">✓ 私钥已上传</div>
+          </div>
+        </el-form-item>
+        
+        <el-form-item label="API回调地址">
+          <el-input v-model="form.notifyUrl" placeholder="请输入支付回调地址,如:https://域名/v1/order/notify" style="width: 400px" />
+        </el-form-item>
+      </el-form>
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue'
+import { ElMessage } from 'element-plus'
+import request from '@/utils/request'
+
+const loading = ref(false)
+const saving = ref(false)
+const privateKeyFile = ref<File | null>(null)
+const certFile = ref<File | null>(null)
+
+const form = ref({
+  mchId: '',
+  apiV3Key: '',
+  notifyUrl: '',
+  privateKeyUploaded: false,
+  certUploaded: false
+})
+
+const handlePrivateKeyChange = (file: any) => {
+  privateKeyFile.value = file.raw
+  ElMessage.success('私钥文件已选择,保存时将一并上传')
+}
+
+const handleCertChange = (file: any) => {
+  certFile.value = file.raw
+  ElMessage.success('证书文件已选择,保存时将一并上传')
+}
+
+const getConfig = async () => {
+  loading.value = true
+  try {
+    const res = await request.get('/miniapp/paymentConfig/list')
+    if (res.code === 200 && res.data) {
+      form.value.mchId = res.data.mchId || ''
+      form.value.apiV3Key = res.data.apiV3Key || ''
+      form.value.notifyUrl = res.data.notifyUrl || ''
+      form.value.privateKeyUploaded = res.data.privateKeyUploaded || false
+      form.value.certUploaded = res.data.certUploaded || false
+    }
+  } catch (e) {
+    console.error('获取配置失败', e)
+  } finally {
+    loading.value = false
+  }
+}
+
+const handleSave = async () => {
+  saving.value = true
+  try {
+    // 上传私钥
+    if (privateKeyFile.value) {
+      const formData = new FormData()
+      formData.append('file', privateKeyFile.value)
+      const uploadRes = await request.post('/miniapp/paymentConfig/uploadPrivateKey', formData, {
+        headers: { 'Content-Type': 'multipart/form-data' }
+      })
+      if (uploadRes.code !== 200) {
+        ElMessage.error('私钥上传失败:' + uploadRes.msg)
+        saving.value = false
+        return
+      }
+      form.value.privateKeyUploaded = true
+      privateKeyFile.value = null
+    }
+
+    // 上传证书
+    if (certFile.value) {
+      const formData = new FormData()
+      formData.append('file', certFile.value)
+      const uploadRes = await request.post('/miniapp/paymentConfig/uploadCert', formData, {
+        headers: { 'Content-Type': 'multipart/form-data' }
+      })
+      if (uploadRes.code !== 200) {
+        ElMessage.error('证书上传失败:' + uploadRes.msg)
+        saving.value = false
+        return
+      }
+      form.value.certUploaded = true
+      certFile.value = null
+    }
+    
+    const res = await request.put('/miniapp/paymentConfig/wxpay', {
+      mchId: form.value.mchId,
+      apiV3Key: form.value.apiV3Key,
+      notifyUrl: form.value.notifyUrl
+    })
+    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;
+}
+.tip-text {
+  color: #909399;
+  font-size: 12px;
+  margin-top: 4px;
+}
+.success-text {
+  color: #67c23a;
+  font-size: 12px;
+  margin-top: 4px;
+}
+</style>

+ 0 - 0
src/views/settings/wxPayConfig/index.vue


+ 28 - 7
src/views/stock/info/index.vue

@@ -38,10 +38,10 @@
             <el-button v-has-permi="['stock:info:remove']" type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete">删除</el-button>
           </el-col>
           <el-col :span="1.5">
-            <el-button type="info" plain icon="Refresh" @click="refreshQuotes" :loading="refreshing">刷新行情</el-button>
+            <el-button v-has-permi="['stock:info:export']" type="warning" plain icon="Download" @click="handleExport" :loading="exporting">导出</el-button>
           </el-col>
           <el-col :span="1.5">
-            <el-button v-has-permi="['stock:info:export']" type="warning" plain icon="Download" @click="handleExport" :loading="exporting">导出</el-button>
+            <el-button type="info" plain icon="Refresh" @click="refreshQuotes" :loading="refreshing">刷新行情</el-button>
           </el-col>
           <right-toolbar v-model:show-search="showSearch" @query-table="getList"></right-toolbar>
         </el-row>
@@ -138,7 +138,7 @@
 
 
 <script setup name="StockInfo" lang="ts">
-import { ref, reactive, onMounted, onUnmounted, onActivated, getCurrentInstance } from 'vue';
+import { ref, reactive, onMounted, onUnmounted, onActivated, onDeactivated, getCurrentInstance } from 'vue';
 import { listStockInfo, getStockInfo, addStockInfo, updateStockInfo, delStockInfo, addToPool, removeFromPool } from '@/api/stock/info';
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
@@ -154,6 +154,7 @@ const multiple = ref(true);
 const total = ref(0);
 
 let refreshTimer: ReturnType<typeof setTimeout> | null = null;
+let isPageVisible = true; // 页面是否可见
 
 const queryFormRef = ref<ElFormInstance>();
 const stockFormRef = ref<ElFormInstance>();
@@ -169,22 +170,24 @@ const queryParams = ref({
 const dialog = reactive({ visible: false, title: '' });
 const form = ref<any>({});
 
-const rules = reactive({
+const rules: Record<string, any[]> = {
   stockCode: [
     { required: true, message: '股票代码不能为空', trigger: 'blur' },
     { pattern: /^\d{6}$/, message: '股票代码必须是6位数字', trigger: 'blur' }
   ],
   stockName: [{ required: true, message: '股票名称不能为空', trigger: 'blur' }],
   market: [{ required: true, message: '市场不能为空', trigger: 'change' }]
-});
+};
 
 /** 获取随机刷新间隔 (2000-3000ms) */
 const getRandomInterval = () => 2000 + Math.random() * 1000;
 
 /** 启动自动刷新 */
 const startAutoRefresh = () => {
+  if (!isPageVisible) return; // 页面不可见时不启动
   stopAutoRefresh();
   refreshTimer = setTimeout(async () => {
+    if (!isPageVisible) return; // 再次检查
     await refreshQuotesOnly();
     startAutoRefresh();
   }, getRandomInterval());
@@ -198,6 +201,17 @@ const stopAutoRefresh = () => {
   }
 };
 
+/** 处理页面可见性变化 */
+const handleVisibilityChange = () => {
+  if (document.hidden) {
+    isPageVisible = false;
+    stopAutoRefresh();
+  } else {
+    isPageVisible = true;
+    startAutoRefresh();
+  }
+};
+
 /** 查询股票信息列表(首次加载或翻页) */
 const getList = async () => {
   loading.value = true;
@@ -223,7 +237,7 @@ const refreshQuotesOnly = async () => {
     const res = await listStockInfo(queryParams.value);
     const newData = res.rows || [];
     // 局部更新行情字段
-    stockList.value.forEach((item, index) => {
+    stockList.value.forEach((item) => {
       const newItem = newData.find((n: any) => n.id === item.id);
       if (newItem) {
         item.currentPrice = newItem.currentPrice;
@@ -386,15 +400,22 @@ const reset = () => {
 };
 
 onMounted(() => {
+  document.addEventListener('visibilitychange', handleVisibilityChange);
   getList();
 });
 
 onActivated(() => {
-  // 页面被激活时刷新数据
+  isPageVisible = true;
   getList();
 });
 
+onDeactivated(() => {
+  isPageVisible = false;
+  stopAutoRefresh();
+});
+
 onUnmounted(() => {
+  document.removeEventListener('visibilitychange', handleVisibilityChange);
   stopAutoRefresh();
 });
 </script>

+ 0 - 0
src/views/stock/paymentConfig/index.vue


+ 34 - 99
src/views/stock/pool/index.vue → src/views/stock/shortPool/index.vue

@@ -11,12 +11,6 @@
             <el-form-item label="股票名称" prop="stockName">
               <el-input v-model="queryParams.stockName" placeholder="请输入股票名称" clearable @keyup.enter="handleQuery" />
             </el-form-item>
-            <el-form-item label="池类型" prop="poolType">
-              <el-select v-model="queryParams.poolType" placeholder="请选择池类型" clearable>
-                <el-option label="超短池" :value="1" />
-                <el-option label="强势池" :value="2" />
-              </el-select>
-            </el-form-item>
             <el-form-item label="状态" prop="status">
               <el-select v-model="queryParams.status" placeholder="请选择状态" clearable>
                 <el-option label="有效" :value="1" />
@@ -46,18 +40,15 @@
     <el-card shadow="hover">
       <template #header>
         <el-row :gutter="10" class="mb-2">
-          <el-col :span="1.5">
-            <el-button v-has-permi="['stock:info:add']" type="primary" plain icon="Plus" @click="handleAddStock">添加股票</el-button>
-          </el-col>
-          <el-col :span="1.5">
-            <el-button type="success" plain icon="Refresh" @click="refreshQuotes" :loading="refreshing">刷新行情</el-button>
-          </el-col>
           <el-col :span="1.5">
             <el-button v-has-permi="['stock:pool:remove']" type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete">移除</el-button>
           </el-col>
           <el-col :span="1.5">
             <el-button v-has-permi="['stock:pool:export']" type="warning" plain icon="Download" @click="handleExport" :loading="exporting">导出</el-button>
           </el-col>
+          <el-col :span="1.5">
+            <el-button type="info" plain icon="Refresh" @click="refreshQuotes" :loading="refreshing">刷新行情</el-button>
+          </el-col>
           <right-toolbar v-model:show-search="showSearch" @query-table="getList"></right-toolbar>
         </el-row>
       </template>
@@ -66,11 +57,6 @@
         <el-table-column type="selection" width="50" align="center" />
         <el-table-column label="股票代码" align="center" prop="stockCode" />
         <el-table-column label="股票名称" align="center" prop="stockName" />
-        <el-table-column label="池类型" align="center" prop="poolTypeName">
-          <template #default="scope">
-            <el-tag :type="scope.row.poolType === 1 ? 'danger' : 'warning'">{{ scope.row.poolTypeName }}</el-tag>
-          </template>
-        </el-table-column>
         <el-table-column label="当前价" align="center" prop="currentPrice">
           <template #default="scope">
             <span :class="getPriceClass(scope.row)">{{ formatPrice(scope.row.currentPrice) }}</span>
@@ -112,36 +98,12 @@
 
       <pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
     </el-card>
-
-    <!-- 添加股票到stock_info对话框 -->
-    <el-dialog v-model="dialog.visible" :title="dialog.title" width="500px" append-to-body>
-      <el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
-        <el-form-item label="股票代码" prop="stockCode">
-          <el-input v-model="form.stockCode" placeholder="请输入6位股票代码" maxlength="6" />
-        </el-form-item>
-        <el-form-item label="股票名称" prop="stockName">
-          <el-input v-model="form.stockName" placeholder="请输入股票名称" maxlength="64" />
-        </el-form-item>
-        <el-form-item label="市场" prop="market">
-          <el-select v-model="form.market" placeholder="请选择市场">
-            <el-option label="上海" value="SH" />
-            <el-option label="深圳" value="SZ" />
-          </el-select>
-        </el-form-item>
-      </el-form>
-      <template #footer>
-        <el-button type="primary" @click="submitForm">确 定</el-button>
-        <el-button @click="cancel">取 消</el-button>
-      </template>
-    </el-dialog>
   </div>
 </template>
 
-
-<script setup name="StockPool" lang="ts">
-import { ref, reactive, onMounted, onUnmounted, onActivated, getCurrentInstance } from 'vue';
+<script setup name="ShortPool" lang="ts">
+import { ref, onMounted, onUnmounted, onActivated, onDeactivated, getCurrentInstance } from 'vue';
 import { listStockPool, removeFromPool } from '@/api/stock/pool';
-import { addStockInfo } from '@/api/stock/info';
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 
@@ -155,10 +117,9 @@ const multiple = ref(true);
 const total = ref(0);
 
 let refreshTimer: ReturnType<typeof setTimeout> | null = null;
+let isPageVisible = true; // 页面是否可见
 
 const queryFormRef = ref<ElFormInstance>();
-const formRef = ref<ElFormInstance>();
-
 const dateRange = ref<string[]>([]);
 
 const queryParams = ref({
@@ -166,31 +127,22 @@ const queryParams = ref({
   pageSize: 20,
   stockCode: '',
   stockName: '',
-  poolType: undefined as number | undefined,
+  poolType: 1,  // 固定为超短池
   status: 1 as number | undefined,
   startDate: undefined as string | undefined,
   endDate: undefined as string | undefined
 });
 
-const dialog = reactive({ visible: false, title: '' });
-const form = ref<any>({});
-
-const rules = {
-  stockCode: [
-    { required: true, message: '股票代码不能为空', trigger: 'blur' },
-    { pattern: /^\d{6}$/, message: '股票代码必须是6位数字', trigger: 'blur' }
-  ],
-  stockName: [{ required: true, message: '股票名称不能为空', trigger: 'blur' }],
-  market: [{ required: true, message: '市场不能为空', trigger: 'change' }]
-};
 
 /** 获取随机刷新间隔 (2000-3000ms) */
 const getRandomInterval = () => 2000 + Math.random() * 1000;
 
 /** 启动自动刷新 */
 const startAutoRefresh = () => {
+  if (!isPageVisible) return; // 页面不可见时不启动
   stopAutoRefresh();
   refreshTimer = setTimeout(async () => {
+    if (!isPageVisible) return; // 再次检查
     await refreshQuotesOnly();
     startAutoRefresh();
   }, getRandomInterval());
@@ -204,7 +156,18 @@ const stopAutoRefresh = () => {
   }
 };
 
-/** 查询列表(首次加载或翻页) */
+/** 处理页面可见性变化 */
+const handleVisibilityChange = () => {
+  if (document.hidden) {
+    isPageVisible = false;
+    stopAutoRefresh();
+  } else {
+    isPageVisible = true;
+    startAutoRefresh();
+  }
+};
+
+/** 查询列表 */
 const getList = async () => {
   loading.value = true;
   stopAutoRefresh();
@@ -214,28 +177,25 @@ const getList = async () => {
     total.value = res.total || 0;
     startAutoRefresh();
   } catch (error) {
-    console.error('获取股票池列表失败:', error);
+    console.error('获取超短池列表失败:', error);
   } finally {
     loading.value = false;
   }
 };
 
-/** 局部刷新行情数据(不显示loading) */
+/** 局部刷新行情数据 */
 const refreshQuotesOnly = async () => {
   if (poolList.value.length === 0) return;
   try {
     const res = await listStockPool(queryParams.value);
     const newData = res.rows || [];
-    // 局部更新行情字段
     poolList.value.forEach((item) => {
       const newItem = newData.find((n: any) => n.id === item.id);
       if (newItem) {
         item.currentPrice = newItem.currentPrice;
         item.changePercent = newItem.changePercent;
-        item.changeAmount = newItem.changeAmount;
         item.turnoverRate = newItem.turnoverRate;
         item.tradeAmount = newItem.tradeAmount;
-        item.yesterdayClose = newItem.yesterdayClose;
         item.profitPercent = newItem.profitPercent;
       }
     });
@@ -256,16 +216,16 @@ const refreshQuotes = async () => {
   }
 };
 
-/** 导出(获取全表数据) */
+/** 导出 */
 const handleExport = async () => {
   exporting.value = true;
   try {
     proxy?.download('stock/pool/export', {
       stockCode: queryParams.value.stockCode,
       stockName: queryParams.value.stockName,
-      poolType: queryParams.value.poolType,
+      poolType: 1,
       status: queryParams.value.status
-    }, `stock_pool_${new Date().getTime()}.xlsx`);
+    }, `short_pool_${new Date().getTime()}.xlsx`);
   } finally {
     setTimeout(() => { exporting.value = false; }, 1000);
   }
@@ -274,7 +234,6 @@ const handleExport = async () => {
 /** 搜索按钮 */
 const handleQuery = () => {
   queryParams.value.pageNum = 1;
-  // 处理日期范围
   if (dateRange.value && dateRange.value.length === 2) {
     queryParams.value.startDate = dateRange.value[0];
     queryParams.value.endDate = dateRange.value[1];
@@ -289,7 +248,6 @@ const handleQuery = () => {
 const resetQuery = () => {
   queryFormRef.value?.resetFields();
   dateRange.value = [];
-  queryParams.value.poolType = undefined;
   queryParams.value.status = 1;
   queryParams.value.startDate = undefined;
   queryParams.value.endDate = undefined;
@@ -302,24 +260,6 @@ const handleSelectionChange = (selection: any[]) => {
   multiple.value = !selection.length;
 };
 
-/** 添加股票按钮(添加到stock_info表) */
-const handleAddStock = () => {
-  reset();
-  dialog.visible = true;
-  dialog.title = '添加股票信息';
-};
-
-/** 提交表单(添加股票到stock_info) */
-const submitForm = () => {
-  formRef.value?.validate(async (valid: boolean) => {
-    if (valid) {
-      await addStockInfo(form.value);
-      proxy?.$modal.msgSuccess('添加成功');
-      dialog.visible = false;
-    }
-  });
-};
-
 /** 删除按钮 */
 const handleDelete = async (row?: any) => {
   const poolIds = row?.id ? [row.id] : ids.value;
@@ -329,18 +269,6 @@ const handleDelete = async (row?: any) => {
   await getList();
 };
 
-/** 取消按钮 */
-const cancel = () => {
-  dialog.visible = false;
-  reset();
-};
-
-/** 表单重置 */
-const reset = () => {
-  form.value = { stockCode: '', stockName: '', market: undefined };
-  formRef.value?.resetFields();
-};
-
 /** 格式化价格 */
 const formatPrice = (val: any) => (val != null ? Number(val).toFixed(2) : '-');
 
@@ -360,15 +288,22 @@ const getChangeClass = (val: any) => {
 };
 
 onMounted(() => {
+  document.addEventListener('visibilitychange', handleVisibilityChange);
   getList();
 });
 
 onActivated(() => {
-  // 页面被激活时(从其他页面切换回来)刷新数据
+  isPageVisible = true;
   getList();
 });
 
+onDeactivated(() => {
+  isPageVisible = false;
+  stopAutoRefresh();
+});
+
 onUnmounted(() => {
+  document.removeEventListener('visibilitychange', handleVisibilityChange);
   stopAutoRefresh();
 });
 </script>

+ 315 - 0
src/views/stock/strongPool/index.vue

@@ -0,0 +1,315 @@
+<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="状态" prop="status">
+              <el-select v-model="queryParams.status" placeholder="请选择状态" clearable>
+                <el-option label="有效" :value="1" />
+                <el-option label="已移除" :value="0" />
+              </el-select>
+            </el-form-item>
+            <el-form-item label="入池时间" prop="dateRange">
+              <el-date-picker
+                v-model="dateRange"
+                type="daterange"
+                range-separator="-"
+                start-placeholder="开始日期"
+                end-placeholder="结束日期"
+                value-format="YYYY-MM-DD"
+                style="width: 240px"
+              />
+            </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" class="mb-2">
+          <el-col :span="1.5">
+            <el-button v-has-permi="['stock:pool:remove']" type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete">移除</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button v-has-permi="['stock:pool:export']" type="warning" plain icon="Download" @click="handleExport" :loading="exporting">导出</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button type="info" plain icon="Refresh" @click="refreshQuotes" :loading="refreshing">刷新行情</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="poolList" @selection-change="handleSelectionChange">
+        <el-table-column type="selection" width="50" align="center" />
+        <el-table-column label="股票代码" align="center" prop="stockCode" />
+        <el-table-column label="股票名称" align="center" prop="stockName" />
+        <el-table-column label="当前价" align="center" prop="currentPrice">
+          <template #default="scope">
+            <span :class="getPriceClass(scope.row)">{{ formatPrice(scope.row.currentPrice) }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="涨跌幅" align="center" prop="changePercent">
+          <template #default="scope">
+            <span :class="getChangeClass(scope.row.changePercent)">{{ formatPercent(scope.row.changePercent) }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="入池价" align="center" prop="addPrice">
+          <template #default="scope">{{ formatPrice(scope.row.addPrice) }}</template>
+        </el-table-column>
+        <el-table-column label="入池收益" align="center" prop="profitPercent">
+          <template #default="scope">
+            <span :class="getChangeClass(scope.row.profitPercent)">{{ formatPercent(scope.row.profitPercent) }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="换手率" align="center" prop="turnoverRate">
+          <template #default="scope">{{ formatPercent(scope.row.turnoverRate) }}</template>
+        </el-table-column>
+        <el-table-column label="成交额(亿)" align="center" prop="tradeAmount">
+          <template #default="scope">{{ scope.row.tradeAmount || '-' }}</template>
+        </el-table-column>
+        <el-table-column label="入池日期" align="center" prop="addDate" />
+        <el-table-column label="状态" align="center" prop="status">
+          <template #default="scope">
+            <el-tag :type="scope.row.status === 1 ? 'success' : 'info'">{{ scope.row.status === 1 ? '有效' : '已移除' }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" align="center" width="100">
+          <template #default="scope">
+            <el-tooltip content="移除" placement="top">
+              <el-button v-has-permi="['stock:pool: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>
+  </div>
+</template>
+
+<script setup name="StrongPool" lang="ts">
+import { ref, onMounted, onUnmounted, onActivated, onDeactivated, getCurrentInstance } from 'vue';
+import { listStockPool, removeFromPool } from '@/api/stock/pool';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+const poolList = ref<any[]>([]);
+const loading = ref(true);
+const refreshing = ref(false);
+const exporting = ref(false);
+const showSearch = ref(true);
+const ids = ref<number[]>([]);
+const multiple = ref(true);
+const total = ref(0);
+
+let refreshTimer: ReturnType<typeof setTimeout> | null = null;
+let isPageVisible = true; // 页面是否可见
+
+const queryFormRef = ref<ElFormInstance>();
+const dateRange = ref<string[]>([]);
+
+const queryParams = ref({
+  pageNum: 1,
+  pageSize: 20,
+  stockCode: '',
+  stockName: '',
+  poolType: 2,  // 固定为强势池
+  status: 1 as number | undefined,
+  startDate: undefined as string | undefined,
+  endDate: undefined as string | undefined
+});
+
+
+/** 获取随机刷新间隔 (2000-3000ms) */
+const getRandomInterval = () => 2000 + Math.random() * 1000;
+
+/** 启动自动刷新 */
+const startAutoRefresh = () => {
+  if (!isPageVisible) return; // 页面不可见时不启动
+  stopAutoRefresh();
+  refreshTimer = setTimeout(async () => {
+    if (!isPageVisible) return; // 再次检查
+    await refreshQuotesOnly();
+    startAutoRefresh();
+  }, getRandomInterval());
+};
+
+/** 停止自动刷新 */
+const stopAutoRefresh = () => {
+  if (refreshTimer) {
+    clearTimeout(refreshTimer);
+    refreshTimer = null;
+  }
+};
+
+/** 处理页面可见性变化 */
+const handleVisibilityChange = () => {
+  if (document.hidden) {
+    isPageVisible = false;
+    stopAutoRefresh();
+  } else {
+    isPageVisible = true;
+    startAutoRefresh();
+  }
+};
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true;
+  stopAutoRefresh();
+  try {
+    const res = await listStockPool(queryParams.value);
+    poolList.value = res.rows || [];
+    total.value = res.total || 0;
+    startAutoRefresh();
+  } catch (error) {
+    console.error('获取强势池列表失败:', error);
+  } finally {
+    loading.value = false;
+  }
+};
+
+/** 局部刷新行情数据 */
+const refreshQuotesOnly = async () => {
+  if (poolList.value.length === 0) return;
+  try {
+    const res = await listStockPool(queryParams.value);
+    const newData = res.rows || [];
+    poolList.value.forEach((item) => {
+      const newItem = newData.find((n: any) => n.id === item.id);
+      if (newItem) {
+        item.currentPrice = newItem.currentPrice;
+        item.changePercent = newItem.changePercent;
+        item.turnoverRate = newItem.turnoverRate;
+        item.tradeAmount = newItem.tradeAmount;
+        item.profitPercent = newItem.profitPercent;
+      }
+    });
+  } catch (error) {
+    console.error('刷新行情失败:', error);
+  }
+};
+
+/** 手动刷新行情 */
+const refreshQuotes = async () => {
+  refreshing.value = true;
+  stopAutoRefresh();
+  try {
+    await refreshQuotesOnly();
+    startAutoRefresh();
+  } finally {
+    refreshing.value = false;
+  }
+};
+
+/** 导出 */
+const handleExport = async () => {
+  exporting.value = true;
+  try {
+    proxy?.download('stock/pool/export', {
+      stockCode: queryParams.value.stockCode,
+      stockName: queryParams.value.stockName,
+      poolType: 2,
+      status: queryParams.value.status
+    }, `strong_pool_${new Date().getTime()}.xlsx`);
+  } finally {
+    setTimeout(() => { exporting.value = false; }, 1000);
+  }
+};
+
+/** 搜索按钮 */
+const handleQuery = () => {
+  queryParams.value.pageNum = 1;
+  if (dateRange.value && dateRange.value.length === 2) {
+    queryParams.value.startDate = dateRange.value[0];
+    queryParams.value.endDate = dateRange.value[1];
+  } else {
+    queryParams.value.startDate = undefined;
+    queryParams.value.endDate = undefined;
+  }
+  getList();
+};
+
+/** 重置按钮 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields();
+  dateRange.value = [];
+  queryParams.value.status = 1;
+  queryParams.value.startDate = undefined;
+  queryParams.value.endDate = undefined;
+  handleQuery();
+};
+
+/** 多选框选中 */
+const handleSelectionChange = (selection: any[]) => {
+  ids.value = selection.map(item => item.id);
+  multiple.value = !selection.length;
+};
+
+/** 删除按钮 */
+const handleDelete = async (row?: any) => {
+  const poolIds = row?.id ? [row.id] : ids.value;
+  await proxy?.$modal.confirm(`是否确认移除选中的股票?`);
+  await removeFromPool(poolIds);
+  proxy?.$modal.msgSuccess('移除成功');
+  await getList();
+};
+
+/** 格式化价格 */
+const formatPrice = (val: any) => (val != null ? Number(val).toFixed(2) : '-');
+
+/** 格式化百分比 */
+const formatPercent = (val: any) => (val != null ? `${Number(val) >= 0 ? '+' : ''}${Number(val).toFixed(2)}%` : '-');
+
+/** 获取价格样式 */
+const getPriceClass = (row: any) => {
+  if (row.changePercent == null) return '';
+  return row.changePercent > 0 ? 'text-red-500' : row.changePercent < 0 ? 'text-green-500' : '';
+};
+
+/** 获取涨跌样式 */
+const getChangeClass = (val: any) => {
+  if (val == null) return '';
+  return val > 0 ? 'text-red-500 font-bold' : val < 0 ? 'text-green-500 font-bold' : '';
+};
+
+onMounted(() => {
+  document.addEventListener('visibilitychange', handleVisibilityChange);
+  getList();
+});
+
+onActivated(() => {
+  isPageVisible = true;
+  getList();
+});
+
+onDeactivated(() => {
+  isPageVisible = false;
+  stopAutoRefresh();
+});
+
+onUnmounted(() => {
+  document.removeEventListener('visibilitychange', handleVisibilityChange);
+  stopAutoRefresh();
+});
+</script>
+
+<style scoped>
+.text-red-500 { color: #ef4444; }
+.text-green-500 { color: #22c55e; }
+.font-bold { font-weight: bold; }
+</style>