first push

This commit is contained in:
shijing 2026-03-27 14:40:43 +08:00
commit 1be2881831
87 changed files with 37672 additions and 0 deletions

9
.editorconfig Normal file
View File

@ -0,0 +1,9 @@
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue}]
charset = utf-8
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
end_of_line = lf
max_line_length = 100

8
.env.development Normal file
View File

@ -0,0 +1,8 @@
# 本地环境
VITE_APP_TITLE=瓷福隧道窑系统
VITE_API_BASE_URL=http://10.0.11.74:60309/api
VITE_APP_BASEURL=http://10.0.11.74:60309
# 本地端口
VITE_APP_PORT=2800
# 是否开启代理
VITE_APP_PROXY=true

8
.env.production Normal file
View File

@ -0,0 +1,8 @@
# 生产环境
VITE_APP_TITLE=瓷福隧道窑系统
VITE_API_BASE_URL=http://10.0.11.74:60309/api
VITE_APP_BASEURL=http://10.0.11.74:60309
# 本地端口
VITE_APP_PORT=2800
# 是否开启代理
VITE_APP_PROXY=true

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
* text=auto eol=lf

30
.gitignore vendored Normal file
View File

@ -0,0 +1,30 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo

3
.vs/ProjectSettings.json Normal file
View File

@ -0,0 +1,3 @@
{
"CurrentProjectSetting": null
}

View File

@ -0,0 +1,9 @@
{
"ExpandedNodes": [
"",
"\\src-tauri",
"\\src-tauri\\capabilities"
],
"SelectedNode": "\\src-tauri\\capabilities\\default.json",
"PreviewInSolutionExplorer": false
}

BIN
.vs/slnx.sqlite Normal file

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,12 @@
{
"Version": 1,
"WorkspaceRootPath": "D:\\testProjects\\yaodaoxiangmu\\vite_vue3_tsproject_change\\",
"Documents": [],
"DocumentGroupContainers": [
{
"Orientation": 0,
"VerticalTabListWidth": 256,
"DocumentGroups": []
}
]
}

8
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,8 @@
{
"recommendations": [
"Vue.volar",
"dbaeumer.vscode-eslint",
"EditorConfig.EditorConfig",
"aminer.codegeex"
]
}

39
README.md Normal file
View File

@ -0,0 +1,39 @@
# vue-project
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Type Support for `.vue` Imports in TS
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
## Customize configuration
See [Vite Configuration Reference](https://vite.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Type-Check, Compile and Minify for Production
```sh
npm run build
```
### Lint with [ESLint](https://eslint.org/)
```sh
npm run lint
```

View File

@ -0,0 +1,378 @@
# Coal Feeding Layout Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Replace the right-side `<table>` in `src/views/coalFeeding.vue` with a block-based unit-column layout while preserving the current row/column visual structure and existing interactions.
**Architecture:** Keep all business state and handlers in `src/views/coalFeeding.vue`, but reorganize the right-side rendering around a fixed parameter label column plus horizontally scrollable unit columns. Use small in-file render configurations for upper rows, lower rows, and fixed-height visual blocks so the new block layout stays aligned with the former table structure.
**Tech Stack:** Vue 3 `<script setup lang="ts">`, existing CSS utility classes, `@opentiny/vue`, Vite build verification
---
### Task 1: Prepare In-File Layout Metadata
**Files:**
- Modify: `D:\testProjects\yaodaoxiangmu\vite_vue3_tsproject_change\src\views\coalFeeding.vue`
- [ ] **Step 1: Add typed row metadata for the upper and lower time-setting blocks**
Insert these constants near the existing type declarations in `src/views/coalFeeding.vue` so the new template can loop over block rows instead of repeating hard-coded table rows:
```ts
type TimeFieldKey =
| "tmyxsds"
| "tmtzsds"
| "tmljsds"
| "blyxsds"
| "tmyxsdx"
| "tmtzsdx"
| "tmljsdx"
| "blyxsdx";
type DisplayFieldKey =
| "tmljsjs"
| "tmljsjx"
| "tempera1"
| "tempera2"
| "carNumber";
type BlockRowConfig = {
key: TimeFieldKey | DisplayFieldKey;
label: string;
kind: "time" | "display";
idPrefix?: string;
};
const upperBlockRows: BlockRowConfig[] = [
{ key: "tmyxsds", label: "投煤运行时间设定", kind: "time", idPrefix: "tmyxsds" },
{ key: "tmtzsds", label: "投煤停止时间设定", kind: "time", idPrefix: "tmtzsds" },
{ key: "tmljsds", label: "投煤累积时间设定", kind: "time", idPrefix: "tmljsds" },
{ key: "blyxsds", label: "布料运行时间设定", kind: "time", idPrefix: "blyxsds" },
{ key: "tmljsjs", label: "投煤累积时间", kind: "display", idPrefix: "tmljsjs" },
];
const lowerBlockRows: BlockRowConfig[] = [
{ key: "tmyxsdx", label: "投煤运行时间设定", kind: "time", idPrefix: "tmyxsdx" },
{ key: "tmtzsdx", label: "投煤停止时间设定", kind: "time", idPrefix: "tmtzsdx" },
{ key: "tmljsdx", label: "投煤累积时间设定", kind: "time", idPrefix: "tmljsdx" },
{ key: "blyxsdx", label: "布料运行时间设定", kind: "time", idPrefix: "blyxsdx" },
{ key: "tmljsjx", label: "投煤累积时间", kind: "display", idPrefix: "tmljsjx" },
];
```
- [ ] **Step 2: Add typed helpers for rendering ids and editable values**
Add these helpers below the row metadata so the template can render the new block layout without string duplication:
```ts
const getCellId = (prefix: string | undefined, index: number) => {
return prefix ? `${prefix}${index}` : undefined;
};
const getTempDropId = (side: "tempera1" | "tempera2", item: any, index: number) => {
return `${side}_unit${item.unit}_${index + 1}`;
};
const isTimeRow = (row: BlockRowConfig) => row.kind === "time";
```
- [ ] **Step 3: Run a quick type sanity scan before touching the template**
Run: `rg -n "upperBlockRows|lowerBlockRows|getCellId|getTempDropId|isTimeRow" src\views\coalFeeding.vue`
Expected: five matches for the newly added metadata/helpers, all inside `src/views\coalFeeding.vue`
### Task 2: Replace the Table Template With a Column Layout
**Files:**
- Modify: `D:\testProjects\yaodaoxiangmu\vite_vue3_tsproject_change\src\views\coalFeeding.vue`
- [ ] **Step 1: Replace the `<table>` wrapper with a block-based layout shell**
Replace the current `<table>` section with this outer structure inside the existing right-side panel container:
```vue
<div class="control-panel">
<div class="control-panel__labels">
<div class="control-panel__corner"></div>
<div class="control-panel__section">
<div
v-for="row in upperBlockRows"
:key="`upper-label-${row.key}`"
class="control-panel__label control-panel__label--row"
>
{{ row.label }}
</div>
<div class="control-panel__label control-panel__label--motor">中间侧</div>
</div>
<div class="control-panel__label control-panel__label--temp">2侧温度</div>
<div class="control-panel__label control-panel__label--car">车位号</div>
<div class="control-panel__label control-panel__label--temp">1侧温度</div>
<div class="control-panel__section">
<div
v-for="row in lowerBlockRows"
:key="`lower-label-${row.key}`"
class="control-panel__label control-panel__label--row"
>
{{ row.label }}
</div>
<div class="control-panel__label control-panel__label--motor">回车侧</div>
</div>
</div>
<div class="control-panel__columns">
<!-- unit columns render here -->
</div>
</div>
```
- [ ] **Step 2: Render each unit as one vertical column**
Inside `control-panel__columns`, render one column per `dataList` item using this structure:
```vue
<div
v-for="(item, index) in dataList"
:key="item.team"
class="control-panel__column"
>
<div class="control-panel__column-header">
<span class="inline_flex items_center justify_center h1_25 min_w_6 px0_375 rounded bg_primary text_primary_color fontw700 text_11">
{{ item.team }}
</span>
</div>
<div class="control-panel__section">
<!-- upper rows -->
</div>
<div
:id="getTempDropId('tempera2', item, index)"
class="control-panel__cell control-panel__cell--temp"
@dragover="onDjDragOver"
@drop="onDjDrop($event, item, getTempDropId('tempera2', item, index))"
>
<span class="text_xs fontw700 min_w_2_25 text_right text_red">{{ item.tempera2 }}</span>
<span> °C</span>
</div>
<div class="control-panel__cell control-panel__cell--car">
<span class="inline_flex items_center justify_center h1_25 min_w_1_75 px0_375 rounded_md bg_primary text_primary_color my0_3 fontw700 text_sm shadow_sm">
{{ item.carNumber }}
</span>
</div>
<div
:id="getTempDropId('tempera1', item, index)"
class="control-panel__cell control-panel__cell--temp"
@dragover="onDjDragOver"
@drop="onDjDrop($event, item, getTempDropId('tempera1', item, index))"
>
<span class="text_xs fontw700 min_w_2_25 text_right text_red">{{ item.tempera1 }}</span>
<span> °C</span>
</div>
<div class="control-panel__section">
<!-- lower rows -->
</div>
</div>
```
- [ ] **Step 3: Render the upper and lower time/display rows from metadata**
Use this pattern in both the upper and lower sections so editable rows and display rows remain consistent:
```vue
<div
v-for="row in upperBlockRows"
:key="`upper-${item.team}-${row.key}`"
:id="getCellId(row.idPrefix, index)"
class="control-panel__cell control-panel__cell--row"
>
<span v-if="!isTimeRow(row) || item.statusAuto" class="px0_25 py0_125 text_center text_xs color_base">
{{ item[row.key] }}
</span>
<input
v-else
v-model="item[row.key]"
class="px0_25 py0_125 text_center text_xs color_base control-panel__input"
/>
</div>
```
For the lower section, switch `upperBlockRows` to `lowerBlockRows` and keep the same markup.
- [ ] **Step 4: Move the middle-side and return-side motor rendering into the new sections**
Render the motor groups as fixed-height cells so the old `中间侧` and `回车侧` rows become block elements:
```vue
<div class="control-panel__cell control-panel__cell--motor">
<div
v-for="dj in item.dianji"
:id="dj.id"
:key="dj.id"
class="flexs flex_col items_center gap0_25 text_center dianji_status"
@dragover="onDjDragOver"
@drop="onDjDrop($event, dj, 'id')"
>
<!-- preserve existing motor inner markup and click handlers -->
</div>
</div>
```
For the lower section, keep the same structure but iterate `item.dianji2` and continue calling `toggleDjStop(dj, item.team, item.statusAuto, 'dianji2')`.
### Task 3: Rebuild Styling for Alignment and Visual Parity
**Files:**
- Modify: `D:\testProjects\yaodaoxiangmu\vite_vue3_tsproject_change\src\views\coalFeeding.vue`
- [ ] **Step 1: Add block-layout container styles**
Append styles like these near the existing `<style>` block:
```css
.control-panel {
display: flex;
align-items: flex-start;
min-width: max-content;
}
.control-panel__labels {
flex: 0 0 132px;
}
.control-panel__columns {
display: flex;
align-items: flex-start;
}
.control-panel__column {
min-width: 96px;
border-right: 1px solid rgba(148, 163, 184, 0.6);
}
```
- [ ] **Step 2: Add fixed row heights so labels and unit cells stay aligned**
Add shared row classes to replace the natural table sizing:
```css
.control-panel__corner,
.control-panel__column-header {
height: 42px;
display: flex;
align-items: center;
justify-content: center;
border-bottom: 1px solid rgba(148, 163, 184, 0.6);
}
.control-panel__label--row,
.control-panel__cell--row {
height: 42px;
}
.control-panel__label--temp,
.control-panel__cell--temp,
.control-panel__label--car,
.control-panel__cell--car {
height: 42px;
}
.control-panel__label--motor,
.control-panel__cell--motor {
min-height: 120px;
}
```
- [ ] **Step 3: Restore row backgrounds and dense control-panel styling**
Add styling hooks so the new block elements preserve the current visual categories:
```css
.control-panel__label,
.control-panel__cell {
display: flex;
align-items: center;
justify-content: center;
padding: 0.25rem;
border-bottom: 1px solid rgba(148, 163, 184, 0.6);
background: #ffffff;
}
.control-panel__label--temp,
.control-panel__cell--temp {
background: #fff1e8;
}
.control-panel__label--car,
.control-panel__cell--car {
background: #dbeafe;
}
.control-panel__label--motor,
.control-panel__cell--motor {
background: #eff6ff;
}
.control-panel__input {
width: 6rem;
}
```
- [ ] **Step 4: Run a targeted selector scan to make sure the old `<table>` layout is gone**
Run: `rg -n "<table|<thead|<tbody|<tr|<td|<th" src\views\coalFeeding.vue`
Expected: no matches
### Task 4: Verify Build and Behavior
**Files:**
- Modify: `D:\testProjects\yaodaoxiangmu\vite_vue3_tsproject_change\src\views\coalFeeding.vue`
- [ ] **Step 1: Run the production build**
Run: `npm run build`
Expected: Vite build completes successfully and outputs the `dist` bundle without Vue template or TypeScript errors.
- [ ] **Step 2: Manually verify the right panel structure in the app**
Open the page and confirm:
```text
1. The right panel no longer uses a table layout.
2. The left parameter column remains visually aligned with each unit column.
3. Each unit column shows:
- upper block
- 2侧温度
- 车位号
- 1侧温度
- lower block
4. 中间侧 and 回车侧 motor areas still render with the expected controls.
```
- [ ] **Step 3: Manually verify preserved interactions**
Confirm these behaviors still work:
```text
1. Auto mode shows text; manual mode shows inputs.
2. Clicking a motor still toggles stop/run state as before.
3. Temperature targets still accept drag-and-drop point binding.
4. Bottom manual/auto switch panel still renders and responds.
```
- [ ] **Step 4: Record completion status instead of committing**
Because this workspace currently has no `.git` directory, do not run commit commands here.
Instead, record completion by listing the verified items in the final handoff:
```text
- build result
- layout conversion status
- interaction checks completed
```

View File

@ -0,0 +1,235 @@
# Coal Feeding Motor Grid Alignment Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Update the motor sections in `src/views/coalFeeding.vue` so both `中间侧` and `回车侧` render motors two per row, with odd counts left-aligned, while keeping the left labels height-aligned with the right motor sections.
**Architecture:** Keep the existing block-based control-panel layout as the baseline. Add small computed helpers in `coalFeeding.vue` to calculate a shared minimum height for the middle and return motor sections, then apply that shared height to both the left labels and right motor containers. Replace the motor-item vertical stack with a two-column CSS grid wrapper while preserving all existing motor card interactions.
**Tech Stack:** Vue 3 `<script setup lang="ts">`, existing `coalFeeding.vue` local state, scoped CSS, Vite build verification
---
### Task 1: Add Shared Motor Height Helpers
**Files:**
- Modify: `D:\testProjects\yaodaoxiangmu\vite_vue3_tsproject_change\src\views\coalFeeding.vue`
- [ ] **Step 1: Add height constants near the existing control-panel helper functions**
Insert these constants near `getCellId`, `getTempDropId`, and `isTimeRow` so the motor section height calculation is explicit and easy to tune:
```ts
const MOTOR_GRID_COLUMNS = 2;
const MOTOR_SECTION_MIN_HEIGHT = 120;
const MOTOR_CARD_HEIGHT = 92;
const MOTOR_SECTION_VERTICAL_PADDING = 8;
const MOTOR_GRID_ROW_GAP = 8;
```
- [ ] **Step 2: Add helpers to calculate motor row counts and shared heights**
Add these helpers below the constants so both left labels and right sections can consume the same height values:
```ts
const getMotorRowCount = (count: number) => {
return Math.max(1, Math.ceil(count / MOTOR_GRID_COLUMNS));
};
const getMotorSectionHeight = (count: number) => {
const rows = getMotorRowCount(count);
const computedHeight =
MOTOR_SECTION_VERTICAL_PADDING * 2 +
rows * MOTOR_CARD_HEIGHT +
(rows - 1) * MOTOR_GRID_ROW_GAP;
return `${Math.max(MOTOR_SECTION_MIN_HEIGHT, computedHeight)}px`;
};
const middleMotorHeight = computed(() => {
const maxCount = dataList.reduce((max, item) => Math.max(max, item.dianji.length), 0);
return getMotorSectionHeight(maxCount);
});
const returnMotorHeight = computed(() => {
const maxCount = dataList.reduce((max, item) => Math.max(max, item.dianji2.length), 0);
return getMotorSectionHeight(maxCount);
});
```
- [ ] **Step 3: Import the missing Vue utility for computed values**
Update the top Vue import so the new computed helpers are available:
```ts
import { computed, onMounted, onUnmounted, reactive, ref } from "vue";
```
- [ ] **Step 4: Run a quick search to confirm the new helper symbols exist once**
Run: `rg -n "MOTOR_GRID_COLUMNS|getMotorRowCount|middleMotorHeight|returnMotorHeight|computed" src\views\coalFeeding.vue`
Expected: matches for the new constants/helpers and the updated Vue import in `src\views\coalFeeding.vue`
### Task 2: Apply Shared Heights to Labels and Motor Sections
**Files:**
- Modify: `D:\testProjects\yaodaoxiangmu\vite_vue3_tsproject_change\src\views\coalFeeding.vue`
- [ ] **Step 1: Bind the left motor labels to the shared section heights**
Update the two left-side labels so they use the same computed heights as the corresponding right-side sections:
```vue
<div class="control-panel__label control-panel__label--motor" :style="{ minHeight: middleMotorHeight }">
中间侧
</div>
```
```vue
<div class="control-panel__label control-panel__label--motor" :style="{ minHeight: returnMotorHeight }">
回车侧
</div>
```
- [ ] **Step 2: Bind the right-side upper motor section to the middle motor height**
Update the `中间侧` motor wrapper from a plain section cell into a shared-height container:
```vue
<div class="control-panel__cell control-panel__cell--motor" :style="{ minHeight: middleMotorHeight }">
<div class="control-panel__motor-grid">
<!-- existing dianji cards stay here -->
</div>
</div>
```
- [ ] **Step 3: Bind the right-side lower motor section to the return motor height**
Update the `回车侧` motor wrapper the same way:
```vue
<div class="control-panel__cell control-panel__cell--motor" :style="{ minHeight: returnMotorHeight }">
<div class="control-panel__motor-grid">
<!-- existing dianji2 cards stay here -->
</div>
</div>
```
### Task 3: Convert Motor Items to Two-Column Grid Layout
**Files:**
- Modify: `D:\testProjects\yaodaoxiangmu\vite_vue3_tsproject_change\src\views\coalFeeding.vue`
- [ ] **Step 1: Wrap the upper motor cards in a dedicated grid container**
Move the existing `item.dianji` loop inside a grid wrapper without changing the motor card internals:
```vue
<div class="control-panel__motor-grid">
<div
v-for="dj in item.dianji"
:key="dj.id"
:id="dj.id"
class="control-panel__motor-card flexs flex_col items_center gap0_25 text_center dianji_status"
@dragover="onDjDragOver"
@drop="onDjDrop($event, dj, 'id')"
>
<!-- keep existing motor content and click handlers -->
</div>
</div>
```
- [ ] **Step 2: Wrap the lower motor cards in the same grid container pattern**
Apply the same grid wrapper to `item.dianji2`, keeping the existing click handler with the `dianji2` motor key:
```vue
<div class="control-panel__motor-grid">
<div
v-for="dj in item.dianji2"
:key="dj.id"
class="control-panel__motor-card flexs flex_col items_center gap0_25 text_center dianji_status"
@dragover="onDjDragOver"
@drop="onDjDrop($event, dj, 'id')"
>
<!-- keep existing motor content and click handlers -->
</div>
</div>
```
- [ ] **Step 3: Add the motor grid styles so cards render two per row**
Append these scoped styles near the current control-panel CSS:
```css
.control-panel__motor-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
align-items: start;
width: 100%;
}
.control-panel__motor-card {
width: 100%;
min-height: 92px;
}
```
This keeps odd counts left-aligned automatically because the final odd card occupies the first cell of the last grid row.
- [ ] **Step 4: Update the motor container styles to support top-aligned grid content**
Refine the existing motor container style so the new grid aligns correctly:
```css
.control-panel__cell--motor {
min-height: 120px;
background: #eff6ff;
align-items: flex-start;
justify-content: flex-start;
padding: 8px;
}
```
### Task 4: Verify Layout and Build
**Files:**
- Modify: `D:\testProjects\yaodaoxiangmu\vite_vue3_tsproject_change\src\views\coalFeeding.vue`
- [ ] **Step 1: Run a selector scan to confirm the new motor-grid classes are present**
Run: `rg -n "control-panel__motor-grid|control-panel__motor-card|middleMotorHeight|returnMotorHeight" src\views\coalFeeding.vue`
Expected: matches for the new template bindings, helper logic, and styles inside `src\views\coalFeeding.vue`
- [ ] **Step 2: Run the production build**
Run: `npm run build`
Expected: Vite finishes successfully with no Vue template or TypeScript errors.
- [ ] **Step 3: Manually verify the refined motor layout**
Open the page and confirm:
```text
1. 中间侧 motors render two per row.
2. 回车侧 motors render two per row.
3. If a section has an odd motor count, the last motor stays on the left side of the final row.
4. The left labels for 中间侧 and 回车侧 stay height-aligned with the right motor blocks.
5. Existing motor click and drag behavior still works.
```
- [ ] **Step 4: Record completion status instead of committing**
Because this workspace has no `.git` directory, do not run commit commands here.
In the final handoff, report:
```text
- build result
- motor grid conversion status
- left/right height alignment status
- interaction checks completed
```

View File

@ -0,0 +1,223 @@
# coalFeeding.vue Right Panel Layout Redesign
## Overview
This design updates the right-side data presentation area in `src/views/coalFeeding.vue`.
The current implementation uses a large HTML `<table>` to render the unit comparison area.
The new implementation will remove `<table>` entirely and replace it with ordinary block elements while preserving the existing row-and-column visual structure.
The redesign will follow the confirmed "path 2" approach:
- Keep the current business fields and interaction logic unchanged.
- Rebuild the right-side layout as a column-oriented block structure.
- Split each unit column into reusable visual sections.
- Preserve the current visual alignment, colors, and row semantics.
## Goals
- Remove the `<table>` layout from the right-side unit display area.
- Preserve the current visual feeling of a multi-column industrial control panel.
- Render the layout as:
- one fixed parameter label column
- multiple unit columns rendered with ordinary elements
- Split each unit column into five ordered sections:
1. upper block: four time-setting rows + middle-side motors + accumulated runtime
2. `2-side temperature`
3. `car number`
4. `1-side temperature`
5. lower block: four independent time-setting rows + return-side motors + accumulated runtime
- Keep existing click, drag-and-drop, and auto/manual behaviors working.
## Non-Goals
- Do not switch the page to the new backend `unit -> equipments -> role_points` structure in this change.
- Do not refactor WebSocket payload handling in this change.
- Do not alter the current business meanings of fields such as `tmyxsds`, `tempera2`, `dianji`, or `dianji2`.
- Do not split logic into separate Vue files unless needed later; this change stays inside `coalFeeding.vue`.
## Current State
The current right panel is rendered as a long `<table>` with:
- a left sticky parameter-name column
- repeated `v-for` cells for each unit
- an upper group of rows before `2-side temperature`
- a middle section for temperatures and car number
- a lower group of rows after `1-side temperature`
This structure visually works, but it is difficult to reshape into two explicit per-column blocks because the DOM is organized by rows, not by columns.
## Chosen Approach
Use a block-based, column-oriented layout.
The new structure will be:
- outer right-panel container
- left parameter column
- horizontally scrollable unit-column area
- each unit rendered as one column card-like container
Each unit column will render these sections in order:
1. Upper block
- `投煤运行时间设定` -> `tmyxsds`
- `投煤停止时间设定` -> `tmtzsds`
- `投煤累积时间设定` -> `tmljsds`
- `布料运行时间设定` -> `blyxsds`
- `中间侧` -> `dianji`
- `投煤累积时间` -> `tmljsjs`
2. `2侧温度` -> `tempera2`
3. `车位号` -> `carNumber`
4. `1侧温度` -> `tempera1`
5. Lower block
- `投煤运行时间设定` -> `tmyxsdx`
- `投煤停止时间设定` -> `tmtzsdx`
- `投煤累积时间设定` -> `tmljsdx`
- `布料运行时间设定` -> `blyxsdx`
- `回车侧` -> `dianji2`
- `投煤累积时间` -> `tmljsjx`
## Data Mapping
This change keeps the existing `dataList` shape unchanged.
Upper block mapping:
- `tmyxsds`
- `tmtzsds`
- `tmljsds`
- `blyxsds`
- `dianji`
- `tmljsjs`
Middle display mapping:
- `tempera2`
- `carNumber`
- `tempera1`
Lower block mapping:
- `tmyxsdx`
- `tmtzsdx`
- `tmljsdx`
- `blyxsdx`
- `dianji2`
- `tmljsjx`
## Interaction Preservation
The redesign must preserve the current interactions:
- Time-setting fields:
- show plain text when `item.statusAuto === true`
- show `<input>` when `item.statusAuto === false`
- Middle-side motor controls continue to use `dianji`
- Return-side motor controls continue to use `dianji2`
- Motor click actions continue to call `toggleDjStop(...)`
- Temperature drop targets keep current DOM id rules so point binding logic continues to work:
- `tempera2_unit...`
- `tempera1_unit...`
- Existing drag/drop handlers stay in place for the fields already using them
- Unit-level manual/auto toggle controls under the panel remain unchanged
## Rendering Structure
The template will be reorganized into these conceptual pieces:
- parameter label column
- contains every row label in display order
- unit columns container
- renders one column per `dataList` item
- unit column header
- shows the unit name badge
- upper section container
- renders upper time rows, motor row, and accumulated runtime row
- middle temperature section
- renders `2-side temperature`, `car number`, and `1-side temperature`
- lower section container
- renders lower time rows, return-side motor row, and accumulated runtime row
Repeated visual blocks inside the unit column will be reduced into small render patterns:
- time item block
- motor group block
- temperature/car display block
These can remain inline in the same file as repeated template fragments or lightweight helper loops.
## Layout and Styling
The new layout will use ordinary block elements instead of table semantics:
- `display: flex` for the main horizontal layout
- fixed-width parameter label column
- horizontally scrollable unit column strip
- each unit column with a consistent minimum width
- each row item with controlled height and border styling
Visual continuity requirements:
- preserve row alignment between label column and unit columns
- preserve the current gray/white parameter rows
- preserve the blue motor-area styling
- preserve the orange temperature styling
- preserve the highlighted car-number styling
- preserve the general dense control-panel appearance
To avoid layout drift after removing table semantics:
- shared row-height classes will be introduced for standard rows
- motor rows will use a dedicated fixed-height style
- upper and lower section wrappers will provide clear grouping without breaking alignment
## Risks and Mitigations
### Risk 1: alignment drift after removing `<table>`
Mitigation:
- define explicit heights for standard rows
- define explicit height for motor rows
- keep label column and unit columns rendered in the same vertical order
### Risk 2: drag/drop regressions on temperature fields
Mitigation:
- preserve the existing id naming convention for temperature targets
- keep existing drop handlers on the same business elements
### Risk 3: motor interactions change unintentionally
Mitigation:
- reuse current click handler calls and existing state fields
- only move markup; do not alter motor state logic during this change
### Risk 4: template becomes harder to read
Mitigation:
- group markup by column section instead of one giant table body
- use repeated rendering patterns for time blocks and motor groups
## Testing Plan
After implementation, verify:
- the right panel renders without `<table>`
- horizontal scrolling still works for the unit columns
- label column and unit columns remain aligned
- upper and lower blocks are visually distinct inside each unit column
- auto/manual display behavior still works for time fields
- motor click interactions still work
- temperature drop targets still accept drag-and-drop
- existing settings section below the panel still renders correctly
## Open Constraints
- The page still uses the current local `dataList` structure.
- Future work may migrate the page to the new backend unit/equipment structure, but that is intentionally out of scope here.
- The current project directory is not a Git repository, so this design document cannot be committed from the current workspace state.

View File

@ -0,0 +1,162 @@
# coalFeeding.vue Motor Grid Alignment Refinement
## Overview
This design refines the already approved block-based right-panel layout in `src/views/coalFeeding.vue`.
It does not change the overall removal of `<table>`.
Instead, it adjusts the motor areas so they better match the desired visual structure.
The refinement introduces two requirements:
- both `中间侧` and `回车侧` motor areas should render motors two per row instead of one per row
- the left-side label blocks for those motor areas must stay height-aligned with the corresponding right-side motor blocks
## Goals
- Render motor items in a 2-column layout for both motor sections
- Keep odd motor counts left-aligned on the final row
- Keep the left label block height aligned with the right motor block height
- Preserve all existing drag-and-drop, click, and status-display logic
- Avoid reworking the surrounding block layout
## Non-Goals
- Do not change the current block layout order
- Do not change time-field rendering
- Do not change temperature or car-number sections
- Do not switch to a new backend data structure
- Do not introduce DOM measurement logic unless strictly necessary
## Chosen Approach
Use a CSS grid for motor layout plus shared computed heights for label and motor sections.
Why this approach:
- It preserves the current block-based layout structure
- It keeps the change small and local to the motor sections
- It avoids fragile DOM measurement code
- It guarantees left/right alignment by making both sides use the same computed height source
## Motor Layout Rules
Both `中间侧` and `回车侧` motor groups will render with:
- two columns per row
- consistent gaps between motor items
- left-aligned final item when the count is odd
Implementation shape:
- wrap each motor list in a dedicated grid container
- use `grid-template-columns: repeat(2, minmax(0, 1fr))`
- allow each motor item to occupy one cell
- do not center or span the final odd item
Expected examples:
- 2 motors -> 1 row of 2
- 3 motors -> 2 rows, with the last item on the left of the second row
- 4 motors -> 2 rows of 2
## Height Alignment Rules
The left labels for `中间侧` and `回车侧` must visually align with the height of their corresponding motor blocks across all unit columns.
To achieve this, compute two shared height values:
- `middleMotorHeight`
- `returnMotorHeight`
These values will be derived from the largest motor-group row count in the current data.
Row count formula:
- `Math.ceil(motorCount / 2)`
Height formula:
- `sectionPaddingTop + sectionPaddingBottom + rows * itemHeight + (rows - 1) * rowGap`
Also enforce a minimum height so single-row sections do not become too short.
Both the left label block and the right motor block must use the same computed height source for each section.
## Rendering Changes
Keep the existing right-panel block structure.
Only refine the motor section internals and the matching left labels.
Changes:
- `中间侧` label gets `:style="{ minHeight: middleMotorHeight }"`
- `回车侧` label gets `:style="{ minHeight: returnMotorHeight }"`
- upper motor section gets `:style="{ minHeight: middleMotorHeight }"`
- lower motor section gets `:style="{ minHeight: returnMotorHeight }"`
- motor items render inside a `control-panel__motor-grid` container
The individual motor cards keep their current:
- drag-and-drop handlers
- run/stop toggles
- local/remote badges
- fault/normal text
## Styling Changes
Add a motor-grid wrapper class with:
- `display: grid`
- two equal-width columns
- fixed row gap
- fixed column gap
- left-aligned item flow
The motor section wrapper should:
- keep the current blue-tinted background
- use the shared computed minimum height
- align its content to the top instead of stretching awkwardly
The left label blocks should:
- continue using the current label styling
- adopt the same computed minimum height as their corresponding right-side section
## Risks and Mitigations
### Risk 1: heights drift if units have different motor counts
Mitigation:
- compute height from the maximum row count across all units for each motor section
### Risk 2: odd counts look visually off
Mitigation:
- keep the final odd item left-aligned and do not stretch it to full width
### Risk 3: click and drag interactions break during markup changes
Mitigation:
- keep the motor card markup intact
- move only the grouping wrapper and layout styles
## Testing Plan
After implementation, verify:
- `中间侧` motors render two per row
- `回车侧` motors render two per row
- odd motor counts leave the last motor on the left side
- left label heights align with right motor-section heights
- motor clicks still toggle state
- drag-and-drop still works on motor items
- the surrounding right-panel layout remains unchanged
## Open Constraints
- The workspace is still not a Git repository, so this design file cannot be committed here.
- This refinement assumes the existing block-based right-panel layout remains the baseline for implementation.

6
env.d.ts vendored Normal file
View File

@ -0,0 +1,6 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}

22
eslint.config.ts Normal file
View File

@ -0,0 +1,22 @@
import pluginVue from 'eslint-plugin-vue'
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
// To allow more languages other than `ts` in `.vue` files, uncomment the following lines:
// import { configureVueProject } from '@vue/eslint-config-typescript'
// configureVueProject({ scriptLangs: ['ts', 'tsx'] })
// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup
export default defineConfigWithVueTs(
{
name: 'app/files-to-lint',
files: ['**/*.{ts,mts,tsx,vue}'],
},
{
name: 'app/files-to-ignore',
ignores: ['**/dist/**', '**/dist-ssr/**', '**/coverage/**'],
},
pluginVue.configs['flat/essential'],
vueTsConfigs.recommended,
)

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>瓮福隧道窑系统</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

8038
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

38
package.json Normal file
View File

@ -0,0 +1,38 @@
{
"name": "vue-project",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --build",
"lint": "eslint . --fix",
"tauri:dev": "tauri dev",
"tauri:build": "tauri build"
},
"dependencies": {
"@opentiny/vue": "^3.29.0",
"@tauri-apps/api": "^2.10.1",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@tauri-apps/cli": "^2.10.1",
"@tsconfig/node22": "^22.0.0",
"@types/node": "^22.13.4",
"@vitejs/plugin-vue": "^5.2.1",
"@vue/eslint-config-typescript": "^14.4.0",
"@vue/tsconfig": "^0.7.0",
"eslint": "^9.20.1",
"eslint-plugin-vue": "^9.32.0",
"jiti": "^2.4.2",
"npm-run-all2": "^7.0.2",
"typescript": "~5.7.3",
"vite": "^6.1.0",
"vite-plugin-vue-devtools": "^7.7.2",
"vue-tsc": "^2.2.2"
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

4
src-tauri/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
# Generated by Cargo
# will have compiled files and executables
/target/
/gen/schemas

5243
src-tauri/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

33
src-tauri/Cargo.toml Normal file
View File

@ -0,0 +1,33 @@
[package]
name = "app"
version = "0.1.0"
description = "A Tauri App"
authors = ["you"]
license = ""
repository = ""
edition = "2021"
rust-version = "1.77.2"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
name = "app_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2.5.6", features = [] }
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
log = "0.4"
tauri = { version = "2.10.3", features = [] }
tauri-plugin-log = "2"
[profile.release]
# Reduce final binary size for distribution builds.
opt-level = "z"
lto = true
codegen-units = 1
panic = "abort"
strip = "symbols"

3
src-tauri/build.rs Normal file
View File

@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

View File

@ -0,0 +1,11 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "enables the default permissions",
"windows": [
"main"
],
"permissions": [
"core:default"
]
}

BIN
src-tauri/icons/128x128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
src-tauri/icons/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
src-tauri/icons/icon.icns Normal file

Binary file not shown.

BIN
src-tauri/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
src-tauri/icons/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

16
src-tauri/src/lib.rs Normal file
View File

@ -0,0 +1,16 @@
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.setup(|app| {
if cfg!(debug_assertions) {
app.handle().plugin(
tauri_plugin_log::Builder::default()
.level(log::LevelFilter::Info)
.build(),
)?;
}
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

6
src-tauri/src/main.rs Normal file
View File

@ -0,0 +1,6 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
app_lib::run();
}

37
src-tauri/tauri.conf.json Normal file
View File

@ -0,0 +1,37 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"productName": "vue-project",
"version": "0.1.0",
"identifier": "com.ctczc.vitevue3tsprojectchange",
"build": {
"frontendDist": "../dist",
"devUrl": "http://localhost:5173",
"beforeDevCommand": "npm run dev",
"beforeBuildCommand": "npm run build"
},
"app": {
"windows": [
{
"title": "vue-project",
"width": 800,
"height": 600,
"resizable": true,
"fullscreen": false
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": ["nsis"],
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
}
}

100
src/App.vue Normal file
View File

@ -0,0 +1,100 @@
<template>
<div class="w_full h_full" style="display: flex; flex-direction: column">
<header>
<div class="headLeft" style="color: rgb(220, 38, 38)">瓮福隧道窑系统</div>
<nav class="headCenter">
<router-link
class="headerNav"
v-for="nav in navData"
:key="nav.path"
:to="nav.path"
:class="{ active_link: $route.path === nav.path }"
>
{{ nav.name }}
</router-link>
</nav>
<div class="headRight">
<span>当前用户{{ currentUser }}</span>
<span style="margin: 0 1rem">PLC连接状态</span>
<span>{{ currentTime }}</span>
</div>
</header>
<RouterView />
</div>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from "vue";
import { RouterLink, RouterView } from "vue-router";
const navData = [
{ name: "PLC列表", path: "/plcList" },
// { name: "", path: "/warning" },
// { name: "", path: "/runsystem" },
// { name: "1", path: "/firesystem" },
// { name: "2", path: "/firesystem2" },
{ name: "投煤器", path: "/coalFeeding" },
{ name: "投煤器电流", path: "/coalFeedingA" },
{ name: "投煤器2", path: "/coalFeeding2" },
// { name: "", path: "/startPage" },
];
const currentUser = "管理员";
const currentTime = ref(new Date().toLocaleString());
let timeTimer: number | undefined;
onMounted(() => {
timeTimer = window.setInterval(() => {
currentTime.value = new Date().toLocaleString();
}, 1000);
});
onUnmounted(() => {
if (timeTimer) window.clearInterval(timeTimer);
});
</script>
<style scoped>
header {
display: flex;
justify-content: space-between;
align-items: center;
}
.headLeft {
height: 35px;
width: 160px;
line-height: 35px;
font-size: 16px;
font-weight: bold;
}
.headCenter {
width: 100%;
font-size: 12px;
text-align: center;
padding-top: 3px;
}
.headRight {
height: 35px;
font-size: 12px;
}
.headerNav {
padding: 0 1rem;
color: #65758b;
height: 29px;
line-height: 29px;
border-radius: 3px;
display: inline-block;
color: var(--color-text-mute);
}
.headerNav:hover {
background-color: rgb(237, 239, 243);
}
.headerNav:active {
color: #ffffff;
background-color: rgb(11, 117, 203);
}
.active_link {
color: #ffffff;
background-color: rgb(11, 117, 203);
}
@media (min-width: 1024px) {
}
.tiny-button.tiny-button--small {
padding: 0.375rem 0.75rem;
min-width: 2.25rem;
}
</style>

50
src/api/index.ts Normal file
View File

@ -0,0 +1,50 @@
import { get, post, put, del, patch } from "../utils/request";
// Example API declarations. Replace paths with your backend endpoints.
export const api = {
getPlcList(params: Object) {
return get("/source", params);
},
createPlc(data: Object) {
console.log("创建", data);
return post("/source", data);
},
updatePlc(id: string, data: Object) {
return put(`/source/${id}`, data);
},
deletePlc(id: string) {
return del(`/source/${id}`);
},
getNodeTree(id: string) {
return get(`/source/${id}/node-tree`);
},
pointCreate( data: Object) {
return post(`/point`, data);
},
pointUpdate( id: String, data: Object) {
return put(`/point/${id}`, data);
},
pointDel( id: String) {
return del(`/point/${id}`);
},
pointList(params: Object) {
return get("/point", params);
},
getPage(data: Object) {
return get("/page", data);
},
pageCreate(data: Object){
return post("/page", data);
},
pageUpdate(id:string,data: Object){
return put(`/page/${id}`, data);
},
pageDel( id: String) {
return del(`/page/${id}`);
},
unitList(params: Object) {
return get("/unit", params);
},
};
export default api;

88
src/assets/base.css Normal file
View File

@ -0,0 +1,88 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-primary: rgb(249, 250, 251);
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-primary);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition:
color 0.5s,
background-color 0.5s;
line-height: 1.6;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
Oxygen,
Ubuntu,
Cantarell,
"Fira Sans",
"Droid Sans",
"Helvetica Neue",
sans-serif;
font-size: 15px;
color: #65758b;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

836
src/assets/coal.css Normal file
View File

@ -0,0 +1,836 @@
@import "./base.css";
* {
border-color: hsl(var(--border));
}
body {
margin: 0;
line-height: inherit;
background-color: hsl(var(--background));
color: hsl(var(--foreground));
}
html,
body {
font-size: 16px;
color: #65758b;
}
:root {
--background: 210 20% 98%;
--foreground: 220 20% 10%;
--card: 0 0% 100%;
--primary: 207 90% 42%;
--primary-foreground: 0 0% 100%;
--muted-foreground: 215 16% 47%;
--border: 214 20% 88%;
--radius: 0.5rem;
--delete: 248 113 113;
--primary_btn: 11 117 203;
--sidebar-accent: 214 20% 94%;
--sidebar-accent-foreground: 220 20% 10%;
}
html,
:host {
line-height: 1.5;
-webkit-text-size-adjust: 100%;
-moz-tab-size: 4;
tab-size: 4;
font-family: var(--font-inter), system-ui, sans-serif;
font-feature-settings: normal;
font-variation-settings: normal;
-webkit-tap-highlight-color: transparent;
}
hr {
height: 0;
color: inherit;
border-top-width: 1px;
}
a {
color: inherit;
text-decoration: inherit;
}
b,
strong {
font-weight: bolder;
}
table {
text-indent: 0;
border-color: inherit;
border-collapse: collapse;
}
button,
select {
text-transform: none;
}
button,
input:where([type="button"]),
input:where([type="reset"]),
input:where([type="submit"]) {
-webkit-appearance: button;
background-color: transparent;
background-image: none;
}
dl,
dd,
h2,
hr,
p {
margin: 0;
}
legend {
padding: 0;
}
ol,
ul,
menu {
list-style: none;
margin: 0;
padding: 0;
}
dialog {
padding: 0;
}
textarea {
resize: vertical;
}
input {
outline: none;
border: 1px solid #e5e7eb;
}
input::placeholder,
textarea::placeholder {
opacity: 1;
color: #9ca3af;
}
button,
[role="button"] {
cursor: pointer;
}
:disabled {
cursor: default;
}
img,
svg,
video,
canvas {
display: block;
vertical-align: middle;
}
img,
video {
max-width: 100%;
height: auto;
}
*,
::before,
::after {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-gradient-from-position: ;
--tw-gradient-via-position: ;
--tw-gradient-to-position: ;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
--tw-contain-size: ;
--tw-contain-layout: ;
--tw-contain-paint: ;
--tw-contain-style: ;
--tw-content: "";
box-sizing: border-box;
border-width: 0;
border-style: solid;
border-color: #e5e7eb;
}
::backdrop {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-gradient-from-position: ;
--tw-gradient-via-position: ;
--tw-gradient-to-position: ;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
--tw-contain-size: ;
--tw-contain-layout: ;
--tw-contain-paint: ;
--tw-contain-style: ;
}
.coal_main {
display: flex;
flex-direction: column;
}
.flexs {
display: flex;
}
.flex_col {
flex-direction: column;
}
.flex_wrap {
flex-wrap: wrap;
}
.flex_1 {
flex: 1 1 0%;
}
.shrink0 {
flex-shrink: 0;
}
.grow {
flex-grow: 1;
}
.flex {
display: flex;
}
.justify_center {
justify-content: center;
}
.inline_flex {
display: inline-flex;
}
.pageTitle {
font-size: 1.125rem;
line-height: 1.75rem;
font-weight: 700;
letter-spacing: 0.1em;
color: rgb(var(--primary_btn));
}
.shadows {
box-shadow: 0 0 10px 3px rgba(213, 208, 208, 0.192);
}
.borderAll {
border: 1px solid #dae0e7;
border-radius: 0.5rem;
}
.bgWhite {
background-color: #ffffff;
}
.cursor_default {
cursor: default;
}
.cursor_pointer {
cursor: pointer;
}
.list_none {
list-style-type: none;
}
.items_center {
align-items: center;
}
.justify_center {
justify-content: center;
}
.text_sm {
font-size: 0.875rem;
line-height: 1.25rem;
}
.text_xs {
font-size: 0.75rem;
line-height: 1rem;
}
.fontw400 {
font-weight: 400;
}
.fontw500 {
font-weight: 500;
}
.fontw600 {
font-weight: 600;
}
.fontw700 {
font-weight: 700;
}
.border-collapse {
border-collapse: collapse;
}
.w_full {
width: 100%;
}
.w_half {
width: 50%;
display: inline-block;
}
.w_px {
width: 1px;
}
.w2 {
width: 2rem;
}
.w4 {
width: 4rem;
}
.w10 {
width: 10rem;
}
.w0_5 {
width: 0.5rem;
}
.w0_625 {
width: 0.625rem;
}
.w0_75 {
width: 0.75rem;
}
.w1_2 {
width: 1.2rem;
}
.min_w_0 {
min-width: 0px;
}
.min_w_1_75 {
min-width: 1.75rem;
}
.min_w_2 {
min-width: 2rem;
}
.min_w_2_25 {
min-width: 2.25rem;
}
.min_w_6 {
min-width: 6rem;
}
.fixed {
position: fixed;
}
.absolute {
position: absolute;
}
.relative {
position: relative;
}
.sticky {
position: sticky;
}
.color_black {
color: #14181f;
}
.color_green {
color: #059669;
}
.color_primary {
color: rgb(var(--primary_btn));
}
.color_red {
color: #dc2626;
}
.color_base {
color: #65758b;
}
.bg_card {
background-color: #ffffff;
}
.bg_header {
background-color: #edeff3;
}
.visible {
visibility: visible;
}
.invisible {
visibility: hidden;
}
.inset_0 {
inset: 0px;
}
.left0 {
left: 0px;
}
.z_10 {
z-index: 10;
}
.ml0_25 {
margin-left: 0.25rem;
}
.block {
display: block;
}
.table {
display: table;
}
.grid {
display: grid;
}
.hidden {
display: none;
}
.h0_5 {
height: 0.5rem;
}
.h0_625 {
height: 0.625rem;
}
.h0_75 {
height: 0.75rem;
}
.h0_9 {
height: 0.9rem;
line-height: 0.85rem;
}
.h1 {
height: 1rem;
}
.h1_25 {
height: 1.25rem;
}
.h_full {
height: 100%;
}
.wh0_4{
width: 0.4rem;
height: 0.4rem;
}
@keyframes ping {
75%,
100% {
transform: scale(2);
opacity: 0;
}
}
.animate-ping {
animation: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
}
@keyframes pulse {
50% {
opacity: 0.5;
}
}
.gap_12 {
gap: 12px;
}
.gap0_125 {
gap: 0.125rem;
}
.gap0_25 {
gap: 0.25rem;
}
.gap0_375 {
gap: 0.375rem;
}
.gap0_5 {
gap: 0.5rem;
}
.gap1_5 {
gap: 1.5rem;
}
.overflow_hidden {
overflow: hidden;
}
.overflowx_auto {
overflow-x: auto;
}
.whitespace_nowrap {
white-space: nowrap;
}
.rounded {
border-radius: 0.25rem;
}
.rounded_full {
border-radius: 9999px;
}
.rounded_md {
border-radius: calc(var(--radius) - 2px);
}
.border {
border-width: 1px;
}
.border_2 {
border-width: 2px;
}
.border_b {
border-bottom-width: 1px;
}
.border_r {
border-right-width: 1px;
}
.border_border0_5 {
border-color: hsl(var(--border) / 0.5);
}
.border_border0_6 {
border-color: hsl(var(--border) / 0.6);
}
.border_circle {
border-color: rgb(218, 224, 231);
}
.border_green_M {
--tw-border-opacity: 1;
border-color: rgb(16, 185, 129);
}
.border_green_yx {
--tw-border-opacity: 1;
border-color: rgb(5, 150, 105);
}
.border_red_M {
--tw-border-opacity: 1;
border-color: rgb(248, 113, 113);
}
.border_red {
--tw-border-opacity: 1;
border-color: rgb(220, 38, 38);
}
.border_yellow {
--tw-border-opacity: 1;
border-color: rgb(234, 179, 8);
}
.bg_blue {
--tw-bg-opacity: 1;
background-color: rgb(239, 246, 255);
}
.bg_border {
background-color: hsl(var(--border));
}
.bg_card {
background-color: hsl(var(--card));
}
.bg_green_yc {
--tw-bg-opacity: 1;
background-color: rgb(52, 211, 153);
}
.bg_green_M {
--tw-bg-opacity: 1;
background-color: rgb(236 253 245 / var(--tw-bg-opacity, 1));
}
.bg_green_yx {
--tw-bg-opacity: 1;
background-color: rgb(16, 185, 129);
}
.bg_muted {
background-color: rgb(234, 237, 240);
}
.bg_orange {
background-color: rgb(255 247 237 / 0.5);
}
.bg_orange:hover {
background-color: rgb(255 247 237 / 0.8);
}
.bg_primary {
background-color: hsl(var(--primary));
}
.bg_red_M {
--tw-bg-opacity: 1;
background-color: rgb(254, 242, 242);
}
.bg_red {
--tw-bg-opacity: 1;
background-color: rgb(239, 68, 68);
}
.bg_temperature {
background-color: rgb(237, 239, 243);
}
.bg_yellow {
--tw-bg-opacity: 1;
background-color: rgb(250, 204, 21);
}
.p0_5 {
padding: 0.5rem;
}
.p0_75 {
padding: 0.75rem;
}
.px0_25 {
padding-left: 0.25rem;
padding-right: 0.25rem;
}
.px0_375 {
padding-left: 0.375rem;
padding-right: 0.375rem;
}
.px0_5 {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
.px1 {
padding-left: 1rem;
padding-right: 1rem;
}
.py0_125 {
padding-top: 0.125rem;
padding-bottom: 0.125rem;
}
.py0_375 {
padding-top: 0.375rem;
padding-bottom: 0.375rem;
}
.py0_5 {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.my0_3 {
margin-top: 0.3rem;
margin-bottom: 0.3rem;
}
.text_left {
text-align: left;
}
.text_center {
text-align: center;
}
.text_right {
text-align: right;
}
.text_10 {
font-size: 10px;
}
.text_11 {
font-size: 11px;
}
.text_7 {
font-size: 7px;
}
.text_8 {
font-size: 8px;
}
.leading_none {
line-height: 1;
}
.letter_space0_05 {
letter-spacing: 0.05em;
}
.text_white {
color: #ffffff;
}
.text_green {
color: rgb(5, 150, 105);
}
.text_emerald {
color: rgb(4, 120, 87);
}
.text_foreground {
color: hsl(var(--foreground));
}
.text_orange_600 {
color: rgb(234, 88, 12);
}
.text_primary {
color: hsl(var(--primary));
}
.text_primary_color {
color: hsl(var(--primary-foreground));
}
.text_red_M {
color: rgb(239, 68, 68);
}
.text_red {
color: rgb(220, 38, 38);
}
.shadow_sm {
--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);
box-shadow:
var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
}
.transition_all {
transition-property: all;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.duration_500 {
transition-duration: 500ms;
}
.running {
animation-play-state: running;
}
:-moz-focusring {
outline: auto;
}
:-moz-ui-invalid {
box-shadow: none;
}
::-webkit-inner-spin-button,
::-webkit-outer-spin-button {
height: auto;
}
[type="search"] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-file-upload-button {
-webkit-appearance: button;
font: inherit;
}
[hidden]:where(:not([hidden="until-found"])) {
display: none;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: hsl(214 20% 94%);
}
::-webkit-scrollbar-thumb {
background: hsl(214 20% 80%);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: hsl(207 90% 42%);
}
.placeholder:text-muted-foreground::placeholder {
color: hsl(var(--muted-foreground));
}
.active:bg-sidebar-accent:active {
background-color: hsl(var(--sidebar-accent));
}
.active:text-sidebar-accent-foreground:active {
color: hsl(var(--sidebar-accent-foreground));
}
@keyframes accordion-up {
from {
height: var(--radix-accordion-content-height);
}
to {
height: 0;
}
}
@keyframes accordion-down {
from {
height: 0;
}
to {
height: var(--radix-accordion-content-height);
}
}
@keyframes enter {
from {
opacity: var(--tw-enter-opacity, 1);
transform: translate3d(
var(--tw-enter-translate-x, 0),
var(--tw-enter-translate-y, 0),
0
)
scale3d(
var(--tw-enter-scale, 1),
var(--tw-enter-scale, 1),
var(--tw-enter-scale, 1)
)
rotate(var(--tw-enter-rotate, 0));
}
}
@keyframes exit {
to {
opacity: var(--tw-exit-opacity, 1);
transform: translate3d(
var(--tw-exit-translate-x, 0),
var(--tw-exit-translate-y, 0),
0
)
scale3d(
var(--tw-exit-scale, 1),
var(--tw-exit-scale, 1),
var(--tw-exit-scale, 1)
)
rotate(var(--tw-exit-rotate, 0));
}
}
.reBtn{
width: 1.5rem;
height: 1.5rem;
}
.c_pointer{
cursor: pointer;
}
.Mcircle {
width: 20px;
height: 20px;
}
.dianji_status {
min-width: 40px;
display: inline-block;
width: 50%;
margin: 0.2rem 0;
}
.btns {
width: 4rem;
height: 1.5rem;
text-align: center;
border-radius: 1px;
background: #e9eaea;
line-height: 1.5rem;
}
.btnsActive {
color: #ffffff;
background: rgb(16, 185, 129);
}
#settings {
margin-top: 1rem;
}

6
src/assets/coal2.css Normal file
View File

@ -0,0 +1,6 @@
.coalMain {
padding: 12px;
gap: 12px;
display: flex;
flex-direction: column;
}

1
src/assets/logo.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 276 B

30
src/assets/main.css Normal file
View File

@ -0,0 +1,30 @@
@import "./base.css";
@import "./coal.css";
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
}
#app {
width: 100%;
height: 100vh;
margin: 0;
font-weight: normal;
color: #65758b;
}
a,
.green {
text-decoration: none;
color: hsla(160, 100%, 37%, 1);
transition: 0.4s;
padding: 3px;
}
@media (hover: hover) {
a:hover {
background-color: rgb(237, 239, 243);
}
}

View File

@ -0,0 +1,41 @@
<script setup lang="ts">
defineProps<{
msg: string
}>()
</script>
<template>
<div class="greetings">
<h1 class="green">{{ msg }}</h1>
<h3>
Youve successfully created a project with
<a href="https://vite.dev/" target="_blank" rel="noopener">Vite</a> +
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>. What's next?
</h3>
</div>
</template>
<style scoped>
h1 {
font-weight: 500;
font-size: 2.6rem;
position: relative;
top: -10px;
}
h3 {
font-size: 1.2rem;
}
.greetings h1,
.greetings h3 {
text-align: center;
}
@media (min-width: 1024px) {
.greetings h1,
.greetings h3 {
text-align: left;
}
}
</style>

View File

@ -0,0 +1,87 @@
<template>
<div class="item">
<i>
<slot name="icon"></slot>
</i>
<div class="details">
<h3>
<slot name="heading"></slot>
</h3>
<slot></slot>
</div>
</div>
</template>
<style scoped>
.item {
margin-top: 2rem;
display: flex;
position: relative;
}
.details {
flex: 1;
margin-left: 1rem;
}
i {
display: flex;
place-items: center;
place-content: center;
width: 32px;
height: 32px;
color: var(--color-text);
}
h3 {
font-size: 1.2rem;
font-weight: 500;
margin-bottom: 0.4rem;
color: var(--color-heading);
}
@media (min-width: 1024px) {
.item {
margin-top: 0;
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
}
i {
top: calc(50% - 25px);
left: -26px;
position: absolute;
border: 1px solid var(--color-border);
background: var(--color-background);
border-radius: 8px;
width: 50px;
height: 50px;
}
.item:before {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
bottom: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:after {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
top: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:first-of-type:before {
display: none;
}
.item:last-of-type:after {
display: none;
}
}
</style>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
/>
</svg>
</template>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
<path
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
/>
</svg>
</template>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
<path
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
/>
</svg>
</template>

View File

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
/>
</svg>
</template>

View File

@ -0,0 +1,19 @@
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
role="img"
class="iconify iconify--mdi"
width="24"
height="24"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 24 24"
>
<path
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
fill="currentColor"
></path>
</svg>
</template>

10
src/main.ts Normal file
View File

@ -0,0 +1,10 @@
import "./assets/main.css";
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import TinyVue from "@opentiny/vue"; // import '@opentiny/vue/dist/index.css'
const app = createApp(App);
app.use(router);
app.use(TinyVue);
app.mount("#app");

66
src/router/index.ts Normal file
View File

@ -0,0 +1,66 @@
import { createRouter, createWebHistory } from "vue-router";
import warning from "../views/warning.vue";
import runsystem from "../views/runsystem.vue";
import firesystem from "../views/firesystem.vue";
import firesystem2 from "../views/firesystem2.vue";
import coalFeeding from "../views/coalFeeding.vue";
import coalFeeding2 from "../views/coalFeeding2.vue";
import coalFeedingA from "../views/coalFeedingA.vue";
import plcList from "../views/plcList.vue";
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: "/",
name: "home",
component: coalFeeding,
},
{
path: "/warning",
name: "warning",
component: warning,
},
{
path: "/plcList",
name: "plcList",
component: plcList,
},
{
path: "/runsystem",
name: "runsystem",
component: runsystem,
},
{
path: "/firesystem",
name: "firesystem",
component: firesystem,
},
{
path: "/firesystem2",
name: "firesystem2",
component: firesystem2,
},
{
path: "/coalFeeding",
name: "coalFeeding",
component: coalFeeding,
},
{
path: "/coalFeedingA",
name: "coalFeedingA",
component: coalFeedingA,
},
{
path: "/coalFeeding2",
name: "coalFeeding2",
component: coalFeeding2,
},
{
path: "/startPage",
name: "startPage",
component: () => import("../views/startPage.vue"),
},
],
});
export default router;

119
src/utils/request.ts Normal file
View File

@ -0,0 +1,119 @@
const DEFAULT_TIMEOUT = 10000;
function toQueryString(params: Record<string, any> = {}): string {
const searchParams = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
if (value === undefined || value === null) return;
if (Array.isArray(value)) {
value.forEach((item) => searchParams.append(key, String(item)));
return;
}
searchParams.append(key, String(value));
});
const query = searchParams.toString();
return query ? `?${query}` : "";
}
function buildUrl(url: string, params?: Record<string, any>): string {
if (!params || Object.keys(params).length === 0) return url;
const query = toQueryString(params);
return `${url}${url.includes("?") ? `&${query.slice(1)}` : query}`;
}
async function parseResponse(response: Response): Promise<any> {
const contentType = response.headers.get("content-type") || "";
if (contentType.includes("application/json")) {
return response.json();
}
return response.text();
}
function normalizeError(status: number, message?: string, payload?: any): Error {
const error = new Error(message || "Request failed");
(error as any).status = status;
(error as any).payload = payload;
return error;
}
export async function request(
url: string,
options: {
method?: string;
params?: Record<string, any>;
data?: any;
headers?: Record<string, string>;
timeout?: number;
baseURL?: string;
cache?: RequestCache;
} = {},
): Promise<any> {
const {
method = "GET",
params,
data,
headers = {},
timeout = DEFAULT_TIMEOUT,
baseURL = import.meta.env.VITE_API_BASE_URL || "",
cache,
...restOptions
} = options;
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeout);
const fullUrl = buildUrl(`${baseURL}${url}`, params);
const finalHeaders = { ...headers };
const fetchOptions: RequestInit = {
method,
headers: finalHeaders,
signal: controller.signal,
cache: cache ?? (method.toUpperCase() === "GET" ? "no-store" : undefined),
...restOptions,
};
if (data !== undefined) {
if (data instanceof FormData) {
fetchOptions.body = data;
} else {
finalHeaders["Content-Type"] = finalHeaders["Content-Type"] || "application/json";
fetchOptions.body = JSON.stringify(data);
}
}
try {
const response = await fetch(fullUrl, fetchOptions);
const payload = await parseResponse(response);
if (!response.ok) {
throw normalizeError(response.status, payload?.message || response.statusText, payload);
}
return payload;
} catch (error) {
if ((error as Error).name === "AbortError") {
throw normalizeError(408, "Request timeout");
}
throw error;
} finally {
clearTimeout(timer);
}
}
export const get = (
url: string,
params?: Record<string, any>,
options: Record<string, any> = {},
): Promise<any> => request(url, { ...options, method: "GET", params });
export const post = (url: string, data: any, options: Record<string, any> = {}): Promise<any> =>
request(url, { ...options, method: "POST", data });
export const put = (url: string, data: any, options: Record<string, any> = {}): Promise<any> =>
request(url, { ...options, method: "PUT", data });
export const patch = (url: string, data: any, options: Record<string, any> = {}): Promise<any> =>
request(url, { ...options, method: "PATCH", data });
export const del = (url: string,params?: Record<string, any>,options: Record<string, any> = {},): Promise<any> =>
request(url, { ...options, method: "DELETE", params });

18921
src/utils/treedata.json Normal file

File diff suppressed because it is too large Load Diff

1241
src/views/coalFeeding.vue Normal file

File diff suppressed because it is too large Load Diff

922
src/views/coalFeeding2.vue Normal file
View File

@ -0,0 +1,922 @@
<template>
<main class="coal_main p0_75 gap_12" style="flex-grow: 1">
<div class="text_center">
<h2 class="pageTitle">1#隧道窑投煤器</h2>
</div>
<div class="flexs items_center justify_center gap1_5 py0_375 px1 bgWhite borderAll shadows">
<div class="flexs items_center gap0_5">
<span>站点总数</span>
<span class="text_sm fontw700 color_black"
>18
<span class="fontw400 color_base ml0_25"></span>
</span>
</div>
<span class="h1 w_px bg_border"></span>
<div class="flexs items_center gap0_5">
<span>运行中</span>
<span class="text_sm fontw700 color_green">
16<span class="fontw400 color_base ml0_25"></span>
</span>
</div>
<span class="h1 w_px bg_border"></span>
<div class="flexs items_center gap0_5">
<span>自动模式</span>
<span class="text_sm fontw700 color_primary">
16<span class="fontw400 color_base ml0_25"></span>
</span>
</div>
<span class="h1 w_px bg_border"></span>
<div class="flexs items_center gap0_5">
<span>最高温度</span>
<span class="text_sm fontw700 color_red">
1000<span class="fontw400 color_base ml0_25">°C</span>
</span>
</div>
</div>
<div class="flex_1 flexs gap0_5">
<!-- 点位列表 -->
<ul class="flexs flex_col gap0_5 w10 borderAll bgWhite p0_5">
<li class="text_10">点位列表
<tiny-button type="info" :icon="iconRefresh" plain size="small" @click="getPointList">刷新</tiny-button>
</li>
<li
v-for="point in pointList"
:key="point.id"
class="point-item"
draggable="true"
@dragstart="onPointDragStart($event, point)"
>
<span class="text_10">{{ point.name }}</span>
<span class="text_10" v-if="point.point_monitor">-[{{ point.point_monitor.value_text }}]</span>
</li>
</ul>
<!-- 数据表格 -->
<div class="flex_1 shrink0 overflowx_auto borderAll bg_card">
<div class="flex_1 overflowx_auto borderAll">
<table class="w_full text_xs">
<thead class="bg_header">
<tr>
<th
class="sticky left0 z_10 p0_5 fontw600 text_left "
style="background-color: #edeff3"
>
参数/站号
</th>
<th
v-for="item in dataList"
:key="item.team"
class="px0_25 py0_5 text_center border_r border_border0_6 min_w_6"
>
<span
class="inline_flex items_center justify_center h1_25 min_w_6 px0_375 rounded bg_primary text_primary_color fontw700 text_11"
>{{ item.team }}</span
>
</th>
</tr>
</thead>
<tbody>
<!-- 投煤运行设定 -->
<tr class="border_border0_5">
<td class="sticky left0 z_10 bg_card px0_5 py0_375 text_11 color_base whitespace_nowrap border_r border_border fontw500">
投煤运行设定
</td>
<td
v-for="item in dataList"
:key="item.team"
class="px0_25 py0_375 text_center border_r border_border0_6 min_w_6"
>
<div v-if="item.statusAuto" class="px0_25 py0_125 text_center text_xs color_base">
{{ item.tmyxsds }}
</div>
<input
v-else
class="px0_25 py0_125 text_center text_xs color_base"
v-model="item.tmyxsds"
style="width: 6rem"
/>
</td>
</tr>
<!-- 投煤停止设定 -->
<tr class="border_border0_5">
<td class="sticky left0 z_10 bg_card px0_5 py0_375 text_11 color_base whitespace_nowrap border_r border_border fontw500">
投煤停止设定
</td>
<td
v-for="item in dataList"
:key="item.team"
class="px0_25 py0_375 text_center border_r border_border0_6 min_w_6"
>
<div v-if="item.statusAuto" class="px0_25 py0_125 text_center text_xs color_base">
{{ item.tmtzsds }}
</div>
<input
v-else
class="px0_25 py0_125 text_center text_xs color_base"
v-model="item.tmtzsds"
style="width: 6rem"
/>
</td>
</tr>
<!-- 投煤累积设定 -->
<tr class="border_border0_5">
<td class="sticky left0 z_10 bg_card px0_5 py0_375 text_11 color_base whitespace_nowrap border_r border_border fontw500">
投煤累积设定
</td>
<td
v-for="item in dataList"
:key="item.team"
class="px0_25 py0_375 text_center border_r border_border0_6 min_w_6"
>
<div v-if="item.statusAuto" class="px0_25 py0_125 text_center text_xs color_base">
{{ item.tmljsds }}
</div>
<input
v-else
class="px0_25 py0_125 text_center text_xs color_base"
v-model="item.tmljsds"
style="width: 6rem"
/>
</td>
</tr>
<!-- 布料运行设定 -->
<tr class="border_border">
<td class="sticky left0 z_10 bg_card px0_5 py0_375 text_11 color_base whitespace_nowrap border_r border_border fontw500">
布料运行设定
</td>
<td
v-for="item in dataList"
:key="item.team"
class="px0_25 py0_375 text_center border_r border_border0_6 min_w_6"
>
<div v-if="item.statusAuto" class="px0_25 py0_125 text_center text_xs color_base">
{{ item.blyxsds }}
</div>
<input
v-else
class="px0_25 py0_125 text_center text_xs color_base"
v-model="item.blyxsds"
style="width: 6rem"
/>
</td>
</tr>
<!-- 中间侧 -->
<tr class="border_border bg_blue">
<td class="sticky left0 z_10 bg_blue px0_5 text_10 fontw700 text_primary border_r border_border letter_space0_05">
中间侧
</td>
<td
v-for="(item,index1) in dataList"
:key="item.team"
class="py0_125 border_r border_border0_6 min_w_6"
>
<div
v-for="(dj,index2) in item.dianji"
class="flexs flex_col items_center gap0_25 text_center dianji_status"
>
<div class="flexs items_center gap0_25 justify_center" :id="'unit'+index1+'_dj'+index2">
<div :class="['relative inline_flex items_center justify_center rounded_full border_2 Mcircle',{'border_red_M bg_red_M': dj.isStop,'border_green_M bg_green_M': !dj.isStop}]">
<span :class="['fontw700 leading_none text_10',{'text_red_M': dj.isStop,'text_green': !dj.isStop}]">M</span>
</div>
<div class="flexs flex_col justify_center items_center gap0_25 my0_3">
<span v-if="dj.isBendi" class="border w1_2 text_8 rounded h0_9 bg_yellow border_yellow text_white">本地</span>
<span v-else class="border w1_2 text_8 rounded h0_9 bg_green_yc border_green_M text_white">远程</span>
<span v-if="dj.isStop" class="border w1_2 text_8 rounded h0_9 bg_red border_red text_white c_pointer">停止</span>
<span v-else class="border w1_2 text_8 rounded h0_9 bg_green_yx border_green_yx text_white c_pointer">运行</span>
</div>
</div>
<div class="items_center">
<span v-if="dj.eqm == '正常'" class="text_10 fontw600 text_emerald">{{dj.eqm}}</span>
<span v-else class="text_10 fontw600 text_red">{{ dj.eqm }}</span>
</div>
</div>
</td>
</tr>
<!-- 投煤累积时间 -->
<tr class="border_border0_5">
<td class="sticky left0 z_10 bg_card px0_5 py0_375 text_11 color_base whitespace_nowrap border_r border_border fontw500">
投煤累积时间
</td>
<td
v-for="item in dataList"
:key="item.team"
class="px0_25 py0_375 text_center border_r border_border0_6 min_w_6"
>
<div class="px0_25 py0_125 text_center text_xs text_foreground fontw600">
{{ item.tmljsjs }}
</div>
</td>
</tr>
<!-- 2侧温度 -->
<tr class="border_border0_5 bg_orange">
<td class="sticky left0 z_10 px0_5 py0_5 text_11 text_orange_600 whitespace_nowrap border_r border_border fontw600">
2侧温度
</td>
<td
v-for="item in dataList"
:key="item.team"
class="px0_25 py0_375 border_r border_border0_6 min_w_6 text_center"
>
<span class="text_xs fontw700 min_w_2_25 text_right text_red">{{item.tempera2}}</span>
</td>
</tr>
<!-- 车位号 -->
<tr class="border_border bg_temperature">
<td class="sticky left0 z_10 bg_temperature px0_5 text_11 fontw700 text_foreground border_r border_border">
车位号
</td>
<td
v-for="item in dataList"
:key="item.team"
class="p0_25 text_center border_r border_border0_6 min_w_6"
>
<span class="inline_flex items_center justify_center h1_25 min_w_1_75 px0_375 rounded_md bg_primary text_primary_color my0_3 fontw700 text_sm shadow_sm">{{ item.carNumber }}</span>
</td>
</tr>
<!-- 1侧温度 -->
<tr class="border_border0_5 bg_orange">
<td class="sticky left0 z_10 px0_5 py0_5 text_11 text_orange_600 whitespace_nowrap border_r border_border fontw600">
1侧温度
</td>
<td
v-for="item in dataList"
:key="item.team"
class="px0_25 py0_375 border_r border_border0_6 min_w_6 text_center"
>
<span class="text_xs fontw700 min_w_2_25 text_right text_red">{{item.tempera1}}</span>
</td>
</tr>
<!-- 投煤累积时间 -->
<tr class="border_border0_5">
<td
class="sticky left0 z_10 bg_card px0_5 py0_375 text_11 color_base whitespace_nowrap border_r border_border fontw500"
>
投煤累积时间
</td>
<td
v-for="item in dataList"
:key="item.team"
class="px0_25 py0_375 text_center border_r border_border0_6 min_w_6"
>
<div class="px0_25 py0_125 text_center text_xs text_foreground fontw600">
{{ item.tmljsjx }}
</div>
</td>
</tr>
<!-- 回车侧 -->
<tr class="border_border bg_blue">
<td class="sticky left0 z_10 bg_blue px0_5 text_10 fontw700 text_primary border_r border_border letter_space0_05">
回车侧
</td>
<td
v-for="item in dataList"
:key="item.team"
class="py0_125 border_r border_border0_6 min_w_6"
>
<div
v-for="dj in item.dianji"
class="flexs flex_col items_center gap0_25 text_center dianji_status"
>
<div class="flexs items_center gap0_25 justify_center">
<div :class="['relative inline_flex items_center justify_center rounded_full border_2 Mcircle',{'border_red_M bg_red_M': dj.isStop,'border_green_M bg_green_M': !dj.isStop}]">
<span :class="['fontw700 leading_none text_10',{'text_red_M': dj.isStop,'text_green': !dj.isStop}]">M</span>
</div>
<div class="flexs flex_col justify_center items_center gap0_25 my0_3">
<span v-if="dj.isBendi" class="border w1_2 text_8 rounded h0_9 bg_yellow border_yellow text_white">本地</span>
<span v-else class="border w1_2 text_8 rounded h0_9 bg_green_yc border_green_M text_white">远程</span>
<span v-if="dj.isStop" class="border w1_2 text_8 rounded h0_9 bg_red border_red text_white c_pointer">停止</span>
<span v-else class="border w1_2 text_8 rounded h0_9 bg_green_yx border_green_yx text_white c_pointer">运行</span>
</div>
</div>
<div class="items_center">
<span v-if="dj.eqm == '正常'" class="text_10 fontw600 text_emerald">{{dj.eqm}}</span>
<span v-else class="text_10 fontw600 text_red">{{ dj.eqm }}</span>
</div>
</div>
</td>
</tr>
<!-- 投煤运行设定 -->
<tr class="border_border0_5">
<td class="sticky left0 z_10 bg_card px0_5 py0_375 text_11 color_base whitespace_nowrap border_r border_border fontw500">
投煤运行设定
</td>
<td
v-for="item in dataList"
:key="item.team"
class="px0_25 py0_375 text_center border_r border_border0_6 min_w_6"
>
<div v-if="item.statusAuto" class="px0_25 py0_125 text_center text_xs color_base">
{{ item.tmyxsdx }}
</div>
<input
v-else
class="px0_25 py0_125 text_center text_xs color_base"
v-model="item.tmyxsdx"
style="width: 6rem"
/>
</td>
</tr>
<!-- 投煤停止设定 -->
<tr class="border_border0_5">
<td class="sticky left0 z_10 bg_card px0_5 py0_375 text_11 color_base whitespace_nowrap border_r border_border fontw500">
投煤停止设定
</td>
<td
v-for="item in dataList"
:key="item.team"
class="px0_25 py0_375 text_center border_r border_border0_6 min_w_6"
>
<div v-if="item.statusAuto" class="px0_25 py0_125 text_center text_xs color_base">
{{ item.tmtzsdx }}
</div>
<input
v-else
class="px0_25 py0_125 text_center text_xs color_base"
v-model="item.tmtzsdx"
style="width: 6rem"
/>
</td>
</tr>
<!-- 投煤累积设定 -->
<tr class="border_border0_5">
<td class="sticky left0 z_10 bg_card px0_5 py0_375 text_11 color_base whitespace_nowrap border_r border_border fontw500">
投煤累积设定
</td>
<td
v-for="item in dataList"
:key="item.team"
class="px0_25 py0_375 text_center border_r border_border0_6 min_w_6"
>
<div v-if="item.statusAuto" class="px0_25 py0_125 text_center text_xs color_base">
{{ item.tmljsdx }}
</div>
<input
v-else
class="px0_25 py0_125 text_center text_xs color_base"
v-model="item.tmljsdx"
style="width: 6rem"
/>
</td>
</tr>
<!-- 布料运行设定 -->
<tr class="border_border">
<td class="sticky left0 z_10 bg_card px0_5 py0_375 text_11 color_base whitespace_nowrap border_r border_border fontw500">
布料运行设定
</td>
<td
v-for="item in dataList"
:key="item.team"
class="px0_25 py0_375 text_center border_r border_border0_6 min_w_6"
>
<div v-if="item.statusAuto" class="px0_25 py0_125 text_center text_xs color_base">
{{ item.blyxsdx }}
</div>
<input
v-else
class="px0_25 py0_125 text_center text_xs color_base"
v-model="item.blyxsdx"
style="width: 6rem"
/>
</td>
</tr>
</tbody>
</table>
</div>
<div id="settings">
<div class="flexs justify_center gap1_5">
<div class="flexs gap0_25 flex_col items_center">
<span class="text_11 fontw600">第一单元投煤器手动切换</span>
<div class="flexs items_center gap0_125">
<span
:class="{ text_10: true, btns: true, btnsActive: !unit1 }"
@click="statusChange('1')"
>手动</span>
<span
:class="{ text_10: true, btns: true, btnsActive: unit1 }"
@click="statusAuto('1')"
>自动</span>
</div>
</div>
<div class="flexs gap0_25 flex_col items_center">
<span class="text_11 fontw600">第二单元投煤器手动切换</span>
<div class="flexs items_center gap0_125">
<span
:class="{ text_10: true, btns: true, btnsActive: !unit2 }"
@click="statusChange('2')"
>手动</span>
<span
:class="{ text_10: true, btns: true, btnsActive: unit2 }"
@click="statusAuto('2')"
>自动</span>
</div>
</div>
<div class="flexs gap0_25 flex_col items_center">
<span class="text_11 fontw600">第三单元投煤器手动切换</span>
<div class="flexs items_center gap0_125">
<span
:class="{ text_10: true, btns: true, btnsActive: !unit3 }"
@click="statusChange('3')"
>手动</span>
<span
:class="{ text_10: true, btns: true, btnsActive: unit3 }"
@click="statusAuto('3')"
>自动</span>
</div>
</div>
<div class="flexs gap0_25 flex_col items_center">
<span class="text_11 fontw600">第四单元投煤器手动切换</span>
<div class="flexs items_center gap0_125">
<span
:class="{ text_10: true, btns: true, btnsActive: !unit4 }"
@click="statusChange('4')"
>手动</span>
<span
:class="{ text_10: true, btns: true, btnsActive: unit4 }"
@click="statusAuto('4')"
>自动</span>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from "vue";
import { IconRefresh } from "@opentiny/vue-icon";
import api from "../api";
const iconRefresh = IconRefresh()
const pointList = ref<PointItem[]>([]);
const ws = ref<WebSocket | null>(null);
const error = ref(null);
type PointItem = {
id?: string | number;
name: string;
point_monitor?: {
value_text: string;
};
};
const dataList = [
{
team: "第一单元",
tmyxsds: 3,
tmyxsdx: 3,
tmtzsds: 55,
tmtzsdx: 55,
tmljsds: 55,
tmljsdx: 58,
blyxsds: 3,
blyxsdx: 3,
tmljsjs: 49,
tmljsjx: 50,
tempera1: 103,
tempera2: 50,
carNumber: 14,
statusAuto: false,
dianji: [
{ isStop: true, isBendi: true, eqm: "正常" },
{ isStop: true, isBendi: true, eqm: "正常" },
{ isStop: true, isBendi: true, eqm: "正常" },
{ isStop: true, isBendi: true, eqm: "正常" },
],
},
{
team: "第二单元第一组",
tmyxsds: 2,
tmyxsdx: 3,
tmtzsds: 120,
tmtzsdx: 120,
tmljsds: 57,
tmljsdx: 57,
blyxsds: 40,
blyxsdx: 40,
tmljsjs: 39,
tmljsjx: 23,
tempera1: 70,
tempera2: 44,
carNumber: 15,
statusAuto: true,
dianji: [
{ isStop: true, isBendi: true, eqm: "正常" },
{ isStop: true, isBendi: true, eqm: "正常" },
{ isStop: true, isBendi: true, eqm: "正常" },
{ isStop: true, isBendi: true, eqm: "正常" },
],
},
{
team: "第二单元第二组",
tmyxsds: 2,
tmyxsdx: 2,
tmtzsds: 480,
tmtzsdx: 480,
tmljsds: 58,
tmljsdx: 58,
blyxsds: 40,
blyxsdx: 40,
tmljsjs: 12,
tmljsjx: 9,
tempera1: 131,
tempera2: 91,
carNumber: 16,
statusAuto: true,
dianji: [
{ isStop: true, isBendi: true, eqm: "正常" },
{ isStop: true, isBendi: true, eqm: "正常" },
{ isStop: true, isBendi: true, eqm: "正常" },
{ isStop: true, isBendi: true, eqm: "正常" },
],
},
{
team: "第二单元第三组",
tmyxsds: 2,
tmyxsdx: 2,
tmtzsds: 240,
tmtzsdx: 240,
tmljsds: 59,
tmljsdx: 59,
blyxsds: 38,
blyxsdx: 38,
tmljsjs: 12,
tmljsjx: 27,
tempera1: 274,
tempera2: 359,
carNumber: 17,
statusAuto: true,
dianji: [
{ isStop: false, isBendi: false, eqm: "正常" },
{ isStop: false, isBendi: false, eqm: "正常" },
{ isStop: false, isBendi: false, eqm: "正常" },
{ isStop: false, isBendi: false, eqm: "正常" },
],
},
{
team: "第二单元第四组",
tmyxsds: 3,
tmyxsdx: 3,
tmtzsds: 120,
tmtzsdx: 120,
tmljsds: 60,
tmljsdx: 60,
blyxsds: 38,
blyxsdx: 38,
tmljsjs: 41,
tmljsjx: 36,
tempera1: 292,
tempera2: 60,
carNumber: 18,
statusAuto: true,
dianji: [
{ isStop: false, isBendi: false, eqm: "正常" },
{ isStop: false, isBendi: false, eqm: "正常" },
{ isStop: false, isBendi: false, eqm: "正常" },
{ isStop: false, isBendi: false, eqm: "正常" },
],
},
{
team: "第二单元第五组",
tmyxsds: 2,
tmyxsdx: 2,
tmtzsds: 240,
tmtzsdx: 240,
tmljsds: 57,
tmljsdx: 57,
blyxsds: 36,
blyxsdx: 36,
tmljsjs: 32,
tmljsjx: 22,
tempera1: 395,
tempera2: 368,
carNumber: 19,
statusAuto: true,
dianji: [
{ isStop: false, isBendi: false, eqm: "正常" },
{ isStop: false, isBendi: false, eqm: "正常" },
{ isStop: false, isBendi: false, eqm: "正常" },
{ isStop: false, isBendi: false, eqm: "正常" },
],
},
{
team: "第二单元第六组",
tmyxsds: 2,
tmyxsdx: 2,
tmtzsds: 180,
tmtzsdx: 180,
tmljsds: 60,
tmljsdx: 60,
blyxsds: 36,
blyxsdx: 36,
tmljsjs: 21,
tmljsjx: 25,
tempera1: 558,
tempera2: 477,
carNumber: 20,
statusAuto: true,
dianji: [
{ isStop: false, isBendi: false, eqm: "正常" },
{ isStop: false, isBendi: false, eqm: "正常" },
{ isStop: false, isBendi: false, eqm: "正常" },
{ isStop: false, isBendi: false, eqm: "正常" },
],
},
{
team: "第三单元第一组",
tmyxsds: 2,
tmyxsdx: 2,
tmtzsds: 90,
tmtzsdx: 90,
tmljsds: 60,
tmljsdx: 60,
blyxsds: 36,
blyxsdx: 36,
tmljsjs: 30,
tmljsjx: 48,
tempera1: 683,
tempera2: 652,
carNumber: 21,
statusAuto: true,
dianji: [
{ isStop: false, isBendi: false, eqm: "正常" },
{ isStop: false, isBendi: false, eqm: "正常" },
{ isStop: false, isBendi: false, eqm: "正常" },
{ isStop: false, isBendi: false, eqm: "正常" },
],
},
{
team: "第三单元第二组",
tmyxsds: 3,
tmyxsdx: 3,
tmtzsds: 90,
tmtzsdx: 90,
tmljsds: 60,
tmljsdx: 60,
blyxsds: 36,
blyxsdx: 36,
tmljsjs: 58,
tmljsjx: 31,
tempera1: 916,
tempera2: 905,
carNumber: 22,
statusAuto: true,
dianji: [
{ isStop: false, isBendi: false, eqm: "正常" },
{ isStop: false, isBendi: false, eqm: "正常" },
{ isStop: false, isBendi: false, eqm: "正常" },
{ isStop: false, isBendi: false, eqm: "正常" },
],
},
{
team: "第三单元第三组",
tmyxsds: 2,
tmyxsdx: 2,
tmtzsds: 180,
tmtzsdx: 180,
tmljsds: 60,
tmljsdx: 60,
blyxsds: 36,
blyxsdx: 36,
tmljsjs: 6,
tmljsjx: 6,
tempera1: 939,
tempera2: 898,
carNumber: 23,
statusAuto: true,
dianji: [
{ isStop: false, isBendi: false, eqm: "正常" },
{ isStop: false, isBendi: false, eqm: "正常" },
{ isStop: false, isBendi: false, eqm: "正常" },
{ isStop: false, isBendi: false, eqm: "正常" },
],
},
{
team: "第三单元第四组",
tmyxsds: 2,
tmyxsdx: 2,
tmtzsds: 480,
tmtzsdx: 480,
tmljsds: 60,
tmljsdx: 60,
blyxsds: 36,
blyxsdx: 36,
tmljsjs: 46,
tmljsjx: 37,
tempera1: 1000,
tempera2: 995,
carNumber: 24,
statusAuto: true,
dianji: [
{ isStop: false, isBendi: false, eqm: "正常" },
{ isStop: false, isBendi: false, eqm: "正常" },
{ isStop: false, isBendi: false, eqm: "正常" },
{ isStop: false, isBendi: false, eqm: "正常" },
],
},
{
team: "第三单元第五组",
tmyxsds: 3,
tmyxsdx: 3,
tmtzsds: 90,
tmtzsdx: 90,
tmljsds: 59,
tmljsdx: 59,
blyxsds: 36,
blyxsdx: 36,
tmljsjs: 3,
tmljsjx: 41,
tempera1: 967,
tempera2: 950,
carNumber: 25,
statusAuto: true,
dianji: [
{ isStop: false, isBendi: false, eqm: "正常" },
{ isStop: false, isBendi: false, eqm: "正常" },
{ isStop: false, isBendi: false, eqm: "正常" },
{ isStop: false, isBendi: false, eqm: "故障" },
],
},
{
team: "第三单元第六组",
tmyxsds: 2,
tmyxsdx: 2,
tmtzsds: 60,
tmtzsdx: 60,
tmljsds: 63,
tmljsdx: 63,
blyxsds: 36,
blyxsdx: 36,
tmljsjs: 41,
tmljsjx: 26,
tempera1: 919,
tempera2: 920,
carNumber: 26,
statusAuto: true,
dianji: [
{ isStop: false, isBendi: false, eqm: "正常" },
{ isStop: false, isBendi: false, eqm: "正常" },
{ isStop: false, isBendi: false, eqm: "正常" },
{ isStop: false, isBendi: false, eqm: "正常" },
],
},
{
team: "第四单元",
tmyxsds: 2,
tmyxsdx: 2,
tmtzsds: 90,
tmtzsdx: 90,
tmljsds: 65,
tmljsdx: 65,
blyxsds: 36,
blyxsdx: 36,
tmljsjs: 34,
tmljsjx: 41,
tempera1: 716,
tempera2: 755,
carNumber: 27,
statusAuto: true,
dianji: [
{ isStop: false, isBendi: false, eqm: "正常" },
{ isStop: false, isBendi: false, eqm: "正常" },
],
},
];
const unit1 = ref(false);
const unit2 = ref(true);
const unit3 = ref(true);
const unit4 = ref(true);
//
const clearError = () => {
error.value = null;
};
const onPointDragStart = (event: DragEvent, point: PointItem) => {
// 使 event.dataTransfer.setData()
if (event.dataTransfer) {
event.dataTransfer.setData('text/plain', JSON.stringify(point));
}
};
// WebSocket
const initWebSocket = () => {
ws.value = new WebSocket(`ws://10.0.11.74:60309/ws/public`);
ws.value.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('WebSocketdata:', data);
if (data.type === "PointNewValue") {
pointList.value.forEach(item => {
if (item.id === data.data.point_id) {
item.point_monitor = data.data;
}
});
}
};
ws.value.onclose = () => {
console.log('WebSocket连接关闭');
// 5
setTimeout(initWebSocket, 5000);
};
ws.value.onerror = (err) => {
console.error('WebSocket错误:', err);
};
};
const getPointList = async () => {
clearError();
try {
const res = await api.pointList({});
pointList.value = res.data || [];
} catch (err) {
console.error("获取点位列表失败:", err);
}
};
// WebSocket
onMounted(() => {
initWebSocket();
getPointList();
});
// WebSocket
onUnmounted(() => {
if (ws.value) {
ws.value.close();
}
});
const statusChange = (unit: string) => {
if (unit == "1") {
unit1.value = false;
dataList.forEach((item) => {
if (item.team.indexOf("第一单元") > -1) {
item.statusAuto = false;
}
});
} else if (unit == "2") {
unit2.value = false;
dataList.forEach((item) => {
if (item.team.indexOf("第二单元") > -1) {
item.statusAuto = false;
}
});
} else if (unit == "3") {
unit3.value = false;
dataList.forEach((item) => {
if (item.team.indexOf("第三单元") > -1) {
item.statusAuto = false;
}
});
} else if (unit == "4") {
unit4.value = false;
dataList.forEach((item) => {
if (item.team.indexOf("第四单元") > -1) {
item.statusAuto = false;
}
});
}
};
const statusAuto = (unit: string) => {
if (unit == "1") {
unit1.value = true;
dataList.forEach((item) => {
if (item.team.indexOf("第一单元") > -1) {
item.statusAuto = true;
}
});
} else if (unit == "2") {
unit2.value = true;
dataList.forEach((item) => {
if (item.team.indexOf("第二单元") > -1) {
item.statusAuto = true;
}
});
} else if (unit == "3") {
unit3.value = true;
dataList.forEach((item) => {
if (item.team.indexOf("第三单元") > -1) {
item.statusAuto = true;
}
});
} else if (unit == "4") {
unit4.value = true;
dataList.forEach((item) => {
if (item.team.indexOf("第四单元") > -1) {
item.statusAuto = true;
}
});
}
};
</script>
<style scoped>
.point-list {
max-height: 400px;
overflow-y: auto;
}
.hover:bg_gray_100:hover {
background-color: #f5f5f5;
}
.cursor-pointer {
cursor: pointer;
}
th.sticky.left0,
td.sticky.left0 {
width: 7rem;
min-width: 7rem;
max-width: 7rem;
}
</style>

View File

@ -0,0 +1,14 @@
<template>
<div class="about">
<h1>This is an electric current page</h1>
</div>
</template>
<style>
@media (min-width: 1024px) {
.about {
display: flex;
align-items: center;
}
}
</style>

14
src/views/firesystem.vue Normal file
View File

@ -0,0 +1,14 @@
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>
<style>
@media (min-width: 1024px) {
.about {
display: flex;
align-items: center;
}
}
</style>

14
src/views/firesystem2.vue Normal file
View File

@ -0,0 +1,14 @@
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>
<style>
@media (min-width: 1024px) {
.about {
display: flex;
align-items: center;
}
}
</style>

175
src/views/plcList.vue Normal file
View File

@ -0,0 +1,175 @@
<template>
<div class="content">
<tiny-layout class="h_full">
<tiny-row :flex="true" :gutter="10" class="h_full">
<tiny-col :span="12" :no="1" class="coal_main h_full">
<tiny-button type="info" size="small" @click="rowAdd" style="width: 50px"
>新增</tiny-button
>
<tiny-grid
border
:data="dataList"
:edit-config="{ trigger: 'click', mode: 'cell', showStatus: true }"
@row-click="handleRowClick"
auto-resize
style="flex-grow: 1"
>
<tiny-grid-column field="name" title="PLC名称"></tiny-grid-column>
<tiny-grid-column field="number" title="PLC编号"></tiny-grid-column>
<tiny-grid-column field="points" title="PLC点位"></tiny-grid-column>
<tiny-grid-column title="操作" width="140">
<template #default="data">
<tiny-button type="info" size="small" @click="rowEdit(data.row)">编辑</tiny-button>
<tiny-button type="danger" size="small" @click="rowDel(data.row)">删除</tiny-button>
</template>
</tiny-grid-column>
</tiny-grid>
</tiny-col>
<tiny-col :span="12" :no="2" class="coal_main h_full">
<!-- <tiny-button type="" size="small" style="width: 50px"></tiny-button> -->
<tiny-grid
:data="pointList"
:edit-config="{ trigger: 'click', mode: 'cell', showStatus: true }"
auto-resize
style="flex-grow: 1"
>
<tiny-grid-column type="index" width="60"></tiny-grid-column>
<tiny-grid-column type="selection" width="60"></tiny-grid-column>
<tiny-grid-column field="name" title="点位名称"></tiny-grid-column>
<tiny-grid-column field="number" title="点位编号"></tiny-grid-column>
<tiny-grid-column field="type" title="点位类型"></tiny-grid-column>
<tiny-grid-column title="操作" width="80">
<template #default="data">
<tiny-button type="danger" size="small" @click="pointDel(data.row)"
>删除</tiny-button
>
</template>
</tiny-grid-column>
</tiny-grid>
</tiny-col>
</tiny-row>
</tiny-layout>
<plcList_form :visible="visible" :mode="mode" @update:visible="visible = $event" />
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from "vue";
import api from "../api";
import plcList_form from "./plcList_form.vue";
const visible = ref(false);
const mode = ref("add");
const plc_id = ref(null);
type PlcItem = {
id?: string | number;
name: string;
number: string;
points: number;
};
type PointItem = {
id?: string | number;
name: string;
number: string;
type: string;
};
const dataList = ref<PlcItem[]>([]);
const pointList = ref<PointItem[]>([]);
const rowDel = (item: PlcItem) => {
console.log("删除", item);
const res = api.deletePlc(String(item.id));
};
const pointDel = (point: PointItem) => {
console.log("点位删除", point);
};
const loadDataList = async () => {
try {
const result = await api.getPlcList({});
if (Array.isArray(result)) {
dataList.value = result;
return;
}
if (Array.isArray(result?.data)) {
dataList.value = result.data;
return;
}
if (Array.isArray(result?.dataList)) {
dataList.value = result.dataList;
return;
}
dataList.value = [];
} catch (error) {
console.error("获取PLC列表失败:", error);
dataList.value = [];
}
};
const loadPointList = async (id: string | number) => {
try {
const result = await api.pointList({ plcId: String(id) });
if (Array.isArray(result)) {
pointList.value = result;
return;
}
if (Array.isArray(result?.data)) {
pointList.value = result.data;
return;
}
if (Array.isArray(result?.points)) {
pointList.value = result.points;
return;
}
if (Array.isArray(result?.data?.points)) {
pointList.value = result.data.points;
return;
}
pointList.value = [];
} catch (error) {
console.error("error:", error);
pointList.value = [];
}
};
const handleRowClick = async ({ row }: { row: PlcItem }) => {
if (!row?.id) {
pointList.value = [];
return;
}
plc_id.value = row.id as any;
await loadPointList(row.id);
};
const rowAdd = () => {
mode.value = "add";
visible.value = true;
};
const rowEdit = (row: { id: any,name: string; number: string; points: number }) => {
plc_id.value = row.id;
mode.value = "edit";
visible.value = true;
};
onMounted(() => {
loadDataList();
});
</script>
<style>
.content {
height: 100%;
}
.tiny-button.tiny-button--small {
padding: 0 5px !important;
min-width: 50px !important;
}
@media (min-width: 1024px) {
.about {
display: flex;
align-items: center;
}
}
</style>

169
src/views/plcList_form.vue Normal file
View File

@ -0,0 +1,169 @@
<template>
<tiny-drawer
title="标题"
:visible="visible"
@update:visible="visible = $event"
@confirm="confirm"
>
<div class="about">
<tiny-form label-width="60px">
<tiny-form-item label="ID">
<tiny-input v-model="createData.id" placeholder="请输入ID"></tiny-input>
</tiny-form-item>
<tiny-form-item label="名称">
<tiny-input v-model="createData.name" placeholder="请输入名称"></tiny-input>
</tiny-form-item>
<tiny-form-item label="协议">
<tiny-input v-model="createData.protocol" placeholder="请输入协议"></tiny-input>
</tiny-form-item>
<tiny-form-item label="端点">
<tiny-input v-model="createData.endpoint" placeholder="请输入端点"></tiny-input>
</tiny-form-item>
<tiny-form-item label="点位" v-if="mode=='edit'">
<tiny-tree
ref="treeRef"
show-checkbox
:data="pointData"
node-key="id"
only-check-children="true"
:props="{ children: 'children', label: 'browse_name' }"
@check="check"
@check-change="checkChange"
default-expand-all
></tiny-tree>
</tiny-form-item>
<tiny-form-item>
<tiny-button type="primary" @click="submitClick"> 提交 </tiny-button>
</tiny-form-item>
</tiny-form>
</div>
</tiny-drawer>
</template>
<script setup lang="jsx">
import { ref } from "vue";
import api from "../api";
import treedata from '@/utils/treedata.json';
const visible = ref(false);
const props = defineProps({
mode: {
type: String,
default: "add",
},
});
const emit = defineEmits(["update:visible"]);
const confirm = () => {
emit("update:visible", false);
};
const treeRef = ref();
const createData = ref({
id: "",
name: "",
protocol: "",
endpoint: "",
});
const pointData = ref(treedata);
// function getChecks() {
// const currentKey = treeRef.value.getCurrentKey();
// //
// const checkedKeys = treeRef.value.getCheckedKeys();
// //
// const checkedKeysOnlyLeaf = treeRef.value.getCheckedKeys(true);
// //
// const checkedNodes = treeRef.value.getCheckedNodes();
// //
// const checkedNodesOnlyLeafAndHalf = treeRef.value.getCheckedNodes(true, true);
// //
// const checkedHalfKeys = treeRef.value.getHalfCheckedKeys();
// //
// const checkedHalfNodes = treeRef.value.getHalfCheckedNodes();
// console.log(" ", treeRef.value, {
// currentKey,
// checkedKeys,
// checkedKeysOnlyLeaf,
// checkedNodes,
// checkedNodesOnlyLeafAndHalf,
// checkedHalfKeys,
// checkedHalfNodes,
// });
// }
// function clear() {
// treeRef.value.setCheckedKeys([]);
// }
// function setChecked() {
// // setChecked: (data, checked, deep) => voiddeep true
// treeRef.value.setChecked({ id: "1-1" }, true, true);
// treeRef.value.setChecked({ id: "3" }, true, false);
// }
// function setCheckedByNodeKey() {
// // setCheckedByNodeKey: (key, checked) => void
// treeRef.value.setCheckedByNodeKey("1-1", false);
// }
// function setCheckedKeys() {
// // setCheckedKeys: (keys, leafOnly)=>void
// treeRef.value.setCheckedKeys(["1-1", "2-1", "3-1"]);
// }
// function setCheckedNodes() {
// // setCheckedNodes: (nodes, leafOnly)=>void leafOnly, 1-1
// treeRef.value.setCheckedNodes([{ id: "1-1" }, { id: "2-1" }, { id: "3-1" }], true);
// }
function check(data, currentChecked) {
console.log("check 事件:", data, currentChecked);
}
function checkChange(data, checked, indeterminate) {
console.log("checkChange 事件:", data, checked, indeterminate);
}
const submitClick = async () => {
//
if (!createData.value.id || !createData.value.name || !createData.value.protocol || !createData.value.endpoint) {
alert('请填写完整信息');
return;
}
//
// const checkedKeys = treeRef.value.getCheckedKeys();
// if (checkedKeys.length === 0) {
// alert('');
// return;
// }
try {
//
const submitData = { ...createData.value};
// API
const res = await api.createPlc(submitData);
alert('提交成功');
//
emit('refresh');
//
visible.value = false;
} catch (error) {
console.error('提交失败:', error);
alert('提交失败,请重试');
}
}
</script>
<style>
.btn {
width: 4rem;
height: 1.9rem;
color: #ffffff;
text-align: center;
background: rgb(var(--primary_btn));
line-height: 1.8rem;
cursor: pointer;
}
.delete_btn {
background: rgb(var(--delete));
}
@media (min-width: 1024px) {
.about {
display: flex;
align-items: center;
}
}
</style>

14
src/views/runsystem.vue Normal file
View File

@ -0,0 +1,14 @@
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>
<style>
@media (min-width: 1024px) {
.about {
display: flex;
align-items: center;
}
}
</style>

14
src/views/startPage.vue Normal file
View File

@ -0,0 +1,14 @@
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>
<style>
@media (min-width: 1024px) {
.about {
display: flex;
align-items: center;
}
}
</style>

14
src/views/warning.vue Normal file
View File

@ -0,0 +1,14 @@
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>
<style>
@media (min-width: 1024px) {
.about {
display: flex;
align-items: center;
}
}
</style>

12
tsconfig.app.json Normal file
View File

@ -0,0 +1,12 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"paths": {
"@/*": ["./src/*"]
}
}
}

16
tsconfig.json Normal file
View File

@ -0,0 +1,16 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
],
"compilerOptions": {
"types": ["vite/client"],
"moduleResolution": "node"
}
}

19
tsconfig.node.json Normal file
View File

@ -0,0 +1,19 @@
{
"extends": "@tsconfig/node22/tsconfig.json",
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"nightwatch.conf.*",
"playwright.config.*",
"eslint.config.*"
],
"compilerOptions": {
"noEmit": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["node"]
}
}

37
vite.config.ts Normal file
View File

@ -0,0 +1,37 @@
import { fileURLToPath, URL } from "node:url";
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
// import vueDevTools from "vite-plugin-vue-devtools";
// https://vite.dev/config/
export default defineConfig({
// plugins: [vue(), vueDevTools()],
plugins: [vue()],
define: {
"process.env": { TINY_MODE: "pc" },
},
build: {
sourcemap: false,
rollupOptions: {
output: {
manualChunks(id) {
if (!id.includes("node_modules")) return;
const parts = id.split("node_modules/")[1]?.split("/");
const name = parts?.[0]?.startsWith("@") ? `${parts[0]}/${parts[1]}` : parts?.[0];
if (!name) return "vendor";
// Keep big UI libs separated for better caching and smaller initial chunk.
if (name.startsWith("@opentiny/")) return "vendor-opentiny";
if (name === "vue" || name === "vue-router") return "vendor-vue";
return "vendor";
},
},
},
},
resolve: {
alias: {
'@': '/src'
// "@": fileURLToPath(new URL("./src", import.meta.url)),
},
}
});