feat 从之前的服务中将前端分离
Some checks failed
Build image / build (push) Failing after 17s

This commit is contained in:
timerzz 2024-05-12 15:53:54 +08:00
commit 43e2dff6a3
26 changed files with 830 additions and 0 deletions

View File

@ -0,0 +1,18 @@
name: Build image
on: [push]
env:
HTTPS_PROXY: "http://192.168.31.55:10809"
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: build
run: docker build -t ${{ vars.DOCKER_REGISTRY }}/${{ vars.IMAGE_NAME }}:1.3 -f Dockerfile .
- name: tag
run: docker tag ${{ vars.DOCKER_REGISTRY }}/${{ vars.IMAGE_NAME }}:1.3 ${{ vars.DOCKER_REGISTRY }}/${{ vars.IMAGE_NAME }}:latest
- name: push 1.3
run: docker push ${{ vars.DOCKER_REGISTRY }}/${{ vars.IMAGE_NAME }}:1.3
- name: push latest
run: docker push ${{ vars.DOCKER_REGISTRY }}/${{ vars.IMAGE_NAME }}:latest

24
.gitignore vendored Normal file
View 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?

13
Dockerfile Normal file
View File

@ -0,0 +1,13 @@
FROM oven/bun:1 as front
COPY . /build
WORKDIR /build/wwwroot
RUN bun install && bun run build
FROM nginx:alpine
COPY --from=front /build/wwwroot/dist /usr/share/
RUN rm /etc/nginx/conf.d/default.conf
# 将自定义配置文件nginx.conf复制到容器内/etc/nginx/conf.d/目录
ADD ./nginx.conf /etc/nginx/conf.d/
CMD ["nginx", "-g", "daemon off;"]

7
README.md Normal file
View File

@ -0,0 +1,7 @@
# Vue 3 + Vite
This template should help get you started developing with Vue 3 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 IDE Setup
- [VS Code](https://code.visualstudio.com/) + [Vue - Official](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (previously Volar) and disable Vetur

BIN
bun.lockb Normal file

Binary file not shown.

2
bunfig.toml Normal file
View File

@ -0,0 +1,2 @@
[install]
registry = "https://registry.npmmirror.com/"

13
index.html Normal file
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>可达鸭海淘</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

21
nginx.conf Normal file
View File

@ -0,0 +1,21 @@
server {
listen 80;
server_name ht.timerzz.com;
#access_log /var/log/nginx/host.access.log main;
location / {
root /usr/share/dist;
index index.html index.htm;
}
location /api/v1 {
resolver 10.43.0.10 valid=10s; # 6.6.6.6 为自建DNS
proxy_pass http://ht-watcher;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}

26
package.json Normal file
View File

@ -0,0 +1,26 @@
{
"name": "wwwroot",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@ant-design/icons-vue": "^7.0.1",
"ant-design-vue": "4.x",
"mande": "^2.0.8",
"moment": "^2.30.1",
"vue": "^3.4.21",
"vue-router": "4"
},
"devDependencies": {
"@types/node": "^20.12.5",
"@vitejs/plugin-vue": "^5.0.4",
"unocss": "^0.59.0",
"unplugin-vue-components": "^0.26.0",
"vite": "^5.2.0"
}
}

15
src/App.vue Normal file
View File

@ -0,0 +1,15 @@
<template>
<a-config-provider :locale="zhCN">
<layout></layout>
</a-config-provider>
</template>
<script setup>
import Layout from '@/views/layout/index.vue'
import zhCN from 'ant-design-vue/es/locale/zh_CN';
</script>
<style scoped>
</style>

7
src/api/proxies.js Normal file
View File

@ -0,0 +1,7 @@
import {mande} from "mande";
const pushers = mande('/api/v1/proxies')
export const getProxiesStatus = () => {
return pushers.get("/status")
}

11
src/api/pusher.js Normal file
View File

@ -0,0 +1,11 @@
import {mande} from "mande";
const pushers = mande('/api/v1/pushers')
export const ListPushers = (query) => {
return pushers.get({query:query})
}
export const AddPusher = (opt)=>{
return pushers.post(opt)
}

22
src/api/watcher.js Normal file
View File

@ -0,0 +1,22 @@
import {mande} from "mande";
const watchers = mande('/api/v1/watchers')
export const ListWatchers = (query) => {
return watchers.get({query:query})
}
export const CreateWatcher = (opt)=>{
return watchers.post(opt)
}
export const DeleteWatcher = (uid)=>{
return watchers.delete(`/${encodeURIComponent(uid)}`)
}
export const StopWatcher = (uid)=>{
return watchers.delete(`/${encodeURIComponent(uid)}/status`)
}
export const StartWatcher = (uid)=>{
return watchers.post(`/${encodeURIComponent(uid)}/status`)
}

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 KiB

10
src/constants/pusher.js Normal file
View File

@ -0,0 +1,10 @@
export const PUSHER = {
ANPUSHER: 1,
}
export const WEBSITE_OPTIONS = [
{
label: 'anPush',
value: PUSHER.ANPUSHER
}
]

15
src/constants/website.js Normal file
View File

@ -0,0 +1,15 @@
export const WEBSITES = {
UNKNOWN: 0,
COACHOUTLET: 1,
}
export const WEBSITE_OPTIONS = [
{
label: '未知',
value: WEBSITES.UNKNOWN
},
{
label: 'coachoutlet',
value: WEBSITES.COACHOUTLET
}
]

6
src/css/base.css Normal file
View File

@ -0,0 +1,6 @@
html,body,#app {
height: 100%;
width: 100%;
padding: 0;
margin: 0;
}

9
src/main.js Normal file
View File

@ -0,0 +1,9 @@
import { createApp } from 'vue'
import './css/base.css'
import App from './App.vue'
import 'virtual:uno.css'
import router from "@/routers/index.js";
const app = createApp(App)
app.use(router)
app.mount('#app')

25
src/routers/index.js Normal file
View File

@ -0,0 +1,25 @@
import {createRouter, createWebHashHistory} from "vue-router";
const routes = [
{
path: '/',
redirect: '/watcher'
},
{
path: '/watcher',
name: 'watcher',
component: ()=>import('@/views/Watcher/index.vue')
},
{
path: '/pusher',
name: 'pusher',
component: ()=>import('@/views/Pusher/index.vue')
}
]
const router = createRouter({
history: createWebHashHistory(),
routes
})
export default router

178
src/views/Pusher/index.vue Normal file
View File

@ -0,0 +1,178 @@
<template>
<div class="h-full m-4 bg-white rounded-2 shadow-lg p-8 flex flex-col justify-between space-y-4">
<div class="flex justify-between">
<a-button type="primary" @click="addModal.visible=true" :disabled="loading">添加</a-button>
<div class="flex space-x-4">
<a-input placeholder="请输入关键词" v-model:value="query.keyword"></a-input>
<a-button type="primary" :disabled="loading" @click="list">搜索</a-button>
</div>
</div>
<div class="h-full border-0 border-t-1 border-solid border-gray-300 pt-4">
<a-spin :spinning="loading" :indicator="indicator">
<a-table :dataSource="data.list" :columns="columns" :pagination="false">
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'type'">
<span>{{WEBSITE_OPTIONS.find(w => w.value === record.type).label}}</span>
</template>
<template v-if="column.dataIndex === 'updatedAt'">
<span>{{moment(record.updatedAt).format('YYYY-MM-DD HH:mm:ss')}}</span>
</template>
<template v-if="column.dataIndex === 'option'">
<span>{{record.option}}</span>
</template>
</template>
</a-table>
</a-spin>
</div>
<a-pagination :disabled="loading" class="text-right" v-model:current="query.page" :total="data.total" show-less-items />
</div>
<a-modal v-model:open="addModal.visible" title="添加推送通知" @ok="handleOk" >
<a-spin :spinning="addModal.loading" :indicator="indicator">
<a-form
:model="addModal.data"
:label-col="{ span: 4 }"
:wrapper-col="{ span: 16 }"
autocomplete="off"
@finish="closeAddModal"
@finishFailed="closeAddModal"
>
<a-form-item label="名称" name="name" :rules="[{ required: true, message: '请填写推送名称' }]">
<a-input v-model:value="addModal.data.name" />
</a-form-item>
<a-form-item label="token" name="option.token" :rules="[{ required: false, message: '请填写token' }]">
<a-input v-model:value="addModal.data.option.token" />
</a-form-item>
<a-form-item label="channel" name="option.channel" :rules="[{ required: false, message: '请填写channel' }]">
<a-input v-model:value="addModal.data.option.channel" />
</a-form-item>
<a-form-item label="备注" name="remark">
<a-textarea v-model:value="addModal.data.remark" />
</a-form-item>
</a-form>
</a-spin>
</a-modal>
</template>
<script setup>
import {h, onMounted, reactive, ref} from "vue";
import {WEBSITE_OPTIONS} from "@/constants/website.js";
import moment from "moment/moment.js";
import {LoadingOutlined} from "@ant-design/icons-vue";
import {AddPusher, ListPushers} from "@/api/pusher.js";
import {PUSHER} from "@/constants/pusher.js";
import {message} from "ant-design-vue";
const loading = ref(false)
onMounted(()=>{
list()
})
const query = reactive({
// Website database.WebsiteType `query:"website,omitempty"` //
// Watch *bool `query:"watch,omitempty"`
// Orderable *bool `query:"orderable,omitempty"`
// Keyword string `query:"keyword,omitempty"`
keyword: '',
page: 1,
size:10
})
const data = ref({
total: 0,
list:[]
})
const list = ()=>{
loading.value = true
ListPushers(query).then(res=>{
data.value = res
}).catch(err => {
console.log(err)
}).finally(()=>{
loading.value = false
})
}
const addModal = reactive({
visible: false,
data: {
type:PUSHER.ANPUSHER,
name:'',
remark:'',
option:{
token:'',
channel: ''
}
},
loading: false
})
const closeAddModal = ()=>{
addModal.visible = false
addModal.data = {
type:PUSHER.ANPUSHER,
name:'',
remark:'',
option:{
token:'',
channel: ''
}
}
}
const handleOk = ()=>{
AddPusher(addModal.data).then(res=>{
message.success("添加成功")
}).catch(err => {
message.error("添加失败")
console.log(err)
}).finally(()=>{
list(false)
closeAddModal()
})
}
const columns = [
{
title: '名称',
dataIndex: 'name',
key: 'name',
},
{
title: '类型',
dataIndex: 'type',
key: 'type',
},
{
title: '配置',
dataIndex: 'option',
key: 'option',
},
{
title: '更新时间',
dataIndex: 'updatedAt',
key: 'updatedAt',
},
{
title: '备注',
dataIndex: 'remark',
key: 'remark',
},
]
const indicator = h(LoadingOutlined, {
style: {
fontSize: '32px',
},
spin: true,
});
</script>
<style scoped>
</style>

258
src/views/Watcher/index.vue Normal file
View File

@ -0,0 +1,258 @@
<template>
<div class="h-full m-4 bg-white rounded-2 shadow-lg p-8 flex flex-col justify-between space-y-4">
<div class="flex justify-between">
<a-button type="primary" @click="addModal.visible=true" :disabled="loading">添加</a-button>
<div class="flex space-x-4">
<a-input placeholder="请输入关键词" v-model:value="query.keyword"></a-input>
<a-button type="primary" :disabled="loading" @click="list">搜索</a-button>
</div>
</div>
<div class="h-full border-0 border-t-1 border-solid border-gray-300 pt-4">
<a-spin :spinning="loading" :indicator="indicator">
<a-table :dataSource="data.list" :columns="columns" :pagination="false">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'website'">
<span>{{WEBSITE_OPTIONS.find(w => w.value === record.website).label}}</span>
</template>
<template v-else-if="column.key === 'watch'">
<a-switch :checked="record.watch" @click="watcherStatusChange(!record.watch,record.uid)"/>
</template>
<template v-else-if="column.key === 'name'">
<a v-if="record.name !== '' " :href="record.link" target="_blank">{{record.name}}</a>
<span v-else>正在抓取信息</span>
</template>
<template v-else-if="column.key === 'updatedAt'">
<span>{{moment(record.updatedAt).format('YYYY-MM-DD HH:mm:ss')}}</span>
</template>
<template v-else-if="column.key === 'pusherIds'">
<template v-if="record.pusherIds" v-for="id in record.pusherIds">
<a-tag :bordered="false" color="processing">{{pusher.list.find(p =>p.id === id)?.name}}</a-tag>
</template>
<span v-else>-</span>
</template>
<template v-else-if="column.key === 'opt'">
<a-button type="link" danger @click="onDelete(record)">删除</a-button>
</template>
</template>
</a-table>
</a-spin>
</div>
<a-pagination :disabled="loading" class="text-right" v-model:current="query.page" :total="data.total" show-less-items />
</div>
<a-modal v-model:open="addModal.visible" title="添加监听任务" @ok="handleOk" >
<a-spin :spinning="addModal.loading" :indicator="indicator">
<a-form
:model="addModal.data"
:label-col="{ span: 4 }"
:wrapper-col="{ span: 16 }"
autocomplete="off"
@finish="closeAddModal"
@finishFailed="closeAddModal"
>
<a-form-item label="ID" name="pid" :rules="[{ required: true, message: '请填写商品id' }]">
<a-input v-model:value="addModal.data.pid" />
</a-form-item>
<a-form-item label="推送" name="pusherIds" :rules="[{ required: true, message: '请选择通知推送' }]">
<a-select
v-model:value="addModal.data.pusherIds"
mode="multiple"
@dropdown-visible-change="getPushers"
placeholder="请选择通知推送" :fieldNames="{label:'name',value:'id'}"
:options="pusher.list"
></a-select>
</a-form-item>
<a-form-item label="备注" name="remark">
<a-textarea v-model:value="addModal.data.remark" />
</a-form-item>
</a-form>
</a-spin>
</a-modal>
</template>
<script setup>
import moment from "moment";
import {h, onMounted, reactive, ref} from "vue";
import {ListWatchers, CreateWatcher, DeleteWatcher, StartWatcher, StopWatcher} from "@/api/watcher.js";
import { LoadingOutlined } from '@ant-design/icons-vue';
import {message, Modal} from 'ant-design-vue';
import {WEBSITE_OPTIONS} from "@/constants/website.js";
import {onBeforeRouteLeave} from "vue-router";
import {ListPushers} from "@/api/pusher.js";
let ticker = null
onMounted(()=>{
list(false)
getPushers()
ticker = setInterval(list, 2000, true)
})
onBeforeRouteLeave(()=>{
if(ticker){
clearInterval(ticker)
}
})
const query = reactive({
// Website database.WebsiteType `query:"website,omitempty"` //
// Watch *bool `query:"watch,omitempty"`
// Orderable *bool `query:"orderable,omitempty"`
// Keyword string `query:"keyword,omitempty"`
page: 1,
size:10
})
const loading = ref(false)
const data = ref({
total: 0,
list:[]
})
const list = (silent)=>{
if(!silent){
loading.value = true
}
ListWatchers(query).then(res=>{
data.value = res
}).catch(err => {
console.log(err)
}).finally(()=>{
if(!silent){
loading.value = false
}
})
}
const addModal = reactive({
visible: false,
data: {
pid:'',
remark:'',
pusherIds:[]
},
loading: false
})
const closeAddModal = ()=>{
addModal.visible = false
addModal.data = {
pid:'',
remark:'',
pusherIds: []
}
}
const handleOk = ()=>{
CreateWatcher(addModal.data).then(res=>{
message.success("添加成功")
}).catch(err => {
message.error("添加失败")
console.log(err)
}).finally(()=>{
list(false)
closeAddModal()
})
}
const pusher = reactive({
list: [],
query: {
keyword:'',
all: true
}
})
const getPushers = ()=>{
ListPushers(pusher.query).then(res=>{
pusher.list = res.list || []
}).catch(err => {
console.log(err)
})
}
const onDelete = ({name, uid})=>{
Modal.confirm({
title: '确认',
content: `确定删除 ${name} 监听?`,
centered: true,
onOk() {
DeleteWatcher(uid).then(res=>{
message.success("删除成功")
list(false)
}).catch(err=>{
message.error("删除失败")
console.log(err)
})
},
});
}
const watcherStatusChange=(changed, uid)=>{
const api = changed ? StartWatcher:StopWatcher
loading.value = true
api(uid).then(res=>{
message.success(`${changed?'开启':'关闭'}成功`)
}).catch(err=>{
message.error(`${changed?'开启':'关闭'}失败`)
}).finally(list)
}
const columns = [
{
title: '名称',
dataIndex: 'name',
key: 'name',
},
{
title: '货号',
dataIndex: 'pid',
key: 'pid',
},
{
title: '品牌',
dataIndex: 'brand',
key: 'brand',
},
{
title: '网站',
dataIndex: 'website',
key: 'website',
},
{
title: '正在蹲货',
dataIndex: 'watch',
key: 'watch',
},
{
title: '抓取时间',
dataIndex: 'updatedAt',
key: 'updatedAt',
},
{
title: '推送',
key: 'pusherIds',
},
{
title: '备注',
dataIndex: 'remark',
key: 'remark',
},
{
title: '操作',
key: 'opt',
},
]
const indicator = h(LoadingOutlined, {
style: {
fontSize: '32px',
},
spin: true,
});
</script>
<style scoped>
</style>

View File

@ -0,0 +1,42 @@
<template>
<a-menu
@click="onclick"
id="aside"
style="width: 256px"
mode="inline"
:items="items"
></a-menu>
</template>
<script setup>
import {AccountBookOutlined, BellOutlined} from "@ant-design/icons-vue";
import {h} from "vue";
import {useRouter} from "vue-router";
const items = [
{
key: 'watcher',
icon: () => h(AccountBookOutlined),
label: '蹲货',
title: '蹲货',
},
{
key: 'pusher',
icon: () => h(BellOutlined),
label: '推送',
title: '推送',
},
]
const router = useRouter()
const onclick = ({key}) => {
router.push({
name: key
})
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,38 @@
<template>
<div class="h-[100px] shadow-lg flex items-center justify-between px-12 z-10">
<div class="flex space-x-4 items-center">
<img class="h-[60px]" src="@/assets/logo.png" alt="">
<div class="text-[24px] font-bold">
可达鸭海淘蹲货
</div>
</div>
<div>
<div>当前代理个数: {{proxiesInfo.list.length}}</div>
<div class="text-[14px]">代理更新时间{{moment(proxiesInfo.updated).format('YYYY-MM-DD HH:mm:ss')}}</div>
</div>
</div>
</template>
<script setup>
import {onMounted, reactive} from "vue";
import {getProxiesStatus} from "@/api/proxies.js";
import moment from "moment";
const proxiesInfo = reactive({
list:[],
updated: ''
})
onMounted(()=>{
loadProxiesInfo()
})
const loadProxiesInfo = ()=>{
getProxiesStatus().then(res=>{
proxiesInfo.list = res.list
proxiesInfo.updated = res.updated
})
}
</script>
<style scoped>
</style>

14
src/views/layout/Main.vue Normal file
View File

@ -0,0 +1,14 @@
<template>
<div class="h-full w-full bg-[#f0f3f7] flex flex-col">
<router-view ></router-view>
</div>
</template>
<script setup>
</script>
<style scoped>
</style>

View File

@ -0,0 +1,17 @@
<template>
<div class="h-full w-full flex flex-col">
<Header></Header>
<div class="h-full w-full flex">
<Aside></Aside>
<Main></Main>
</div>
</div>
</template>
<script setup>
import Header from "@/views/layout/Header.vue";
import Aside from "@/views/layout/Aside.vue";
import Main from "@/views/layout/Main.vue";
</script>
<style scoped>
</style>

39
vite.config.js Normal file
View File

@ -0,0 +1,39 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import UnoCSS from 'unocss/vite'
import path from 'path'
import Components from 'unplugin-vue-components/vite';
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
UnoCSS(),
Components({
resolvers: [
AntDesignVueResolver({
importStyle: false, // css in js
}),
],
}),
],
server:{
open: true,
proxy: {
'/api/v1': {
target: 'http://172.25.168.160:2280/',
// target: 'http://192.168.31.55:2280/',
changeOrigin: true,
secure: false,
ws: true,
},
},
},
resolve: {
alias: {
'@': path.resolve(__dirname, "./src")
}
}
})