Browse Source

疫苗预约系统

master
科技小王子 8 months ago
parent
commit
306bfcf5dd
  1. 4
      api/cms/form-record/model/index.ts
  2. 13
      components/AppHeader.vue
  3. 15
      composables/useClientRequest.ts
  4. 4
      composables/useServerRequest.ts
  5. 2
      layouts/default.vue
  6. 8
      nuxt.config.ts
  7. 23
      pages/components/Flash.vue
  8. 86
      pages/components/FormList.vue
  9. 150
      pages/form/components/Comments.vue
  10. 100
      pages/form/components/PageBanner.vue
  11. 65
      pages/form/index.vue
  12. 9
      pages/index.vue
  13. 4
      pages/passport/login.vue
  14. 3
      utils/common.ts

4
api/cms/form-record/model/index.ts

@ -7,9 +7,13 @@ export interface FormRecord {
formRecordId?: number;
formId?: number;
name?: string;
age?: number;
type?: number;
extra?: string;
formData?: string;
formObj?: Object;
userId?: number;
phone?: string;
sortNumber?: number;
comments?: string;
status?: number;

13
components/AppHeader.vue

@ -5,23 +5,22 @@
:class="affix ? 'absolute blur-xs' : 'sticky bg-white/75'">
<div class="xl:w-screen-xl xl:p-0 px-4 flex items-center between w-full m-auto">
<div class="header___left flex items-center">
<div class="logo mt-1 sm:w-[150px] h-7 w-auto py-2 flex items-center">
<div class="logo mt-1 sm:w-[250px] h-7 py-2 flex items-center">
<nuxt-link v-if="config?.siteLogo" to="/">
<div class="flex flex-col text-center xl:p-0">
<el-image
:src="config.siteLogo"
shape="square"
fit="fill"
class="lg:h-7 lg:w-auto pb-2 h-5 w-[90px] h-[28px]"
class="lg:h-7 lg:w-auto h-5 sm:w-[120px] h-[35px]"
:alt="config.siteName"
:title="config.siteName"
/>
<!-- <span class="text-gray-500 text-2.5" style="line-height: 1rem">云应用开发平台</span>-->
</div>
</nuxt-link>
<nuxt-link v-else to="/">
<text>{{ config?.siteName }}</text>
<nuxt-link to="/">
<text class="text-xl">{{ config?.siteName }}</text>
</nuxt-link>
</div>
<div class="hidden sm:flex">
@ -86,8 +85,8 @@
<el-dropdown>
<el-space class="el-dropdown-link flex items-center">
<span>{{ user.nickname }}</span>
<el-avatar v-if="token" class="cursor-pointer" :src="user?.avatar" :size="30"/>
<el-button v-else :icon="ElIconMenu"></el-button>
<!-- <el-avatar v-if="token" class="cursor-pointer" :src="user?.avatar" :size="30"/>-->
<!-- <el-button v-else :icon="ElIconMenu"></el-button>-->
</el-space>
<template #dropdown>
<el-dropdown-menu>

15
composables/useClientRequest.ts

@ -6,16 +6,23 @@ export type FetchOptions = Parameters<FetchType>[1];
export const useClientRequest = <T = unknown>(url: string, opts?: FetchOptions) => {
const token = useCookie<string | undefined>('token');
// 配置信息
const runtimeConfig = useRuntimeConfig();
// 请求接口
const baseUrl = ref('');
baseUrl.value = runtimeConfig.public.apiServer;
// 开发环境
if(process.env.NODE_ENV === 'development'){
baseUrl.value = 'http://127.0.0.1:9002/api'
}
const defaultOptions: FetchOptions = {
baseURL: getBaseUrl(),
baseURL: baseUrl.value,
onRequest({ options }) {
options.headers = (options.headers || {}) as { [key: string]: string };
if (token.value) {
options.headers.Authorization = token.value;
}
options.headers.Tenantd = '5';
options.headers.tenantid = '10145';
},
onResponse({ response }) {
if (+response.status === 0 && +response._data.code !== 0) {
@ -27,5 +34,5 @@ export const useClientRequest = <T = unknown>(url: string, opts?: FetchOptions)
}
};
return $fetch<T>(getBaseUrl() + url, { ...defaultOptions, ...opts });
return $fetch<T>(url, { ...defaultOptions, ...opts });
};

4
composables/useServerRequest.ts

@ -24,7 +24,7 @@ export const useServerRequest = <T>(url: string, opts?: UseFetchOptions<T, unkno
// 开发环境
if(process.env.NODE_ENV === 'development'){
baseUrl.value = `${runtimeConfig.public.apiServer}`
baseUrl.value = 'http://127.0.0.1:9002/api'
}
const defaultOptions: UseFetchOptions<unknown> = {
baseURL: baseUrl.value,
@ -42,7 +42,7 @@ export const useServerRequest = <T>(url: string, opts?: UseFetchOptions<T, unkno
// options.headers.tenantid = `${subDomain}`;
}
// TODO 2 从绑定域名解构的租户ID
// options.headers.tenantid = `5`;
options.headers.tenantid = `${runtimeConfig.public.tenantId}`;
if(localStorage.getItem('TID_ADMIN')){
options.headers.tenantid = `${localStorage.getItem('TID_ADMIN')}`;
}

2
layouts/default.vue

@ -76,7 +76,7 @@ const reload = async () => {
}
// TODO 2
const {data: websiteInfo} = await useServerRequest<ApiResult<Website>>('/cms/cms-website/getSiteInfo', {
const {data: websiteInfo} = await useServerRequest<ApiResult<Website>>('/cms/website/getSiteInfo', {
baseURL: runtimeConfig.public.apiServer
});
if (!websiteInfo.value) {

8
nuxt.config.ts

@ -27,16 +27,16 @@ export default defineNuxtConfig({
}
},
devServer: {
port: 16880
port: 16881
},
runtimeConfig: {
public: {
// 开发环境配置
// tenantId: '5',
// apiServer: 'http://127.0.0.1:30000/api',
tenantId: '10145',
// apiServer: 'http://127.0.0.1:9002/api',
// 生产环境
apiServer: 'https://server.gxwebsoft.com/api',
apiServer: 'https://modules.gxwebsoft.com/api',
globalTitle: '网宿软件',
domain: 'websoft.top'
},

23
pages/components/Flash.vue

@ -84,24 +84,23 @@
</svg>
<div class="mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl gap-16 sm:gap-y-24 flex flex-col ">
<div class="text-center relative z-[1]">
<div class="mb-8 cursor-pointer">
<el-tag type="warning" round @click="openSpmUrl(`/detail`, {},730,true)">v3.0 版本发布
</el-tag>
</div>
<!-- <div class="mb-8 cursor-pointer">-->
<!-- <el-tag type="warning" round @click="openSpmUrl(`/detail`, {},730,true)">v3.0 版本发布-->
<!-- </el-tag>-->
<!-- </div>-->
<h1 class="text-5xl font-bold tracking-tight text-gray-900 dark:text-white sm:text-7xl">
<span>构建现代Web应用</span><br/>
<span class="text-primary block lg:inline-block text-green-500">Vue 框架</span>
<span>宜州区乙肝检测</span><br/>
<span class="text-primary block lg:inline-block text-green-500">疫苗预约</span>
</h1>
<div class="mt-6 text-lg tracking-tight text-gray-600 dark:text-gray-300"> WEBSOFT是一个基于Vue和Nuxt构建的web框架使web开发更直观而强大<br>
自信地创建高性能和生产级的全栈web应用程序和网站
<div class="mt-6 text-lg tracking-tight text-gray-600 dark:text-gray-300"> 暂无描述
</div>
<div class="mt-10 flex flex-wrap gap-x-6 gap-y-3 justify-center">
<div class="flex flex-col gap-4">
<div class="flex items-center">
<el-button size="large" type="primary" v-if="!token" :icon="ElIconArrowRight" @click="openSpmUrl(`/passport/login`)">立即开始</el-button>
<el-button size="large" type="primary" v-else :icon="ElIconArrowRight" @click="loginAdminByToken">进入控制台</el-button>
<el-button size="large" type="success" :icon="ElIconDownload" @click="openSpmUrl(`/down`, {},0,true)">源码下载</el-button>
<el-button size="large" type="warning" :icon="ElIconShoppingCart" @click="openSpmUrl(`/item`, {},124)">购买授权</el-button>
<el-button size="large" type="primary" v-if="!token" :icon="ElIconArrowRight" @click="openSpmUrl(`/form`, {},282)">立即预约</el-button>
<!-- <el-button size="large" type="primary" v-else :icon="ElIconArrowRight" @click="loginAdminByToken">进入控制台</el-button>-->
<!-- <el-button size="large" type="success" :icon="ElIconDownload" @click="openSpmUrl(`/down`, {},0,true)">源码下载</el-button>-->
<!-- <el-button size="large" type="warning" :icon="ElIconShoppingCart" @click="openSpmUrl(`/item`, {},124)">购买授权</el-button>-->
</div>
</div>
</div>

86
pages/components/FormList.vue

@ -0,0 +1,86 @@
<template>
<!-- <div class="text-center flex flex-col items-center pb-10">-->
<!-- <h2 class="text-3xl font-bold tracking-tight text-gray-900 dark:text-white sm:text-4xl lg:text-5xl">-->
<!-- {{ title }}-->
<!-- </h2>-->
<!-- <div class="sub-title">-->
<!-- <p class="text-gray-500 dark:text-gray-400 py-3">-->
<!-- {{ comments }}-->
<!-- </p>-->
<!-- </div>-->
<!-- </div>-->
<div class="xl:w-screen-xl sm:flex xl:p-0 p-4 m-auto relative">
<el-row :gutter="24" class="flex">
<template v-for="(item,index) in list" :key="index">
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6" class="mb-5 min-w-xs">
<el-card shadow="hover" :body-style="{ padding: '0px' }" class="hover:bg-gray-50 cursor-pointer" @click="openSpmUrl(`/form`, item,item.formId)">
<el-image
:src="`${item.photo}`"
fit="fill" :lazy="true" class="w-full md:h-[150px] h-[199px] cursor-pointer"/>
<div class="flex-1 px-4 py-5 sm:p-6 !p-4">
<div class="text-gray-700 dark:text-white text-base font-semibold flex flex-col gap-1.5">
<div class="flex-1 text-xl cursor-pointer">{{ item.name }}</div>
<!-- <div class="text-red-500">{{ item.price }}</div>-->
</div>
<!-- <div v-if="item.price && item.price > 0" class="flex items-center gap-1.5 py-2 text-gray-500 justify-between">-->
<!-- <div class="text-gray-500">{{ item.comments }}</div>-->
<!-- </div>-->
<div class="button-group flex justify-center mt-3">
<el-button class="w-full" size="large" :icon="ElIconView" @click="openSpmUrl('/form', item,item.formId)">
查看详情
</el-button>
<el-button class="w-full" size="large" :icon="ElIconEdit" @click="openSpmUrl('/form', item,item.formId)">立即预约</el-button>
</div>
</div>
</el-card>
</el-col>
</template>
</el-row>
</div>
<div v-if="disabled" class="px-1 text-center text-gray-500 min-h-xs">
没有更多了
</div>
</template>
<script setup lang="ts">
import {openSpmUrl} from "~/utils/common";
import dayjs from "dayjs";
import {useServerRequest} from "~/composables/useServerRequest";
import type {ApiResult, PageResult} from "~/api";
import type {Product} from "~/api/oa/product/model";
import type {Form} from "~/api/cms/form/model";
const props = withDefaults(
defineProps<{
disabled?: boolean;
title?: string;
comments?: string;
}>(),
{
title: '卡片标题',
comments: '卡片描述'
}
);
const emit = defineEmits<{
(e: 'done'): void;
}>();
const runtimeConfig = useRuntimeConfig();
const list = ref<Form[]>([]);
//
const reload = async () => {
const {data: response} = await useServerRequest<ApiResult<PageResult<Product>>>('/cms/form/page', {
baseURL: runtimeConfig.public.apiServer, params: {
limit: 8
}
})
if (response.value?.data) {
if (response.value?.data.list) {
list.value = response.value?.data.list;
}
}
}
reload();
</script>

150
pages/form/components/Comments.vue

@ -0,0 +1,150 @@
<template>
<div class="page-main md:w-screen-xl m-auto p-3" v-if="form">
<el-row :gutter="24">
<el-col :span="18" :xs="24">
<el-card shadow="hover" class="hover:border-green-50 hover:border-2 mb-5">
<template #header>
<div class="card-header font-bold text-xl">
<span>{{ data?.name }}</span>
</div>
</template>
<p v-html="data?.comments"></p>
</el-card>
<el-card shadow="hover" class="hover:border-green-50 hover:border-2 mb-5">
<template #header>
<div class="card-header font-bold text-xl">
<span>预约报名</span>
</div>
</template>
<el-form ref="formRef" :model="form" label-position="top" class="w-full sm:py-2" size="large" status-icon>
<el-form-item label="您的姓名" prop="name">
<el-input
v-model="form.name"
placeholder="张三"
/>
</el-form-item>
<el-form-item label="您的年龄(岁)" prop="age">
<el-input-number
v-model="form.age"
placeholder="24"
:min="6"
:max="99"
/>
</el-form-item>
<el-form-item label="联系电话" prop="phone">
<el-input
v-model="form.phone"
:maxlength="11"
placeholder="13800138000"
/>
</el-form-item>
<el-form-item label="您预约的疫苗" prop="age">
<el-radio-group v-model="form.type">
<el-radio value="九阶" size="large" border>九阶</el-radio>
<el-radio value="四阶" size="large" border>四阶</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="本次接种的剂次" prop="number">
<el-checkbox-group v-model="checkList">
<el-checkbox label="第一剂" value="第一剂" />
<el-checkbox label="第二剂" value="第二剂" />
<el-checkbox label="第三剂" value="第三剂" />
</el-checkbox-group>
</el-form-item>
<el-form-item label="备注信息" prop="comments">
<el-input
v-model="form.comments"
:rows="5"
type="textarea"
placeholder="请输入备注信息,最多300字"
/>
</el-form-item>
<div class="dialog-footer w-full">
<el-button type="primary" size="large" class="w-full" @click="submitForm(formRef)"> 提交 </el-button>
</div>
</el-form>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
import {FullScreen} from '@element-plus/icons-vue'
import type {ApiResult} from "~/api";
import type {FormInstance, FormRules} from "element-plus";
import {useClientRequest} from "~/composables/useClientRequest";
import {reactive, ref} from "vue";
import useFormData from "~/utils/use-form-data";
import type {FormRecord} from "~/api/cms/form-record/model";
import type {Form} from "~/api/cms/form/model";
const props = withDefaults(
defineProps<{
title?: string;
data?: Form;
}>(),
{}
);
const formRef = ref<FormInstance>()
const visible = ref<boolean>(false);
const visible2 = ref<boolean>(false);
const checkList = ref<string[]>([]);
const loading = ref<boolean>(true)
const emit = defineEmits<{
(e: 'done', page: number): void
}>()
const { form, resetFields } = useFormData<FormRecord>({
formRecordId: undefined,
formId: undefined,
name: undefined,
age: undefined,
type: undefined,
extra: undefined,
formData: undefined,
formObj: undefined,
userId: undefined,
phone: undefined,
sortNumber: undefined,
comments: undefined,
status: undefined,
createTime: undefined,
layout: undefined
});
const onComments = () => {
visible.value = true;
}
const onComplaint = () => {
visible2.value = true;
}
const onPageChange = (page: number) => {
emit('done', page)
}
const submitForm = async (formEl: FormInstance | undefined) => {
if (!formEl) return
form.formId = props.data?.formId;
form.extra = checkList.value.join(',')
useClientRequest<ApiResult<any>>(`/cms/form-record`, {
method: 'POST',
body: form
}).then(res => {
if (res.code == 0) {
ElMessage.success(res.message)
visible.value = false
resetFields();
emit('done',1)
} else {
return ElMessage.error(res.message)
}
})
}
</script>

100
pages/form/components/PageBanner.vue

@ -0,0 +1,100 @@
<template>
<div class="banner m-auto relative sm:flex">
<svg viewBox="0 0 1440 181" fill="none" xmlns="http://www.w3.org/2000/svg"
class="pointer-events-none absolute w-full top-[-2px] transition-all text-green-5 flex-shrink-0 opacity-100 duration-[400ms] opacity-80 -z-10">
<mask id="path-1-inside-1_414_5526" fill="white">
<path d="M0 0H1440V181H0V0Z"></path>
</mask>
<path d="M0 0H1440V181H0V0Z" fill="url(#paint0_linear_414_5526)" fill-opacity="0.22"></path>
<path d="M0 2H1440V-2H0V2Z" fill="url(#paint1_linear_414_5526)" mask="url(#path-1-inside-1_414_5526)"></path>
<defs>
<linearGradient id="paint0_linear_414_5526" x1="720" y1="0" x2="720" y2="181" gradientUnits="userSpaceOnUse">
<stop stop-color="currentColor"></stop>
<stop offset="1" stop-color="currentColor" stop-opacity="0"></stop>
</linearGradient>
<linearGradient id="paint1_linear_414_5526" x1="0" y1="90.5" x2="1440" y2="90.5" gradientUnits="userSpaceOnUse">
<stop stop-color="currentColor" stop-opacity="0"></stop>
<stop offset="0.395" stop-color="currentColor"></stop>
<stop offset="1" stop-color="currentColor" stop-opacity="0"></stop>
</linearGradient>
</defs>
</svg>
<div class="md:w-screen-xl m-auto">
<div class="sm:py-4 sm:px-0 mx-3 py-2">
<el-breadcrumb :separator-icon="ArrowRight">
<el-breadcrumb-item :to="{ path: '/' }">
<el-icon class="cursor-pointer">
<ElIconHouse/>
</el-icon>
</el-breadcrumb-item>
<el-breadcrumb-item>项目详情</el-breadcrumb-item>
</el-breadcrumb>
</div>
<div class="py-1 sm:py-16 px-3" _path="/templates" _dir="" _draft="false" _partial="false" _locale=""
_id="content:4.templates.yml" _type="yaml" _source="content" _file="4.templates.yml" _stem="4.templates"
_extension="yml">
<div id="mse"></div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import {ArrowRight} from '@element-plus/icons-vue'
import Player from "xgplayer";
import 'xgplayer/dist/index.min.css';
const props = withDefaults(
defineProps<{
title?: string;
desc?: string;
buyUrl?: string;
form?: any;
value?: number;
}>(),
{
title: 'Templates',
desc: 'Explore community templates to get up and running in a few seconds.',
demoUrl: '/product/website',
buyUrl: 'https://github.com/websoft9/ansible-templates',
value: 4.2
}
);
const url = ref<string>('');
const poster = ref<string>('');
onMounted(() => {
new Player({
id: "mse", //id
lang: "zh", //
volume: 0, //
autoplay: false, //
screenShot: false, //
//
url: url.value,
//
poster: poster.value,
fluid: true, //
playbackRate: [0.5, 1, 2] //
});
});
watch(
() => props.form.video,
(video) => {
console.log(video,'=>video')
url.value = 'https://oss.wsdns.cn/20240417/9339681f3bc14999bfb2d26491f1c96e.mp4';
poster.value = props.form.photo;
},
{ immediate: true }
);
</script>
<style scoped lang="less">
.rounded-avatar{
border-radius: 30px;
}
.rounded-avatar-xs{
border-radius: 20px;
}
</style>

65
pages/form/index.vue

@ -0,0 +1,65 @@
<!-- 文章详情 -->
<template>
<PageBanner :form="form" />
<Comments :data="form" />
</template>
<script setup lang="ts">
import type { ApiResult } from '~/api';
import { useServerRequest } from '~/composables/useServerRequest';
import { useWebsite } from '~/composables/configState';
import { getIdBySpm } from '~/utils/common';
import useFormData from '~/utils/use-form-data';
import PageBanner from './components/PageBanner.vue';
import type { Form } from '~/api/cms/form/model';
import Comments from "~/pages/form/components/Comments.vue";
//
const route = useRoute();
const website = useWebsite();
//
const { form, assignFields } = useFormData<Form>({
formId: undefined,
name: '',
photo: '',
background: '',
video: '',
layout: '',
comments: '',
status: undefined,
createTime: ''
});
//
const reload = async () => {
//
// if (!token.value || token.value == '') {
// openSpmUrl('/passport/login');
// return;
// }
// spm()
const { data: item } = await useServerRequest<ApiResult<Form>>('/cms/form/' + getIdBySpm(5));
if (item.value?.data) {
assignFields(item.value.data);
form.comments = item.value?.data?.comments;
}
// seo
useHead({
title: `${form.name} - ${website.value.websiteName}`,
bodyAttrs: {
class: 'page-container'
}
});
};
watch(
() => route.path,
path => {
console.log(path, '=>Path');
reload();
},
{ immediate: true }
);
</script>

9
pages/index.vue

@ -2,19 +2,14 @@
<Flash/>
<ProductList title="产品服务" comments="拥抱开源、坚守品质;致力于打造安全稳定高可用的WEB应用!"/>
<ProductList title="插件市场" comments="安装插件,几秒钟内即可启动并运行"/>
<ArticleList title="开发者社区" comments="分享研发成果 交流技术经验"/>
<FormList title="项目列表" comments="预约项目列表"/>
</template>
<script setup lang="ts">
import {useConfigInfo, useForm, useToken, useWebsite} from "~/composables/configState";
import type {BreadcrumbItem} from "~/types/global";
import Flash from './components/Flash.vue';
import ProductList from "./components/ProductList.vue";
import ArticleList from './components/ArticleList.vue';
import FormList from "./components/FormList.vue";
//
const route = useRoute();

4
pages/passport/login.vue

@ -282,7 +282,7 @@ useHead({
* 执行登录
*/
const onSubmit = async () => {
const {data: response} = await useServerRequest<ApiResult<LoginResult>>('/login',{baseURL: runtimeConfig.public.apiServer,method: "post",body: form})
const {data: response} = await useServerRequest<ApiResult<LoginResult>>('/login',{baseURL: 'https://server.gxwebsoft.com/api',method: "post",body: form})
//
if(response.value?.code == 0){
ElMessage.success(response.value?.message)
@ -298,7 +298,7 @@ const onSubmit = async () => {
* 短信验证码登录
*/
const onSubmitBySms = async () => {
const {data: response} = await useServerRequest<ApiResult<LoginResult>>('/loginBySms',{baseURL: runtimeConfig.public.apiServer,method: "post",body: {
const {data: response} = await useServerRequest<ApiResult<LoginResult>>('/loginBySms',{baseURL: 'https://server.gxwebsoft.com/api',method: "post",body: {
phone: form.phone,
code: form.code,
isSuperAdmin: true

3
utils/common.ts

@ -136,6 +136,9 @@ export function openSpmUrl(path: string, d?: any, id = 0, isOpen?: boolean, isTo
if(d?.plugId){
model.value = 'plug';
}
if(d?.formId){
model.value = 'form';
}
// TODO 封装spm
spm.value = `?spm=${model.value}.${tid}.${mid}.${pid}.${cid}.${id}.${uid}.${timestamp.value}&token=${token}`;

Loading…
Cancel
Save