20260715221223

This commit is contained in:
oneao committed 2026-07-15 22:12:23 +08:00
1 parent 7049584d1e
commit 704dda07b5
21 files changed
+2649 -427

No files matched your search

@@ -0,0 +1,218 @@
<script setup>
import {
CheckCircleOutlined,
ClockCircleOutlined,
DollarOutlined,
FileTextOutlined,
PlusOutlined,
} from "@antdv-next/icons";
const metrics = [
{ label: "今日新增", value: "0", tone: "blue", icon: FileTextOutlined },
{ label: "待处理", value: "0", tone: "amber", icon: ClockCircleOutlined },
{ label: "本月应收", value: "¥ 0.00", tone: "green", icon: DollarOutlined },
{ label: "本月完成", value: "0", tone: "gray", icon: CheckCircleOutlined },
];
const columns = [
{ title: "单据编号", dataIndex: "id", key: "id" },
{ title: "客户名称", dataIndex: "customer", key: "customer" },
{ title: "业务状态", dataIndex: "status", key: "status" },
{ title: "更新时间", dataIndex: "updatedAt", key: "updatedAt" },
];
</script>
<template>
<div class="dashboard">
<section class="metric-grid" aria-label="业务概览">
<article v-for="item in metrics" :key="item.label" class="metric-card">
<div class="metric-card__copy">
<span>{{ item.label }}</span>
<strong>{{ item.value }}</strong>
</div>
<div class="metric-card__icon" :class="`metric-card__icon--${item.tone}`">
<component :is="item.icon" />
</div>
</article>
</section>
<section class="dashboard-grid">
<div class="panel work-panel">
<div class="panel__header">
<div>
<h2>待办业务</h2>
<span>最近更新</span>
</div>
<a-button type="primary">
<template #icon><PlusOutlined /></template>
新建业务
</a-button>
</div>
<a-table
:columns="columns"
:data-source="[]"
:pagination="false"
size="middle"
:scroll="{ x: 620 }"
/>
</div>
<div class="panel quick-panel">
<div class="panel__header">
<div>
<h2>快捷入口</h2>
<span>常用操作</span>
</div>
</div>
<nav class="quick-links">
<router-link to="/business/orders">
<FileTextOutlined />
<span>业务单据</span>
</router-link>
<router-link to="/fees/receivable">
<DollarOutlined />
<span>应收费用</span>
</router-link>
<router-link to="/reports/operations">
<CheckCircleOutlined />
<span>运营报表</span>
</router-link>
</nav>
</div>
</section>
</div>
</template>
<style lang="scss" scoped>
.dashboard {
display: grid;
gap: 8px;
}
.metric-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 8px;
}
.metric-card {
display: flex;
min-width: 0;
align-items: center;
justify-content: space-between;
padding: 10px 12px;
border: 1px solid var(--fms-border);
border-radius: 4px;
background: var(--fms-surface);
&__copy {
display: flex;
min-width: 0;
flex-direction: column;
span {
color: var(--fms-text-secondary);
font-size: 12px;
}
strong {
margin-top: 4px;
overflow: hidden;
color: var(--fms-text);
font-size: 19px;
line-height: 1.2;
text-overflow: ellipsis;
white-space: nowrap;
}
}
&__icon {
display: grid;
flex: none;
width: 34px;
height: 34px;
place-items: center;
border-radius: 4px;
font-size: 17px;
&--blue {
background: #e6f4ff;
color: #1677ff;
}
&--amber {
background: #fff7e6;
color: #d46b08;
}
&--green {
background: #f0f9eb;
color: #389e0d;
}
&--gray {
background: #f2f3f5;
color: #4e5969;
}
}
}
.dashboard-grid {
display: grid;
grid-template-columns: minmax(0, 2fr) minmax(260px, 1fr);
gap: 8px;
}
.panel {
min-width: 0;
border: 1px solid var(--fms-border);
border-radius: 4px;
background: var(--fms-surface);
&__header {
display: flex;
min-height: 46px;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 7px 10px;
border-bottom: 1px solid var(--fms-border);
h2 {
margin: 0;
color: var(--fms-text);
font-size: 13px;
}
span {
color: var(--fms-text-secondary);
font-size: 11px;
}
}
}
.work-panel :deep(.ant-table-wrapper) {
padding: 0 2px 2px;
}
.quick-links {
display: grid;
padding: 4px;
a {
display: flex;
min-height: 36px;
align-items: center;
gap: 8px;
padding: 5px 7px;
border-radius: 3px;
color: var(--fms-text);
font-size: 12px;
&:hover {
background: var(--fms-hover);
color: #1677ff;
}
}
}
</style>
+140
View File
@@ -0,0 +1,140 @@
<script setup>
import { computed, reactive, ref, watch } from "vue";
import { useRoute } from "vue-router";
import { PlusOutlined, ReloadOutlined, SearchOutlined } from "@antdv-next/icons";
const route = useRoute();
const loading = ref(false);
const rows = ref([]);
const total = ref(0);
const filters = reactive({ keyword: "" });
const pagination = reactive({ current: 1, pageSize: 20 });
const title = computed(() => route.meta.title);
const columns = computed(() => [
{ title: "编号", dataIndex: "id", key: "id", width: 180 },
{ title: "名称", dataIndex: "name", key: "name" },
{ title: "状态", dataIndex: "state", key: "state", width: 120 },
{ title: "更新时间", dataIndex: "updated_at", key: "updated_at", width: 180 },
]);
function loadData() {
loading.value = true;
rows.value = [];
total.value = 0;
loading.value = false;
}
function reset() {
filters.keyword = "";
pagination.current = 1;
loadData();
}
watch(() => route.meta.source, loadData, { immediate: true });
</script>
<template>
<div class="data-page">
<section class="data-toolbar">
<div class="data-toolbar__title">
<h1>{{ title }}</h1>
<span>共 {{ total }} 条</span>
</div>
<a-button type="primary">
<template #icon><PlusOutlined /></template>
新增
</a-button>
</section>
<section class="filter-bar">
<a-input
v-model:value="filters.keyword"
class="keyword-input"
placeholder="输入名称查询"
allow-clear
@press-enter="loadData"
>
<template #prefix><SearchOutlined /></template>
</a-input>
<a-button type="primary" @click="loadData">查询</a-button>
<a-button @click="reset">
<template #icon><ReloadOutlined /></template>
重置
</a-button>
</section>
<section class="table-panel">
<a-table
row-key="id"
:columns="columns"
:data-source="rows"
:loading="loading"
:pagination="{
current: pagination.current,
pageSize: pagination.pageSize,
total,
showSizeChanger: true,
showTotal: (value) => `共 ${value} 条`,
}"
:scroll="{ x: 760 }"
/>
</section>
</div>
</template>
<style lang="scss" scoped>
.data-page {
display: grid;
gap: 8px;
}
.data-toolbar,
.filter-bar {
display: flex;
align-items: center;
}
.data-toolbar {
justify-content: space-between;
gap: 8px;
&__title {
display: flex;
align-items: baseline;
gap: 6px;
h1 {
margin: 0;
color: var(--fms-text);
font-size: 16px;
}
span {
color: var(--fms-text-secondary);
font-size: 11px;
}
}
}
.filter-bar {
flex-wrap: wrap;
gap: 6px;
padding: 8px;
border: 1px solid var(--fms-border);
border-radius: 4px;
background: var(--fms-surface);
}
.keyword-input {
width: 240px;
}
.table-panel {
min-width: 0;
padding: 0;
border: 1px solid var(--fms-border);
border-radius: 4px;
background: var(--fms-surface);
}
</style>
+158
View File
@@ -0,0 +1,158 @@
<script setup>
import { reactive, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import { LockOutlined, UserOutlined } from "@antdv-next/icons";
import { authApi } from "../services/api";
import { useAuthStore } from "../stores/auth";
const route = useRoute();
const router = useRouter();
const authStore = useAuthStore();
const loading = ref(false);
const errorMessage = ref("");
const form = reactive({
orgid: "",
username: "",
password: "",
});
async function submit() {
errorMessage.value = "";
loading.value = true;
try {
const session = await authApi.login(form);
authStore.setSession(session, form.orgid);
await router.replace(route.query.redirect || "/dashboard");
} catch (error) {
errorMessage.value = error.message || "登录失败";
} finally {
loading.value = false;
}
}
</script>
<template>
<main class="login-page">
<section class="login-shell">
<div class="login-brand">
<div class="login-brand__mark">F</div>
<div>
<h1>FMS 管理系统</h1>
<p>机构业务管理平台</p>
</div>
</div>
<a-form layout="vertical" :model="form" @finish="submit">
<a-alert
v-if="errorMessage"
class="login-alert"
type="error"
show-icon
:message="errorMessage"
/>
<a-form-item
label="机构码"
name="orgid"
:rules="[{ required: true, message: '请输入机构码' }]"
>
<a-input v-model:value="form.orgid" size="large" autocomplete="organization" />
</a-form-item>
<a-form-item
label="用户名"
name="username"
:rules="[{ required: true, message: '请输入用户名' }]"
>
<a-input v-model:value="form.username" size="large" autocomplete="username">
<template #prefix><UserOutlined /></template>
</a-input>
</a-form-item>
<a-form-item
label="密码"
name="password"
:rules="[{ required: true, message: '请输入密码' }]"
>
<a-input-password
v-model:value="form.password"
size="large"
autocomplete="current-password"
>
<template #prefix><LockOutlined /></template>
</a-input-password>
</a-form-item>
<a-button type="primary" html-type="submit" size="large" block :loading="loading">
登录
</a-button>
</a-form>
</section>
</main>
</template>
<style lang="scss" scoped>
.login-page {
display: grid;
min-height: 100vh;
place-items: center;
padding: 24px;
background:
linear-gradient(90deg, var(--fms-border) 1px, transparent 1px),
linear-gradient(var(--fms-border) 1px, transparent 1px), var(--fms-bg);
background-size: 32px 32px;
}
.login-shell {
width: min(100%, 400px);
padding: 30px;
border: 1px solid var(--fms-border);
border-radius: 8px;
background: var(--fms-surface);
box-shadow: var(--fms-shadow);
}
.login-brand {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 28px;
&__mark {
display: grid;
width: 44px;
height: 44px;
place-items: center;
border-radius: 7px;
background: #1677ff;
color: #fff;
font-size: 22px;
font-weight: 700;
}
h1 {
margin: 0;
color: var(--fms-text);
font-size: 20px;
}
p {
margin: 3px 0 0;
color: var(--fms-text-secondary);
font-size: 13px;
}
}
.login-alert {
margin-bottom: 18px;
}
@media (max-width: 480px) {
.login-page {
padding: 0;
background: var(--fms-surface);
}
.login-shell {
border: 0;
box-shadow: none;
}
}
</style>