feat: init
This commit is contained in:
24
frontend/.gitignore
vendored
Normal file
24
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
3
frontend/.vscode/extensions.json
vendored
Normal file
3
frontend/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
||||
9
frontend/README.md
Normal file
9
frontend/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# 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.
|
||||
|
||||
## Recommended Setup
|
||||
|
||||
- [VS Code](https://code.visualstudio.com/) + [Vue - Official](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (previously Volar) and disable Vetur
|
||||
|
||||
- Use [vue-tsc](https://github.com/vuejs/language-tools/tree/master/packages/tsc) for performing the same type checking from the command line, or for generating d.ts files for SFCs.
|
||||
13
frontend/index.html
Normal file
13
frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en" data-theme="light">
|
||||
<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>专家粉丝统计</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
27
frontend/package.json
Normal file
27
frontend/package.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host",
|
||||
"build": "vue-tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.7.2",
|
||||
"moment": "^2.30.1",
|
||||
"vue": "^3.4.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/typography": "^0.5.13",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"daisyui": "^4.11.1",
|
||||
"postcss": "^8.4.38",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.2.0",
|
||||
"vue-tsc": "^2.0.6"
|
||||
}
|
||||
}
|
||||
1807
frontend/pnpm-lock.yaml
generated
Normal file
1807
frontend/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
6
frontend/postcss.config.js
Normal file
6
frontend/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
41
frontend/src/App.vue
Normal file
41
frontend/src/App.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import Devices from "./components/Devices.vue";
|
||||
import Experts from "./components/Experts.vue";
|
||||
|
||||
const active = ref<Number>(1);
|
||||
|
||||
const activeTab = (index: number) => {
|
||||
active.value = index;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="container mx-auto py-10">
|
||||
<div role="tablist" class="tabs tabs-boxed">
|
||||
<a
|
||||
role="tab"
|
||||
class="tab"
|
||||
:class="{ 'tab-active': active == 0 }"
|
||||
@click="activeTab(0)"
|
||||
>专家管理</a
|
||||
>
|
||||
<a
|
||||
role="tab"
|
||||
class="tab"
|
||||
:class="{ 'tab-active': active == 1 }"
|
||||
@click="activeTab(1)"
|
||||
>设备管理</a
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="tab-data mt-10">
|
||||
<div v-show="active == 0">
|
||||
<Experts />
|
||||
</div>
|
||||
<div v-show="active == 1">
|
||||
<Devices />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
1
frontend/src/assets/vue.svg
Normal file
1
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 |
192
frontend/src/components/Devices.vue
Normal file
192
frontend/src/components/Devices.vue
Normal file
@@ -0,0 +1,192 @@
|
||||
<script setup lang="ts">
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
interface Device {
|
||||
ID: number
|
||||
UUID: string
|
||||
Name: string
|
||||
Expert: string
|
||||
ExpertName: string
|
||||
State: string
|
||||
Note: string
|
||||
}
|
||||
|
||||
interface Expert {
|
||||
ID: number
|
||||
UID: string
|
||||
SecUID: string
|
||||
ShortID: string
|
||||
RealName: string
|
||||
NickName: string
|
||||
State: string
|
||||
Since: number
|
||||
Focus: number
|
||||
Total: number
|
||||
Voice: string
|
||||
Hello: string
|
||||
}
|
||||
|
||||
|
||||
const devices = ref<Device[]>([]);
|
||||
|
||||
const experts = ref<Expert[]>([]);
|
||||
|
||||
// use axios get /experts onMount
|
||||
const loadExperts = function () {
|
||||
axios.get<Expert[]>('/api/experts').then((resp: AxiosResponse) => {
|
||||
experts.value = resp.data;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// use axios get /experts onMount
|
||||
const loadDevices = function () {
|
||||
axios.get<Device[]>('/api/devices').then((resp: AxiosResponse) => {
|
||||
devices.value = resp.data;
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadDevices()
|
||||
});
|
||||
|
||||
setInterval(loadDevices, 30 * 1000);
|
||||
|
||||
const currentDevice = ref<Device | null>(null);
|
||||
const showDeviceUpdateModal = (id: number) => {
|
||||
loadExperts();
|
||||
|
||||
currentDevice.value = devices.value.find((device) => device.ID === id) || null;
|
||||
|
||||
selectExpert.value = currentDevice.value?.Expert
|
||||
deviceName.value = currentDevice.value?.Name
|
||||
|
||||
const dialog = document.getElementById("show_device_update_modal") as HTMLDialogElement;
|
||||
dialog.showModal();
|
||||
};
|
||||
|
||||
const resetStateNormal = (id: number) => {
|
||||
currentDevice.value = devices.value.find((device) => device.ID === id) || null;
|
||||
if (!currentDevice.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
// alert to confirm
|
||||
if (!confirm(`确定要恢复 ${currentDevice.value.UUID} 的自动关注吗?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = { state: "" };
|
||||
axios.patch(`/api/devices/${currentDevice.value?.UUID}/state`, data).then((resp: AxiosResponse) => {
|
||||
console.log(resp.data)
|
||||
loadDevices();
|
||||
});
|
||||
};
|
||||
|
||||
const setStateStop = (id: number) => {
|
||||
currentDevice.value = devices.value.find((device) => device.ID === id) || null;
|
||||
if (!currentDevice.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
// alert to confirm
|
||||
if (!confirm(`确定要停止 ${currentDevice.value.UUID} 的自动关注吗?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = { state: "stop" };
|
||||
axios.patch(`/api/devices/${currentDevice.value?.UUID}/state`, data).then((resp: AxiosResponse) => {
|
||||
console.log(resp.data)
|
||||
loadDevices();
|
||||
});
|
||||
};
|
||||
|
||||
const closeModal = function () {
|
||||
const dialog = document.getElementById("show_device_update_modal") as HTMLDialogElement;
|
||||
dialog.close();
|
||||
}
|
||||
|
||||
const deviceName = ref<String | null>();
|
||||
const selectExpert = ref<String | null>();
|
||||
const saveDate = () => {
|
||||
const data = {
|
||||
name: deviceName.value,
|
||||
};
|
||||
console.log(data)
|
||||
axios.patch(`/api/devices/${currentDevice.value?.UUID}/experts/${selectExpert.value}`, data).then((resp: AxiosResponse) => {
|
||||
console.log(resp.data)
|
||||
loadDevices();
|
||||
setTimeout(closeModal, 500)
|
||||
});
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<dialog id="show_device_update_modal" class="modal">
|
||||
<div class="modal-box">
|
||||
<form method="dialog">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
|
||||
</form>
|
||||
|
||||
<h3 class="font-bold text-lg">{{ currentDevice?.UUID }}</h3>
|
||||
<div class="py-5">
|
||||
<label class="form-control w-full max-w-xs">
|
||||
<div class="label">
|
||||
<span class="label-text">设备名称</span>
|
||||
</div>
|
||||
<input type="text" placeholder="设备名称" class="input input-bordered w-full max-w-xs" v-model="deviceName"/>
|
||||
</label>
|
||||
|
||||
<label class="form-control w-full">
|
||||
<div class="label">
|
||||
<span class="label-text">选择专家</span>
|
||||
</div>
|
||||
<select class="select select-bordered w-full max-w-xs" v-model="selectExpert">
|
||||
<option v-for="expert in experts" :key="expert.ID" :value="expert.UID">{{ expert.RealName }}</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex justify-end gap-3">
|
||||
<button class="btn btn-wide btn-primary" @click="saveDate">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<div v-if="devices.length == 0">
|
||||
<h1 class="text-lg text-center">还没有设备</h1>
|
||||
</div>
|
||||
|
||||
<table className="table" v-else>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>设备号</th>
|
||||
<th>名称</th>
|
||||
<th>专家</th>
|
||||
<th>停止关注</th>
|
||||
<th>备注</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
<tr :class='idx % 2 == 1 ? "bg-slate-50" : ""' v-for="(item, idx) in devices" :key="item.ID">
|
||||
<td class="flex flex-col">
|
||||
<div class="text-lg font-semibold">{{ item.UUID }}</div>
|
||||
</td>
|
||||
<td>{{ item.Name }}</td>
|
||||
<td>
|
||||
<button class="btn btn-sm" @click="showDeviceUpdateModal(item.ID)">{{ item.ExpertName }}</button>
|
||||
</td>
|
||||
<td>
|
||||
<button v-if="item.State == 'stop'" class="btn btn-warning btn-sm"
|
||||
@click="resetStateNormal(item.ID)">恢复</button>
|
||||
<button v-else class="btn btn-error btn-sm text-white" @click="setStateStop(item.ID)">停止</button>
|
||||
</td>
|
||||
<td>{{ item.Note }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
288
frontend/src/components/Experts.vue
Normal file
288
frontend/src/components/Experts.vue
Normal file
@@ -0,0 +1,288 @@
|
||||
<script setup lang="ts">
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
import moment from 'moment';
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
interface Expert {
|
||||
ID: number
|
||||
UID: string
|
||||
SecUID: string
|
||||
ShortID: string
|
||||
RealName: string
|
||||
NickName: string
|
||||
State: string
|
||||
Since: number
|
||||
Focus: number
|
||||
Total: number
|
||||
Conf: Conf
|
||||
}
|
||||
|
||||
interface Conf {
|
||||
Voice: string
|
||||
Hello: string
|
||||
Wechat: string
|
||||
Region: string[]
|
||||
NameKeyword: string[]
|
||||
Produce: boolean
|
||||
DefaultName: boolean
|
||||
DefaultAvatar: boolean
|
||||
}
|
||||
|
||||
|
||||
const experts = ref<Expert[]>([]);
|
||||
|
||||
// use axios get /experts onMount
|
||||
const loadExperts = function () {
|
||||
axios.get<Expert[]>('/api/experts').then((resp: AxiosResponse) => {
|
||||
experts.value = resp.data;
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(loadExperts);
|
||||
setInterval(loadExperts, 30 * 1000);
|
||||
|
||||
|
||||
const parseTime = function (timestamp: number) {
|
||||
if (timestamp == 0) {
|
||||
return '不限制'
|
||||
}
|
||||
|
||||
return moment.unix(timestamp).format('YY/MM/DD HH:mm:ss')
|
||||
}
|
||||
const currentExpert = ref<Expert | null>(null);
|
||||
|
||||
const showExpertDateModal = (id: number) => {
|
||||
currentExpert.value = experts.value.find((expert) => expert.ID === id) || null;
|
||||
|
||||
date.value = moment.unix(currentExpert.value?.Since || 0).format('YYYY-MM-DDTHH:mm');
|
||||
|
||||
voice.value = currentExpert.value?.Conf.Voice || '';
|
||||
hello.value = currentExpert.value?.Conf.Hello || '';
|
||||
wechat.value = currentExpert.value?.Conf.Wechat || '';
|
||||
region.value = (currentExpert.value?.Conf.Region || []).join(',');
|
||||
produce.value = currentExpert.value?.Conf.Produce || false;
|
||||
default_name.value = currentExpert.value?.Conf.DefaultName || false;
|
||||
default_avatar.value = currentExpert.value?.Conf.DefaultAvatar || false;
|
||||
name_keyword.value = (currentExpert.value?.Conf.NameKeyword || []).join(',');
|
||||
|
||||
|
||||
const dialog = document.getElementById("set_date_modal") as HTMLDialogElement;
|
||||
dialog.showModal();
|
||||
};
|
||||
|
||||
const resetStateNormal = (id: number) => {
|
||||
currentExpert.value = experts.value.find((expert) => expert.ID === id) || null;
|
||||
if (!currentExpert.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
// alert to confirm
|
||||
if (!confirm(`确定要恢复 ${currentExpert.value.RealName} 的自动关注吗?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = { state: "" };
|
||||
axios.patch(`/api/experts/${currentExpert.value?.UID}/state`, data).then((resp: AxiosResponse) => {
|
||||
console.log(resp.data)
|
||||
loadExperts();
|
||||
});
|
||||
};
|
||||
|
||||
const setStateStop = (id: number) => {
|
||||
currentExpert.value = experts.value.find((expert) => expert.ID === id) || null;
|
||||
if (!currentExpert.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
// alert to confirm
|
||||
if (!confirm(`确定要停止 ${currentExpert.value.RealName} 的自动关注吗?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = { state: "stop" };
|
||||
axios.patch(`/api/experts/${currentExpert.value?.UID}/state`, data).then((resp: AxiosResponse) => {
|
||||
console.log(resp.data)
|
||||
loadExperts();
|
||||
});
|
||||
};
|
||||
|
||||
const closeModal = function () {
|
||||
const dialog = document.getElementById("set_date_modal") as HTMLDialogElement;
|
||||
dialog.close();
|
||||
}
|
||||
|
||||
const resetDate = () => {
|
||||
const data = { since: 0 };
|
||||
axios.patch(`/api/experts/${currentExpert.value?.UID}/config`, data).then((resp: AxiosResponse) => {
|
||||
console.log(resp.data)
|
||||
loadExperts();
|
||||
setTimeout(closeModal, 500)
|
||||
});
|
||||
};
|
||||
|
||||
const date = ref('2022-02-01T01:10');
|
||||
const voice = ref('');
|
||||
const hello = ref('');
|
||||
const wechat = ref('');
|
||||
const region = ref('');
|
||||
const produce = ref(false);
|
||||
const default_name = ref(false);
|
||||
const default_avatar = ref(false);
|
||||
const name_keyword = ref('');
|
||||
|
||||
const saveDate = () => {
|
||||
let regions = region.value.replace(/\s/g, '').replace(",", "").split(',');
|
||||
let name_keywords = name_keyword.value.replace(/\s/g, '').replace(",", "").split(',');
|
||||
|
||||
const data = {
|
||||
since: Date.parse(date.value) / 1000,
|
||||
voice: voice.value,
|
||||
hello: hello.value,
|
||||
wechat: wechat.value,
|
||||
region: regions,
|
||||
produce: produce.value,
|
||||
DefaultName: default_name.value,
|
||||
DefaultAvatar: default_avatar.value,
|
||||
NameKeyword: name_keywords,
|
||||
};
|
||||
console.log(data)
|
||||
axios.patch(`/api/experts/${currentExpert.value?.UID}/config`, data).then((resp: AxiosResponse) => {
|
||||
console.log(resp.data)
|
||||
loadExperts();
|
||||
setTimeout(closeModal, 500)
|
||||
});
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<dialog id="set_date_modal" class="modal">
|
||||
<div class="modal-box">
|
||||
<form method="dialog">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
|
||||
</form>
|
||||
|
||||
<h3 class="font-bold text-lg">{{ currentExpert?.RealName }}</h3>
|
||||
<div class="py-5">
|
||||
|
||||
<label class="form-control w-full mb-5">
|
||||
<div class="label">
|
||||
<span class="label-text">微信号</span>
|
||||
</div>
|
||||
<input type="text" placeholder="微信号" class="input input-bordered w-full " v-model="wechat" />
|
||||
</label>
|
||||
|
||||
<label class="form-control w-full mb-5">
|
||||
<div class="label">
|
||||
<span class="label-text">语音ID</span>
|
||||
</div>
|
||||
<input type="text" placeholder="语音ID" class="input input-bordered w-full " v-model="voice" />
|
||||
</label>
|
||||
|
||||
<label class="form-control w-full " mb-5>
|
||||
<div class="label">
|
||||
<span class="label-text">打招呼模板</span>
|
||||
</div>
|
||||
<textarea v-model="hello" class="textarea textarea-bordered w-full " placeholder="打招呼模板"></textarea>
|
||||
</label>
|
||||
|
||||
<label class="form-control w-full mb-5">
|
||||
<div class="label">
|
||||
<span class="label-text">屏蔽IP区域用户</span>
|
||||
</div>
|
||||
<input type="text" placeholder="屏蔽IP区域用户" class="input input-bordered w-full" v-model="region" />
|
||||
<div class="label">
|
||||
<span class="label-text-alt">多个区域用 , 号分割</span>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="form-control w-full mb-5">
|
||||
<div class="label">
|
||||
<span class="label-text">屏蔽用户名关键字</span>
|
||||
</div>
|
||||
<input type="text" placeholder="屏蔽用户名关键字" class="input input-bordered w-full" v-model="name_keyword" />
|
||||
<div class="label">
|
||||
<span class="label-text-alt">多个用,号分割</span>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="form-control w-full ">
|
||||
<div class="form-control">
|
||||
<label class="cursor-pointer label justify-start gap-1">
|
||||
<input type="checkbox" v-model="produce" class="checkbox checkbox-success" />
|
||||
<span class="label-text">屏蔽0作品用户</span>
|
||||
</label>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="form-control w-full ">
|
||||
<div class="form-control">
|
||||
<label class="cursor-pointer label justify-start gap-1">
|
||||
<input type="checkbox" v-model="default_name" class="checkbox checkbox-success" />
|
||||
<span class="label-text">屏蔽默认用户名用户</span>
|
||||
</label>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
|
||||
<label class="form-control w-full ">
|
||||
<div class="form-control">
|
||||
<label class="cursor-pointer label justify-start gap-1">
|
||||
<input type="checkbox" v-model="default_avatar" class="checkbox checkbox-success" />
|
||||
<span class="label-text">屏蔽默认头像名用户</span>
|
||||
</label>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<label class="form-control w-full">
|
||||
<div class="label">
|
||||
<span class="label-text">选择时间</span>
|
||||
</div>
|
||||
<input type="datetime-local" v-model="date" placeholder="请选择时间" class="input input-bordered w-full" />
|
||||
<div class="label">
|
||||
<span class="label-text-alt">获取晚于这个时间关注的粉丝数据</span>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex justify-end gap-3">
|
||||
<button class="btn btn-default" @click="resetDate">恢复不限制</button>
|
||||
<button class="btn btn-wide btn-primary" @click="saveDate">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<div v-if="experts.length == 0">
|
||||
<h1 class="text-lg text-center">还没有专家</h1>
|
||||
</div>
|
||||
|
||||
<table className="table" v-else>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>专家</th>
|
||||
<th>已关注/所有</th>
|
||||
<th>配置</th>
|
||||
<th>停止关注</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
<tr :class='idx % 2 == 1 ? "bg-slate-50" : ""' v-for="(item, idx) in experts" :key="item.ID">
|
||||
<td class="flex flex-col">
|
||||
<div class="text-lg font-semibold">{{ item.RealName }}</div>
|
||||
<div>{{ item.UID }}</div>
|
||||
</td>
|
||||
<td>{{ item.Focus }} / {{ item.Total }}</td>
|
||||
<td>
|
||||
<button class="btn btn-sm" @click="showExpertDateModal(item.ID)">{{ parseTime(item.Since) }}</button>
|
||||
</td>
|
||||
<td>
|
||||
<button v-if="item.State == 'stop'" class="btn btn-warning btn-sm"
|
||||
@click="resetStateNormal(item.ID)">恢复</button>
|
||||
<button v-else class="btn btn-error btn-sm text-white" @click="setStateStop(item.ID)">停止</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
5
frontend/src/main.ts
Normal file
5
frontend/src/main.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { createApp } from 'vue'
|
||||
import './style.css'
|
||||
import App from './App.vue'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
3
frontend/src/style.css
Normal file
3
frontend/src/style.css
Normal file
@@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
1
frontend/src/vite-env.d.ts
vendored
Normal file
1
frontend/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
6
frontend/static.go
Normal file
6
frontend/static.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package frontend
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed dist
|
||||
var Static embed.FS
|
||||
18
frontend/tailwind.config.js
Normal file
18
frontend/tailwind.config.js
Normal file
@@ -0,0 +1,18 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{vue,js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
daisyui: {
|
||||
themes: ["light", "dark"],
|
||||
},
|
||||
plugins: [
|
||||
require("@tailwindcss/typography"),
|
||||
require('daisyui'),
|
||||
],
|
||||
}
|
||||
|
||||
25
frontend/tsconfig.json
Normal file
25
frontend/tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "preserve",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
11
frontend/tsconfig.node.json
Normal file
11
frontend/tsconfig.node.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
15
frontend/vite.config.ts
Normal file
15
frontend/vite.config.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:9090',
|
||||
changeOrigin: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user