提交饱和编辑的相关设计,及检验代码
This commit is contained in:
0
interactive/README.md
Normal file
0
interactive/README.md
Normal file
222
interactive/db.py
Normal file
222
interactive/db.py
Normal file
@@ -0,0 +1,222 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import peewee as pw
|
||||
import re
|
||||
import csv
|
||||
import gzip
|
||||
|
||||
from typing import Dict
|
||||
|
||||
|
||||
db = pw.SqliteDatabase("./pegrna.db")
|
||||
|
||||
|
||||
class BaseModel(pw.Model):
|
||||
|
||||
class Meta:
|
||||
database = db
|
||||
|
||||
|
||||
KEY_MAP = {
|
||||
"pridict2": {
|
||||
"sequence_name": "sequence",
|
||||
"EditedAllele": "dst",
|
||||
"OriginalAllele": "src",
|
||||
"PRIDICT2_0_editing_Score_deep_K562": "k562",
|
||||
"PRIDICT2_0_editing_Score_deep_HEK": "hek",
|
||||
"K562_rank": "k562_rank",
|
||||
"HEK_rank": "hek_rank",
|
||||
"PRIDICT2_Format": "template",
|
||||
"Target-Strand": "strand",
|
||||
"PBSlength": "pbs_len",
|
||||
"RToverhanglength": "rtt_oh_len",
|
||||
"RTlength": "rtt_len",
|
||||
"Spacer-Sequence": "spacer",
|
||||
"Scaffold_Optimized": "scaffold",
|
||||
"pegRNA": "pegrna",
|
||||
"PBSrevcomp": "pbs",
|
||||
"RTseqoverhangrevcomp": "rtt_oh",
|
||||
"RTrevcomp": "rtt",
|
||||
},
|
||||
"prime_design": {
|
||||
"Target_name": "sequence",
|
||||
# "": "dst",
|
||||
# "": "src",
|
||||
"Target_sequence": "template",
|
||||
"Strand": "strand",
|
||||
"PBS_length": "pbs_len",
|
||||
"RTT_length": "rtt_len",
|
||||
"Spacer_sequence": "spacer",
|
||||
"PAM_sequence": "pam",
|
||||
"Extension_sequence": "extension", # RTT + PBS
|
||||
"Spacer_sequence_order_TOP": "before_spacer",
|
||||
"Spacer_sequence_order_BOTTOM": "after_spacer",
|
||||
"pegRNA_extension_sequence_order_TOP": "before_pegnra_ext",
|
||||
"pegRNA_extension_sequence_order_BOTTOM": "after_pegnra_ext",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def bulk_insert(table, data, chunk = 100):
|
||||
with db.atomic():
|
||||
for i in range(0, len(data), chunk):
|
||||
table.insert_many(data[i:i + chunk]).execute()
|
||||
|
||||
|
||||
class Pridict2(BaseModel):
|
||||
gene = pw.CharField()
|
||||
aa = pw.IntegerField()
|
||||
|
||||
sequence = pw.CharField()
|
||||
|
||||
src = pw.CharField()
|
||||
dst = pw.CharField()
|
||||
|
||||
k562 = pw.FloatField()
|
||||
hek = pw.FloatField()
|
||||
|
||||
k562_rank = pw.IntegerField()
|
||||
hek_rank = pw.IntegerField()
|
||||
|
||||
template = pw.CharField()
|
||||
strand = pw.CharField()
|
||||
|
||||
pbs_len = pw.IntegerField()
|
||||
rtt_oh_len = pw.IntegerField()
|
||||
rtt_len = pw.IntegerField()
|
||||
|
||||
spacer = pw.CharField()
|
||||
scaffold = pw.CharField()
|
||||
pegrna = pw.CharField()
|
||||
pbs = pw.CharField()
|
||||
rtt_oh = pw.CharField()
|
||||
rtt = pw.CharField()
|
||||
|
||||
class Meta:
|
||||
table_name = "pridict2"
|
||||
|
||||
|
||||
class PrimeDesign(BaseModel):
|
||||
gene = pw.CharField()
|
||||
aa = pw.IntegerField()
|
||||
sequence = pw.CharField()
|
||||
src = pw.CharField()
|
||||
dst = pw.CharField()
|
||||
|
||||
template = pw.CharField()
|
||||
strand = pw.CharField()
|
||||
|
||||
pbs_len = pw.IntegerField()
|
||||
rtt_len = pw.IntegerField()
|
||||
|
||||
pam = pw.CharField()
|
||||
spacer = pw.CharField()
|
||||
extension = pw.CharField()
|
||||
pbs = pw.CharField()
|
||||
rtt = pw.CharField()
|
||||
before_spacer = pw.CharField()
|
||||
after_spacer = pw.CharField()
|
||||
|
||||
before_pegnra_ext = pw.CharField()
|
||||
after_pegnra_ext= pw.CharField()
|
||||
|
||||
class Meta:
|
||||
table_name = "prime_design"
|
||||
|
||||
|
||||
def format_data(value: Dict, mapping: Dict[str, str]) -> Dict[str, any]:
|
||||
res = {}
|
||||
for key, value in value.items():
|
||||
if key in mapping.keys():
|
||||
res[mapping[key]] = value
|
||||
|
||||
if not res.get("src"):
|
||||
res["src"] = res["sequence"].split("_")[-2]
|
||||
res["dst"] = res["sequence"].split("_")[-1]
|
||||
|
||||
res["aa"] = int(re.sub(r"\D", "", res["sequence"].split("_")[1]))
|
||||
res["gene"] = res["sequence"].split("_")[0]
|
||||
|
||||
if not res.get("pbs") and res.get("extension") and res.get("pbs_len") and res.get("rtt_len"):
|
||||
if len(res["extension"]) == int(res["pbs_len"]) + int(res["rtt_len"]):
|
||||
res["pbs"] = res["extension"][:int(res["pbs_len"])]
|
||||
res["rtt"] = res["extension"][int(res["pbs_len"]):]
|
||||
return res
|
||||
|
||||
|
||||
def insert(path: str, kind: str = "PRIDICT2", chunk: int = 10000):
|
||||
|
||||
if not Pridict2.table_exists():
|
||||
Pridict2.create_table()
|
||||
|
||||
if not PrimeDesign.table_exists():
|
||||
PrimeDesign.create_table()
|
||||
|
||||
|
||||
kind = kind.lower()
|
||||
|
||||
assert kind in KEY_MAP.keys()
|
||||
|
||||
data = []
|
||||
rows = 0
|
||||
with gzip.open(path, 'rt', encoding='utf-8') as file:
|
||||
csv_dict_reader = csv.DictReader(file)
|
||||
|
||||
# 逐行读取,每行是一个字典
|
||||
for row in csv_dict_reader:
|
||||
# 通过列名访问数据
|
||||
data.append(format_data(row, KEY_MAP[kind]))
|
||||
rows += 1
|
||||
if len(data) >= chunk:
|
||||
print(f"finished {rows} rows")
|
||||
bulk_insert(Pridict2 if kind == "pridict2" else PrimeDesign, data)
|
||||
data = []
|
||||
|
||||
if data:
|
||||
bulk_insert(Pridict2 if kind == "pridict2" else PrimeDesign, data)
|
||||
|
||||
|
||||
def index():
|
||||
# 创建简单索引
|
||||
|
||||
for i in [
|
||||
Pridict2.gene,
|
||||
Pridict2.aa,
|
||||
Pridict2.sequence,
|
||||
Pridict2.dst,
|
||||
Pridict2.src,
|
||||
|
||||
Pridict2.k562,
|
||||
Pridict2.hek,
|
||||
|
||||
Pridict2.pbs_len,
|
||||
Pridict2.rtt_len,
|
||||
]:
|
||||
print(Pridict2.__name__, i.name)
|
||||
sql = f"CREATE INDEX IF NOT EXISTS {Pridict2.__name__}_{i.name}_idx ON pridict2 ({i.name});"
|
||||
db.execute_sql(sql)
|
||||
|
||||
|
||||
for i in [
|
||||
PrimeDesign.gene,
|
||||
PrimeDesign.aa,
|
||||
PrimeDesign.sequence,
|
||||
PrimeDesign.dst,
|
||||
PrimeDesign.src,
|
||||
|
||||
PrimeDesign.pbs_len,
|
||||
PrimeDesign.rtt_len,
|
||||
]:
|
||||
print(PrimeDesign.__name__, i.name)
|
||||
sql = f"CREATE INDEX IF NOT EXISTS {PrimeDesign.__name__}_{i.name}_idx ON prime_design ({i.name});"
|
||||
db.execute_sql(sql)
|
||||
|
||||
|
||||
def table_columns(table):
|
||||
return {x: y for x, y in table.__dict__.items() if "__" not in x}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(table_columns(Pridict2))
|
||||
pass
|
||||
|
||||
5
interactive/frontend/README.md
Normal file
5
interactive/frontend/README.md
Normal 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
77
interactive/frontend/auto-imports.d.ts
vendored
Normal 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
30
interactive/frontend/components.d.ts
vendored
Normal 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']
|
||||
}
|
||||
}
|
||||
13
interactive/frontend/index.html
Normal file
13
interactive/frontend/index.html
Normal 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>
|
||||
26
interactive/frontend/package.json
Normal file
26
interactive/frontend/package.json
Normal 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
1554
interactive/frontend/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
1
interactive/frontend/public/vite.svg
Normal file
1
interactive/frontend/public/vite.svg
Normal 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 |
26
interactive/frontend/src/App.vue
Normal file
26
interactive/frontend/src/App.vue
Normal 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>
|
||||
1
interactive/frontend/src/assets/vue.svg
Normal file
1
interactive/frontend/src/assets/vue.svg
Normal 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 |
375
interactive/frontend/src/components/HelloWorld.vue
Normal file
375
interactive/frontend/src/components/HelloWorld.vue
Normal 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>
|
||||
4
interactive/frontend/src/main.ts
Normal file
4
interactive/frontend/src/main.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { createApp } from "vue";
|
||||
import App from "./App.vue";
|
||||
|
||||
createApp(App).mount("#app");
|
||||
79
interactive/frontend/src/style.css
Normal file
79
interactive/frontend/src/style.css
Normal 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;
|
||||
}
|
||||
}
|
||||
16
interactive/frontend/tsconfig.app.json
Normal file
16
interactive/frontend/tsconfig.app.json
Normal 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"]
|
||||
}
|
||||
7
interactive/frontend/tsconfig.json
Normal file
7
interactive/frontend/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
26
interactive/frontend/tsconfig.node.json
Normal file
26
interactive/frontend/tsconfig.node.json
Normal 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"]
|
||||
}
|
||||
38
interactive/frontend/vite.config.ts
Normal file
38
interactive/frontend/vite.config.ts
Normal 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()],
|
||||
}),
|
||||
],
|
||||
});
|
||||
133
interactive/main.py
Normal file
133
interactive/main.py
Normal file
@@ -0,0 +1,133 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
from peewee import SQL
|
||||
from flask import Flask, jsonify, request, abort, send_from_directory
|
||||
from flask_cors import CORS
|
||||
from db import insert, index, Pridict2, PrimeDesign, table_columns
|
||||
|
||||
|
||||
app = Flask(__name__, static_folder="./frontend/dist")
|
||||
CORS(app)
|
||||
|
||||
|
||||
@app.route("/<path:filename>")
|
||||
def static_files(filename):
|
||||
"""专门处理带扩展名的文件"""
|
||||
if "." not in filename:
|
||||
abort(404) # 无扩展名不应走这里
|
||||
try:
|
||||
return send_from_directory(app.static_folder, filename)
|
||||
except FileNotFoundError:
|
||||
abort(404) # 静态文件不存在就是 404
|
||||
|
||||
@app.route("/", defaults={"path": ""})
|
||||
@app.route("/<path:path>")
|
||||
def main(path):
|
||||
"""仅处理 SPA 路由(无扩展名)"""
|
||||
if "." in os.path.basename(path):
|
||||
# 包含扩展名?说明应该是静态文件,但没被上面的路由捕获 → 404
|
||||
abort(404)
|
||||
return send_from_directory(app.static_folder, "index.html")
|
||||
|
||||
|
||||
def default_value(val, default):
|
||||
try:
|
||||
return int(val)
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
|
||||
@app.route("/api/gene")
|
||||
def gene():
|
||||
genes = set()
|
||||
|
||||
source = request.args.get("source", "pridict2")
|
||||
|
||||
tables = {
|
||||
"pridict2": Pridict2,
|
||||
"prime_design": PrimeDesign,
|
||||
}
|
||||
|
||||
table = tables.get(source)
|
||||
if not table:
|
||||
return jsonify({"message": "No such table"}), 404
|
||||
|
||||
for i in table.select(table.gene.distinct()):
|
||||
genes.add(i.gene)
|
||||
|
||||
return jsonify(sorted(genes))
|
||||
|
||||
|
||||
@app.route("/api/records")
|
||||
def records():
|
||||
source = request.args.get("source", "pridict2")
|
||||
|
||||
tables = {
|
||||
"pridict2": Pridict2,
|
||||
"prime_design": PrimeDesign,
|
||||
}
|
||||
|
||||
table = tables.get(source)
|
||||
if not table:
|
||||
return jsonify({"message": "No such table"}), 404
|
||||
|
||||
columns = table_columns(table)
|
||||
where = None
|
||||
for i in ["gene", "dst", "src"]:
|
||||
value = request.args.get(i)
|
||||
if value:
|
||||
if where is None:
|
||||
where = (SQL(i) == value)
|
||||
else:
|
||||
where = (where) & (SQL(i) == value)
|
||||
|
||||
for i in ["pbs_len", "rtt_len"]:
|
||||
value = default_value(request.args.get(i), 0)
|
||||
if value:
|
||||
if where is None:
|
||||
where = (SQL(i) <= value)
|
||||
else:
|
||||
where = (where) & (SQL(i) <= value)
|
||||
|
||||
query = table.select().where(where)
|
||||
total = query.count()
|
||||
|
||||
order_by = request.args.get("order_by")
|
||||
if order_by and order_by in columns:
|
||||
order = request.args.get("order", "asc")
|
||||
if "desc" in order:
|
||||
query = query.order_by(SQL(order_by).desc())
|
||||
else:
|
||||
query = query.order_by(SQL(order_by))
|
||||
else:
|
||||
query = query.order_by(table.gene, table.aa, table.src, table.dst)
|
||||
|
||||
offset = default_value(request.args.get("offset"), 1)
|
||||
if offset <= 0:
|
||||
offset = 1
|
||||
|
||||
length = default_value(request.args.get("length"), 1)
|
||||
if length > 200:
|
||||
length = 200
|
||||
query = query.offset((int(offset) - 1) * length).limit(int(length))
|
||||
print(query.sql())
|
||||
return jsonify({
|
||||
"data": [x for x in query.dicts()],
|
||||
"total": total,
|
||||
"offset": offset,
|
||||
"length": length,
|
||||
})
|
||||
|
||||
|
||||
def main(host: str="0.0.0.0", port=5555):
|
||||
app.run(host=host, port=port, threaded=True, debug=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from fire import Fire
|
||||
Fire({
|
||||
"insert": insert,
|
||||
"index": index,
|
||||
"server": main
|
||||
})
|
||||
9
interactive/pyproject.toml
Normal file
9
interactive/pyproject.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
[project]
|
||||
name = "interactive"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"peewee>=3.19.0",
|
||||
]
|
||||
Reference in New Issue
Block a user