提交饱和编辑的相关设计,及检验代码

This commit is contained in:
2026-02-26 14:02:42 +08:00
commit cb556b47c0
36 changed files with 5437 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
# Vue 3 + TypeScript + Vite
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).

77
interactive/frontend/auto-imports.d.ts vendored Normal file
View File

@@ -0,0 +1,77 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
const EffectScope: typeof import('vue').EffectScope
const computed: typeof import('vue').computed
const createApp: typeof import('vue').createApp
const customRef: typeof import('vue').customRef
const defineAsyncComponent: typeof import('vue').defineAsyncComponent
const defineComponent: typeof import('vue').defineComponent
const effectScope: typeof import('vue').effectScope
const getCurrentInstance: typeof import('vue').getCurrentInstance
const getCurrentScope: typeof import('vue').getCurrentScope
const getCurrentWatcher: typeof import('vue').getCurrentWatcher
const h: typeof import('vue').h
const inject: typeof import('vue').inject
const isProxy: typeof import('vue').isProxy
const isReactive: typeof import('vue').isReactive
const isReadonly: typeof import('vue').isReadonly
const isRef: typeof import('vue').isRef
const isShallow: typeof import('vue').isShallow
const markRaw: typeof import('vue').markRaw
const nextTick: typeof import('vue').nextTick
const onActivated: typeof import('vue').onActivated
const onBeforeMount: typeof import('vue').onBeforeMount
const onBeforeUnmount: typeof import('vue').onBeforeUnmount
const onBeforeUpdate: typeof import('vue').onBeforeUpdate
const onDeactivated: typeof import('vue').onDeactivated
const onErrorCaptured: typeof import('vue').onErrorCaptured
const onMounted: typeof import('vue').onMounted
const onRenderTracked: typeof import('vue').onRenderTracked
const onRenderTriggered: typeof import('vue').onRenderTriggered
const onScopeDispose: typeof import('vue').onScopeDispose
const onServerPrefetch: typeof import('vue').onServerPrefetch
const onUnmounted: typeof import('vue').onUnmounted
const onUpdated: typeof import('vue').onUpdated
const onWatcherCleanup: typeof import('vue').onWatcherCleanup
const provide: typeof import('vue').provide
const reactive: typeof import('vue').reactive
const readonly: typeof import('vue').readonly
const ref: typeof import('vue').ref
const resolveComponent: typeof import('vue').resolveComponent
const shallowReactive: typeof import('vue').shallowReactive
const shallowReadonly: typeof import('vue').shallowReadonly
const shallowRef: typeof import('vue').shallowRef
const toRaw: typeof import('vue').toRaw
const toRef: typeof import('vue').toRef
const toRefs: typeof import('vue').toRefs
const toValue: typeof import('vue').toValue
const triggerRef: typeof import('vue').triggerRef
const unref: typeof import('vue').unref
const useAttrs: typeof import('vue').useAttrs
const useCssModule: typeof import('vue').useCssModule
const useCssVars: typeof import('vue').useCssVars
const useDialog: typeof import('naive-ui').useDialog
const useId: typeof import('vue').useId
const useLoadingBar: typeof import('naive-ui').useLoadingBar
const useMessage: typeof import('naive-ui').useMessage
const useModel: typeof import('vue').useModel
const useNotification: typeof import('naive-ui').useNotification
const useSlots: typeof import('vue').useSlots
const useTemplateRef: typeof import('vue').useTemplateRef
const watch: typeof import('vue').watch
const watchEffect: typeof import('vue').watchEffect
const watchPostEffect: typeof import('vue').watchPostEffect
const watchSyncEffect: typeof import('vue').watchSyncEffect
}
// for type re-export
declare global {
// @ts-ignore
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
}

30
interactive/frontend/components.d.ts vendored Normal file
View File

@@ -0,0 +1,30 @@
/* eslint-disable */
// @ts-nocheck
// biome-ignore lint: disable
// oxlint-disable
// ------
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
NDataTable: typeof import('naive-ui')['NDataTable']
NFlex: typeof import('naive-ui')['NFlex']
NForm: typeof import('naive-ui')['NForm']
NFormItem: typeof import('naive-ui')['NFormItem']
NGi: typeof import('naive-ui')['NGi']
NGrid: typeof import('naive-ui')['NGrid']
NInputNumber: typeof import('naive-ui')['NInputNumber']
NLayout: typeof import('naive-ui')['NLayout']
NLayoutContent: typeof import('naive-ui')['NLayoutContent']
NLayoutHeader: typeof import('naive-ui')['NLayoutHeader']
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
NPagination: typeof import('naive-ui')['NPagination']
NSelect: typeof import('naive-ui')['NSelect']
}
}

View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>frontend</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@@ -0,0 +1,26 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.13.5",
"naive-ui": "^2.43.2",
"unplugin-auto-import": "^21.0.0",
"unplugin-vue-components": "^31.0.0",
"vue": "^3.5.25"
},
"devDependencies": {
"@types/node": "^24.10.1",
"@vitejs/plugin-vue": "^6.0.2",
"@vue/tsconfig": "^0.8.1",
"typescript": "~5.9.3",
"vite": "^7.3.1",
"vue-tsc": "^3.1.5"
}
}

1554
interactive/frontend/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,26 @@
<script setup lang="ts">
import HelloWorld from './components/HelloWorld.vue'
</script>
<template>
<n-config-provider>
<n-message-provider>
<HelloWorld />
</n-message-provider>
</n-config-provider>
</template>
<style scoped>
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vue:hover {
filter: drop-shadow(0 0 2em #42b883aa);
}
</style>

View File

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

After

Width:  |  Height:  |  Size: 496 B

View File

@@ -0,0 +1,375 @@
<script setup lang="ts">
import {onMounted, ref, h, watch} from 'vue'
import axios, { AxiosError, type AxiosResponse } from "axios";
import type {SelectOption, DataTableSortState} from "naive-ui"
import { useMessage, NEllipsis, NButton, NPopover } from "naive-ui"
const BASE_URL = "/api" // http://10.126.126.11:5555/api
const APIs = {
unique: `${BASE_URL}/gene`,
content: `${BASE_URL}/records`,
};
const message = useMessage();
const genes = ref<SelectOption[]|null>(null);
const tables = [
{"value": "pridict2", "label": "Pridict2"},
{"value": "prime_design", "label": "Prime Design"},
]
interface FormData {
gene: string | null
source: string
pbs_len: number | null
rtt_len: number | null
}
const formData = ref<FormData>({
source: "pridict2",
gene: null,
pbs_len: 0,
rtt_len: 0,
})
interface Pagination {
order: string;
order_by: string;
total: number;
page: number;
length: number;
}
const pagination = ref<Pagination>({
order: "desc",
order_by: "gene",
total: 10,
length: 10,
page: 1,
});
function processSorter(
options: DataTableSortState | DataTableSortState[] | null,
) {
if (options !== null) {
options = options as DataTableSortState;
pagination.value.order_by = options.columnKey.toString();
pagination.value.order =
typeof options.order === "boolean" ? "asc" : options.order;
} else {
pagination.value.order_by = "id";
pagination.value.order = "descend";
}
}
interface RowData {
id: number;
gene: string
aa: number
sequence: string
src: string
dst: string
k562: number|null
hek: number|null
k562_rank: number|null
hek_rank: number|null
template: string
strand: string
pbs_len: number
rtt_len: number
spacer: string|null
scaffold: string|null
pegrna: string|null
pbs: string
rtt: string
extension: string|null
before_spacer: string|null
after_spacer: string|null
before_pegnra_ext: string|null
after_pegnra_ext: string|null
}
let columns = [
{
title: "Gene", key: "gene", defaultSortOrder: "descend", width: 60, resizable: true,
},
{
title: "AA (n)", key: "aa", defaultSortOrder: "ascend", width: 50, resizable: true, sorter: true,
},
{
title: "name", key: "sequence", defaultSortOrder: "ascend", width: 100, resizable: true, sorter: true,
},
{
title: "原序列", key: "src", defaultSortOrder: "ascend", width: 60, resizable: true, sorter: true,
},
{
title: "编辑后", key: "dst", defaultSortOrder: "ascend", width: 60, resizable: true, sorter: true,
},
]
let post_columns = [
{title: "strand", key: "strand", defaultSortOrder: "ascend", maxWidth: 20, resizable: true,},
{title: "PBS len", key: "pbs_len", defaultSortOrder: "ascend", maxWidth: 20, resizable: true, sorter: true,},
{title: "RTT len", key: "rtt_len", defaultSortOrder: "ascend", maxWidth: 20, resizable: true, sorter: true,},
{title: "PBS", key: "pbs", defaultSortOrder: "ascend", width: 120, resizable: true,},
{title: "RTT", key: "rtt", defaultSortOrder: "ascend", width: 120, resizable: true,}
]
const copyText = (text: string) => {
const textArea = document.createElement("textarea");
textArea.value = text;
textArea.style.top = "0";
textArea.style.left = "0";
textArea.style.position = "fixed";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand("copy");
document.body.removeChild(textArea);
return Promise.resolve();
} catch (err) {
document.body.removeChild(textArea);
return Promise.reject(err);
}
};
const createColumns = () => {
let real_columns = [...columns]
let rest_columns = ["spacer", "extension", "before_spacer", "after_spacer", "before_pegnra_ext", "after_pegnra_ext"]
if (formData.value.source === "pridict2") {
rest_columns = ["template", "spacer", "scaffold", "pegrna"]
for (let i of ["k562", "hek"]) {
real_columns.push(
{
title: i, key: i, width: 60, resizable: true, sorter: true,
render: function(row: RowData) {
return h(
NPopover, {
trigger: "hover"
}, {
trigger: () => `${parseFloat(row[i].toFixed(2))} (${row[i + "_rank"]})`,
default: () => {`Score: ${row[i]}; Rank=${row[i + "_rank"]}`}
}
)
}
},
)
}
}
real_columns = real_columns.concat(post_columns)
for (let i of rest_columns) {
real_columns.push({
title:i,
key: i,
width: 240,
resizable: true,
render: (row: RowData) => {
return h(
NButton, {
size:"small", type: "primary", dashed: true,
onClick: () => {copyText(row[i])}
},
{
default: () => {
return h(
NEllipsis, {
style: "max-width: 200px",
tooltip: {
style: {
maxWidth: '300px',
whiteSpace: 'pre-wrap', // 关键:允许换行
wordBreak: 'break-word' // 允许单词内换行
}
}
},
{default: () => row[i]}
)
}
}
)
}
})
}
return real_columns
};
const loading = ref(false)
const data = ref<RowData[]>([]);
const getRecords = () => {
loading.value = true;
let params = {
gene: formData.value.gene,
pbs_len: formData.value.pbs_len,
rtt_len: formData.value.rtt_len,
source: formData.value.source,
offset: pagination.value.page,
length: pagination.value.length,
order_by: pagination.value.order_by,
order: pagination.value.order,
}
axios
.get(APIs.content, { params: params })
.then((response: AxiosResponse) => {
let resp = response.data;
data.value = resp.data;
if (resp.total !== pagination.value.total) {
pagination.value.total = resp.total;
}
if (resp.length !== pagination.value.length) {
pagination.value.length = resp.length;
}
if (pagination.value.page > Math.ceil(resp.total / resp.length)) {
pagination.value.page = Math.ceil(resp.total / resp.length);
}
})
.catch((error: Error | AxiosError) => {
message.error(error.message);
}).finally(() => {
loading.value = false;
});
}
onMounted(() => {
axios.get(APIs.unique).then((response: AxiosResponse) => {
let res = []
for (let i of response.data) {
res.push({"value": i, "label": i})
}
genes.value = res
formData.value.gene = res[0].value
}).finally(() => {
getRecords()
})
})
watch(
() => [formData, pagination],
(_) => {
axios.get(APIs.unique, {params: {source: formData.value.source}}).then((response: AxiosResponse) => {
let res = []
for (let i of response.data) {
res.push({"value": i, "label": i})
}
genes.value = res
}).finally(() => {
getRecords()
})
},
{ deep: true },
);
</script>
<template>
<n-grid cols="24" :y-gap="8" item-responsive>
<n-gi span="0 400:1 800:2" responsive="self" />
<n-gi span="24 400:22 600:20" responsive="self">
<n-layout>
<n-layout-header style="min-height: 30px; padding: 10px" bordered>
<n-form label-placement="left">
<n-flex justify="space-around" style="margin-right: 10px">
<!-- 查询界面 -->
<n-grid cols="4" :x-gap="12" :y-gap="8" item-responsive>
<n-gi span="4 400:2 800:1" responsive="self">
<n-form-item label="表">
<n-select v-model:value="formData.source" :options="tables" filterable clearable/>
</n-form-item>
</n-gi>
<n-gi span="4 400:2 800:1" responsive="self">
<n-form-item label="基因">
<n-select v-model:value="formData.gene" :options="genes" filterable clearable/>
</n-form-item>
</n-gi>
<n-gi span="4 400:2 800:1" responsive="self">
<n-form-item label="PBS len <= ">
<n-input-number v-model:value="formData.pbs_len" clearable/>
</n-form-item>
</n-gi>
<n-gi span="4 400:2 800:1" responsive="self">
<n-form-item label="RTT len <= ">
<n-input-number v-model:value="formData.rtt_len" clearable/>
</n-form-item>
</n-gi>
</n-grid>
</n-flex>
</n-form>
</n-layout-header>
<n-layout-content style="padding-top: 10px; padding-left: 5px" bordered>
<n-flex justify="center">
<n-pagination
v-model:page="pagination.page"
:page-sizes="[10, 20, 30, 40]"
:item-count="pagination.total"
v-model:page-size="pagination.length"
show-quick-jumper
show-size-picker
style="padding: 5px"
/>
</n-flex>
<n-data-table
:columns="createColumns()"
:data="data"
:loading="loading"
:scroll-x="1800"
width="100%"
:max-height="600"
:row-key="
(row: RowData) => (row.id)
"
striped
bordered
@update:sorter="processSorter"
sticky-expanded-rows
/>
<n-flex justify="center">
<n-pagination
v-model:page="pagination.page"
:page-sizes="[10, 20, 30, 40]"
:item-count="pagination.total"
v-model:page-size="pagination.length"
show-quick-jumper
show-size-picker
style="padding: 5px"
/>
</n-flex>
</n-layout-content>
</n-layout>
</n-gi>
</n-grid>
</template>
<style scoped>
/* 关键:为表格容器设置固定宽度和溢出控制 */
.table-container {
width: 100%; /* 或者固定宽度,如 1200px */
overflow-x: auto; /* 确保容器可水平滚动 */
}
</style>

View File

@@ -0,0 +1,4 @@
import { createApp } from "vue";
import App from "./App.vue";
createApp(App).mount("#app");

View File

@@ -0,0 +1,79 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
.card {
padding: 2em;
}
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

View File

@@ -0,0 +1,16 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"types": ["vite/client"],
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
}

View File

@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View File

@@ -0,0 +1,26 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

View File

@@ -0,0 +1,38 @@
import vue from "@vitejs/plugin-vue";
import AutoImport from "unplugin-auto-import/vite";
import { NaiveUiResolver } from "unplugin-vue-components/resolvers";
import Components from "unplugin-vue-components/vite";
// vite.config.ts
import { defineConfig } from "vite";
// https://vitejs.dev/config/
export default defineConfig({
server: {
watch: {
// 使用轮询模式,避免文件描述符问题
usePolling: true,
interval: 1000,
// 忽略不需要监视的目录
ignored: ["**/node_modules/**", "**/.git/**", "**/.next/**"],
},
},
plugins: [
vue(),
AutoImport({
imports: [
"vue",
{
"naive-ui": [
"useDialog",
"useMessage",
"useNotification",
"useLoadingBar",
],
},
],
}),
Components({
resolvers: [NaiveUiResolver()],
}),
],
});