Compare commits
2
Commits
4a7a3e76ee
...
8816c32025
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8816c32025 | ||
|
|
4be0853ade |
@@ -26,7 +26,6 @@ Bun 工具链(选型见 `docs/design/技术选型.md`)。**Windows 上 `bun`
|
|||||||
| 命令 | 用途 |
|
| 命令 | 用途 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| `bun install` | 安装 / 链接 workspace 依赖(在仓库根) |
|
| `bun install` | 安装 / 链接 workspace 依赖(在仓库根) |
|
||||||
| `bun test` | 全部测试(core 契约 + server HTTP + **集成**);集成需 `DATABASE_URL`(compose 中的 PG),无库时自动跳过 |
|
|
||||||
| `bun run db:generate` | 生成 SQL 迁移(`packages/db/drizzle/`) |
|
| `bun run db:generate` | 生成 SQL 迁移(`packages/db/drizzle/`) |
|
||||||
| `bun run db:push` | 推送到本地库(开发期) |
|
| `bun run db:push` | 推送到本地库(开发期) |
|
||||||
| `bun run db:studio` | Drizzle Studio 看库 |
|
| `bun run db:studio` | Drizzle Studio 看库 |
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
/**
|
/**
|
||||||
* 订阅选择弹层:登录(仅一次/标签页)+ 列表选用,与登记表单解耦
|
* 订阅选择弹层:登录(仅一次/标签页)+ 列表选用,与登记表单解耦
|
||||||
*/
|
*/
|
||||||
import { computed, ref, watch } from "vue";
|
import { ref, watch } from "vue";
|
||||||
import {
|
import {
|
||||||
listPlatformSubscriptions,
|
listPlatformSubscriptions,
|
||||||
loginPlatform,
|
loginPlatform,
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* 表格工具行刷新钮(纯图标 + tooltip)——放在筛选行下一行、右对齐
|
||||||
|
*/
|
||||||
|
import { ReloadOutlined } from "@antdv-next/icons";
|
||||||
|
|
||||||
|
defineProps<{ loading?: boolean }>();
|
||||||
|
const emit = defineEmits<{ (e: "refresh"): void }>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="table-refresh">
|
||||||
|
<a-tooltip title="刷新">
|
||||||
|
<a-button
|
||||||
|
:loading="loading"
|
||||||
|
aria-label="刷新"
|
||||||
|
@click="emit('refresh')"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<ReloadOutlined />
|
||||||
|
</template>
|
||||||
|
</a-button>
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.table-refresh {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
|
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
|
||||||
import PageHeader from "@/components/common/page-header.vue";
|
import PageHeader from "@/components/common/page-header.vue";
|
||||||
|
import TableRefresh from "@/components/common/table-refresh.vue";
|
||||||
import TimeRangePicker from "@/components/common/time-range-picker.vue";
|
import TimeRangePicker from "@/components/common/time-range-picker.vue";
|
||||||
import { useAuditLogs } from "@/queries/admin.ts";
|
import { useAuditLogs } from "@/queries/admin.ts";
|
||||||
import { formatDateTime, recentDaysRange } from "@/utils/time.ts";
|
import { formatDateTime, recentDaysRange } from "@/utils/time.ts";
|
||||||
@@ -88,6 +89,11 @@ onUnmounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
|
<TableRefresh
|
||||||
|
:loading="auditQuery.isFetching.value"
|
||||||
|
@refresh="() => auditQuery.refetch()"
|
||||||
|
/>
|
||||||
|
|
||||||
<div ref="boxRef" class="audit__box">
|
<div ref="boxRef" class="audit__box">
|
||||||
<a-table
|
<a-table
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { useRoute, useRouter } from "vue-router";
|
|||||||
import type { KeyItem } from "@new-api-key-usage/core";
|
import type { KeyItem } from "@new-api-key-usage/core";
|
||||||
import type { ApiError } from "@/api/http.ts";
|
import type { ApiError } from "@/api/http.ts";
|
||||||
import PageHeader from "@/components/common/page-header.vue";
|
import PageHeader from "@/components/common/page-header.vue";
|
||||||
|
import TableRefresh from "@/components/common/table-refresh.vue";
|
||||||
import RegisterKeyModal from "@/components/admin/register-key-modal.vue";
|
import RegisterKeyModal from "@/components/admin/register-key-modal.vue";
|
||||||
import { useEngine, useKeys, useMembers, usePatchKey } from "@/queries/admin.ts";
|
import { useEngine, useKeys, useMembers, usePatchKey } from "@/queries/admin.ts";
|
||||||
import { formatInt } from "@/utils/format.ts";
|
import { formatInt } from "@/utils/format.ts";
|
||||||
@@ -230,6 +231,11 @@ function onRowClass(record: KeyItem) {
|
|||||||
</a-space>
|
</a-space>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<TableRefresh
|
||||||
|
:loading="keysQuery.isFetching.value"
|
||||||
|
@refresh="() => keysQuery.refetch()"
|
||||||
|
/>
|
||||||
|
|
||||||
<a-empty
|
<a-empty
|
||||||
v-if="!keysQuery.isPending.value && rows.length === 0"
|
v-if="!keysQuery.isPending.value && rows.length === 0"
|
||||||
:description="
|
:description="
|
||||||
|
|||||||
@@ -6,11 +6,12 @@ import { computed, onMounted, onUnmounted, ref, watch } from "vue";
|
|||||||
import type { LogItem } from "@new-api-key-usage/core";
|
import type { LogItem } from "@new-api-key-usage/core";
|
||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import PageHeader from "@/components/common/page-header.vue";
|
import PageHeader from "@/components/common/page-header.vue";
|
||||||
|
import TableRefresh from "@/components/common/table-refresh.vue";
|
||||||
import TimeRangePicker from "@/components/common/time-range-picker.vue";
|
import TimeRangePicker from "@/components/common/time-range-picker.vue";
|
||||||
import QuotaText from "@/components/common/quota-text.vue";
|
import QuotaText from "@/components/common/quota-text.vue";
|
||||||
import { useLiveTimeRange } from "@/composables/use-live-time-range.ts";
|
import { useLiveTimeRange } from "@/composables/use-live-time-range.ts";
|
||||||
import { useAdminLogs, useKeys, useMembers } from "@/queries/admin.ts";
|
import { useAdminLogs, useKeys, useMembers } from "@/queries/admin.ts";
|
||||||
import { formatDateTime, recentDaysRange } from "@/utils/time.ts";
|
import { formatDateTime } from "@/utils/time.ts";
|
||||||
import { formatInt } from "@/utils/format.ts";
|
import { formatInt } from "@/utils/format.ts";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -272,6 +273,11 @@ onUnmounted(() => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<TableRefresh
|
||||||
|
:loading="logsQuery.isFetching.value"
|
||||||
|
@refresh="() => logsQuery.refetch()"
|
||||||
|
/>
|
||||||
|
|
||||||
<a-empty
|
<a-empty
|
||||||
v-if="!logsQuery.isPending.value && rows.length === 0"
|
v-if="!logsQuery.isPending.value && rows.length === 0"
|
||||||
description="该时段内无调用日志"
|
description="该时段内无调用日志"
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const router = useRouter();
|
|||||||
import type { MemberItem } from "@new-api-key-usage/core";
|
import type { MemberItem } from "@new-api-key-usage/core";
|
||||||
import type { ApiError } from "@/api/http.ts";
|
import type { ApiError } from "@/api/http.ts";
|
||||||
import PageHeader from "@/components/common/page-header.vue";
|
import PageHeader from "@/components/common/page-header.vue";
|
||||||
|
import TableRefresh from "@/components/common/table-refresh.vue";
|
||||||
import MemberTokenModal from "@/components/admin/member-token-modal.vue";
|
import MemberTokenModal from "@/components/admin/member-token-modal.vue";
|
||||||
import {
|
import {
|
||||||
useCreateMember,
|
useCreateMember,
|
||||||
@@ -195,6 +196,11 @@ async function onToggle(record: MemberItem) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<TableRefresh
|
||||||
|
:loading="membersQuery.isFetching.value"
|
||||||
|
@refresh="() => membersQuery.refetch()"
|
||||||
|
/>
|
||||||
|
|
||||||
<a-empty
|
<a-empty
|
||||||
v-if="!membersQuery.isPending.value && rows.length === 0"
|
v-if="!membersQuery.isPending.value && rows.length === 0"
|
||||||
:description="hasFilter ? '没有符合筛选的成员' : '还没有成员——为每位分摊朋友开户并发放访问令牌'"
|
:description="hasFilter ? '没有符合筛选的成员' : '还没有成员——为每位分摊朋友开户并发放访问令牌'"
|
||||||
|
|||||||
@@ -2,11 +2,12 @@
|
|||||||
/**
|
/**
|
||||||
* 全员总览(交互设计-全员总览):健康带 · 时间范围 · 按成员 · 按 key
|
* 全员总览(交互设计-全员总览):健康带 · 时间范围 · 按成员 · 按 key
|
||||||
*/
|
*/
|
||||||
import { computed, h, ref } from "vue";
|
import { computed, h } from "vue";
|
||||||
import { Tooltip } from "antdv-next";
|
import { Tooltip } from "antdv-next";
|
||||||
import { QuestionCircleOutlined } from "@antdv-next/icons";
|
import { QuestionCircleOutlined } from "@antdv-next/icons";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import PageHeader from "@/components/common/page-header.vue";
|
import PageHeader from "@/components/common/page-header.vue";
|
||||||
|
import TableRefresh from "@/components/common/table-refresh.vue";
|
||||||
import TimeRangePicker from "@/components/common/time-range-picker.vue";
|
import TimeRangePicker from "@/components/common/time-range-picker.vue";
|
||||||
import QuotaText from "@/components/common/quota-text.vue";
|
import QuotaText from "@/components/common/quota-text.vue";
|
||||||
import SourceBar from "@/components/common/source-bar.vue";
|
import SourceBar from "@/components/common/source-bar.vue";
|
||||||
@@ -217,6 +218,10 @@ const keyColumns = [
|
|||||||
<!-- 按成员 -->
|
<!-- 按成员 -->
|
||||||
<section class="overview-block">
|
<section class="overview-block">
|
||||||
<h2 class="overview-block__title">按成员汇总</h2>
|
<h2 class="overview-block__title">按成员汇总</h2>
|
||||||
|
<TableRefresh
|
||||||
|
:loading="overviewQuery.isFetching.value"
|
||||||
|
@refresh="() => overviewQuery.refetch()"
|
||||||
|
/>
|
||||||
<a-empty
|
<a-empty
|
||||||
v-if="!loading && byMember.length === 0"
|
v-if="!loading && byMember.length === 0"
|
||||||
description="本时段内没有任何消费调用,因此没有成员用量可汇总"
|
description="本时段内没有任何消费调用,因此没有成员用量可汇总"
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type { ApiError } from "@/api/http.ts";
|
|||||||
import { fetchLogs } from "@/api/admin/logs.ts";
|
import { fetchLogs } from "@/api/admin/logs.ts";
|
||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import PageHeader from "@/components/common/page-header.vue";
|
import PageHeader from "@/components/common/page-header.vue";
|
||||||
|
import TableRefresh from "@/components/common/table-refresh.vue";
|
||||||
import PaymentFormModal from "@/components/admin/payment-form-modal.vue";
|
import PaymentFormModal from "@/components/admin/payment-form-modal.vue";
|
||||||
import QuotaText from "@/components/common/quota-text.vue";
|
import QuotaText from "@/components/common/quota-text.vue";
|
||||||
import {
|
import {
|
||||||
@@ -215,6 +216,10 @@ watch(
|
|||||||
</a-alert>
|
</a-alert>
|
||||||
|
|
||||||
<template v-if="tab === 'subs'">
|
<template v-if="tab === 'subs'">
|
||||||
|
<TableRefresh
|
||||||
|
:loading="subsQuery.isFetching.value"
|
||||||
|
@refresh="() => subsQuery.refetch()"
|
||||||
|
/>
|
||||||
<a-empty
|
<a-empty
|
||||||
v-if="!subsQuery.isPending.value && subRows.length === 0"
|
v-if="!subsQuery.isPending.value && subRows.length === 0"
|
||||||
description="还没有订阅登记——从平台选用或手工登记"
|
description="还没有订阅登记——从平台选用或手工登记"
|
||||||
@@ -272,6 +277,10 @@ watch(
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
|
<TableRefresh
|
||||||
|
:loading="topupsQuery.isFetching.value"
|
||||||
|
@refresh="() => topupsQuery.refetch()"
|
||||||
|
/>
|
||||||
<a-empty
|
<a-empty
|
||||||
v-if="!topupsQuery.isPending.value && topRows.length === 0"
|
v-if="!topupsQuery.isPending.value && topRows.length === 0"
|
||||||
description="还没有充值登记"
|
description="还没有充值登记"
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { Tooltip } from "antdv-next";
|
|||||||
import { QuestionCircleOutlined } from "@antdv-next/icons";
|
import { QuestionCircleOutlined } from "@antdv-next/icons";
|
||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import PageHeader from "@/components/common/page-header.vue";
|
import PageHeader from "@/components/common/page-header.vue";
|
||||||
|
import TableRefresh from "@/components/common/table-refresh.vue";
|
||||||
import TimeRangePicker from "@/components/common/time-range-picker.vue";
|
import TimeRangePicker from "@/components/common/time-range-picker.vue";
|
||||||
import QuotaText from "@/components/common/quota-text.vue";
|
import QuotaText from "@/components/common/quota-text.vue";
|
||||||
import { useMemberRange } from "@/composables/use-member-range.ts";
|
import { useMemberRange } from "@/composables/use-member-range.ts";
|
||||||
@@ -275,6 +276,11 @@ watch(
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<TableRefresh
|
||||||
|
:loading="logsQuery.isFetching.value"
|
||||||
|
@refresh="() => logsQuery.refetch()"
|
||||||
|
/>
|
||||||
|
|
||||||
<a-alert
|
<a-alert
|
||||||
v-if="logsQuery.isError.value"
|
v-if="logsQuery.isError.value"
|
||||||
type="error"
|
type="error"
|
||||||
@@ -457,7 +463,7 @@ watch(
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.member-logs__box {
|
.member-logs__box {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import PageHeader from "@/components/common/page-header.vue";
|
import PageHeader from "@/components/common/page-header.vue";
|
||||||
|
import TableRefresh from "@/components/common/table-refresh.vue";
|
||||||
import TimeRangePicker from "@/components/common/time-range-picker.vue";
|
import TimeRangePicker from "@/components/common/time-range-picker.vue";
|
||||||
import SourceBar from "@/components/common/source-bar.vue";
|
import SourceBar from "@/components/common/source-bar.vue";
|
||||||
import QuotaText from "@/components/common/quota-text.vue";
|
import QuotaText from "@/components/common/quota-text.vue";
|
||||||
@@ -131,6 +132,10 @@ const keyColumns = [
|
|||||||
|
|
||||||
<section class="mv-block">
|
<section class="mv-block">
|
||||||
<h2 class="mv-block__title">我的 Key</h2>
|
<h2 class="mv-block__title">我的 Key</h2>
|
||||||
|
<TableRefresh
|
||||||
|
:loading="overviewQuery.isFetching.value"
|
||||||
|
@refresh="() => overviewQuery.refetch()"
|
||||||
|
/>
|
||||||
<a-table
|
<a-table
|
||||||
:columns="keyColumns"
|
:columns="keyColumns"
|
||||||
:data-source="keys"
|
:data-source="keys"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
import PageHeader from "@/components/common/page-header.vue";
|
import PageHeader from "@/components/common/page-header.vue";
|
||||||
|
import TableRefresh from "@/components/common/table-refresh.vue";
|
||||||
import TimeRangePicker from "@/components/common/time-range-picker.vue";
|
import TimeRangePicker from "@/components/common/time-range-picker.vue";
|
||||||
import QuotaText from "@/components/common/quota-text.vue";
|
import QuotaText from "@/components/common/quota-text.vue";
|
||||||
import { useMemberRange } from "@/composables/use-member-range.ts";
|
import { useMemberRange } from "@/composables/use-member-range.ts";
|
||||||
@@ -111,6 +112,10 @@ const topColumns = [
|
|||||||
/>
|
/>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<h3 class="mv-sec">订阅</h3>
|
<h3 class="mv-sec">订阅</h3>
|
||||||
|
<TableRefresh
|
||||||
|
:loading="overviewQuery.isFetching.value"
|
||||||
|
@refresh="() => overviewQuery.refetch()"
|
||||||
|
/>
|
||||||
<a-empty v-if="subs.length === 0" description="暂无订阅登记" />
|
<a-empty v-if="subs.length === 0" description="暂无订阅登记" />
|
||||||
<a-table
|
<a-table
|
||||||
v-else
|
v-else
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
|
// 固定开发口:避免在 5173/5174 被占时静默跳到 5175(strictPort 不自动改口)
|
||||||
|
port: 5175,
|
||||||
|
strictPort: true,
|
||||||
proxy: {
|
proxy: {
|
||||||
// 契约:/api/* 专属接口;SPA 路由不进 /api(公共约定 §2)
|
// 契约:/api/* 专属接口;SPA 路由不进 /api(公共约定 §2)
|
||||||
"/api": {
|
"/api": {
|
||||||
|
|||||||
+5
-1
@@ -8,7 +8,11 @@
|
|||||||
"typecheck": "tsc -p tsconfig.json --noEmit",
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
||||||
"dev:server": "bun --hot --env-file=.env apps/server/src/index.ts",
|
"dev:server": "bun --hot --env-file=.env apps/server/src/index.ts",
|
||||||
"dev:engine": "bun --env-file=.env apps/engine/src/index.ts",
|
"dev:engine": "bun --env-file=.env apps/engine/src/index.ts",
|
||||||
"dev:web": "bun --filter @new-api-key-usage/web dev"
|
"dev:web": "bun run --cwd apps/web dev",
|
||||||
|
"db:generate": "bun run --cwd packages/db db:generate",
|
||||||
|
"db:push": "bun run --cwd packages/db db:push",
|
||||||
|
"db:studio": "bun run --cwd packages/db db:studio",
|
||||||
|
"build:web": "bun run --cwd apps/web build"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "^1.4.2",
|
"@types/bun": "^1.4.2",
|
||||||
|
|||||||
Reference in New Issue
Block a user