first commit

This commit is contained in:
wintsa committed 2026-02-22 23:42:49 +08:00
commit d120622de1
36 files changed
+1177

No files matched your search

+11
View File
@@ -0,0 +1,11 @@
<script setup lang="ts">
import Tab from '@/layout/tab.vue'
</script>
<template>
<tab></tab>
</template>
<style scoped>
</style>
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

+29
View File
@@ -0,0 +1,29 @@
<script setup lang="ts">
import type { PrimitiveProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import type { ButtonVariants } from "."
import { Primitive } from "reka-ui"
import { cn } from "@/lib/utils"
import { buttonVariants } from "."
interface Props extends PrimitiveProps {
variant?: ButtonVariants["variant"]
size?: ButtonVariants["size"]
class?: HTMLAttributes["class"]
}
const props = withDefaults(defineProps<Props>(), {
as: "button",
})
</script>
<template>
<Primitive
data-slot="button"
:as="as"
:as-child="asChild"
:class="cn(buttonVariants({ variant, size }), props.class)"
>
<slot />
</Primitive>
</template>
+38
View File
@@ -0,0 +1,38 @@
import type { VariantProps } from "class-variance-authority"
import { cva } from "class-variance-authority"
export { default as Button } from "./Button.vue"
export const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
"default": "h-9 px-4 py-2 has-[>svg]:px-3",
"sm": "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
"lg": "h-10 rounded-md px-6 has-[>svg]:px-4",
"icon": "size-9",
"icon-sm": "size-8",
"icon-lg": "size-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
)
export type ButtonVariants = VariantProps<typeof buttonVariants>
+22
View File
@@ -0,0 +1,22 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<div
data-slot="card"
:class="
cn(
'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm',
props.class,
)
"
>
<slot />
</div>
</template>
+17
View File
@@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<div
data-slot="card-action"
:class="cn('col-start-2 row-span-2 row-start-1 self-start justify-self-end', props.class)"
>
<slot />
</div>
</template>
+17
View File
@@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<div
data-slot="card-content"
:class="cn('px-6', props.class)"
>
<slot />
</div>
</template>
@@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<p
data-slot="card-description"
:class="cn('text-muted-foreground text-sm', props.class)"
>
<slot />
</p>
</template>
+17
View File
@@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<div
data-slot="card-footer"
:class="cn('flex items-center px-6 [.border-t]:pt-6', props.class)"
>
<slot />
</div>
</template>
+17
View File
@@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<div
data-slot="card-header"
:class="cn('@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6', props.class)"
>
<slot />
</div>
</template>
+17
View File
@@ -0,0 +1,17 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<h3
data-slot="card-title"
:class="cn('leading-none font-semibold', props.class)"
>
<slot />
</h3>
</template>
+7
View File
@@ -0,0 +1,7 @@
export { default as Card } from "./Card.vue"
export { default as CardAction } from "./CardAction.vue"
export { default as CardContent } from "./CardContent.vue"
export { default as CardDescription } from "./CardDescription.vue"
export { default as CardFooter } from "./CardFooter.vue"
export { default as CardHeader } from "./CardHeader.vue"
export { default as CardTitle } from "./CardTitle.vue"
@@ -0,0 +1,33 @@
<script setup lang="ts">
import type { ScrollAreaRootProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import {
ScrollAreaCorner,
ScrollAreaRoot,
ScrollAreaViewport,
} from "reka-ui"
import { cn } from "@/lib/utils"
import ScrollBar from "./ScrollBar.vue"
const props = defineProps<ScrollAreaRootProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<ScrollAreaRoot
data-slot="scroll-area"
v-bind="delegatedProps"
:class="cn('relative', props.class)"
>
<ScrollAreaViewport
data-slot="scroll-area-viewport"
class="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
>
<slot />
</ScrollAreaViewport>
<ScrollBar />
<ScrollAreaCorner />
</ScrollAreaRoot>
</template>
@@ -0,0 +1,32 @@
<script setup lang="ts">
import type { ScrollAreaScrollbarProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { ScrollAreaScrollbar, ScrollAreaThumb } from "reka-ui"
import { cn } from "@/lib/utils"
const props = withDefaults(defineProps<ScrollAreaScrollbarProps & { class?: HTMLAttributes["class"] }>(), {
orientation: "vertical",
})
const delegatedProps = reactiveOmit(props, "class")
</script>
<template>
<ScrollAreaScrollbar
data-slot="scroll-area-scrollbar"
v-bind="delegatedProps"
:class="
cn('flex touch-none p-px transition-colors select-none',
orientation === 'vertical'
&& 'h-full w-2.5 border-l border-l-transparent',
orientation === 'horizontal'
&& 'h-2.5 flex-col border-t border-t-transparent',
props.class)"
>
<ScrollAreaThumb
data-slot="scroll-area-thumb"
class="bg-border relative flex-1 rounded-full"
/>
</ScrollAreaScrollbar>
</template>
+2
View File
@@ -0,0 +1,2 @@
export { default as ScrollArea } from "./ScrollArea.vue"
export { default as ScrollBar } from "./ScrollBar.vue"
+70
View File
@@ -0,0 +1,70 @@
<script setup lang="ts">
import { ref } from 'vue'
import draggable from 'vuedraggable'
import { Card, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { useTabStore } from '@/stores/tab'
import { Plus, Trash2, Edit3, ExternalLink } from 'lucide-vue-next'
const tabStore = useTabStore()
const contracts = ref([
{ id: 'ct-1', name: '土建一标段' },
{ id: 'ct-2', name: '绿化配套标段' }
])
// 添加合同
const addContract = () => {
const newId = `ct-${Date.now()}`
contracts.value.push({ id: newId, name: '新建合同段' })
}
// 删除合同
const deleteContract = (id: string) => {
contracts.value = contracts.value.filter(c => c.id !== id)
}
// 【关键逻辑】点击进入详细页面,打开新 Tab
const goToDetail = (item: any) => {
tabStore.openTab({
id: `detail-${item.id}`,
title: `合同详情: ${item.name}`,
componentName: 'ContractDetailView', // 确保在 tab.vue 的 componentMap 里注册了
props: { contractId: item.id, contractName: item.name }
})
}
</script>
<template>
<div>
<div class="flex justify-between items-center mb-6">
<h3 class="text-lg font-bold">合同段列表</h3>
<Button @click="addContract"><Plus class="mr-2 h-4 w-4" /> 添加合同段</Button>
</div>
<draggable
v-model="contracts"
item-key="id"
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"
animation="200"
>
<template #item="{ element }">
<Card class="group cursor-move hover:border-primary transition-colors">
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle class="text-sm font-medium">
<Input v-model="element.name" class="h-7 border-none focus-visible:ring-1 px-1" />
</CardTitle>
<div class="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<Button variant="ghost" size="icon" class="h-7 w-7" @click="goToDetail(element)">
<ExternalLink class="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon" class="h-7 w-7 text-destructive" @click="deleteContract(element.id)">
<Trash2 class="h-4 w-4" />
</Button>
</div>
</CardHeader>
</Card>
</template>
</draggable>
</div>
</template>
+4
View File
@@ -0,0 +1,4 @@
<template>1</template>
<script>
// const typeLine=markRaw(defineAsyncComponent(() => import('@/layout/typeLine.vue')))
</script>
+32
View File
@@ -0,0 +1,32 @@
<template>
<div class="space-y-6">
<div class="p-4 border rounded-lg bg-card shadow-sm">
<label class="text-sm font-medium mb-2 block">项目名称</label>
<Input placeholder="请输入项目名称" class="max-w-md" />
</div>
<Table class="border rounded-md">
<TableHeader>
<TableRow>
<TableHead>专业编码</TableHead>
<TableHead>工程专业名称</TableHead>
<TableHead>造价金额(万元)</TableHead>
<TableHead>用地面积(亩)</TableHead>
<TableHead class="text-right">操作</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="i in 3" :key="i">
<TableCell>010{{i}}</TableCell>
<TableCell>土木工程建设</TableCell>
<TableCell><Input type="number" class="h-8 w-24" value="1200" /></TableCell>
<TableCell><Input type="number" class="h-8 w-24" value="50" /></TableCell>
<TableCell class="text-right">
<Button variant="ghost" size="sm" class="text-destructive">删除</Button>
</TableCell>
</TableRow>
</TableBody>
</Table>
<Button variant="outline" class="w-full border-dashed"><Plus class="mr-2 h-4 w-4" /> 新增专业行</Button>
</div>
</template>
+62
View File
@@ -0,0 +1,62 @@
<script setup lang="ts">
import { useTabStore } from '@/pinia/tab'
import { defineAsyncComponent, markRaw } from 'vue'
import { Button } from '@/components/ui/button'
import { ScrollArea ,ScrollBar} from '@/components/ui/scroll-area'
import { X } from 'lucide-vue-next'
// 组件映射表:所有的动态组件需要在这里注册
const componentMap: Record<string, any> = {
XmView: markRaw(defineAsyncComponent(() => import('@/components/views/Xm.vue'))),
}
const tabStore = useTabStore()
// useTabStore
</script>
<template>
<div class="flex flex-col w-full h-screen bg-background overflow-hidden">
<div class="flex items-center border-b bg-muted/30 px-2 pt-2 flex-none">
<ScrollArea class="flex-1 whitespace-nowrap">
<div class="flex gap-1">
<div
v-for="(tab, index) in tabStore.tabs"
:key="tab.id"
@click="tabStore.activeTabId = tab.id"
:class="[
'group relative flex items-center h-9 px-4 min-w-[120px] max-w-[200px] cursor-pointer rounded-t-md border-x border-t transition-all text-sm',
tabStore.activeTabId === tab.id
? 'bg-background border-border font-medium'
: 'border-transparent hover:bg-muted text-muted-foreground'
]"
>
<span class="truncate mr-2">{{ tab.title }}</span>
<Button
v-if="index !== 0"
variant="ghost"
size="icon"
class="h-4 w-4 ml-auto opacity-0 group-hover:opacity-100 hover:bg-destructive hover:text-destructive-foreground transition-opacity"
@click="tabStore.removeTab(tab.id)"
>
<X class="h-3 w-3" />
</Button>
</div>
</div>
<ScrollBar orientation="horizontal" class="invisible" />
</ScrollArea>
</div>
<div class="flex-1 overflow-auto relative">
<div
v-for="tab in tabStore.tabs"
:key="tab.id"
v-show="tabStore.activeTabId === tab.id"
class="h-full w-full p-6 animate-in fade-in duration-300"
>
<component :is="componentMap[tab.componentName]" />
</div>
</div>
</div>
</template>
+50
View File
@@ -0,0 +1,50 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import xmView from '@/components/views/xmInfo.vue'
// 使用 sessionStorage 管理左侧分类切换状态
const activeCategory = ref(sessionStorage.getItem('project-active-cat') || 'info')
const switchCategory = (cat: string) => {
activeCategory.value = cat
sessionStorage.setItem('project-active-cat', cat)
}
</script>
<template>
<div class="flex h-full w-full bg-background">
<div class="w-1/5 border-r p-6 flex flex-col gap-8 relative">
<div class="font-bold text-lg mb-4 text-primary">项目配置</div>
<div class="flex flex-col gap-10 relative">
<div class="absolute left-[11px] top-2 bottom-2 w-[2px] bg-muted"></div>
<div class="relative flex items-center gap-4 cursor-pointer group" @click="switchCategory('info')">
<div :class="['z-10 w-6 h-6 rounded-full border-2 flex items-center justify-center transition-all',
activeCategory === 'info' ? 'bg-primary border-primary scale-110' : 'bg-background border-muted-foreground']">
<div v-if="activeCategory === 'info'" class="w-2 h-2 bg-background rounded-full"></div>
</div>
<span :class="['text-sm transition-colors', activeCategory === 'info' ? 'font-bold text-primary' : 'text-muted-foreground group-hover:text-foreground']">
分类信息
</span>
</div>
<div class="relative flex items-center gap-4 cursor-pointer group" @click="switchCategory('contract')">
<div :class="['z-10 w-6 h-6 rounded-full border-2 flex items-center justify-center transition-all',
activeCategory === 'contract' ? 'bg-primary border-primary scale-110' : 'bg-background border-muted-foreground']">
<div v-if="activeCategory === 'contract'" class="w-2 h-2 bg-background rounded-full"></div>
</div>
<span :class="['text-sm transition-colors', activeCategory === 'contract' ? 'font-bold text-primary' : 'text-muted-foreground group-hover:text-foreground']">
合同段管理
</span>
</div>
</div>
</div>
<div class="w-4/5 p-8 overflow-auto">
<keep-alive>
<component :is="activeCategory === 'info' ? xmView : xmView" />
</keep-alive>
</div>
</div>
</template>
+7
View File
@@ -0,0 +1,7 @@
import type { ClassValue } from "clsx"
import { clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
+8
View File
@@ -0,0 +1,8 @@
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' // 引入
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
createApp(App).use(pinia).mount('#app')
+36
View File
@@ -0,0 +1,36 @@
// src/stores/tab.ts
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useTabStore = defineStore('tabs', () => {
const tabs = ref([
{ id: 'XmView', title: '项目卡片', componentName: 'XmView' }
])
const activeTabId = ref('XmView')
const openTab = (config: { id: string; title: string; componentName: string; props?: any }) => {
const exists = tabs.value.find(t => t.id === config.id)
if (!exists) {
tabs.value.push(config)
}
activeTabId.value = config.id
}
const removeTab = (id: string) => {
if (id === 'XmView') return // 首页不可删除
const index = tabs.value.findIndex(t => t.id === id)
if (activeTabId.value === id) {
activeTabId.value = (tabs.value[index - 1]?.id || tabs.value[index + 1]?.id ) as string
}
tabs.value.splice(index, 1)
}
return { tabs, activeTabId, openTab, removeTab }
}, {
// --- 关键配置:开启持久化 ---
persist: {
key: 'tabs', // 存储在 localStorage 里的 key
storage: localStorage, // 也可以改用 sessionStorage
pick: ['tabs', 'activeTabId'], // 指定哪些变量需要持久化
}
})
+122
View File
@@ -0,0 +1,122 @@
@import "tailwindcss";
@import "tw-animate-css";
#app{
height: 100vh;
}
@custom-variant dark (&:is(.dark *));
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}