Compare commits

..

10 Commits

Author SHA1 Message Date
xjs ce32206709 fix: 修复样式 2026-06-04 15:01:40 +08:00
xjs 2902d26d02 feat: 调整逻辑和修改样式 2026-06-04 10:46:54 +08:00
xjs b7b3baf17b feat: 新的保存逻辑 2026-06-01 10:41:00 +08:00
xjs 7a4d32eaa2 feat: 学校列表以及详情页面调整 2026-05-29 13:58:52 +08:00
xjs 95ad03bdf1 feat: ai基础更新 2026-05-29 10:23:20 +08:00
xjs a4249f95c3 feat: 增加模式选择 2026-05-28 11:45:48 +08:00
xjs 14bd6d0ea8 refactor: 删除不要的腾讯组件 2026-05-28 10:18:40 +08:00
xjs 4f75f2e4e0 refactor: 删除不要的腾讯组建 2026-05-28 10:17:34 +08:00
xjs a4e0d74342 feat: 增加腾讯AI 2026-05-28 10:17:05 +08:00
xjs 021f65a7f9 feat: 增加裂变页面 2026-05-26 10:13:02 +08:00
148 changed files with 8434 additions and 678 deletions

18
env/.env vendored
View File

@ -2,7 +2,23 @@ VITE_APP_TITLE = '六纬中考通'
VITE_APP_PORT = 9000 VITE_APP_PORT = 9000
VITE_UNI_APPID = 'H57F2ACE4' VITE_UNI_APPID = 'H57F2ACE4'
VITE_WX_APPID = 'wxc48ad15d58a3e417' # VITE_WX_APPID = 'wxc48ad15d58a3e417' 六纬中考通
# VITE_WX_APPID = 'wx4b925e36c17dd54a' 智能中专学校的
# wx487ac749c0e10be5 深泉外国语的
VITE_WX_APPID = 'wx487ac749c0e10be5'
# VITE_WX_VIDEO_ID= 'sphju9MCfZetYHP' 智能中专学校的
# VITE_WX_VIDEO_ID= 'spht0MPpWjxEKb4' 深泉外国语
# spheuUaMulnlpHH 深泉外国语学院 认证的
VITE_WX_VIDEO_ID= 'spht0MPpWjxEKb4'
# 微信小程序 AI
# 智能中专学校的
# VITE_CLOUD_ENV_ID= 'cloud1-d3g5q6bq61a240786',
# VITE_CLOUD_BOT_ID= 'agent-wxai-4gl75um61026324f',
# 深泉外国语学院的
VITE_CLOUD_ENV_ID='cloud1-d9g4odcaqf6a2114f'
VITE_CLOUD_BOT_ID='agent-wxai-9gi3rts96061202f'
# h5部署网站的base配置到 manifest.config.ts 里的 h5.router.base # h5部署网站的base配置到 manifest.config.ts 里的 h5.router.base
# https://uniapp.dcloud.net.cn/collocation/manifest.html#h5-router # https://uniapp.dcloud.net.cn/collocation/manifest.html#h5-router

View File

@ -107,6 +107,7 @@
"pinia": "2.0.36", "pinia": "2.0.36",
"pinia-plugin-persistedstate": "3.2.1", "pinia-plugin-persistedstate": "3.2.1",
"sard-uniapp": "^1.22.1", "sard-uniapp": "^1.22.1",
"uqrcodejs": "^4.0.7",
"vue": "^3.4.21", "vue": "^3.4.21",
"z-paging": "^2.8.8" "z-paging": "^2.8.8"
}, },

View File

@ -8,6 +8,10 @@ export default defineUniPages({
navigationBarBackgroundColor: '#FFFFFF', navigationBarBackgroundColor: '#FFFFFF',
navigationBarTextStyle: 'black', navigationBarTextStyle: 'black',
backgroundColor: '#FFFFFF', backgroundColor: '#FFFFFF',
// 全局注册 markdown 渲染组件,自定义聊天 UI 使用
usingComponents: {
'markdown-preview': '/wxcomponents/agent-ui/wd-markdown/index',
},
}, },
easycom: { easycom: {
autoscan: true, autoscan: true,
@ -23,7 +27,7 @@ export default defineUniPages({
preloadRule: { preloadRule: {
'pages/index/index': { 'pages/index/index': {
network: 'all', network: 'all',
packages: ['chart-sub'], packages: ['chart-sub', 'pages-ai'],
}, },
}, },
}) })

View File

@ -9,14 +9,14 @@
</text> </text>
<view class="mt-[20rpx] text-center"> <view class="mt-[20rpx] text-center">
<view class="text-[#000] text-[26rpx] font-700">学习风格表现</view> <view class="text-[#000] text-[26rpx] font-700">学习风格表现</view>
<view class="mt-[10rpx]" v-for="(item, index) in item.learning_performance" :key="index"> <view class="mt-[10rpx]" v-for="(sonItem, index) in item.learning_performance" :key="index">
{{ item }} {{ sonItem }}
</view> </view>
</view> </view>
<view class="mt-[20rpx] text-center"> <view class="mt-[20rpx] text-center">
<view class="text-[#000] text-[26rpx] font-700">学习风格特点</view> <view class="text-[#000] text-[26rpx] font-700">学习风格特点</view>
<view class="mt-[10rpx]" v-for="(item, index) in item.features" :key="index"> <view class="mt-[10rpx]" v-for="(sonItem, index) in item.features" :key="index">
{{ item }} {{ sonItem }}
</view> </view>
</view> </view>
</view> </view>

View File

@ -16,37 +16,6 @@
避免过度放松保持适度的学习节奏 避免过度放松保持适度的学习节奏
</view> </view>
</view> </view>
<view class="flex flex-col gap-[12rpx]">
<view class="flex items-center gap-[10rpx]">
<view class="w-[38rpx] h-[38rpx]">
<image
src="https://api.static.ycymedu.com/src/images/home/diet-icon.png"
mode="scaleToFill"
class="w-[38rpx] h-[38rpx]"
/>
</view>
<text class="text-[32rpx] text-[#000] font-700">饮食建议</text>
</view>
<view class="text-[26rpx] text-[#666] font-400">
保持规律作息早睡早起避免熬夜 每天适当运动如散步跑步保持精力充沛
避免过度放松保持适度的学习节奏
</view>
</view>
<view class="flex flex-col gap-[12rpx]">
<view class="flex items-center gap-[10rpx]">
<view class="w-[38rpx] h-[38rpx]">
<image
src="https://api.static.ycymedu.com/src/images/home/learn-icon.png"
mode="scaleToFill"
class="w-[38rpx] h-[38rpx]"
/>
</view>
<text class="text-[32rpx] text-[#000] font-700">学习建议</text>
</view>
<view class="text-[26rpx] text-[#666] font-400">
保持规律作息早睡早起避免熬夜 每天适当运动如散步跑步保持精力充沛
避免过度放松保持适度的学习节奏
</view>
</view>
</view> </view>
</template> </template>

View File

@ -14,7 +14,7 @@
</template> </template>
</Navbar> </Navbar>
<view class="flex-1 overflow-auto relative"> <view class="flex-1 overflow-auto relative flex flex-col">
<view class="flex flex-col flex-1 overflow-auto pb-[20rpx]"> <view class="flex flex-col flex-1 overflow-auto pb-[20rpx]">
<!-- 顶部卡片 --> <!-- 顶部卡片 -->
<view class="mt-[30rpx] mx-[24rpx]"> <view class="mt-[30rpx] mx-[24rpx]">

View File

@ -14,7 +14,7 @@
</template> </template>
</Navbar> </Navbar>
<view class="flex-1 overflow-auto relative"> <view class="flex-1 overflow-auto relative flex flex-col">
<view class="flex flex-col flex-1 overflow-auto pb-[20rpx]"> <view class="flex flex-col flex-1 overflow-auto pb-[20rpx]">
<!-- 顶部卡片 --> <!-- 顶部卡片 -->
<view class="mt-[30rpx] mx-[24rpx]"> <view class="mt-[30rpx] mx-[24rpx]">

2
src/env.d.ts vendored
View File

@ -15,6 +15,8 @@ interface ImportMetaEnv {
readonly VITE_SERVER_PORT: string readonly VITE_SERVER_PORT: string
/** 后台接口地址 */ /** 后台接口地址 */
readonly VITE_SERVER_BASEURL: string readonly VITE_SERVER_BASEURL: string
/** 微信视频号 ID */
readonly VITE_WX_VIDEO_ID: string
/** H5是否需要代理 */ /** H5是否需要代理 */
readonly VITE_APP_PROXY_ENABLE: 'true' | 'false' readonly VITE_APP_PROXY_ENABLE: 'true' | 'false'
/** H5是否需要代理需要的话有个前缀 */ /** H5是否需要代理需要的话有个前缀 */

258
src/pages-ai/ai/index.vue Normal file
View File

@ -0,0 +1,258 @@
<script lang="ts" setup>
import type { AgentConfig, ModelConfig } from '../components/agent-config'
import type { AiMessage } from '@/store/ai'
import { useAiStore } from '@/store'
import {
defaultModelAgentConfig,
defaultModelConfig,
generateMessageId,
} from '../components/agent-config'
import { fetchRecommendQuestions, streamMessage } from '../components/agent-service'
import AgentHeader from '../components/AgentHeader.vue'
import AgentInput from '../components/AgentInput.vue'
import AgentMessage from '../components/AgentMessage.vue'
definePage({
style: {
navigationBarTitleText: 'AI助手',
},
})
const agentConfig = reactive<AgentConfig>({
// bot ...defaultAgentConfig
...defaultModelAgentConfig,
})
const modelConfig = reactive<ModelConfig>({ ...defaultModelConfig })
const aiStore = useAiStore()
const paging = ref<any>(null)
const messages = ref<AiMessage[]>([])
const loading = ref(false)
const initQuestionCapsules = computed(() => agentConfig.initQuestions.slice(0, 3))
const shouldShowInitQuestionCapsules = computed(() => aiStore.messages.length === 0 && initQuestionCapsules.value.length > 0)
// controller cancelled
let cancelFlag = false
onLoad(() => {
// #ifdef MP-WEIXIN
if (typeof wx !== 'undefined' && wx.cloud) {
wx.cloud.init({
env: agentConfig.cloudEnvId,
traceUser: true,
})
}
// #endif
aiStore.ensureThreadId()
})
// z-paging store
// 0 =
function queryList() {
const welcomeMsg = agentConfig.welcomeMsg?.trim()
if (aiStore.messages.length === 0 && welcomeMsg) {
aiStore.addMessage({
id: generateMessageId(),
role: 'assistant',
content: welcomeMsg,
createdAt: Date.now(),
})
}
paging.value?.complete(aiStore.messages.slice().reverse())
}
async function handleSend(text: string) {
if (loading.value) {
return
}
cancelFlag = false
loading.value = true
const userMsg: AiMessage = {
id: generateMessageId(),
role: 'user',
content: text,
createdAt: Date.now(),
}
const aiMsg: AiMessage = {
id: generateMessageId(),
role: 'assistant',
content: '',
pending: true,
// +1 userMsg
createdAt: Date.now() + 1,
}
// store z-paging addChatRecordData +
// totalData = [aiMsg, userMsg, ...] 0 = " AI "
aiStore.addMessage(userMsg)
aiStore.addMessage(aiMsg)
paging.value?.addChatRecordData([userMsg, aiMsg])
try {
await streamMessage({
chatMode: agentConfig.chatMode,
// bot
botId: agentConfig.botId,
threadId: aiStore.ensureThreadId(),
// model
modelProvider: modelConfig.modelProvider,
quickResponseModel: modelConfig.quickResponseModel,
// aiMsg
history: aiStore.messages.slice(0, -1),
prompt: text,
onDelta: (delta) => {
if (cancelFlag) {
return
}
aiStore.appendDelta(aiMsg.id, delta)
},
onError: (msg) => {
aiStore.updateMessage(aiMsg.id, {
error: true,
pending: false,
content: msg,
})
},
})
aiStore.updateMessage(aiMsg.id, { pending: false })
// V2 agent getRecommendQuestions 404
// bot-xxx agent
if (!cancelFlag) {
void loadRecommendQuestions(aiMsg.id, text)
}
}
catch (err) {
aiStore.updateMessage(aiMsg.id, {
pending: false,
error: true,
content: aiStore.messages.find(m => m.id === aiMsg.id)?.content
|| (err instanceof Error ? err.message : '生成失败'),
})
}
finally {
loading.value = false
}
}
function handleStop() {
cancelFlag = true
loading.value = false
// pending
const pending = aiStore.messages.find(m => m.pending)
if (pending) {
aiStore.updateMessage(pending.id, { pending: false })
}
}
function handleRetry(id: string) {
const failedIdx = aiStore.messages.findIndex(m => m.id === id)
if (failedIdx <= 0) {
return
}
const userMsg = aiStore.messages[failedIdx - 1]
if (!userMsg || userMsg.role !== 'user') {
return
}
aiStore.removeMessage(id)
handleSend(userMsg.content)
}
function handleClickQuestion(question: string) {
handleSend(question)
}
/** 请求追问建议,结果挂到对应 AI 消息的 recommendQuestions */
async function loadRecommendQuestions(aiMsgId: string, prompt: string) {
const last = aiStore.messages.slice(-2).map(m => ({
role: m.role === 'user' ? ('user' as const) : ('assistant' as const),
content: m.content,
}))
try {
const questions = await fetchRecommendQuestions({
botId: agentConfig.botId,
lastPair: last,
prompt,
max: 3,
onProgress: (qs) => {
aiStore.updateMessage(aiMsgId, { recommendQuestions: qs })
},
})
aiStore.updateMessage(aiMsgId, { recommendQuestions: questions })
}
catch (error) {
console.error('[recommend]', error)
}
}
function handleClear() {
aiStore.clear()
paging.value?.reload()
}
</script>
<template>
<view class="h-screen w-full flex flex-col bg-[#F4F6FA]">
<!-- 消息列表聊天模式 -->
<view class="min-h-0 flex-1">
<z-paging
ref="paging"
v-model="messages"
:default-page-size="50"
:auto-show-system-loading="false"
:loading-more-enabled="false"
:show-refresher-when-reload="false"
:show-loading-more-no-more-view="false"
use-chat-record-mode
auto-adjust-position-when-chat
auto-to-bottom-when-chat
:paging-style="{ backgroundColor: '#F4F6FA' }"
@query="queryList"
>
<!-- 首屏推荐问题 -->
<template v-if="shouldShowInitQuestionCapsules" #top>
<agent-header
:questions="initQuestionCapsules"
@pick="handleClickQuestion"
/>
</template>
<template #default>
<agent-message
v-for="item in messages"
:key="item.id"
:message="item"
:bot-name="agentConfig.botName"
@retry="handleRetry"
@pick-recommend="handleClickQuestion"
/>
</template>
<template #bottom>
<view>
<view class="text-[24rpx] text-[#999] text-center my-[16rpx]">AI建议仅供参考请以官方政策为准</view>
<agent-input
:placeholder="agentConfig.placeholder"
:loading="loading"
@send="handleSend"
@stop="handleStop"
@clear="handleClear"
/>
</view>
</template>
<template #empty>
<view />
</template>
</z-paging>
</view>
</view>
</template>
<style lang="scss" scoped>
//
</style>

View File

@ -0,0 +1,53 @@
<script lang="ts" setup>
import {defaultAgentConfig} from "./agent-config"
const props = withDefaults(defineProps<{
questions?: string[]
}>(), {
questions: () => [],
})
const emit = defineEmits<{
(event: 'pick', question: string): void
}>()
function handlePick(question: string) {
emit('pick', question)
}
</script>
<template>
<view class="mt-[110rpx] box-border w-full px-[34rpx]">
<view
class="relative z-0 mb-[-108rpx] h-[210rpx] border-[2rpx] border-white rounded-[100rpx_26rpx_26rpx_26rpx] border-solid bg-[#B0E4FF] bg-opacity-80">
<view
class="agent-hello__glow absolute bottom-[46rpx] left-[12rpx] z-[2] h-[64rpx] w-[266rpx] rounded-[40rpx] blur-[20rpx]" />
<image class="absolute bottom-[70rpx] h-[246rpx] w-[240rpx]"
src="https://lw-zk.oss-cn-hangzhou.aliyuncs.com/img/home/ai_zanyong.png" mode="aspectFit" />
<view class="mr-[28rpx] mt-[30rpx] text-right text-[32rpx] text-[#000] font-500 leading-[1.4]">
你好我是你的{{ defaultAgentConfig.botName }}
</view>
</view>
<view
class="relative z-[1] box-border border-[2rpx] border-white rounded-[26rpx] border-solid bg-white bg-opacity-70 px-[34rpx] pb-[36rpx] pt-[32rpx] backdrop-blur-[20rpx]">
<view class="text-[28rpx] text-[#1F2329] leading-[1.5]">
你好我可以帮你解读济南中考政策分数线和志愿填报问题
</view>
</view>
<view v-if="props.questions.length" class="mt-[28rpx] flex flex-wrap gap-[16rpx]">
<view v-for="item in props.questions" :key="item"
class="border-[1rpx] border-[#E8E8E8] rounded-full border-solid bg-white px-[32rpx] py-[16rpx] text-[30rpx] text-[#000] active:opacity-60"
@click="handlePick(item)">
{{ item }}
</view>
</view>
</view>
</template>
<style lang="scss" scoped>
.agent-hello__glow {
background: radial-gradient(ellipse at top, #1580ff, #fff);
}
</style>

View File

@ -0,0 +1,105 @@
<script lang="ts" setup>
import { ref } from 'vue'
const props = defineProps<{
placeholder?: string
loading?: boolean
}>()
const emit = defineEmits<{
(event: 'send', text: string): void
(event: 'stop'): void
(event: 'clear'): void
}>()
const inputValue = ref('')
const showActions = ref(false)
function handleSend() {
const text = inputValue.value.trim()
if (!text || props.loading) {
return
}
emit('send', text)
inputValue.value = ''
}
function toggleActions() {
showActions.value = !showActions.value
}
function handleClickClear() {
showActions.value = false
uni.showModal({
title: '清空对话',
content: '将删除所有对话记录,确认继续?',
confirmColor: '#EB5241',
success: (res) => {
if (res.confirm) {
emit('clear')
}
},
})
}
</script>
<template>
<view class="flex flex-col bg-white pb-safe">
<!-- 输入区 -->
<view class="flex items-center gap-[16rpx] px-[24rpx] py-[18rpx]">
<view class="flex flex-1 items-center rounded-full bg-[#F5F6F8] p-[16rpx]">
<textarea v-model="inputValue" class="max-h-[200rpx] w-full bg-transparent text-[28rpx] text-[#1F2329] ml-[14rpx]"
:placeholder="props.placeholder || '有中考问题,直接问我!'" placeholder-style="color:#A6AFBD;font-size:28rpx;"
auto-height :show-confirm-bar="false" :adjust-position="true" :cursor-spacing="20" confirm-type="send"
@confirm="handleSend" @focus="showActions = false" />
<!-- 发送 / 停止 / 展开更多 -->
<view v-if="!props.loading && !inputValue.trim()"
class="h-[52rpx] w-[52rpx] flex items-center justify-center rounded-full "
:class="showActions ? 'rotate-45' : ''" @click="toggleActions">
<image
src="https://lw-zk.oss-cn-hangzhou.aliyuncs.com/img/home/ai_tianjia.png"
mode="scaleToFill"
class="w-[52rpx] h-[52rpx]"
/>
</view>
<view v-else-if="!props.loading"
class="h-[52rpx] w-[52rpx] flex items-center justify-center rounded-[36rpx] "
@click="handleSend">
<image
src="https://lw-zk.oss-cn-hangzhou.aliyuncs.com/img/home/ai_fasong.png"
mode="scaleToFill"
class="h-[52rpx] w-[52rpx]"
/>
</view>
<view v-else
class="h-[52rpx] w-[52rpx] flex items-center justify-center"
@click="emit('stop')">
<image
src="https://lw-zk.oss-cn-hangzhou.aliyuncs.com/img/home/ai_zanting.png"
mode="scaleToFill"
class="h-[52rpx] w-[52rpx]"
/>
</view>
</view>
</view>
<!-- 展开的操作面板 -->
<view v-if="showActions" class="flex gap-[16rpx] px-[24rpx] pb-[24rpx] pt-[12rpx]">
<view class="flex items-center justify-center p-[20rpx] bg-[#F5F6F8] active:opacity-60"
@click="handleClickClear">
<image
src="https://lw-zk.oss-cn-hangzhou.aliyuncs.com/img/home/ai_qingkong.png"
mode="scaleToFill"
class="h-[52rpx] w-[52rpx]"
/>
</view>
</view>
</view>
</template>
<style lang="scss" scoped>
.rotate-45 {
transform: rotate(45deg);
}
</style>

View File

@ -0,0 +1,96 @@
<script lang="ts" setup>
import type { AiMessage } from '@/store/ai'
const props = defineProps<{
message: AiMessage
botName?: string
}>()
defineEmits<{
(event: 'retry', id: string): void
(event: 'pickRecommend', question: string): void
}>()
</script>
<template>
<view class="cell-flip flex px-[24rpx] py-[16rpx]"
:class="props.message.role === 'user' ? 'justify-end' : 'justify-start'">
<!-- AI 头像 -->
<!-- <view v-if="props.message.role !== 'user'"
class="mr-[16rpx] h-[64rpx] w-[64rpx] flex shrink-0 items-center justify-center rounded-full bg-[#1580FF] text-[24rpx] text-white">
AI
</view> -->
<view class="max-w-[90%] flex flex-col gap-[8rpx]">
<view class="break-all rounded-[16rpx] px-[24rpx] py-[18rpx] text-[28rpx] leading-[1.6]" :class="props.message.role === 'user'
? 'bg-[#1580FF] text-white rounded-tr-[4rpx]'
: 'rounded-tl-[4rpx] bg-white text-[#1F2329] shadow-[0_2rpx_12rpx_rgba(0,0,0,0.04)]'">
<!-- AI 回复用 markdown 渲染仅微信小程序 -->
<!-- #ifdef MP-WEIXIN -->
<markdown-preview v-if="props.message.role !== 'user'"
:markdown="props.message.content || (props.message.pending ? '' : '')" :font-size="28" />
<text v-else class="select-text">
{{ props.message.content }}
</text>
<!-- #endif -->
<!-- 其它端 fallback -->
<!-- #ifndef MP-WEIXIN -->
<text class="select-text">
{{ props.message.content || (props.message.pending ? '...' : '') }}
</text>
<!-- #endif -->
<text v-if="props.message.pending" class="ml-[6rpx] animate-pulse">
</text>
</view>
<view v-if="props.message.error" class="self-start text-[24rpx] text-[#EB5241] leading-[1.4] active:opacity-60"
@click="$emit('retry', props.message.id)">
生成失败点击重试
</view>
<!-- 追问建议 AI 消息且生成结束后展示 -->
<view
v-if="props.message.role === 'assistant' && !props.message.pending && props.message.recommendQuestions && props.message.recommendQuestions.length"
class="mt-[8rpx] flex flex-col gap-[12rpx]">
<view v-for="q in props.message.recommendQuestions" :key="q"
class="self-start border-1 border-[#1580FF] rounded-[24rpx] border-solid bg-white px-[20rpx] py-[10rpx] text-[24rpx] text-[#1580FF] active:opacity-60"
@click="$emit('pickRecommend', q)">
{{ q }}
</view>
</view>
</view>
<!-- 用户头像 -->
<!-- <view v-if="props.message.role === 'user'"
class="ml-[16rpx] h-[64rpx] w-[64rpx] flex shrink-0 items-center justify-center rounded-full bg-[#FFA94D] text-[28rpx] text-white">
U
</view> -->
</view>
</template>
<style lang="scss" scoped>
/* z-paging 聊天模式整体 scaleY(-1)cell 必须再反转一次回正 */
.cell-flip {
transform: scaleY(-1);
}
.animate-pulse {
animation: pulse 1.4s ease-in-out infinite;
}
@keyframes pulse {
0%,
80%,
100% {
opacity: 0.2;
}
40% {
opacity: 1;
}
}
</style>

View File

@ -0,0 +1,72 @@
// AI Agent 相关运行时配置(页面、组件共享)
/** 组件对接的 AI 类型 */
export type ChatMode = 'bot' | 'model'
export interface AgentConfig {
/** 必填:组件对接的 AI 类型;'bot' 走 agent 能力,'model' 走大模型能力 */
chatMode: ChatMode
/** 微信云开发环境 ID用于 wx.cloud.init */
cloudEnvId: string
/** Agent ID对应 wx.cloud.extend.AI.bot.sendMessage 的 botIdchatMode=bot 必填) */
botId: string
/** 首屏欢迎语;不设置或留空时不插入欢迎消息 */
welcomeMsg?: string
/** 输入框 placeholder */
placeholder: string
/** 显示在 AI 头像位置的图标 / 文字 */
botName: string
/** 默认推荐问题,点击后直接发送 */
initQuestions: string[]
}
export interface ModelConfig {
/** createModel 的 provider / GroupName如 'cloudbase' / 'hunyuan-exp' / 'custom-xxx' */
modelProvider: string
/** 具体的模型 ID */
quickResponseModel: string
/** logo 图,可选 */
logo: string
}
export const defaultAgentConfig: AgentConfig = {
chatMode: 'bot',
cloudEnvId: import.meta.env.VITE_CLOUD_ENV_ID,
botId: import.meta.env.VITE_CLOUD_BOT_ID,
placeholder: '有中考问题,直接问我!',
botName: '中考小助手',
initQuestions: [
'济南500分还能上哪些高中',
'第二批志愿怎么填最稳',
'指标生和统招生区别',
],
}
/**
* model
* 1. defaultAgentConfig.chatMode 'model'
* 2. createModel provider / GroupName quickResponseModel
*
*
* wx.cloud.extend.AI.createModel('cloudbase').streamText({
* data: { model: 'hy3-preview', messages: [...] },
* })
*/
export const defaultModelConfig: ModelConfig = {
modelProvider: 'hunyuan-v3',
quickResponseModel: 'hy3-preview',
logo: '',
}
/** 切换到 model 模式时可直接复用的示例 */
export const defaultModelAgentConfig: AgentConfig = {
...defaultAgentConfig,
chatMode: 'bot',
botName: 'AI 模型助手',
}
export function generateMessageId() {
const timestamp = Date.now().toString().slice(-8)
const random = Math.floor(Math.random() * 10000).toString().padStart(4, '0')
return `${timestamp}${random}`
}

View File

@ -0,0 +1,242 @@
// 与 wx.cloud.extend.AI 通讯的服务层
// 根据 chatMode 自动分流:'bot' 走 ai.bot.sendMessage'model' 走 ai.createModel().streamText
// UI 层只关心 onDelta 增量与最终错误,不感知具体走哪条链路
import type { ChatMode } from './agent-config'
import type { AiMessage } from '@/store/ai'
import { generateMessageId } from './agent-config'
interface BotEventData {
type: string
delta?: string
message?: string
[key: string]: unknown
}
interface ModelEventData {
id?: string
choices?: {
delta?: {
content?: string
reasoning_content?: string
role?: string
}
finish_reason?: string
}[]
}
export interface SendMessageOptions {
/** 'bot' | 'model' */
chatMode: ChatMode
/** 历史消息(最近若干轮) */
history: AiMessage[]
/** 当前用户输入 */
prompt: string
/** 流式增量回调 */
onDelta: (delta: string) => void
/** 错误回调,会拿到服务端返回的 message 字段 */
onError?: (message: string) => void
/** chatMode = 'bot' 时必传 */
botId?: string
/** chatMode = 'bot' 时必传 */
threadId?: string
/** chatMode = 'model' 时必传 */
modelProvider?: string
/** chatMode = 'model' 时必传 */
quickResponseModel?: string
}
export async function streamMessage(options: SendMessageOptions): Promise<string> {
if (options.chatMode === 'model') {
return streamModel(options)
}
return streamBot(options)
}
function isValidHistoryMessage(message: AiMessage) {
return !message.error
&& (message.role === 'user' || message.role === 'assistant')
&& message.content.trim().length > 0
}
// ---------- bot 模式wx.cloud.extend.AI.bot.sendMessage ----------
async function streamBot(options: SendMessageOptions): Promise<string> {
const { botId, threadId, history, prompt, onDelta, onError } = options
if (!botId || !threadId) {
throw new Error('chatMode=bot 时必须提供 botId 与 threadId')
}
const messages = [
...history
.filter(isValidHistoryMessage)
.map(m => ({ id: m.id, role: m.role, content: m.content })),
{
id: generateMessageId(),
role: 'user',
content: prompt,
},
]
// #ifdef MP-WEIXIN
// @ts-expect-error wx.cloud.extend 由微信小程序基础库注入
const res = await wx.cloud.extend.AI.bot.sendMessage({
data: {
botId,
threadId,
runId: `run_id_${generateMessageId()}`,
messages,
tools: [],
context: [],
state: {},
forwardedProps: {},
},
})
let response = ''
for await (const event of res.eventStream) {
let data: BotEventData
try {
data = JSON.parse(event.data) as BotEventData
}
catch {
continue
}
switch (data.type) {
case 'TEXT_MESSAGE_CONTENT':
if (data.delta) {
response += data.delta
onDelta(data.delta)
}
break
case 'RUN_ERROR':
onError?.(data.message || '请求出错,请稍后再试')
throw new Error(data.message || 'RUN_ERROR')
case 'RUN_FINISHED':
break
}
}
return response
// #endif
// #ifndef MP-WEIXIN
throw new Error('AI 助手仅在微信小程序内可用')
// #endif
}
// ---------- model 模式wx.cloud.extend.AI.createModel().streamText ----------
async function streamModel(options: SendMessageOptions): Promise<string> {
const { modelProvider, quickResponseModel, history, prompt, onDelta, onError } = options
if (!modelProvider || !quickResponseModel) {
throw new Error('chatMode=model 时必须提供 modelProvider 与 quickResponseModel')
}
const messages = [
...history
.filter(isValidHistoryMessage)
.map(m => ({ role: m.role, content: m.content })),
{ role: 'user', content: prompt },
]
// #ifdef MP-WEIXIN
const ai = wx.cloud.extend.AI
const aiModel = ai.createModel(modelProvider)
console.log('aimodel创建', aiModel)
const res = await aiModel.streamText({
data: {
model: quickResponseModel,
messages,
},
})
let response = ''
try {
for await (const event of res.eventStream) {
let data: ModelEventData
try {
data = JSON.parse(event.data) as ModelEventData
}
catch {
continue
}
const choice = data.choices?.[0]
if (choice?.finish_reason === 'stop') {
break
}
const content = choice?.delta?.content
if (content) {
response += content
onDelta(content)
}
}
return response
}
catch (err) {
const message = err instanceof Error ? err.message : '请求出错,请稍后再试'
onError?.(message)
throw err
}
// #endif
// #ifndef MP-WEIXIN
throw new Error('AI 助手仅在微信小程序内可用')
// #endif
}
// ---------- 追问建议(仅 bot 模式且后端支持时使用) ----------
interface RecommendOptions {
botId: string
/** 最近一对 user/assistant 消息,用于上下文 */
lastPair: { role: 'user' | 'assistant', content: string }[]
/** 触发追问的用户原问题 */
prompt: string
/** 最多返回多少条 */
max?: number
/** 流式返回时增量更新(用来做"边收边渲染"),可选 */
onProgress?: (questions: string[]) => void
}
/**
* wx.cloud.extend.AI.bot.getRecommendQuestions
* bot-xxx agent-xxx 404
*/
export async function fetchRecommendQuestions(options: RecommendOptions): Promise<string[]> {
const { botId, lastPair, prompt, max = 3, onProgress } = options
// #ifdef MP-WEIXIN
// @ts-expect-error wx.cloud.extend 由微信小程序基础库注入
const res = await wx.cloud.extend.AI.bot.getRecommendQuestions({
data: {
botId,
history: lastPair
.filter(item => item.content.trim().length > 0)
.map(item => ({ role: item.role, content: item.content })),
msg: prompt,
agentSetting: '',
introduction: '',
name: '',
},
})
let buffer = ''
for await (const chunk of res.textStream) {
buffer += chunk
const questions = buffer.split('\n').map(s => s.trim()).filter(Boolean).slice(0, max)
onProgress?.(questions)
}
return buffer.split('\n').map(s => s.trim()).filter(Boolean).slice(0, max)
// #endif
// #ifndef MP-WEIXIN
return []
// #endif
}

View File

@ -1,14 +1,13 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useLogin } from '@/pages-fg/hooks/useUserInfo'
import { import {
getSessionKey, getSessionKey,
getWxUserInfo, getWxUserInfo,
setWxInfo, setWxInfo,
} from '@/service/' } from '@/service/'
import { useTokenStore, useUserStore } from '@/store'
import Checkbox from '../components/check-group/Checkbox.vue' import Checkbox from '../components/check-group/Checkbox.vue'
import CheckboxGroup from '../components/check-group/CheckboxGroup.vue' import CheckboxGroup from '../components/check-group/CheckboxGroup.vue'
import { useLogin } from '@/pages-fg/hooks/useUserInfo'
import { useTokenStore, useUserStore } from '@/store'
const checked = ref([]) // const checked = ref([]) //
const getPhoneInfo = ref(null) const getPhoneInfo = ref(null)
@ -16,8 +15,8 @@ const getPhoneInfo = ref(null)
const tokenStore = useTokenStore() const tokenStore = useTokenStore()
const userStore = useUserStore() const userStore = useUserStore()
const getPhoneNumber = async (e: any) => { async function getPhoneNumber(e: any) {
if (e.detail.errMsg == 'getPhoneNumber:ok') { if (e.detail.errMsg === 'getPhoneNumber:ok') {
const detail = e.detail const detail = e.detail
const _getPhoneInfo = { const _getPhoneInfo = {
iv: detail.iv, iv: detail.iv,
@ -26,12 +25,14 @@ const getPhoneNumber = async (e: any) => {
} }
getPhoneInfo.value = _getPhoneInfo getPhoneInfo.value = _getPhoneInfo
await getUserInfo(detail.code) await getUserInfo(detail.code)
} else if (e.detail.errMsg == 'getPhoneNumber:fail not login') { }
else if (e.detail.errMsg === 'getPhoneNumber:fail not login') {
uni.showToast({ uni.showToast({
title: '请先登录', title: '请先登录',
icon: 'none', icon: 'none',
}) })
} else { }
else {
uni.showToast({ uni.showToast({
title: '获取手机号失败', title: '获取手机号失败',
icon: 'none', icon: 'none',
@ -39,7 +40,7 @@ const getPhoneNumber = async (e: any) => {
} }
} }
const handleClick = () => { function handleClick() {
if (!checked.value) { if (!checked.value) {
uni.showToast({ uni.showToast({
title: '您需先同意《服务条款》和《隐私条款》', title: '您需先同意《服务条款》和《隐私条款》',
@ -60,49 +61,46 @@ const handleClick = () => {
} }
// //
const handleAuthReady = () => { function handleAuthReady() {
uni.navigateBack() uni.navigateBack()
} }
function handleClickUserAgreement() {
const handleClickUserAgreement = () => {
uni.navigateTo({ uni.navigateTo({
url: '/pages-fg/login/userAgreement', url: '/pages-fg/login/userAgreement',
}) })
} }
const handleClickPrivacyPolicy = () => { function handleClickPrivacyPolicy() {
uni.navigateTo({ uni.navigateTo({
url: '/pages-fg/login/privacyPolicy', url: '/pages-fg/login/privacyPolicy',
}) })
} }
async function getUserInfo(_code: string) {
const userInfo = (await useLogin()) as { code: string, errMsg: string }
if (userInfo.errMsg === 'login:ok') {
const getUserInfo = async (_code: string) => {
const userInfo = (await useLogin()) as { code: string; errMsg: string }
if (userInfo.errMsg == 'login:ok') {
const resp = await getSessionKey({ query: { JsCode: userInfo.code } }) const resp = await getSessionKey({ query: { JsCode: userInfo.code } })
if(resp.code !== 200){ if (resp.code !== 200) {
uni.showModal({title:'登陆失败',content:resp.message}) uni.showModal({ title: '登陆失败', content: resp.message })
uni.navigateBack(); uni.navigateBack()
} }
tokenStore.setTokenInfo({ token: resp.result.accessToken, expiresIn: 7 * 24 * 60 * 60 }) tokenStore.setTokenInfo({ token: resp.result.accessToken, expiresIn: 7 * 24 * 60 * 60 })
userStore.setUserOpenId(resp.result.openId) userStore.setUserOpenId(resp.result.openId)
setWxInfo({options:{query:{code:_code, openId:resp.result.openId}}}) setWxInfo({ options: { query: { code: _code, openId: resp.result.openId } } })
getWxUserInfo().then(resp => { getWxUserInfo().then((resp) => {
if(resp.code == 200){ if (resp.code === 200) {
userStore.setUserExtend(resp.result.userExtend) userStore.setUserExtend(resp.result.userExtend)
userStore.setUserInfo({nickName:resp.result.nickName,mobile:resp.result.mobile}) userStore.setUserInfo({ nickName: resp.result.nickName, mobile: resp.result.mobile })
userStore.setSex(resp.result.sex) userStore.setSex(resp.result.sex)
userStore.setUserAvatar(resp.result.avatar) userStore.setUserAvatar(resp.result.avatar)
} }
}) })
uni.navigateBack() uni.navigateBack()
} else { }
else {
uni.showToast({ uni.showToast({
title: '您需先授权', title: '您需先授权',
icon: 'none', icon: 'none',
@ -113,35 +111,36 @@ const getUserInfo = async (_code: string) => {
<template> <template>
<view class="h-screen flex flex-col bg-white"> <view class="h-screen flex flex-col bg-white">
<view class="flex flex-col justify-center items-center flex-1 pb-safe mt-[-100px]"> <view class="mt-[-100px] flex flex-1 flex-col items-center justify-center pb-safe">
<view class="w-[424rpx] h-[424rpx]"> <view class="h-[424rpx] w-[424rpx]">
<image class="w-[424rpx] h-[424rpx]" src="https://lwzk.ycymedu.com/img/home/logo.png" mode="aspectFit"> <image class="h-[424rpx] w-[424rpx]" src="https://lwzk.ycymedu.com/img/home/logo.png" mode="aspectFit" />
</image>
</view> </view>
<!-- #ifdef MP-WEIXIN --> <!-- #ifdef MP-WEIXIN -->
<button <button
class="w-[493rpx]! mb-[40rpx] h-[88rpx]! rounded-[44rpx] text-[32rpx] text-white flex items-center justify-center " class="mb-[40rpx] flex items-center justify-center rounded-[44rpx] text-[32rpx] text-white h-[88rpx]! w-[493rpx]!"
:class="checked.length > 0 ? 'bg-[#1580FF]' : 'bg-[#BFBFBF]'" @click.stop="handleClick" :class="checked.length > 0 ? 'bg-[#1580FF]' : 'bg-[#BFBFBF]'" open-type="getPhoneNumber"
open-type="getPhoneNumber" @getphonenumber="getPhoneNumber" :disabled="checked.length === 0"> :disabled="checked.length === 0" @click.stop="handleClick" @getphonenumber="getPhoneNumber"
>
一键登录 一键登录
</button> </button>
<!-- #endif --> <!-- #endif -->
<!-- #ifdef MP-ALIPAY --> <!-- #ifdef MP-ALIPAY -->
<button <button
class="w-[493rpx]! mb-[40rpx] h-[88rpx]! rounded-[44rpx] text-[32rpx] text-white flex items-center justify-center " class="mb-[40rpx] flex items-center justify-center rounded-[44rpx] text-[32rpx] text-white h-[88rpx]! w-[493rpx]!"
:class="checked.length > 0 ? 'bg-[#1580FF]' : 'bg-[#BFBFBF]'" @click.stop="handleClick"> :class="checked.length > 0 ? 'bg-[#1580FF]' : 'bg-[#BFBFBF]'" @click.stop="handleClick"
>
一键登录 一键登录
</button> </button>
<!-- #endif --> <!-- #endif -->
<view class="flex items-center flex-nowrap"> <view class="flex flex-nowrap items-center">
<CheckboxGroup v-model="checked" class="check-class mr-[10rpx]"> <CheckboxGroup v-model="checked" class="check-class mr-[10rpx]">
<Checkbox name="1" cell shape="button" class="custom-checkbox"></Checkbox> <Checkbox name="1" cell shape="button" class="custom-checkbox" />
</CheckboxGroup> </CheckboxGroup>
<view class="flex items-center"> <view class="flex items-center">
<text class="text-[24rpx] whitespace-nowrap"> <text class="whitespace-nowrap text-[24rpx]">
已阅读并同意 已阅读并同意
<text class="text-[#1580FF]" @click.stop="handleClickUserAgreement"> <text class="text-[#1580FF]" @click.stop="handleClickUserAgreement">
<text>用户协议</text> <text>用户协议</text>

View File

@ -1,53 +1,50 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useUserStore,useTokenStore } from '@/store'
import { getAssistant } from '@/service' import { getAssistant } from '@/service'
import { useTokenStore, useUserStore } from '@/store'
definePage({ definePage({
style: { style: {
navigationBarTitleText: '六纬AI小助手', navigationBarTitleText: '六纬AI小助手',
}, },
excludeLoginPath: true, excludeLoginPath: true,
}) })
const userStore = useUserStore() const userStore = useUserStore()
const tokenStore = useTokenStore() const tokenStore = useTokenStore()
// chat.ycymedu.com
//chat.ycymedu.com // chatv2.ycymedu.com
//chatv2.ycymedu.com
const url = ref(``) const url = ref(``)
const handleChildMessage = (event) => { function handleChildMessage(event) {
console.log('子应用传递的消息', event) console.log('子应用传递的消息', event)
} }
onLoad((options) => { onLoad((options) => {
getAssistant().then((res) => { getAssistant().then((res) => {
if (res.code === 200) { if (res.code === 200) {
const data = res.result as unknown as string const data = res.result as unknown as string
url.value = `${data}?userId=${userStore.userInfo.userExtend.wxId}&token=${tokenStore.validToken}&timestamp=${new Date().getTime()}` url.value = `${data}?userId=${userStore.userInfo.userExtend.wxId}&token=${tokenStore.validToken}&timestamp=${new Date().getTime()}`
} }
if (options.id) { if (options.id) {
url.value += `&reportId=${options.id}` url.value += `&reportId=${options.id}`
} }
if (options.type) { if (options.type) {
url.value += `&reportType=${options.type}` url.value += `&reportType=${options.type}`
} }
if (options.fileId) { if (options.fileId) {
url.value += `&fileId=${options.fileId}` url.value += `&fileId=${options.fileId}`
} }
if (options.talentTypeId){ if (options.talentTypeId) {
url.value +=`&talentTypeId=${options.talentTypeId}` url.value += `&talentTypeId=${options.talentTypeId}`
} }
}) })
}) })
</script> </script>
<template> <template>
<web-view :src="url" @message="handleChildMessage" :update-title="false" /> <web-view :src="url" :update-title="false" @message="handleChildMessage" />
</template> </template>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@ -35,6 +35,10 @@ const props = defineProps({
readonly: { readonly: {
type: Boolean, type: Boolean,
default: false default: false
},
inputDirection: {
type: String as PropType<'text-left' | 'text-right' | 'text-center'>,
default: 'text-left'
} }
}) })
@ -52,7 +56,7 @@ const innerValue = computed({
<template> <template>
<view class="relative w-full" :class="rootClass"> <view class="relative w-full" :class="rootClass">
<input :type="type" v-model="innerValue" :placeholder="placeholder" confirm-type="done" :disabled="readonly" <input :type="type" v-model="innerValue" :placeholder="placeholder" confirm-type="done" :disabled="readonly"
placeholder-style="color:#C5C8D1;font-size:30rpx;text-align:left;" class="text-left" /> :placeholder-style="`color:#C5C8D1;font-size:30rpx;${inputDirection === 'text-right' ? 'text-align:right;' : inputDirection === 'text-center' ? 'text-align:center;' : 'text-align:left;'} `" />
<view class="absolute top-0 left-0 w-full h-full" v-if="readonly"></view> <view class="absolute top-0 left-0 w-full h-full" v-if="readonly"></view>
</view> </view>
</template> </template>

View File

@ -1,78 +1,89 @@
<script setup lang="ts"> <script setup lang="ts">
type RadioValue = string | number | null | undefined
type RadioOptionValue = string | number | null | undefined
type RadioOption = Record<string, RadioOptionValue>
const props = defineProps({ interface RadioProps {
value: { value?: RadioValue
type: [String,Number], options?: RadioOption[]
default: "" valueKey?: string
}, labelKey?: string
options: { customRootClass?: string
type: Array, customRootWidth?: string
default: () => [] customRootColsClass?: string
}, customItemClass?: string
valueKey: { defaultItemClass?: string
type: String, activeItemClass?: string
default: '' }
},
labelKey: {
type: String,
default: ''
},
customRootClass: { const props = withDefaults(defineProps<RadioProps>(), {
type: String, value: '',
default: "" options: () => [],
}, valueKey: '',
labelKey: '',
customRootWidth:{ customRootClass: '',
type: String, customRootWidth: '',
default: "" customRootColsClass: 'grid gap-[20rpx] grid-cols-3',
}, customItemClass: '',
defaultItemClass: 'bg-[#F3F4F8] border-[#F3F4F8]',
customRootColsClass:{ activeItemClass: 'bg-white text-[#1580FF] border-[#1580FF]',
type: String,
default: "grid gap-[20rpx] grid-cols-3"
},
customItemClass:{
type: String,
default: ""
},
defaultItemClass:{
type: String,
default: "bg-[#F3F4F8] border-[#F3F4F8]"
},
activeItemClass:{
type:String,
default: "bg-white text-[#1580FF] border-[#1580FF]"
}
}) })
const emits = defineEmits<{
'update:value': [value: RadioValue]
'update:label': [label: string]
'change': []
}>()
type ToggleFn = (value: RadioValue) => void
const getOptionValue = (option: RadioOption): RadioValue => option[props.valueKey]
function getOptionLabel(option: RadioOption): string {
const label = option[props.labelKey]
return label == null ? '' : String(label)
}
function getOptionKey(option: RadioOption, index: number): string | number {
const value = getOptionValue(option)
return value ?? index
}
const innerValue = computed({ const innerValue = computed({
get: () => props.value, get: () => props.value,
set: (val) => emits("update:value",val) set: val => emits('update:value', val),
}) })
const emits = defineEmits(["update:value","update:label","change"])
const handleToggle = (val:any) => { function handleToggle(val: RadioValue) {
const chooseItem = props.options.filter(item => item[props.valueKey] === val) const chooseItem = props.options.find(item => getOptionValue(item) === val)
emits("update:value",val)
emits("update:label",chooseItem[0][props.labelKey]) emits('update:value', val)
emits("change") emits('update:label', chooseItem ? getOptionLabel(chooseItem) : '')
emits('change')
}
function handleOptionClick(option: RadioOption, toggle: ToggleFn) {
const value = getOptionValue(option)
toggle(value)
handleToggle(value)
} }
</script> </script>
<template> <template>
<sar-radio-group v-model="innerValue"> <sar-radio-group v-model="innerValue">
<template #custom="{ toggle, value }"> <template #custom="{ toggle, value: currentValue }">
<view :class="`bg-white rounded-[0_0_12rpx_12rpx] ${customRootClass} ${customRootColsClass}`"> <view :class="`bg-white rounded-[0_0_12rpx_12rpx] ${customRootClass} ${customRootColsClass}`">
<view v-for="val in options" :key="val[valueKey]" <view
:class="`${val[valueKey] === value ? activeItemClass : defaultItemClass} ${customItemClass} rounded-[12rpx] text-[28rpx]`" v-for="(option, index) in options" :key="getOptionKey(option, index)"
@click="() => {toggle(val[valueKey]); handleToggle(val[valueKey])}"> :class="`${getOptionValue(option) === currentValue ? activeItemClass : defaultItemClass} ${customItemClass} rounded-[12rpx] text-[28rpx]`"
{{ val[labelKey] }} @click="handleOptionClick(option, toggle)"
</view> >
</view> {{ getOptionLabel(option) }}
</template> </view>
</sar-radio-group> </view>
</template> </template>
</sar-radio-group>
</template>

View File

@ -1,17 +1,18 @@
<template> <template>
<view class="flex items-center bg-gray-100 rounded-full px-[30rpx] py-[20rpx]" :class="rootClass" :style="rootStyle"> <view class="flex items-center bg-gray-100 rounded-full" :class="rootClass" :style="rootStyle">
<view class="w-[36rpx] h-[36rpx] mr-2 flex items-center"> <view class="w-[32rpx] h-[32rpx] mr-2 flex items-center">
<image :src="searchIconUrl" class="w-[36rpx] h-[36rpx]" mode="aspectFit" /> <image :src="searchIconUrl" class="w-[32rpx] h-[32rpx]" mode="aspectFit" />
</view> </view>
<input :value="searchInnerText" type="text" :placeholder="placeholder" <input :value="searchInnerText" :type="inputType" :placeholder="placeholder"
class="flex-1 bg-transparent text-sm text-gray-800 outline-none text-start" :class="inputClass" @input="handleInput" @confirm="handleChange" @blur="handleChange"/> class="flex-1 bg-transparent text-sm text-gray-800 outline-none text-start" :class="inputClass" @input="handleInput" @confirm="handleChange" @blur="handleChange"/>
<view class="w-5 h-5 ml-2 flex items-center" v-if="clearIcon"> <view class="w-5 h-5 ml-2 flex items-center" v-if="clearIcon">
<image v-if="searchInnerText" src="" class="w-5 h-5" mode="aspectFit" @tap="clearInput" /> <image v-if="searchInnerText" src="" class="w-5 h-5" mode="aspectFit" @tap="clearInput" />
</view> </view>
<view v-if="searchIcon" class="text-[30rpx] px-[48rpx] py-[12rpx] bg-[#1580FF] text-[#fff] rounded-full" @click="handleSubmit"></view>
</view> </view>
</template> </template>
@ -44,9 +45,17 @@ const props = defineProps({
type:String, type:String,
default:'' default:''
}, },
inputType:{
type:String,
default:'text'
},
clearIcon:{ clearIcon:{
type:Boolean, type:Boolean,
default: true default: true
},
searchIcon:{
type:Boolean,
default: false
} }
}); });
@ -76,4 +85,8 @@ const handleChange = (event) => {
emit('update:searchText', event.detail.value); emit('update:searchText', event.detail.value);
emit("complete",event.detail.value) emit("complete",event.detail.value)
} }
const handleSubmit = () => {
emit("submit",searchInnerText.value)
}
</script> </script>

View File

@ -1,37 +1,40 @@
<script lang="ts" setup> <script lang="ts" setup>
// code here
import RequestComp from './components/request.vue'
definePage({ definePage({
style: { style: {
navigationBarTitleText: '分包页面', navigationBarTitleText: '分包页面',
}, },
}) })
function gotoScroll() { function navigateToVideoFn() {
uni.navigateTo({ uni.openChannelsLive({
url: '/pages-sub/demo/scroll', finderUserName: 'sphju9MCfZetYHP',
success: () => {
},
fail: (err) => {
console.error('跳转失败:', err)
},
}) })
} }
onLoad(() => {
uni.getChannelsLiveNoticeInfo({
finderUserName: 'sphju9MCfZetYHP',
success: (res) => {
console.log('res', res)
},
fail: (res) => {
console.log(res)
},
})
})
</script> </script>
<template> <template>
<view class="text-center"> <view class="text-center">
<view class="m-8"> <button @click="navigateToVideoFn">
http://localhost:9000/#/pages-sub/demo/index 点击跳视频号
</view>
<view class="my-4 text-green-500">
分包页面demo
</view>
<view class="text-blue-500">
分包页面里面的components示例
</view>
<button class="my-4" type="primary" size="mini" @click="gotoScroll">
跳转到上拉刷新和下拉加载更多
</button> </button>
<view>
<RequestComp />
</view>
</view> </view>
</template> </template>

View File

@ -0,0 +1,131 @@
<script lang="ts" setup>
import { systemInfo } from '@/utils/systemInfo'
import MxRadioGroup from "@/pages-sub/components/radio/index.vue?async"
import { getAreaList, getHistoryYearList, getBatchLine } from "@/service"
// #ifdef MP-WEIXIN
definePage({
style: {
navigationStyle: 'custom',
},
excludeLoginPath: false,
})
// #endif
// #ifndef MP-WEIXIN
definePage({
style: {
navigationStyle: 'custom',
transparentTitle: 'always',
navigationBarTitleText: ''
},
excludeLoginPath: false,
})
// #endif
const handleBack = () => {
uni.navigateBack({ delta: 1 })
}
const areaList = ref<any[]>([])
const searchParams = ref({
keyword: '',
region: null,
nature: null,
natureLabel: "",
year: null
})
const visibleFlag1 = ref(false)
const visibleFlag3 = ref(false)
const partialData = ref<any[]>([])
const yearList = ref<any[]>([])
const handleChange = () => {
visibleFlag1.value = false;
visibleFlag3.value = false;
getBatchLine({
query: {
Area: searchParams.value.region,
Year: searchParams.value.year
}
}).then(resp => {
partialData.value = resp.result.items
})
}
onShow(() => {
Promise.all([
getAreaList().then(resp => {
if (resp.code === 200) {
areaList.value = [{ value: '', label: '不限' }, ...resp.result]
}
}),
getHistoryYearList().then(resp => {
if (resp.code === 200) {
yearList.value = [...resp.result]
searchParams.value.year = yearList.value[yearList.value.length - 1]?.value
}
})
]).then(() => {
handleChange()
})
})
</script>
<template>
<view class="gradient-custom flex flex-col h-screen">
<sar-navbar title="批次线" :show-back="true" @back="handleBack"
:root-style="{ '--sar-navbar-bg': `rgba(255, 255, 255, 0)`, 'padding-top': `${systemInfo?.statusBarHeight}px`, '--sar-navbar-item-color': 'black' }">
</sar-navbar>
<sar-dropdown
root-style="--sar-dropdown-placeholder-color:#666;--sar-dropdown-value-font-size:28rpx;--sar-dropdown-option-active-color:#1580FF;--sar-dropdown-box-shadow:0rpx 6rpx 8rpx 0rpx rgba(0,0,0,0.04);">
<sar-dropdown-item v-model:visible="visibleFlag1" :title="searchParams.region || '区域'">
<mx-radio-group v-model:value="searchParams.region" :options="areaList" label-key="label" value-key="value"
custom-root-class="px-[32rpx] pt-[30rpx] pb-[40rpx]"
active-item-class="bg-white text-[#1580FF] border-1 border-solid border-[#1580FF]!"
default-item-class="bg-[#F3F4F8] border-[#F3F4F8]"
custom-item-class="w-full py-[16rpx] text-center border-[1rpx] border-solid" @change="handleChange" />
</sar-dropdown-item>
<sar-dropdown-item v-model:visible="visibleFlag3" v-model="searchParams.year" :title="searchParams.year || '年份'">
<mx-radio-group v-model:value="searchParams.year" :options="yearList" label-key="label" value-key="value"
custom-root-class="px-[32rpx] pt-[30rpx] pb-[40rpx]"
custom-item-class="w-full py-[16rpx] text-center border-[1rpx] border-solid"
active-item-class="bg-white text-[#1580FF] border-1 border-solid border-[#1580FF]!"
default-item-class="bg-[#F3F4F8] border-[#F3F4F8]" @change="handleChange" />
</sar-dropdown-item>
</sar-dropdown>
<scroll-view :scroll-y="true" class="flex-1 bg-[#f8f8f8]">
<view class="p-[30rpx]">
<view class="flex justify-between gap-[40rpx] bg-white rounded-[16rpx] mb-[20rpx] p-[30rpx]"
v-for="(val, index) in partialData" :key="index">
<view class="max-w-[502rpx]">
<view class="text-[30rpx] text-wrap">{{ val.batch }}</view>
<view class="flex flex-wrap gap-[10rpx] mt-[10rpx]">
<view class="rounded-[8rpx] bg-[#F8F8F8] px-[10rpx] py-[4rpx] text-[24rpx] text-[#666] w-max">{{
val.area }}</view>
<view class="rounded-[8rpx] bg-[#F8F8F8] px-[10rpx] py-[4rpx] text-[24rpx] text-[#666] w-max">{{ val.year
}}</view>
</view>
</view>
<view class="flex text-[#1E40AF] items-center min-w-[100rpx]">
<view class="text-[40rpx] font-[DinBold]">
{{ val.score }}
</view>
<view class="text-[24rpx]"></view>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<style lang="scss" scoped></style>

View File

@ -20,7 +20,7 @@ const userStore = useUserStore();
const { userInfo } = storeToRefs(userStore) const { userInfo } = storeToRefs(userStore)
const isStar = ref(false) const isStar = ref(false)
const handlePreviewImage = (src: string, index: number) => { const handlePreviewImage = (src: string, index: number|string) => {
uni.previewImage({ uni.previewImage({
urls: props.schoolDetail.imageList, urls: props.schoolDetail.imageList,
current: index, current: index,
@ -44,7 +44,7 @@ const starSchool = () => {
onLoad(() => { onLoad(() => {
getSchoolCollection().then((resp) => { getSchoolCollection().then((resp) => {
if (resp.code === 200) { if (resp.code === 200) {
isStar.value = resp.result.some(school => school.id === props.schoolDetail.id) isStar.value = resp.result.some((school:any) => school.id === props.schoolDetail.id)
} }
}) })
}) })
@ -57,9 +57,16 @@ onLoad(() => {
<view class="flex flex-col"> <view class="flex flex-col">
<text class="font-600 text-[40rpx] text-[#000]">{{ schoolDetail.schoolName }}</text> <text class="font-600 text-[40rpx] text-[#000]">{{ schoolDetail.schoolName }}</text>
<view class="flex items-center gap-[10rpx] my-[14rpx] flex-wrap" v-if="schoolDetail.tags"> <view class="flex items-center gap-[10rpx] my-[14rpx] flex-wrap" v-if="schoolDetail.tags">
<view class="rounded-[8rpx] bg-[#E03C331A] px-[10rpx] py-[4rpx] text-[24rpx] text-[#E03C33] flex items-center gap-[4rpx]" v-if="schoolDetail.schoolName === '济南市深泉外国语学校'">
<image
src="https://lw-zk.oss-cn-hangzhou.aliyuncs.com/img/home/xuexiao_remen.png"
mode="widthFix"
class="w-[24rpx] h-[24rpx]"
/>
{{ new Date().getFullYear() - 1 }}招生计划完成率100%</view>
<view <view
class="rounded-[8rpx] bg-[#F8F8F8] px-[10rpx] py-[4rpx] text-[24rpx] text-[#666] first:border-solid border-red border-[1rpx]" class="bg-[#F8F8F8] px-[10rpx] py-[4rpx] text-[24rpx] text-[#666]"
v-for="feature in schoolDetail.tags.split('、')" :key="feature">{{ feature }}</view> v-for="feature in schoolDetail.tags.split(/[\s,,、]+/)" :key="feature">{{ feature }}</view>
</view> </view>
<view class="text-[#303030] text-[24rpx] mb-[10rpx]">{{ schoolDetail.region }}·{{ <view class="text-[#303030] text-[24rpx] mb-[10rpx]">{{ schoolDetail.region }}·{{
schoolDetail.schoolNature }}</view> schoolDetail.schoolNature }}</view>
@ -72,7 +79,7 @@ onLoad(() => {
收藏 收藏
</view> </view>
</view> </view>
<swiper class="basis-full h-[126rpx]" circular :autoplay="true" :indicator="false" v-if="schoolDetail.imageList" <swiper class="basis-full h-[126rpx] grid grid-cols-3" circular :autoplay="true" :indicator="false" v-if="schoolDetail.imageList"
:display-multiple-items="schoolDetail.imageList.length > 2 ? 3 : 1"> :display-multiple-items="schoolDetail.imageList.length > 2 ? 3 : 1">
<swiper-item v-for="(item, index) in schoolDetail.imageList" :key="item" class="flex justify-center"> <swiper-item v-for="(item, index) in schoolDetail.imageList" :key="item" class="flex justify-center">
<image :src="item" mode="scaleToFill" class="w-full h-full mx-[4rpx] rounded-[8rpx]" <image :src="item" mode="scaleToFill" class="w-full h-full mx-[4rpx] rounded-[8rpx]"

View File

@ -23,10 +23,10 @@ definePage({
excludeLoginPath: false, excludeLoginPath: false,
}) })
// #endif // #endif
const regions = ref([]) const regions = ref<any[]>([])
const natureList = ref([]) const natureList = ref<any[]>([])
const schoolTypeList = ref([]) const schoolTypeList = ref<any[]>([])
const searchParams = ref({ const searchParams = ref({
region: null, region: null,
@ -68,8 +68,8 @@ onLoad(() => {
}) })
}) })
const paging = ref(null) const paging = ref<any>(null)
const schoolList = ref([]) const schoolList = ref<any[]>([])
const queryList = (page: number, pageSize: number) => { const queryList = (page: number, pageSize: number) => {
getBusHightSchoolList({ getBusHightSchoolList({
data: { data: {
@ -86,7 +86,7 @@ const queryList = (page: number, pageSize: number) => {
} }
}) })
} }
const virtualListChange = (_vList) => { const virtualListChange = (_vList:any) => {
schoolList.value = _vList schoolList.value = _vList
} }
@ -109,7 +109,7 @@ const handleComplete = () => {
:root-style="{ '--sar-navbar-bg': `rgba(255, 255, 255, 0)`, '--sar-navbar-item-color': 'black', '--sar-navbar-title-max-width': '100%' }"> :root-style="{ '--sar-navbar-bg': `rgba(255, 255, 255, 0)`, '--sar-navbar-item-color': 'black', '--sar-navbar-title-max-width': '100%' }">
<template #title> <template #title>
<view class="w-[448rpx] ml-[90rpx]"> <view class="w-[448rpx] ml-[90rpx]">
<mx-search v-model:searchText="searchParams.keyword" placeholder="输入高中名称" @complete="handleComplete"/> <mx-search root-class="pl-[30rpx] py-[20rpx]" v-model:searchText="searchParams.keyword" placeholder="输入高中名称" @complete="handleComplete"/>
</view> </view>
</template> </template>
</sar-navbar> </sar-navbar>
@ -146,9 +146,16 @@ const handleComplete = () => {
<view v-for="(val, index) in schoolList" :key="index" class="py-[30rpx] border-b-[#ededed] border-1 border-b-solid" <view v-for="(val, index) in schoolList" :key="index" class="py-[30rpx] border-b-[#ededed] border-1 border-b-solid"
@click="navigateToDetail(val.id)"> @click="navigateToDetail(val.id)">
<view class="text-[#000] text-[32rpx] font-600">{{ val.schoolName }}</view> <view class="text-[#000] text-[32rpx] font-600">{{ val.schoolName }}</view>
<view class="flex items-center gap-[12rpx] my-[10rpx]"> <view class="flex items-center flex-wrap gap-[12rpx] my-[10rpx]">
<view class="rounded-[8rpx] bg-[#E03C331A] px-[10rpx] py-[4rpx] text-[24rpx] text-[#E03C33] flex items-center gap-[4rpx]" v-if="val.schoolName === '济南市深泉外国语学校'">
<image
src="https://lw-zk.oss-cn-hangzhou.aliyuncs.com/img/home/xuexiao_remen.png"
mode="widthFix"
class="w-[24rpx] h-[24rpx]"
/>
{{ new Date().getFullYear() - 1 }}招生计划完成率100%</view>
<view class="rounded-[8rpx] bg-[#F8F8F8] px-[10rpx] py-[4rpx] text-[24rpx] text-[#666]" <view class="rounded-[8rpx] bg-[#F8F8F8] px-[10rpx] py-[4rpx] text-[24rpx] text-[#666]"
v-for="feature in val.tags.split('、').slice(0, 2)" :key="feature">{{ feature }}</view> v-for="feature in val.tags.split(/[\s,,、]+/).slice(0, 2)" :key="feature">{{ feature }}</view>
</view> </view>
<view class="text-[#666] text-[24rpx]">{{ val.region }}·{{val.schoolNature }}</view> <view class="text-[#666] text-[24rpx]">{{ val.region }}·{{val.schoolNature }}</view>
</view> </view>

View File

@ -1,10 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import HighSchoolDetailHeader from './components/HighSchoolDetailHeader.vue' import HighSchoolDetailHeader from './components/HighSchoolDetailHeader.vue'
import MxTabs from "@/pages-sub/components/tabs/index.vue"
import { systemInfo } from '@/utils/systemInfo' import { systemInfo } from '@/utils/systemInfo'
import SchoolIntroduce from './components/SchoolIntroduce.vue' import SchoolIntroduce from './components/SchoolIntroduce.vue'
import EnrollmentIntroDetail from './components/EnrollmentIntroDetail.vue'
import QuotaAndScore from './components/QuotaAndScore.vue'
import { getSchoolInfo } from '@/service' import { getSchoolInfo } from '@/service'
// #ifdef MP-WEIXIN // #ifdef MP-WEIXIN
@ -32,7 +29,7 @@ const handleBack = () => {
} }
const tabs = [{ name: '院校简介' }, { name: "招生简章" }, { name: '名额分数' }] const tabs = [{ name: '院校简介' },]
const activeIndex = ref(0) const activeIndex = ref(0)
const handleChange = (val: number) => { const handleChange = (val: number) => {
activeIndex.value = val activeIndex.value = val
@ -41,7 +38,7 @@ const handleChange = (val: number) => {
const schoolDetail = ref({}) const schoolDetail = ref({})
onLoad((options) => { onLoad((options) => {
if(options.id){ if(options?.id){
getSchoolInfo({query:{id:options.id}}).then(resp => { getSchoolInfo({query:{id:options.id}}).then(resp => {
if(resp.code == 200){ if(resp.code == 200){
schoolDetail.value = resp.result schoolDetail.value = resp.result
@ -59,13 +56,9 @@ onLoad((options) => {
:root-style="{ '--sar-navbar-bg': `rgba(255, 255, 255, 0)`, 'padding-top': `${systemInfo?.statusBarHeight}px`, '--sar-navbar-item-color': 'black' }"> :root-style="{ '--sar-navbar-bg': `rgba(255, 255, 255, 0)`, 'padding-top': `${systemInfo?.statusBarHeight}px`, '--sar-navbar-item-color': 'black' }">
</sar-navbar> </sar-navbar>
<HighSchoolDetailHeader :schoolDetail="schoolDetail"/> <HighSchoolDetailHeader :schoolDetail="schoolDetail"/>
<mx-tabs :tabsList="tabs" @tab-change="handleChange" />
<view class="bg-[#f8f8f8] h-[20rpx]"></view> <view class="bg-[#f8f8f8] h-[20rpx]"></view>
<SchoolIntroduce :schoolDetail="schoolDetail" v-if="activeIndex === 0" /> <SchoolIntroduce :schoolDetail="schoolDetail" v-if="activeIndex === 0" />
<EnrollmentIntroDetail :schoolDetail="schoolDetail" v-if="activeIndex === 1" />
<QuotaAndScore :schoolDetail="schoolDetail" v-if="activeIndex === 2" />
</view> </view>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped></style>
//</style>

View File

@ -1,39 +1,47 @@
<script lang="ts" setup> <script lang="ts" setup>
import { getNewsDetail } from "@/service" import { getNewsDetail } from '@/service'
const newsDetail = ref({ title: '', publishTime: "", remark: "",content:"" }) const newsDetail = ref({ title: '', publishTime: '', remark: '', content: '' })
onLoad(options => { onLoad((options) => {
if (options.id) { if (options.id) {
getNewsDetail({ query: { id: options.id } }).then(resp => { getNewsDetail({ query: { id: options.id } }).then((resp) => {
if (resp.code == 200) { if (resp.code === 200) {
newsDetail.value = resp.result newsDetail.value = resp.result
} }
}) })
} else { }
uni.showModal({ title: "没有获取到新闻ID" }) else {
} uni.showModal({ title: '没有获取到新闻ID' })
}
}) })
const navigateToCustom = () => { function navigateToCustom() {
uni.navigateTo({url:"/pages-sub/about/onlineCustom"}) uni.navigateTo({ url: '/pages-sub/about/onlineCustom' })
} }
</script> </script>
<template> <template>
<view class="pt-[30rpx] px-[30rpx] pb-safe"> <view class="px-[30rpx] pt-[30rpx] pb-safe">
<view class="text-[40rpx] font-600 text-[#000]">{{ newsDetail.title }}</view> <view class="text-[40rpx] text-[#000] font-600">
<view class="text-[#999] text-[28rpx] mt-[8rpx]">发布时间: {{ newsDetail.publishTime }}</view> {{ newsDetail.title }}
<view class="border-b-[2rpx] border-b-dashed border-b-[#D8D8D8] mt-[20rpx] mb-[30rpx]"></view>
<view class="pt-[30rpx]">
<view v-html="newsDetail.content"></view>
</view>
<view class="fixed right-[20rpx] bottom-[120rpx] w-[160rpx] h-[160rpx]" @click="navigateToCustom">
<image src="https://lwzk.ycymedu.com/img/qt/qt_more.png" mode="scaleToFill"
class="w-[160rpx] h-[160rpx]" />
</view>
</view> </view>
<view class="mt-[8rpx] text-[28rpx] text-[#999]">
发布时间: {{ newsDetail.publishTime }}
</view>
<view class="mb-[30rpx] mt-[20rpx] border-b-[2rpx] border-b-[#D8D8D8] border-b-dashed" />
<view class="pt-[30rpx]">
<!-- <view v-html="newsDetail.content" /> -->
<rich-text :nodes="newsDetail.content" />
</view>
<view class="fixed bottom-[120rpx] right-[20rpx] h-[160rpx] w-[160rpx]" @click="navigateToCustom">
<image
src="https://lwzk.ycymedu.com/img/qt/qt_more.png" mode="scaleToFill"
class="h-[160rpx] w-[160rpx]"
/>
</view>
</view>
</template> </template>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@ -24,8 +24,8 @@ definePage({
}) })
// #endif // #endif
const tabs = ref([]) const tabs = ref<any[]>([])
const articles = ref([]) const articles = ref<any[]>([])
const handleBack = () => { const handleBack = () => {
uni.navigateBack({ delta: 1 }) uni.navigateBack({ delta: 1 })
} }
@ -44,7 +44,7 @@ const toDetailPage = (id: number) => {
const searchParams = ref({ keyword: "", activeTab: '' }) const searchParams = ref({ keyword: "", activeTab: '' })
const paging = ref(null) const paging = ref<any>(null)
const queryList = (page: number, pageSize: number) => { const queryList = (page: number, pageSize: number) => {
if (searchParams.value.activeTab === "") { if (searchParams.value.activeTab === "") {
@ -69,7 +69,7 @@ const queryList = (page: number, pageSize: number) => {
} }
} }
const virtualListChange = (_vList) => { const virtualListChange = (_vList:any) => {
articles.value = _vList articles.value = _vList
} }
@ -87,7 +87,7 @@ const handleComplete = () => {
<sar-navbar title="中考资讯" :show-back="true" @back="handleBack" <sar-navbar title="中考资讯" :show-back="true" @back="handleBack"
:root-style="{ '--sar-navbar-bg': `rgba(255, 255, 255, 0)`, 'padding-top': `${systemInfo?.statusBarHeight}px`, '--sar-navbar-item-color': 'black' }"> :root-style="{ '--sar-navbar-bg': `rgba(255, 255, 255, 0)`, 'padding-top': `${systemInfo?.statusBarHeight}px`, '--sar-navbar-item-color': 'black' }">
</sar-navbar> </sar-navbar>
<mx-search v-model:searchText="searchParams.keyword" rootStyle="margin: 16rpx 30rpx 0;" @complete="handleComplete"/> <mx-search v-model:searchText="searchParams.keyword" rootStyle="margin: 16rpx 30rpx 0;" root-class="pl-[30rpx] py-[20rpx]" @complete="handleComplete"/>
<mx-tabs :tabsList="tabs" rootClass="shadow-md mb-[20rpx]" @tab-change="handleChange" /> <mx-tabs :tabsList="tabs" rootClass="shadow-md mb-[20rpx]" @tab-change="handleChange" />
</view> </view>
</template> </template>

View File

@ -1,8 +1,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import MxRadioGroup from '@/pages-sub/components/radio/index.vue?async'
import MxSearch from '@/pages-sub/components/search/index.vue?async'
import { getAreaList, getHistoryYearList, getSchoolHistoricalScores, getSchoolNature } from '@/service'
import { systemInfo } from '@/utils/systemInfo' import { systemInfo } from '@/utils/systemInfo'
import MxSearch from "@/pages-sub/components/search/index.vue?async"
import MxRadioGroup from "@/pages-sub/components/radio/index.vue?async"
import { getAreaList, getHistoryYearList, getSchoolHistoricalScores, getSchoolNature } from "@/service"
// #ifdef MP-WEIXIN // #ifdef MP-WEIXIN
definePage({ definePage({
@ -18,165 +18,211 @@ definePage({
style: { style: {
navigationStyle: 'custom', navigationStyle: 'custom',
transparentTitle: 'always', transparentTitle: 'always',
navigationBarTitleText: '' navigationBarTitleText: '',
}, },
excludeLoginPath: false, excludeLoginPath: false,
}) })
// #endif // #endif
const handleBack = () => { function handleBack() {
uni.navigateBack({ delta: 1 }) uni.navigateBack({ delta: 1 })
} }
const areaList = ref([]) const areaList = ref<any[]>([])
const natureList = ref([]) const natureList = ref<any[]>()
const searchParams = ref({ const searchParams = ref({
keyword: '', keyword: '',
region: null, region: null,
nature: null, nature: null,
natureLabel: "", natureLabel: '',
year: null year: null,
}) })
const partialColumns = [
{
title: '院校名称',
prop: 'schoolName',
width: '32%',
align: "left"
},
{
title: '所在区',
prop: 'region',
width: '16%',
align: "center"
},
{
title: '统招位次',
prop: 'ranking',
width: '18%',
align: "center"
},
{
title: '办学性质',
prop: 'schoolNature',
width: '18%',
align: "center"
},
{
title: '分数',
prop: 'admissionScore',
width: '15%',
align: "center"
},
] as const
const visibleFlag1 = ref(false) const visibleFlag1 = ref(false)
const visibleFlag2 = ref(false) const visibleFlag2 = ref(false)
const visibleFlag3 = ref(false) const visibleFlag3 = ref(false)
const partialData = ref([]) interface HistoricalScore {
const yearList = ref([]) schoolName?: string
region?: string
schoolNature?: string
tags?: string
ranking?: number | string | null
admissionScore?: number | string | null
}
interface PagingRef {
reload: () => void
complete: (data: HistoricalScore[] | false) => void
}
const paging = ref<PagingRef | null>(null)
const partialData = ref<HistoricalScore[]>([])
const yearList = ref<any[]>([])
function handleChange() {
visibleFlag1.value = false
visibleFlag2.value = false
visibleFlag3.value = false
paging.value?.reload()
}
function queryList(page: number, _pageSize: number) {
if (page > 1) {
paging.value?.complete([])
return
}
const handleChange = () => {
visibleFlag1.value = false;
visibleFlag2.value = false;
visibleFlag3.value = false;
getSchoolHistoricalScores({ getSchoolHistoricalScores({
query: { query: {
SchoolName: searchParams.value.keyword, SchoolName: searchParams.value.keyword,
Region: searchParams.value.region, Region: searchParams.value.region,
SchoolType: searchParams.value.nature, SchoolType: searchParams.value.nature,
Year: searchParams.value.year Year: searchParams.value.year,
},
}).then((resp) => {
if (resp.code === 200) {
paging.value?.complete(Array.isArray(resp.result) ? resp.result : [])
} }
}).then(resp => { else {
partialData.value = resp.result paging.value?.complete(false)
}
}).catch(() => {
paging.value?.complete(false)
}) })
} }
function virtualListChange(_vList: HistoricalScore[]) {
partialData.value = _vList
}
function getTagList(tags?: string) {
return (tags || '').split(/[\s,,、]+/).filter(Boolean).slice(0, 2)
}
onShow(() => { onShow(() => {
Promise.all([
getAreaList().then(resp => { getAreaList().then((resp) => {
if (resp.code === 200) { if (resp.code === 200) {
areaList.value = [{ value: '', label: '不限' }, ...resp.result] areaList.value = [{ value: '', label: '不限' }, ...resp.result]
} }
}),
getSchoolNature().then((resp) => {
if (resp.code === 200) {
natureList.value = [{ value: '', label: '不限' }, ...resp.result]
}
}),
getHistoryYearList().then((resp) => {
if (resp.code === 200) {
yearList.value = [...resp.result]
searchParams.value.year = yearList.value[yearList.value.length - 1]?.value
}
}),
]).then(() => {
handleChange()
}) })
getSchoolNature().then(resp => {
if (resp.code === 200) {
natureList.value = [{ value: '', label: '不限' }, ...resp.result]
}
})
getHistoryYearList().then(resp => {
if (resp.code === 200) {
yearList.value = [{ value: '', label: '不限' }, ...resp.result]
}
})
handleChange()
}) })
</script> </script>
<template> <template>
<view class="gradient-custom flex flex-col h-screen"> <z-paging
<sar-navbar title="历年分数" :show-back="true" @back="handleBack" ref="paging" use-virtual-list :force-close-inner-list="true" cell-height-mode="dynamic"
:root-style="{ '--sar-navbar-bg': `rgba(255, 255, 255, 0)`, 'padding-top': `${systemInfo?.statusBarHeight}px`, '--sar-navbar-item-color': 'black' }"> :auto="false" :auto-show-system-loading="false" :safe-area-inset-bottom="true" :paging-style="{ backgroundColor: '#f8f8f8' }"
</sar-navbar> @virtual-list-change="virtualListChange" @query="queryList"
<mx-search v-model:searchText="searchParams.keyword" rootStyle="margin: 16rpx 30rpx 0;" @complete="handleChange" /> >
<sar-dropdown <template #top>
root-style="--sar-dropdown-placeholder-color:#666;--sar-dropdown-value-font-size:28rpx;--sar-dropdown-option-active-color:#1580FF;--sar-dropdown-box-shadow:0rpx 6rpx 8rpx 0rpx rgba(0,0,0,0.04);"> <view class="gradient-custom" :style="{ 'padding-top': `${systemInfo?.statusBarHeight}px` }">
<sar-dropdown-item v-model:visible="visibleFlag1" :title="searchParams.region || '区域'"> <sar-navbar
<mx-radio-group v-model:value="searchParams.region" :options="areaList" label-key="label" value-key="value" title="历年分数" :show-back="true" :root-style="{ '--sar-navbar-bg': `rgba(255, 255, 255, 0)`, '--sar-navbar-item-color': 'black', '--sar-navbar-title-max-width': '100%' }"
custom-root-class="px-[32rpx] pt-[30rpx] pb-[40rpx]" @back="handleBack"
active-item-class="bg-white text-[#1580FF] border-1 border-solid border-[#1580FF]!" >
default-item-class="bg-[#F3F4F8] border-[#F3F4F8]" <template #title>
custom-item-class="w-full py-[16rpx] text-center border-[1rpx] border-solid" @change="handleChange" /> <view class="ml-[90rpx] w-[448rpx]">
</sar-dropdown-item> <mx-search v-model:search-text="searchParams.keyword" root-class="py-[20rpx] pl-[30rpx]" @complete="handleChange" />
<sar-dropdown-item v-model:visible="visibleFlag2" :title="searchParams.natureLabel || '办学性质'"> </view>
<mx-radio-group v-model:value="searchParams.nature" v-model:label="searchParams.natureLabel" </template>
:options="natureList" label-key="label" value-key="value" custom-root-class="px-[32rpx] pt-[30rpx] pb-[40rpx]" </sar-navbar>
active-item-class="bg-white text-[#1580FF] border-1 border-solid border-[#1580FF]!"
default-item-class="bg-[#F3F4F8] border-[#F3F4F8]"
custom-item-class="w-full py-[16rpx] text-center border-[1rpx] border-solid" @change="handleChange" />
</sar-dropdown-item>
<sar-dropdown-item v-model:visible="visibleFlag3" v-model="searchParams.year" :title="searchParams.year || '年份'">
<mx-radio-group v-model:value="searchParams.year" :options="yearList" label-key="label" value-key="value"
custom-root-class="px-[32rpx] pt-[30rpx] pb-[40rpx]"
custom-item-class="w-full py-[16rpx] text-center border-[1rpx] border-solid"
active-item-class="bg-white text-[#1580FF] border-1 border-solid border-[#1580FF]!"
default-item-class="bg-[#F3F4F8] border-[#F3F4F8]" @change="handleChange" />
</sar-dropdown-item>
</sar-dropdown>
<scroll-view :scroll-y="true" class="flex-1"> <sar-dropdown
<view class="p-[30rpx]"> root-style="--sar-dropdown-placeholder-color:#666;--sar-dropdown-value-font-size:28rpx;--sar-dropdown-option-active-color:#1580FF;--sar-dropdown-box-shadow:0rpx 6rpx 8rpx 0rpx rgba(0,0,0,0.04);"
<sar-table bordered> >
<sar-table-row> <sar-dropdown-item v-model:visible="visibleFlag1" :title="searchParams.region || '区域'">
<sar-table-cell v-for="item in partialColumns" :key="item.prop" bold :width="item.width"> <mx-radio-group
<view v-model:value="searchParams.region" :options="areaList" label-key="label" value-key="value"
:class="`${item.align === 'center' ? '' : 'pl-[20rpx]'} py-[13rpx] text-[#333] text-[24rpx] font-500 bg-[#F3F4F8]`" custom-root-class="px-[32rpx] pt-[30rpx] pb-[40rpx]"
:style="{ 'text-align': item.align }">{{ item.title }}</view> active-item-class="bg-white text-[#1580FF] border-1 border-solid border-[#1580FF]!"
</sar-table-cell> default-item-class="bg-[#F3F4F8] border-[#F3F4F8]"
</sar-table-row> custom-item-class="w-full py-[16rpx] text-center border-[1rpx] border-solid" @change="handleChange"
<sar-table-row v-for="record in partialData" :key="record.id"> />
<sar-table-cell v-for="item in partialColumns" :key="item.prop" :width="item.width"> </sar-dropdown-item>
<view :class="`${item.align === 'center' ? '' : 'pl-[20rpx]'} text-[24rpx] text-[#333] py-[13rpx]`" <sar-dropdown-item v-model:visible="visibleFlag2" :title="searchParams.natureLabel || '办学性质'">
:style="{ 'text-align': item.align }">{{ record[item.prop] }}</view> <mx-radio-group
</sar-table-cell> v-model:value="searchParams.nature" v-model:label="searchParams.natureLabel"
</sar-table-row> :options="natureList" label-key="label" value-key="value" custom-root-class="px-[32rpx] pt-[30rpx] pb-[40rpx]"
<sar-table-row v-if="partialData.length === 0"> active-item-class="bg-white text-[#1580FF] border-1 border-solid border-[#1580FF]!"
<sar-table-cell :colspan="partialColumns.length"> default-item-class="bg-[#F3F4F8] border-[#F3F4F8]"
<view class="text-center text-[24rpx] text-[#999] py-[20rpx]">暂无数据</view> custom-item-class="w-full py-[16rpx] text-center border-[1rpx] border-solid" @change="handleChange"
</sar-table-cell> />
</sar-table-row> </sar-dropdown-item>
</sar-table> <sar-dropdown-item v-model:visible="visibleFlag3" v-model="searchParams.year" :title="searchParams.year || '年份'">
<mx-radio-group
v-model:value="searchParams.year" :options="yearList" label-key="label" value-key="value"
custom-root-class="px-[32rpx] pt-[30rpx] pb-[40rpx]"
custom-item-class="w-full py-[16rpx] text-center border-[1rpx] border-solid"
active-item-class="bg-white text-[#1580FF] border-1 border-solid border-[#1580FF]!"
default-item-class="bg-[#F3F4F8] border-[#F3F4F8]" @change="handleChange"
/>
</sar-dropdown-item>
</sar-dropdown>
</view> </view>
</scroll-view> </template>
</view>
<view class="p-[30rpx]">
<view
v-for="(val, index) in partialData"
:key="index" class="mb-[20rpx] flex justify-between gap-[40rpx] rounded-[16rpx] bg-white p-[30rpx]"
>
<view class="">
<view class="max-w-[408rpx] text-wrap text-[30rpx]">
{{ val.schoolName }}
</view>
<view class="mt-[10rpx] flex flex-wrap gap-[10rpx]">
<view class="w-max px-[10rpx] py-[4rpx] text-[24rpx] text-[#666]">
{{
val.region }}·{{ val.schoolNature }}
</view>
<view
v-for="feature in getTagList(val.tags)"
:key="feature" class="rounded-[8rpx] bg-[#F8F8F8] px-[10rpx] py-[4rpx] text-[24rpx] text-[#666]"
>
{{ feature }}
</view>
</view>
</view>
<view class="flex items-center gap-[30rpx]">
<view class="flex flex-col items-end text-[#1E40AF]">
<view class="text-[40rpx] font-[DinBold]">
{{ val.ranking ?? '--' }}
</view>
<view class="mt-[10rpx] w-max text-[24rpx]">
位次
</view>
</view>
<view class="flex flex-col items-end text-[#1E40AF]">
<view class="text-[40rpx] font-[DinBold]">
{{ val.admissionScore ?? '--' }}
</view>
<view class="mt-[10rpx] w-max text-[24rpx]">
分数
</view>
</view>
</view>
</view>
</view>
</z-paging>
</template> </template>
<style lang="scss" scoped></style> <style lang="scss" scoped>
@import url('@/style/index.scss');
</style>

View File

@ -29,9 +29,9 @@ const handleBack = () => {
} }
const regionVisible = ref(false) const regionVisible = ref(false)
const regions = ref([]) const regions = ref<{label:string;value:string|number}[]>([])
const schools = ref([]) const schools = ref<{schoolName:string}[]>([])
const searchParams = ref({ const searchParams = ref({
keyword: "", keyword: "",
region: "", region: "",
@ -75,7 +75,7 @@ onLoad(() => {
</view> </view>
</view> </view>
<view class="flex-1 ml-[30rpx]"> <view class="flex-1 ml-[30rpx]">
<mx-search root-class="mr-[40rpx]" placeholder="输入学校名称" v-model:searchText="searchParams.keyword" <mx-search root-class="pl-[30rpx] py-[20rpx] mr-[40rpx]" placeholder="输入学校名称" v-model:searchText="searchParams.keyword"
@complete="handleChange" /> @complete="handleChange" />
</view> </view>
</view> </view>

View File

@ -0,0 +1,199 @@
<script lang="ts" setup>
import { systemInfo } from '@/utils/systemInfo'
import MxRadioGroup from "@/pages-sub/components/radio/index.vue?async"
import MxSearch from "@/pages-sub/components/search/index.vue?async"
import { getAreaList, getHistoryYearList, getRankTable } from "@/service"
import { useUserStore } from "@/store"
// #ifdef MP-WEIXIN
definePage({
style: {
navigationStyle: 'custom',
},
excludeLoginPath: true,
})
// #endif
// #ifndef MP-WEIXIN
definePage({
style: {
navigationStyle: 'custom',
transparentTitle: 'always',
navigationBarTitleText: ''
},
excludeLoginPath: true,
})
// #endif
const userStore = useUserStore()
const handleBack = () => {
uni.navigateBack({ delta: 1 })
}
const areaList = ref<any[]>([])
const searchParams = ref({
score: userStore.userInfo.userExtend.expectedScore || '',
region: userStore.userInfo.userExtend.area || null,
nature: null,
natureLabel: "",
year: null
})
const visibleFlag1 = ref(false)
const visibleFlag3 = ref(false)
const partialData = ref<any[]>([])
const yearList = ref<any[]>([])
const minScore = ref(0)
const maxScore = ref(0)
const exceedRatioText = ref("0%")
const estimatedRank = ref(0)
const nearbyItems = ref<any[]>([])
const areaName = ref("")
const handleChange = () => {
visibleFlag1.value = false;
visibleFlag3.value = false;
getRankTable({
query: {
Area: searchParams.value.region,
Year: searchParams.value.year,
Score: searchParams.value.score || 0
}
}).then(resp => {
partialData.value = resp.result.items
minScore.value = resp.result.minScore
maxScore.value = resp.result.maxScore
exceedRatioText.value = resp.result.exceedRatioText
estimatedRank.value = resp.result.estimatedRank
yearList.value = [...resp.result.years]
searchParams.value.year = yearList.value[yearList.value.length - 1]?.value
nearbyItems.value = [...resp.result.nearbyItems.reverse()]
areaName.value=resp.result.selectedArea
if (searchParams.value.score === '') {
searchParams.value.score = resp.result.minScore
handleChange();
}
})
}
const handleComplete = () => {
if(searchParams.value.score === '' || searchParams.value.score < minScore.value){
searchParams.value.score = minScore.value
} else if(searchParams.value.score > maxScore.value){
searchParams.value.score = maxScore.value
}
}
onShow(() => {
Promise.all([
getAreaList().then(resp => {
if (resp.code === 200) {
areaList.value = [...resp.result]
if(!searchParams.value.region){
searchParams.value.region = areaList.value[0]?.value || null
}
}
}),
]).then(() => {
handleChange()
})
})
</script>
<template>
<view class="gradient-custom flex flex-col h-screen">
<sar-navbar title="中考位次" :show-back="true" @back="handleBack"
:root-style="{ '--sar-navbar-bg': `rgba(255, 255, 255, 0)`, 'padding-top': `${systemInfo?.statusBarHeight}px`, '--sar-navbar-item-color': 'black' }">
</sar-navbar>
<sar-dropdown
root-style="--sar-dropdown-placeholder-color:#666;--sar-dropdown-value-font-size:28rpx;--sar-dropdown-option-active-color:#1580FF;--sar-dropdown-box-shadow:0rpx 6rpx 8rpx 0rpx rgba(0,0,0,0.04);">
<sar-dropdown-item v-model:visible="visibleFlag1" :title="searchParams.region || '区域'">
<mx-radio-group v-model:value="searchParams.region" :options="areaList" label-key="label"
value-key="value" custom-root-class="px-[32rpx] pt-[30rpx] pb-[40rpx]"
active-item-class="bg-white text-[#1580FF] border-1 border-solid border-[#1580FF]!"
default-item-class="bg-[#F3F4F8] border-[#F3F4F8]"
custom-item-class="w-full py-[16rpx] text-center border-[1rpx] border-solid"
@change="handleChange" />
</sar-dropdown-item>
<sar-dropdown-item v-model:visible="visibleFlag3" v-model="searchParams.year"
:title="searchParams.year || '年份'">
<mx-radio-group v-model:value="searchParams.year" :options="yearList" label-key="label"
value-key="value" custom-root-class="px-[32rpx] pt-[30rpx] pb-[40rpx]"
custom-item-class="w-full py-[16rpx] text-center border-[1rpx] border-solid"
active-item-class="bg-white text-[#1580FF] border-1 border-solid border-[#1580FF]!"
default-item-class="bg-[#F3F4F8] border-[#F3F4F8]" @change="handleChange" />
</sar-dropdown-item>
</sar-dropdown>
<view class="bg-[#F8F8F8] flex-1">
<mx-search v-model:searchText="searchParams.score" input-type="number" :searchIcon="true"
rootStyle="margin: 32rpx 30rpx 0;background-color:#fff;" root-class="pl-[30rpx]" @complete="handleComplete" @submit="handleChange" />
<view class="text-[#9E9E9E] text-[24rpx] bg-[#F8F8F8] ml-[50rpx] my-[20rpx]">支持查询180-660</view>
<view class="mx-[30rpx] bg-white px-[30rpx] pt-[30rpx] pb-[20rpx] rounded-[20rpx]">
<view class="flex items-center justify-between">
<view class="flex flex-col">
<view class="text-[#333] font-500 text-[28rpx]">你的预估位次</view>
<view class="flex items-baseline text-[#333] text-[28rpx] mt-[20rpx]">
<view></view>
<view class="font-[DinBold] text-[68rpx] font-900 text-[#000] mx-[10rpx]">{{ estimatedRank }}</view>
<view></view>
</view>
<view class="text-[24rpx] text-[#666] mt-[20rpx]">超过了{{ areaName }}{{ exceedRatioText }}的学生</view>
</view>
<image src="https://lw-zk.oss-cn-hangzhou.aliyuncs.com/img/home/sy_weicichaxun.png"
mode="scaleToFill" class="w-[208rpx] h-[208rpx]" />
</view>
<view class="mt-[30rpx] min-h-[300rpx]">
<view class="text-[28rpx] font-500 text-[#333] mb-[10rpx]">附近分数段</view>
<view class="">
<view class="grid grid-cols-4 bg-[#F3F4F8] text-[24prx] text-[#333] font-500 py-[14rpx]">
<view class="flex items-center justify-center">分数</view>
<view class="flex items-center justify-center">人数</view>
<view class="flex items-center justify-center">累计人数</view>
<view class="flex items-center justify-center">超越比例</view>
</view>
<view class="grid grid-cols-4 py-[8rpx]"
:class="searchParams.score === val.score ? 'text-[#1580FF] bg-[#F3F4F8] text-[26prx] font-500' : 'text-[24prx] text-[#666] bg-[#fff]'"
v-for="(val, index) in nearbyItems" :key="index">
<view class="flex items-center justify-center">{{ val.score }}</view>
<view class="flex items-center justify-center">{{ val.currentCount }}</view>
<view class="flex items-center justify-center">{{ val.cumulativeCount }}</view>
<view class="flex items-center justify-center">{{ val.exceedRatioText }}</view>
</view>
</view>
</view>
</view>
<view class="text-[#9E9E9E] text-[24rpx] mt-[20rpx] ml-[50rpx]">:本表数据为累计人数:即分数{{'>'}}该分数的考生总数</view>
<view class="mx-[40rpx] mt-[68rpx] bg-[#FEF6F6] rounded-[16rpx]">
<view class="h-[52rpx] w-[178rpx] flex items-center m-[-8rpx] ml-[20rpx]">
<image src="https://lw-zk.oss-cn-hangzhou.aliyuncs.com/img/home/tb_weicishuoming.png" mode="scaleToFill"
class="h-[52rpx] w-[178rpx]" />
</view>
<view class="flex flex-col text-[#E03C33] p-[20rpx]">
<view class="text-[24rpx] mt-[4rpx]">
<view>位次比分数更重要建议结合近年各高中录取位次科学填报志愿 </view>
</view>
</view>
</view>
</view>
</view>
</template>
<style lang="scss" scoped>
.gradient-custom {
background: linear-gradient(180deg, #ecf2ff 0%, #f8f8f8 40rpx, #fff 128rpx);
background-position: 50% 50%;
background-origin: padding-box;
background-clip: border-box;
background-size: auto auto;
}
</style>

View File

@ -0,0 +1,98 @@
<script lang="ts" setup>
import { getLiveLinks, trackPromoterRedirect } from '@/service'
let referralCode = ''
let weChatLiveId = import.meta.env.VITE_WX_VIDEO_ID || ''
let douyinLiveUrl = ''
onLoad((options) => {
referralCode = options?.referralCode || ''
getLiveLinks().then((resp) => {
douyinLiveUrl = resp?.douyinLiveUrl || ''
}).catch((error) => {
console.error('[live-links]', error)
})
})
function navigateToVideoFn() {
if (!weChatLiveId) {
uni.showToast({ title: '直播链接获取中,请稍后再试', icon: 'none' })
return
}
uni.openChannelsLive({
finderUserName: weChatLiveId,
success: () => {
if (!referralCode) {
return
}
trackPromoterRedirect(referralCode).catch((error) => {
console.error('[redirect]', error)
})
},
fail: (err) => {
console.error('跳转失败:', err)
},
})
}
function navigateToDouyinFn() {
if (!douyinLiveUrl) {
uni.showToast({ title: '直播链接获取中,请稍后再试', icon: 'none' })
return
}
if (!referralCode) {
return
}
trackPromoterRedirect(referralCode).catch((error) => {
console.error('[redirect]', error)
})
uni.setClipboardData({
data: douyinLiveUrl,
success: () => {
uni.hideToast()
uni.showModal({
title: '链接已复制',
content: '请打开抖音 App 粘贴查看直播',
showCancel: false,
confirmText: '我知道了',
})
},
fail: () => {
uni.showToast({ title: '复制失败,请稍后重试', icon: 'none' })
},
})
}
</script>
<template>
<view class="h-screen w-screen flex flex-col bg-[#F4F6FA]">
<image
src="https://lw-zk.oss-cn-hangzhou.aliyuncs.com/liebian/ZZBG.png"
mode="widthFix"
class="w-full"
/>
<view class="mt-[80rpx] px-[54rpx]">
<view class="w-full flex items-center justify-center gap-[16rpx] rounded-[210rpx] bg-[#1580FF] py-[30rpx] text-white font-500" @click="navigateToVideoFn">
<view class="h-[52rpx] w-[46rpx] rounded-[210rpx] text-[32rpx]">
<image
src="https://lw-zk.oss-cn-hangzhou.aliyuncs.com/liebian/shipin.png"
mode="widthFix"
/>
</view>
视频号查看
</view>
<view class="mt-[48rpx] w-full flex items-center justify-center gap-[16rpx] border-1 border-[#1580FF] rounded-[210rpx] border-solid bg-white py-[30rpx] text-[#1580FF] font-500" @click="navigateToDouyinFn">
<view class="h-[52rpx] w-[46rpx] rounded-[210rpx] text-[32rpx]">
<image
src="https://lw-zk.oss-cn-hangzhou.aliyuncs.com/liebian/douyin.png"
mode="widthFix"
/>
</view>
抖音查看
</view>
</view>
</view>
</template>

View File

@ -0,0 +1,166 @@
<template>
<view class="relative min-h-screen flex flex-col bg-[#F6F8FB]">
<image
src="https://lw-zk.oss-cn-hangzhou.aliyuncs.com/liebian/fenxiang.png"
mode="widthFix"
class="absolute left-0 top-0 aspect-[1.75/1] w-full"
/>
<form
class="relative z-1 mx-[30rpx] mt-[280rpx] h-max flex flex-col rounded-[20rpx] bg-[#fff] px-[30rpx] pb-[60rpx] pt-[30rpx]"
@submit="handleSubmit"
>
<label class="block">
<view class="mb-[12rpx] block text-[28rpx] text-[#111111] font-500 leading-[1]">
<text class="text-[#EB5241]">*</text>
<text>教师姓名</text>
</view>
<view class="h-[88rpx] flex items-center gap-[18rpx] border border-[#D9D9D9] rounded-[10rpx] border-solid px-[20rpx]">
<input
v-model.trim="form.name"
class="min-w-0 flex-1 select-text border-none bg-transparent p-0 text-[30rpx] text-[#1F2329] outline-none"
type="text"
placeholder="请填写教师姓名"
confirm-type="next"
placeholder-style="color:#A6AFBD;font-size:30rpx;text-align:left;"
>
</view>
</label>
<label class="mt-[50rpx] block">
<view class="mb-[12rpx] block text-[30rpx] text-[#111111] font-500 leading-[1]">
<text class="text-[#EB5241]">*</text>
<text>教师手机号</text>
</view>
<view class="h-[88rpx] flex items-center gap-[18rpx] border border-[#D9D9D9] rounded-[10rpx] border-solid px-[20rpx]">
<input
v-model.trim="form.phone"
class="min-w-0 flex-1 select-text border-none bg-transparent p-0 text-[30rpx] text-[#1F2329] outline-none"
type="number"
:maxlength="11"
inputmode="numeric"
placeholder="请填写教师手机号"
confirm-type="done"
placeholder-style="color:#A6AFBD;font-size:30rpx;text-align:left;"
@input="handlePhoneInput"
>
</view>
</label>
<button
form-type="submit"
class="mx-auto mt-[60rpx] h-[88rpx] w-[480rpx] rounded-[12rpx] border-none bg-[#1580FF] text-[32rpx] text-[#fff] font-500 active:bg-[#1580FF] disabled:bg-[#A9D0FF]"
:disabled="!canSubmit || loading"
:loading="loading"
>
{{ loading ? '生成中...' : '生成' }}
</button>
<view
v-if="errorMessage"
class="mb-0 mt-[24rpx] text-center text-[24rpx] text-[#EB5241] leading-[1.4]"
>
{{ errorMessage }}
</view>
</form>
<view class="mt-[50rpx] px-[30rpx] text-[26rpx] text-[#666] leading-[36rpx]">
填写教师信息后系统将生成专属直播二维码教师可将二维码分享给学生或家长学生扫码进入直播间后系统会自动统计进入人数
</view>
</view>
</template>
<script lang="ts" setup>
import type { Promoter } from '@/service/invite'
import { createPromoter } from '@/service'
import { useInviteStore } from '@/store/invite'
definePage({
style: {
navigationBarTitleText: '教师直播码',
},
})
const inviteStore = useInviteStore()
const loading = ref(false)
const errorMessage = ref('')
const promoter = ref<Promoter | null>(null)
const form = reactive({
name: '',
phone: '',
referralCode: '',
})
onLoad((options) => {
console.log('canshu', options)
const referralCode = options?.referralCode || options?.code
if (referralCode) {
form.referralCode = decodeURIComponent(referralCode)
}
})
onShareAppMessage(() => {
return {
title: '六纬中考通',
path: '/pages-sub/invite/login',
imageUrl: 'https://lw-zk.oss-cn-hangzhou.aliyuncs.com/liebian/zhuanshumafenxiangtu.png',
}
})
const canSubmit = computed(() => {
return form.name.trim().length > 0 && /^1[3-9]\d{9}$/.test(form.phone)
})
function handlePhoneInput(event: { detail?: { value?: string } }) {
form.phone = (event.detail?.value || '').replace(/\D/g, '').slice(0, 11)
}
async function handleSubmit() {
if (loading.value) {
return
}
if (!form.name.trim()) {
uni.showToast({ title: '请填写教师姓名', icon: 'none' })
return
}
if (!/^1[3-9]\d{9}$/.test(form.phone)) {
uni.showToast({ title: '请填写正确手机号', icon: 'none' })
return
}
loading.value = true
errorMessage.value = ''
try {
const result = await createPromoter({
name: form.name.trim(),
phone: form.phone,
referralCode: form.referralCode || undefined,
})
promoter.value = result
inviteStore.setPromoter(result)
uni.showToast({ title: '生成成功', icon: 'success' })
uni.navigateTo({ url: '/pages-sub/invite/qrcode' })
}
catch (error) {
const message = error instanceof Error ? error.message : '提交失败,请稍后重试'
errorMessage.value = message
uni.showToast({ title: message, icon: 'none' })
}
finally {
loading.value = false
}
}
</script>
<style lang="scss" scoped>
/* uni-app 中 button 默认带边框,使用原子化 border-none 已覆盖;如有需要可在此处补充覆盖样式 */
button::after {
border: none;
}
</style>

View File

@ -0,0 +1,26 @@
<template>
<view class="min-h-screen flex flex-col items-center bg-[#313131]">
<image
src="https://lw-zk.oss-cn-hangzhou.aliyuncs.com/liebian/zhuanshuyaoqing.png"
mode="widthFix"
/>
</view>
</template>
<script lang="ts" setup>
import { useInviteStore } from '@/store/invite'
const inviteStore = useInviteStore()
const referralCode = inviteStore.currentPromoter?.referralCode
onShareAppMessage(() => {
return {
title: '六纬中考通',
path: `/pages-sub/invite/jump?referralCode=${referralCode}`,
imageUrl: 'https://lw-zk.oss-cn-hangzhou.aliyuncs.com/liebian/yaoqingzhibotu.png',
}
})
</script>
<style lang="scss" scoped>
//
</style>

View File

@ -9,9 +9,9 @@ definePage({
}) })
const wishlistStore = useWishlistStore() const wishlistStore = useWishlistStore()
const wishlist = ref([]) const wishlist = ref<any[]>([])
const handleDeleteWishlist = (val,index) => { const handleDeleteWishlist = (val:any,index:number) => {
uni.showModal({ uni.showModal({
title: "确认需要删除吗?", success: ({confirm,cancel}) => { title: "确认需要删除吗?", success: ({confirm,cancel}) => {
if(confirm){ if(confirm){
@ -23,15 +23,15 @@ const handleDeleteWishlist = (val,index) => {
}) })
} }
const getTag = (val) => { const getTag = (val:any) => {
let content = JSON.parse(val.contents) let content = JSON.parse(val.contents)
let schools = content.schools let schools = content.schools
let zbsTag = schools.some(item => item.type === '指标生') let zbsTag = schools.some((item:{type:string}) => item.type === '指标生')
let adjustTag = content.adjust let adjustTag = content.adjust
return zbsTag ? "指标生" : adjustTag ? "服从调剂" : false return zbsTag ? "指标生" : adjustTag ? "服从调剂" : false
} }
const navigateToDetail = (val) => { const navigateToDetail = (val:any) => {
let content = JSON.parse(val.contents) let content = JSON.parse(val.contents)
wishlistStore.setExtendWishlist({title:val.title,contents:val.contents,adjust:content.adjust,batchName:val.batchName}) wishlistStore.setExtendWishlist({title:val.title,contents:val.contents,adjust:content.adjust,batchName:val.batchName})
uni.navigateTo({url:"/pages-sub/me/wishlistInfo"}) uni.navigateTo({url:"/pages-sub/me/wishlistInfo"})

View File

@ -25,7 +25,12 @@ const schools = ref(content.schools)
<view v-for="(val, index) in schools" :key="index" class="flex mt-[26rpx]"> <view v-for="(val, index) in schools" :key="index" class="flex mt-[26rpx]">
<view <view
class="py-[29rpx] text-[30rpx] text-[#333] grid gap-[8rpx] bg-white px-[30rpx] py-[20rpx] not-last:mb-[30rpx] rounded-[16rpx] w-full"> class="py-[29rpx] text-[30rpx] text-[#333] grid gap-[8rpx] bg-white px-[30rpx] py-[20rpx] not-last:mb-[30rpx] rounded-[16rpx] w-full">
<view class="text-[32rpx] font-600">{{ val.schoolName }}</view> <view class="text-[32rpx] font-600 flex items-center gap-[10rpx]">
<view
class="w-[44rpx] h-[44rpx] rounded-[8rpx] text-[26rpx] font-600 flex items-center justify-center"
:class="`${val.tags === '稳' ? 'text-[#FA8E23] bg-[#ffeede]' : val.tags === '保' ? 'bg-[#dcf6f0] text-[#15C496]' : 'bg-[#fce5e3] text-[#EB5241]'}`">
{{ val.tags }}</view>
{{ val.schoolName }}</view>
<view class="text-[24rpx] text-[#333]">{{new Date().getFullYear()}}计划招生{{ val.planCount }}</view> <view class="text-[24rpx] text-[#333]">{{new Date().getFullYear()}}计划招生{{ val.planCount }}</view>
<view class="text-[#333] text-[24rpx]"> <view class="text-[#333] text-[24rpx]">

View File

@ -3,9 +3,9 @@ import MxSearch from "@/pages-sub/components/search/index.vue"
import MxCheckbox from "@/pages-sub/components/checkbox/index.vue?async" import MxCheckbox from "@/pages-sub/components/checkbox/index.vue?async"
import MxRadio from "@/pages-sub/components/radio/index.vue?async" import MxRadio from "@/pages-sub/components/radio/index.vue?async"
import { getAreaList, getBusSchoolAdmission, getSchoolNature } from "@/service" import { getAreaList, getBusSchoolAdmission, getSchoolNature } from "@/service"
import { useWishlistStore } from "@/store" import { useWishlistStore,useUserStore } from "@/store"
const tableData = ref([]) const tableData = ref<any[]>([])
const tableColumns = [ const tableColumns = [
{ title: '院校名称', prop: 'schoolName', width: '40%', align: 'left' }, { title: '院校名称', prop: 'schoolName', width: '40%', align: 'left' },
{ title: '冲稳保', prop: 'tags', width: "15%", align: "center" }, { title: '冲稳保', prop: 'tags', width: "15%", align: "center" },
@ -14,10 +14,10 @@ const tableColumns = [
] as const ] as const
const wishlistStore = useWishlistStore() const wishlistStore = useWishlistStore()
const chooseData = ref([]) const chooseData = ref<any[]>([])
const chooseDataMap = new Map() const chooseDataMap = new Map()
const handleChoose = (val) => { const handleChoose = (val:any) => {
if (chooseData.value.includes(val.schoolId)) { if (chooseData.value.includes(val.schoolId)) {
const index = chooseData.value.indexOf(val.schoolId); const index = chooseData.value.indexOf(val.schoolId);
@ -26,14 +26,14 @@ const handleChoose = (val) => {
chooseDataMap.delete(val.schoolId) chooseDataMap.delete(val.schoolId)
} }
} else { } else {
if (chooseData.value.length > 2) { if (chooseData.value.length > 6) {
uni.showToast({ title: "第二志愿最多只能3个", icon: "none" }) uni.showToast({ title: "志愿最多只能7个", icon: "none" })
return; return;
} }
chooseData.value.push(val.schoolId); chooseData.value.push(val.schoolId);
chooseDataMap.set(val.schoolId, val) chooseDataMap.set(val.schoolId, val)
} }
let tempSchool = [] let tempSchool:any[] = []
chooseDataMap.forEach((val, key) => { chooseDataMap.forEach((val, key) => {
tempSchool.push({ ...val, type: "secondBatch" }) tempSchool.push({ ...val, type: "secondBatch" })
}) })
@ -46,6 +46,7 @@ const cwbs = ref([{ value: '冲', label: '冲' }, { value: '稳', label: '稳' }
const natureList = ref([]) const natureList = ref([])
const totalCount = ref(0) const totalCount = ref(0)
const regions = ref([]) const regions = ref([])
const userStore = useUserStore();
const searchParams = ref({ const searchParams = ref({
cwb: null, cwb: null,
@ -74,7 +75,8 @@ const handleChangeSchool = () => {
schoolName: searchParams.value.keyword, schoolName: searchParams.value.keyword,
schoolNature: searchParams.value.nature, schoolNature: searchParams.value.nature,
region: searchParams.value.region, region: searchParams.value.region,
tags: searchParams.value.cwb tags: searchParams.value.cwb,
score: userStore.userInfo.userExtend.expectedScore || 0
} }
}).then(resp => { }).then(resp => {
popVisible.value = false; popVisible.value = false;

View File

@ -2,11 +2,10 @@
import { systemInfo } from '@/utils/systemInfo' import { systemInfo } from '@/utils/systemInfo'
import MxRadio from "@/pages-sub/components/radio/index.vue" import MxRadio from "@/pages-sub/components/radio/index.vue"
import MxInput from "@/pages-sub/components/input/index.vue" import MxInput from "@/pages-sub/components/input/index.vue"
import { getGradeList, getAreaList, saveUserInfo } from "@/service" import { getGradeList, getAreaList, saveUserAreaScore } from "@/service"
import { useUserStore } from "@/store" import { useUserStore } from "@/store"
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
import { checkEmptyValues } from '@/utils'
// #ifdef MP-WEIXIN // #ifdef MP-WEIXIN
definePage({ definePage({
@ -45,22 +44,15 @@ const userStore = useUserStore();
const { userInfo } = storeToRefs(userStore) const { userInfo } = storeToRefs(userStore)
const handleSubmit = () => { const handleSubmit = () => {
if (!userInfo.value.nickName || !userInfo.value.sex || checkEmptyValues(userInfo.value.userExtend, ["gradeId", "schoolName", "area"])) {
uni.showToast({ title: "完善所需信息", icon: "error" })
return
}
let params = { let params = {
nickName: userInfo.value.nickName,
gradeId: userInfo.value.userExtend.gradeId,
gradeName: userInfo.value.userExtend.gradeName,
areaName: userInfo.value.userExtend.area, areaName: userInfo.value.userExtend.area,
schoolName: userInfo.value.userExtend.schoolName, totalScore: userInfo.value.userExtend.expectedScore,
sex: userInfo.value.sex, id: userInfo.value.userExtend.id
rank: userInfo.value.userExtend.rank
} }
saveUserInfo({ data: params }).then((resp) => { saveUserAreaScore({ data: params }).then((resp) => {
if (resp.code === 200) { if (resp.code === 200) {
uni.navigateTo({ url: '/pages-sub/wishlist/create/second' }) uni.navigateTo({ url: '/pages-sub/wishlist/create/secondBatch' })
} }
}) })
} }
@ -117,16 +109,13 @@ onShow(() => {
</sar-navbar> </sar-navbar>
<view class="flex items-center mx-[30rpx] mb-[20rpx]"> <view class="flex items-center mx-[30rpx] mb-[20rpx]">
<view class="w-[112rpx] h-[104rpx] flex items-center">
<image src="https://lwzk.ycymedu.com/img/tianbao/tb_01.png" mode="scaleToFill"
class="w-[112rpx] h-[104rpx]" />
</view>
<view class="flex flex-col ml-[10rpx]"> <view class="flex flex-col ml-[10rpx]">
<view class="text-[44rpx] font-500 mb-[14rpx]">完善基础信息</view> <view class="text-[44rpx] font-500 mb-[14rpx]">一键生成志愿表</view>
<view class="text-[#B0B3B8] text-[30rpx]">详细的信息获取精准推荐</view> <view class="text-[#B0B3B8] text-[30rpx]">生成我的专属志愿表</view>
</view> </view>
<view class="w-[160rpx] h-[160rpx] ml-auto"> <view class="w-[160rpx] h-[160rpx] ml-auto">
<image src="https://lwzk.ycymedu.com/img/tianbao/tb_zk01.png" mode="scaleToFill" <image src="https://lwzk.ycymedu.com/img/tianbao/tb_zk03.png" mode="scaleToFill"
class="w-[160rpx] h-[160rpx]" /> class="w-[160rpx] h-[160rpx]" />
</view> </view>
</view> </view>
@ -134,86 +123,47 @@ onShow(() => {
<form @submit="handleSubmit" class="flex-1 pb-safe"> <form @submit="handleSubmit" class="flex-1 pb-safe">
<view class="flex flex-col h-full"> <view class="flex flex-col h-full">
<view <view
class="flex items-center text-[30rpx] justify-between mx-[40rpx] py-[34rpx] border-b-1 border-b-[#eaeaea] border-b-solid"> class="flex items-center text-[30rpx] justify-between mx-[40rpx] py-[34rpx] px-[30rpx] bg-[#F7F8FA] mb-[20rpx]">
<view class="text-[#404142] text-left min-w-[150rpx] mr-[40rpx]">学生姓名</view> <view class="text-[#404142] text-left min-w-[150rpx] mr-[40rpx] ">总分</view>
<view class="flex-1"> <view class="flex-1 flex items-center">
<input type="text" v-model="userInfo.nickName" placeholder="请输入姓名" confirm-type="done" <MxInput type="number" v-model:value="userInfo.userExtend.expectedScore" placeholder="请填写"
placeholder-style="color:#C5C8D1;font-size:30rpx;text-align:left;" class="text-left"> root-class="text-right" class="w-full" inputDirection="text-right"/>
</view> </view>
</view> </view>
<view <view
class="flex items-center text-[30rpx] justify-between mx-[40rpx] py-[34rpx] border-b-1 border-b-[#eaeaea] border-b-solid"> class="flex items-center text-[30rpx] justify-between mx-[40rpx] py-[34rpx] px-[30rpx] bg-[#F7F8FA]">
<view class="text-[#404142] text-left min-w-[150rpx] mr-[40rpx]">性别</view>
<view class="flex-1 flex items-center" @click="handleOpenPopup('gender')">
<MxInput v-model:value="formData.genderLabel" placeholder="请选择您的性别" root-class="text-left"
:readonly="true" />
<view class="w-[18rpx] h-[36rpx] flex items-center ml-auto">
<image src="https://lwzk.ycymedu.com/img/tianbao/tb_jiantou.png"
mode="scaleToFill" class="w-[18rpx] h-[36rpx]" />
</view>
</view>
</view>
<view
class="flex items-center text-[30rpx] justify-between mx-[40rpx] py-[34rpx] border-b-1 border-b-[#eaeaea] border-b-solid">
<view class="text-[#404142] text-left min-w-[150rpx] mr-[40rpx]">年级</view>
<view class="flex-1 flex items-center" @click="handleOpenPopup('grade')">
<MxInput v-model:value="userInfo.userExtend.gradeName" placeholder="请选择您的年级"
root-class="text-left" :readonly="true" />
<view class="w-[18rpx] h-[36rpx] flex items-center ml-auto">
<image src="https://lwzk.ycymedu.com/img/tianbao/tb_jiantou.png"
mode="scaleToFill" class="w-[18rpx] h-[36rpx]" />
</view>
</view>
</view>
<view
class="flex items-center text-[30rpx] justify-between mx-[40rpx] py-[34rpx] border-b-1 border-b-[#eaeaea] border-b-solid">
<view class="text-[#404142] text-left min-w-[150rpx] mr-[40rpx] ">中考所在区</view> <view class="text-[#404142] text-left min-w-[150rpx] mr-[40rpx] ">中考所在区</view>
<view class="flex-1 flex items-center" @click="handleOpenPopup('examinationArea')"> <view class="flex-1 flex items-center" @click="handleOpenPopup('examinationArea')">
<MxInput v-model:value="userInfo.userExtend.area" placeholder="请选择您的中考所在区" <MxInput v-model:value="userInfo.userExtend.area" placeholder="请选择您的中考所在区"
root-class="text-left" :readonly="true" /> root-class="text-right" class="w-full" inputDirection="text-right" :readonly="true" />
<view class="w-[18rpx] h-[36rpx] flex items-center ml-auto"> <view class="w-[18rpx] h-[36rpx] flex items-center ml-[10rpx]">
<image src="https://lwzk.ycymedu.com/img/tianbao/tb_jiantou.png" <image src="https://lwzk.ycymedu.com/img/tianbao/tb_jiantou.png"
mode="scaleToFill" class="w-[18rpx] h-[36rpx]" /> mode="scaleToFill" class="w-[18rpx] h-[36rpx]" />
</view> </view>
</view> </view>
</view> </view>
<view
class="flex items-center text-[30rpx] justify-between mx-[40rpx] py-[34rpx] border-b-1 border-b-[#eaeaea] border-b-solid">
<view class="text-[#404142] text-left min-w-[150rpx] mr-[40rpx] ">就读学校</view>
<view class="flex-1 flex items-center" @click="navigateToSchool">
<view class="flex-1">
<MxInput v-model:value="userInfo.userExtend.schoolName" placeholder="请选择您的就读学校"
root-class="text-left" :readonly="true" />
</view>
<view class="w-[18rpx] h-[36rpx] flex items-center ml-auto"> <view class="mx-[40rpx] mt-[68rpx] bg-[#FEF6F6] rounded-[16rpx]">
<image src="https://lwzk.ycymedu.com/img/tianbao/tb_jiantou.png" <view class="h-[52rpx] w-[178rpx] flex items-center m-[-8rpx] ml-[20rpx]">
mode="scaleToFill" class="w-[18rpx] h-[36rpx]" /> <image src="https://lw-zk.oss-cn-hangzhou.aliyuncs.com/img/home/tb_shuoming.png" mode="scaleToFill"
class="h-[52rpx] w-[178rpx]" />
</view>
<view class="flex flex-col text-[#E03C33] m-[20rpx]">
<view class="text-[24rpx] mt-[4rpx]">
<view>根据济南2026年中考招生政策进入模拟志愿填报阶段的考生默认历史生物地理道法等水平考试等级均已达到普通高中基础填报要求C级及以上本产品仅用于统招平行志愿的模拟</view>
</view> </view>
</view> </view>
</view>
<view
class="flex items-center text-[30rpx] justify-between mx-[40rpx] py-[34rpx] border-b-1 border-b-[#eaeaea] border-b-solid">
<view class="text-[#404142] text-left min-w-[150rpx] mr-[40rpx] ">校内名次</view>
<view class="flex-1 flex items-center">
<input type="number" inputmode="numeric" v-model="userInfo.userExtend.rank"
placeholder="请输入您的校内名次" confirm-type="done"
placeholder-style="color:#C5C8D1;font-size:30rpx;text-align:left;" class="text-left">
</view>
</view> </view>
<view class="px-[30rpx] py-[16rpx] border-t-solid border-t-[#ededed] border-t-1 mt-auto"> <view class="px-[30rpx] py-[16rpx] border-t-solid border-t-[#ededed] border-t-1 mt-auto">
<button form-type="submit" class="rounded-[16rpx] bg-[#1580FF] text-[#fff] submit-btn">下一步</button> <button form-type="submit" class="rounded-[16rpx] bg-[#1580FF] text-[#fff] submit-btn">开始推荐</button>
</view> </view>
</view> </view>
</form> </form>
@ -227,19 +177,8 @@ onShow(() => {
mode="scaleToFill" class="w-[36rpx] h-[36rpx]" /> mode="scaleToFill" class="w-[36rpx] h-[36rpx]" />
</view> </view>
</view> </view>
<MxRadio v-model:value="userInfo.sex" v-model:label="formData.genderLabel" v-if="activeType == 'gender'"
:options="genderClassification" label-key="label" value-key="value"
custom-root-cols-class="grid grid-cols-2 items-center gap-[20rpx]"
custom-root-class="pb-[20rpx]" custom-item-class="py-[12rpx] text-center "
active-item-class="bg-white text-[#1580FF] border-[#1580FF] border-[2rpx] border-solid" @change="visible = false" />
<MxRadio v-model:value="userInfo.userExtend.gradeId" v-model:label="userInfo.userExtend.gradeName"
v-if="activeType == 'grade'" :options="gradeClassification" label-key="label" value-key="value"
custom-root-cols-class="grid grid-cols-3 items-center gap-[20rpx]"
custom-root-class="pb-[20rpx]" custom-item-class="py-[12rpx] text-center "
active-item-class="bg-white text-[#1580FF] border-[#1580FF] border-[2rpx] border-solid" @change="visible = false"/>
<MxRadio v-model:value="userInfo.userExtend.area" v-if="activeType == 'examinationArea'" <MxRadio v-model:value="userInfo.userExtend.area" v-if="activeType == 'examinationArea'"
:options="areaList" label-key="label" value-key="value" :options="areaList" label-key="label" value-key="value"
custom-root-cols-class="grid grid-cols-3 items-center gap-[20rpx]" custom-root-cols-class="grid grid-cols-3 items-center gap-[20rpx]"

View File

@ -58,10 +58,15 @@ onBackPress(() => {
<view v-for="(val, index) in schools" :key="index" class="flex mt-[26rpx]"> <view v-for="(val, index) in schools" :key="index" class="flex mt-[26rpx]">
<view <view
class="py-[29rpx] text-[30rpx] text-[#333] grid gap-[8rpx] bg-white px-[30rpx] py-[20rpx] not-last:mb-[30rpx] rounded-[16rpx] w-full"> class="py-[29rpx] text-[30rpx] text-[#333] grid gap-[8rpx] bg-white px-[30rpx] py-[20rpx] not-last:mb-[30rpx] rounded-[16rpx] w-full">
<view class="text-[32rpx] font-600">{{ val.schoolName }}</view> <view class="text-[32rpx] font-600 flex items-center gap-[10rpx]">
<view
class="w-[44rpx] h-[44rpx] rounded-[8rpx] text-[26rpx] font-600 flex items-center justify-center"
:class="`${val.tags === '稳' ? 'text-[#FA8E23] bg-[#ffeede]' : val.tags === '保' ? 'bg-[#dcf6f0] text-[#15C496]' : 'bg-[#fce5e3] text-[#EB5241]'}`">
{{ val.tags }}</view>
{{ val.schoolName }}</view>
<view class="text-[24rpx] text-[#333]">{{new Date().getFullYear()}}计划招生{{ val.planCount }}</view> <view class="text-[24rpx] text-[#333]">{{new Date().getFullYear()}}计划招生{{ val.planCount }}</view>
<!-- <view class="flex items-center"> <!-- <view class="flex items-center">
<view class="text-[#666] bg-[#F8F8F8] rounded-[8rpx] px-[10rpx] py-[4rpx]" <view class="rgb(173 116 116) bg-[#F8F8F8] rounded-[8rpx] px-[10rpx] py-[4rpx]"
v-for="value in 2" :key="value">重点高中</view> v-for="value in 2" :key="value">重点高中</view>
</view> --> </view> -->
<view class="text-[#333] text-[24rpx]"> <view class="text-[#333] text-[24rpx]">
@ -70,24 +75,6 @@ onBackPress(() => {
</view> </view>
</view> </view>
<view class=" bg-[#FEF6F6] rounded-[16rpx] mt-[30rpx]">
<view class="h-[52rpx] w-[178rpx] flex items-center m-[-8rpx] ml-[20rpx]">
<image src="https://lwzk.ycymedu.com/img/tianbao/tb_shuoming.png"
mode="scaleToFill" class="h-[52rpx] w-[178rpx]" />
</view>
<view class="flex flex-col text-[#E03C33] py-[20rpx] pl-[26rpx] pr-[34rpx]">
<view class="text-[26rpx] mt-[4rpx]">
<view>
二志愿录取成功后不再进入后续批次
</view>
<view>
建议选择目标明确的学校同时关注第三批次平行志愿机会
</view>
</view>
</view>
</view>
</view> </view>
<view class="grid grid-cols-2 gap-[20rpx] px-[28rpx] py-[16rpx]"> <view class="grid grid-cols-2 gap-[20rpx] px-[28rpx] py-[16rpx]">
<view class="rounded-[16rpx] bg-[#f5f5f5] text-[#333] text-[36rpx] py-[18rpx] text-center" @click="navigateToHome"></view> <view class="rounded-[16rpx] bg-[#f5f5f5] text-[#333] text-[36rpx] py-[18rpx] text-center" @click="navigateToHome"></view>

View File

@ -51,7 +51,7 @@ const disableSubmit = computed(() => {
<sar-navbar show-back @back="handleBack" :fixed="true" fixation-style="top:unset;" <sar-navbar show-back @back="handleBack" :fixed="true" fixation-style="top:unset;"
:root-style="{ '--sar-navbar-bg': `transparent`, '--sar-navbar-item-color': 'black', 'padding-top': `${systemInfo?.statusBarHeight}px` }"> :root-style="{ '--sar-navbar-bg': `transparent`, '--sar-navbar-item-color': 'black', 'padding-top': `${systemInfo?.statusBarHeight}px` }">
<template #title> <template #title>
<view class="flex justify-center">第二批次</view> <view class="flex justify-center">志愿表</view>
</template> </template>
</sar-navbar> </sar-navbar>
<view class="flex-1 overflow-y-auto"> <view class="flex-1 overflow-y-auto">
@ -93,11 +93,11 @@ const disableSubmit = computed(() => {
</view> </view>
<view class="text-[30rpx] items-start mt-[50rpx]"> <view class="text-[30rpx] items-start mt-[50rpx]">
<view class="text-[#333]"> <view class="text-[#333]">
未被第一批次录取的考生可填报第二批次 可选择3所普通高中 本工具用于模拟济南中考志愿填报
</view> 根据分数区域和学校信息生成志愿表
<view class="my-[20rpx] text-[#333]"> 结果仅供参考最终以官方公布为准
系统按分数优先投档平行志愿3所学校并列不分先后
</view> </view>
</view> </view>
<view <view
class="text-[34rpx] font-500 text-white bg-[#1580FF] rounded-full py-[20rpx] text-center mx-[40rpx] mt-[40rpx]" class="text-[34rpx] font-500 text-white bg-[#1580FF] rounded-full py-[20rpx] text-center mx-[40rpx] mt-[40rpx]"

View File

@ -61,7 +61,7 @@ const navigateToFirst = () => {
const totalScore = ref(0) const totalScore = ref(0)
onLoad(()=>{ onLoad(()=>{
getMyScore().then(resp => { getMyScore().then(resp => {
if (resp.code === 200 && resp.result) { if (resp.code === 200 && resp.result) {
totalScore.value = resp.result.totalScore totalScore.value = resp.result.totalScore
} }

View File

@ -0,0 +1,313 @@
import { ref } from 'vue'
type ChannelLiveStatus = 1 | 2 | 3 | 4
type ChannelNoticeStatus = 0 | 1 | 2
type ChannelVideoType = 'live' | 'notice'
interface ChannelLiveInfo {
feedId?: string
nonceId?: string
description?: string
status?: ChannelLiveStatus
headUrl?: string
nickname?: string
replayStatus?: string
otherInfos?: ChannelLiveInfo[]
}
interface ChannelNoticeInfo {
noticeId?: string
status?: ChannelNoticeStatus
startTime?: string
headUrl?: string
nickname?: string
reservable?: boolean
otherInfos?: ChannelNoticeInfo[]
}
export interface ChannelVideoItem {
id: string
type: ChannelVideoType
title: string
coverUrl: string
statusText: string
buttonText: string
nickname: string
startTimeText: string
startTimeStamp: number
feedId?: string
nonceId?: string
noticeId?: string
reservable?: boolean
}
interface ChannelsQueryApi {
getChannelsLiveInfo: (options: {
finderUserName: string
success: (res: ChannelLiveInfo) => void
fail: (err: unknown) => void
}) => void
getChannelsLiveNoticeInfo: (options: {
finderUserName: string
success: (res: ChannelNoticeInfo) => void
fail: (err: unknown) => void
}) => void
}
interface ChannelsActionApi {
openChannelsLive: (options: {
finderUserName: string
feedId?: string
nonceId?: string
fail: (err: unknown) => void
}) => void
reserveChannelsLive: (options: {
noticeId: string
success: () => void
fail: (err: unknown) => void
}) => void
}
const DEFAULT_CHANNEL_COVER = 'https://lwzk.ycymedu.com/img/home/sy_daoxiao.png'
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null
}
function isFunction(value: unknown): value is (...args: unknown[]) => unknown {
return typeof value === 'function'
}
function getChannelsQueryApi(): ChannelsQueryApi | null {
const api = uni as unknown
if (!isRecord(api)) {
return null
}
const canGetLiveInfo = isFunction(api.getChannelsLiveInfo)
const canGetNoticeInfo = isFunction(api.getChannelsLiveNoticeInfo)
if (!canGetLiveInfo || !canGetNoticeInfo) {
return null
}
return api as unknown as ChannelsQueryApi
}
function getChannelsActionApi(): ChannelsActionApi | null {
const api = uni as unknown
if (!isRecord(api)) {
return null
}
const canOpenLive = isFunction(api.openChannelsLive)
const canReserveLive = isFunction(api.reserveChannelsLive)
if (!canOpenLive || !canReserveLive) {
return null
}
return api as unknown as ChannelsActionApi
}
function getStartTimeStamp(startTime?: string): number {
if (!startTime) {
return Number.MAX_SAFE_INTEGER
}
const timestamp = Number(startTime)
if (Number.isFinite(timestamp)) {
return String(Math.trunc(timestamp)).length === 10 ? timestamp * 1000 : timestamp
}
const dateTime = new Date(startTime).getTime()
return Number.isFinite(dateTime) ? dateTime : Number.MAX_SAFE_INTEGER
}
function formatStartTime(startTime?: string): string {
const timestamp = getStartTimeStamp(startTime)
if (timestamp === Number.MAX_SAFE_INTEGER) {
return startTime ? `${startTime}开播` : '直播预告'
}
const date = new Date(timestamp)
const month = date.getMonth() + 1
const day = date.getDate()
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${month}.${day} ${hours}:${minutes}开播`
}
function flattenLiveInfo(info: ChannelLiveInfo): ChannelLiveInfo[] {
return [info, ...(info.otherInfos ?? [])]
}
function flattenNoticeInfo(info: ChannelNoticeInfo): ChannelNoticeInfo[] {
return [info, ...(info.otherInfos ?? [])]
}
function normalizeLiveInfo(info: ChannelLiveInfo, index: number): ChannelVideoItem {
const id = info.feedId || info.nonceId || `live-${index}`
return {
id: `live-${id}`,
type: 'live',
title: info.description || '视频号直播',
coverUrl: info.headUrl || DEFAULT_CHANNEL_COVER,
statusText: '正在直播',
buttonText: '查看直播',
nickname: info.nickname || '',
startTimeText: '',
startTimeStamp: 0,
feedId: info.feedId,
nonceId: info.nonceId,
}
}
function normalizeNoticeInfo(info: ChannelNoticeInfo, index: number): ChannelVideoItem {
const id = info.noticeId || `notice-${index}`
const startTimeStamp = getStartTimeStamp(info.startTime)
return {
id: `notice-${id}`,
type: 'notice',
title: info.nickname ? `${info.nickname}的直播预告` : '视频号直播预告',
coverUrl: info.headUrl || DEFAULT_CHANNEL_COVER,
statusText: formatStartTime(info.startTime),
buttonText: info.reservable === false ? '直播预告' : '预约直播',
nickname: info.nickname || '',
startTimeText: formatStartTime(info.startTime),
startTimeStamp,
noticeId: info.noticeId,
reservable: info.reservable,
}
}
function getLiveVideoList(finderUserName: string, channelsApi: ChannelsQueryApi): Promise<ChannelVideoItem[]> {
return new Promise((resolve) => {
channelsApi.getChannelsLiveInfo({
finderUserName,
success: (res) => {
const liveList = flattenLiveInfo(res)
.filter(item => item.status === 2)
.map(normalizeLiveInfo)
resolve(liveList)
},
fail: (err) => {
console.error('获取视频号直播信息失败:', err)
resolve([])
},
})
})
}
function getNoticeVideoList(finderUserName: string, channelsApi: ChannelsQueryApi): Promise<ChannelVideoItem[]> {
return new Promise((resolve) => {
channelsApi.getChannelsLiveNoticeInfo({
finderUserName,
success: (res) => {
const noticeList = flattenNoticeInfo(res)
.filter(item => item.status === 0)
.map(normalizeNoticeInfo)
.sort((prev, next) => prev.startTimeStamp - next.startTimeStamp)
resolve(noticeList)
},
fail: (err) => {
console.error('获取视频号直播预告失败:', err)
resolve([])
},
})
})
}
export function useChannelLive(finderUserName: string) {
const channelVideoList = ref<ChannelVideoItem[]>([])
function goToLiveHomePage(){
uni.openChannelsUserProfile({
finderUserName,
success: () => {
console.log('跳转视频号主页成功')
},
fail: (err) => {
console.error('跳转视频号主页失败:', err)
},
})
}
async function getChannelLiveNoticeInfo() {
if (!finderUserName) {
uni.showToast({ title: '直播链接获取中,请稍后再试', icon: 'none' })
return
}
const channelsApi = getChannelsQueryApi()
if (!channelsApi) {
channelVideoList.value = []
return
}
const [liveList, noticeList] = await Promise.all([
getLiveVideoList(finderUserName, channelsApi),
getNoticeVideoList(finderUserName, channelsApi),
])
channelVideoList.value = [...liveList, ...noticeList]
}
function handleChannelVideoAction(item: ChannelVideoItem) {
const channelsApi = getChannelsActionApi()
if (!channelsApi) {
return
}
if (item.type === 'live') {
channelsApi.openChannelsLive({
finderUserName,
feedId: item.feedId,
nonceId: item.nonceId,
fail: (err) => {
console.error('跳转视频号直播失败:', err)
},
})
return
}
if (!item.noticeId || item.reservable === false) {
uni.showToast({ title: '该直播暂不可预约', icon: 'none' })
return
}
channelsApi.reserveChannelsLive({
noticeId: item.noticeId,
success: () => {
uni.showModal({
title: '预约成功',
content: `开播时间: ${item.startTimeText} 请留意视频号消息提醒。`,
showCancel: false,
confirmText:'我知道了',
})
},
fail: (err) => {
console.error('预约视频号直播失败:', err)
},
})
}
return {
channelVideoList,
getChannelLiveNoticeInfo,
handleChannelVideoAction,
goToLiveHomePage
}
}

View File

@ -1,8 +1,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import { systemInfo } from '@/utils/systemInfo' import { getCountdown, getFirstPageInfo, getMyScore, getTopNew } from '@/service'
import { getMyScore, getTopNew, getCountdown, getFirstPageInfo } from "@/service"
import { useTokenStore } from '@/store' import { useTokenStore } from '@/store'
import { systemInfo } from '@/utils/systemInfo'
import { useChannelLive } from './hooks/useChannelLive'
defineOptions({ defineOptions({
name: 'Home', name: 'Home',
@ -29,7 +29,8 @@ definePage({
// #endif // #endif
const tokenStore = useTokenStore() const tokenStore = useTokenStore()
console.log(systemInfo); const wechatVideoId = import.meta.env.VITE_WX_VIDEO_ID || ''
const { channelVideoList, getChannelLiveNoticeInfo, handleChannelVideoAction,goToLiveHomePage } = useChannelLive(wechatVideoId)
onShareAppMessage(() => { onShareAppMessage(() => {
return { return {
@ -44,17 +45,22 @@ onShareTimeline(() => {
} }
}) })
const subMenus = [{ name: '中考资讯', imgPath: 'https://lwzk.ycymedu.com/img/home/sy_zixun.png', url: "/pages-sub/information/middleSchool" }, { name: '高中汇总', imgPath: 'https://lwzk.ycymedu.com/img/home/sy_huizong.png', url: '/pages-sub/information/highSchool' }, { name: '历年分数', imgPath: 'https://lwzk.ycymedu.com/img/home/sy_fenshu.png', url: '/pages-sub/information/overTheYear' }, { name: '指标生', imgPath: 'https://lwzk.ycymedu.com/img/home/sy_daoxiao.png', url: '/pages-sub/information/quota' }] const subMenus = [
{ name: '历年分数', imgPath: 'https://lwzk.ycymedu.com/img/home/sy_fenshu.png', url: '/pages-sub/information/overTheYear' },
{ name: '高中汇总', imgPath: 'https://lwzk.ycymedu.com/img/home/sy_daoxiao.png', url: '/pages-sub/information/highSchool' },
{ name: '批次线', imgPath: 'https://lw-zk.oss-cn-hangzhou.aliyuncs.com/img/home/sy_picixian.png', url: '/pages-sub/information/batchline' },
{ name: '位次查询', imgPath: 'https://lw-zk.oss-cn-hangzhou.aliyuncs.com/img/home/syweici.png', url: '/pages-sub/information/rank' },
]
const notifies = ref([]) const notifies = ref<any[]>([])
const opacity = ref(0) const opacity = ref(0)
const totalScore = ref("0") const totalScore = ref('0')
const schoolCount = ref("0") const schoolCount = ref('0')
const wishlistCount = ref("0") const wishlistCount = ref('0')
const navigateToCreateWish = () => { function navigateToCreateWish() {
uni.navigateTo({ url: "/pages-sub/wishlist/create/first" }) uni.navigateTo({ url: '/pages-sub/wishlist/create/first' })
} }
onPageScroll((e) => { onPageScroll((e) => {
@ -62,25 +68,25 @@ onPageScroll((e) => {
opacity.value = Math.min(scrollTop / 100, 1) opacity.value = Math.min(scrollTop / 100, 1)
}) })
const navigateToCountdownPage = () => { function navigateToCountdownPage() {
uni.navigateTo({ url: '/pages-sub/countdown/index' }) uni.navigateTo({ url: '/pages-sub/countdown/index' })
} }
const navigateToSubPage = (url: string) => { function navigateToSubPage(url: string) {
uni.navigateTo({ url }) uni.navigateTo({ url })
} }
const navigateToCustom = () => { function navigateToCustom() {
uni.navigateTo({ url: "/pages-sub/about/onlineCustom" }) uni.navigateTo({ url: '/pages-sub/about/onlineCustom' })
} }
const navigateToNewsPage = () => { function navigateToNewsPage() {
uni.navigateTo({ uni.navigateTo({
url: '/pages-sub/information/middleSchool' url: '/pages-sub/information/middleSchool',
}) })
} }
const navigateToNewsDetail = (id: number) => { function navigateToNewsDetail(id: number) {
uni.navigateTo({ url: `/pages-sub/information/middleDetail?id=${id}` }) uni.navigateTo({ url: `/pages-sub/information/middleDetail?id=${id}` })
} }
@ -88,7 +94,7 @@ const countdown = ref(0)
onLoad(() => { onLoad(() => {
const updateManager = uni.getUpdateManager() const updateManager = uni.getUpdateManager()
updateManager.onCheckForUpdate(function (res) { updateManager.onCheckForUpdate((res) => {
// //
if (res.hasUpdate) { if (res.hasUpdate) {
uni.showToast({ uni.showToast({
@ -98,11 +104,11 @@ onLoad(() => {
} }
}) })
updateManager.onUpdateReady(function () { updateManager.onUpdateReady(() => {
uni.showModal({ uni.showModal({
title: '更新提示', title: '更新提示',
content: '新版本已经准备好,是否重启应用?', content: '新版本已经准备好,是否重启应用?',
success: function (res) { success(res) {
if (res.confirm) { if (res.confirm) {
// applyUpdate // applyUpdate
updateManager.applyUpdate() updateManager.applyUpdate()
@ -111,167 +117,279 @@ onLoad(() => {
}) })
}) })
updateManager.onUpdateFailed(function () { updateManager.onUpdateFailed(() => {
// //
uni.showToast({ uni.showToast({
title: '更新失败', title: '更新失败',
icon: 'none', icon: 'none',
}) })
}) })
}) })
onShow(() => { onShow(() => {
getTopNew({ query: { Top: 3 } }).then(resp => { getTopNew({ query: { Top: 3 } }).then((resp) => {
if (resp.code === 200) { if (resp.code === 200) {
notifies.value = resp.result notifies.value = resp.result
} }
}) })
if (tokenStore.hasLogin) { if (tokenStore.hasLogin) {
getMyScore().then(resp => { getMyScore().then((resp) => {
if (resp.code === 200 && resp.result) { if (resp.code === 200 && resp.result) {
totalScore.value = resp.result.totalScore totalScore.value = resp.result.totalScore
} }
}) })
getFirstPageInfo().then(resp => { getFirstPageInfo().then((resp) => {
if (resp.code === 200) { if (resp.code === 200) {
schoolCount.value = resp.result.schoolCount schoolCount.value = resp.result.schoolCount
wishlistCount.value = resp.result.zyCount wishlistCount.value = resp.result.zyCount
} }
}) })
} }
getCountdown().then(resp => { getCountdown().then((resp) => {
if (resp.code === 200) { if (resp.code === 200) {
countdown.value = resp.result countdown.value = resp.result
} }
}) })
getChannelLiveNoticeInfo()
}) })
</script> </script>
<template> <template>
<view class="custom-background flex flex-col"> <view class="custom-background flex flex-col">
<sar-navbar :fixed="true" fixation-style="top:unset;" <sar-navbar
:root-style="{ '--sar-navbar-bg': `rgba(255, 255, 255, ${opacity})`, '--sar-navbar-height': `${systemInfo?.statusBarHeight + 44}px` }"> :fixed="true" fixation-style="top:unset;"
:root-style="{ '--sar-navbar-bg': `rgba(255, 255, 255, ${opacity})`, '--sar-navbar-height': `${systemInfo?.statusBarHeight + 44}px` }"
>
<template #left> <template #left>
<view class="flex items-center justify-center text-[#333] text-[32rpx] ml-[32rpx]" <view
:style="{ 'padding-top': `${systemInfo?.statusBarHeight}px` }">济南</view> class="ml-[32rpx] flex items-center justify-center text-[32rpx] text-[#333]"
:style="{ 'padding-top': `${systemInfo?.statusBarHeight}px` }"
>
济南
</view>
</template> </template>
<template #title> <template #title>
<view :style="{ 'padding-top': `${systemInfo?.statusBarHeight}px` }" class="flex justify-center"> <view :style="{ 'padding-top': `${systemInfo?.statusBarHeight}px` }" class="flex justify-center">
<view class="h-[32rpx] w-[226rpx] flex justify-center"> <view class="h-[32rpx] w-[226rpx] flex justify-center">
<image class="h-[32rpx] w-[226rpx]" src="https://lwzk.ycymedu.com/img/home/sy_logo.png" <image
mode="aspectFit" /> class="h-[32rpx] w-[226rpx]" src="https://lwzk.ycymedu.com/img/home/sy_logo.png"
mode="aspectFit"
/>
</view> </view>
</view> </view>
</template> </template>
</sar-navbar> </sar-navbar>
<scroll-view scroll-y class="flex-1"> <scroll-view scroll-y class="flex-1">
<view class="mx-[40rpx] bg-[#3D72FD] rounded-[32rpx] min-h-[460rpx] mt-[20rpx] font-[DinBold] overflow-hidden"> <view class="mx-[40rpx] mt-[20rpx] min-h-[460rpx] overflow-hidden rounded-[32rpx] bg-[#3D72FD] font-[DinBold]">
<view <view
class="bg-[#4c7bfc] flex items-baseline justify-center rounded-[32rpx] py-[18rpx] text-white text-[36rpx] font-[JinBuFont]" v-if="countdown > 0"
@click="navigateToCountdownPage" v-if="countdown > 0"> class="flex items-baseline justify-center rounded-[32rpx] bg-[#4c7bfc] py-[18rpx] text-[36rpx] text-white font-[JinBuFont]" @click="navigateToCountdownPage"
>
距离中考还有&nbsp;<text class="text-[52rpx] text-white">{{ countdown }}</text>&nbsp; 距离中考还有&nbsp;<text class="text-[52rpx] text-white">{{ countdown }}</text>&nbsp;
<view class="w-[14rpx] h-[24rpx] ml-[10rpx]"> <view class="ml-[10rpx] h-[24rpx] w-[14rpx]">
<image src="https://lwzk.ycymedu.com/img/home/sy_gengduo.png" mode="scaleToFill" <image
class="w-[14rpx] h-[24rpx]" /> src="https://lwzk.ycymedu.com/img/home/sy_gengduo.png" mode="scaleToFill"
class="h-[24rpx] w-[14rpx]"
/>
</view> </view>
</view> </view>
<view <view
class="bg-[#4c7bfc] flex items-baseline justify-center rounded-[32rpx] py-[18rpx] text-white text-[36rpx] font-[JinBuFont]" v-else
v-else> class="flex items-baseline justify-center rounded-[32rpx] bg-[#4c7bfc] py-[18rpx] text-[36rpx] text-white font-[JinBuFont]"
>
正在考试 正在考试
</view> </view>
<view class="flex items-center justify-between mt-[48rpx] pl-[46rpx] pr-[64rpx]"> <view class="mt-[48rpx] flex items-center justify-between pl-[46rpx] pr-[64rpx]">
<view class="flex flex-col text-white items-center gap-[8rpx]"> <view class="flex flex-col items-center gap-[8rpx] text-white">
<view class="flex items-center"> <view class="flex items-center">
<text class="text-[68rpx] font-700">{{ totalScore }}</text> <text class="text-[68rpx] font-700">{{ totalScore }}</text>
<view class="w-[30rpx] h-[30rpx] ml-[4rpx]"> <view class="ml-[4rpx] h-[30rpx] w-[30rpx]">
<image class="w-[30rpx] h-[30rpx]" <image
src="https://lwzk.ycymedu.com/img/home/sy_bianji.png" mode="scaleToFill" /> class="h-[30rpx] w-[30rpx]"
src="https://lwzk.ycymedu.com/img/home/sy_bianji.png" mode="scaleToFill"
/>
</view> </view>
</view> </view>
<view class="text-[30rpx] text-[#b0c6ff]">预估成绩</view> <view class="text-[30rpx] text-[#b0c6ff]">
预估成绩
</view>
</view> </view>
<view class="flex flex-col text-white items-center gap-[8rpx]"> <view class="flex flex-col items-center gap-[8rpx] text-white">
<view class="flex items-baseline"> <view class="flex items-baseline">
<text class="text-[68rpx] font-700">{{ schoolCount }}</text> <text class="text-[68rpx] font-700">{{ schoolCount }}</text>
<view class="text-[32rpx] ml-[4rpx]"></view> <view class="ml-[4rpx] text-[32rpx]">
</view>
</view>
<view class="text-[30rpx] text-[#b0c6ff]">
适合普高
</view> </view>
<view class="text-[30rpx] text-[#b0c6ff]">适合普高</view>
</view> </view>
<view class="flex flex-col text-white items-center gap-[8rpx]"> <view class="flex flex-col items-center gap-[8rpx] text-white">
<view class="flex items-baseline"> <view class="flex items-baseline">
<text class="text-[68rpx] font-700">{{ wishlistCount }}</text> <text class="text-[68rpx] font-700">{{ wishlistCount }}</text>
<view class="text-[32rpx] ml-[4rpx]"></view> <view class="ml-[4rpx] text-[32rpx]">
</view>
</view>
<view class="text-[30rpx] text-[#b0c6ff]">
志愿表
</view> </view>
<view class="text-[30rpx] text-[#b0c6ff]">志愿表</view>
</view> </view>
</view> </view>
<view <view
class="flex items-center justify-center rounded-[52rpx] py-[28rpx] mx-[74rpx] bg-gradient-to-b from-[#FCFDFF] to-[#B8D3FF] shadow-[0_8px_20px_-6px_rgba(38,129,255,0.39)] my-[48rpx]" class="mx-[74rpx] my-[48rpx] flex items-center justify-center rounded-[52rpx] from-[#FCFDFF] to-[#B8D3FF] bg-gradient-to-b py-[28rpx] shadow-[0_8px_20px_-6px_rgba(38,129,255,0.39)]"
@click="navigateToCreateWish"> @click="navigateToCreateWish"
<view class="w-[40rpx] h-[40rpx]"> >
<image class="w-[40rpx] h-[40rpx]" <view class="h-[40rpx] w-[40rpx]">
src="https://lwzk.ycymedu.com/img/home/sy_chuangjian.png" mode="scaleToFill" /> <image
class="h-[40rpx] w-[40rpx]"
src="https://lwzk.ycymedu.com/img/home/sy_chuangjian.png" mode="scaleToFill"
/>
</view>
<view class="ml-[8rpx] text-[40rpx] text-[#3D72FD] font-600">
创建志愿表
</view> </view>
<view class="text-[40rpx] text-[#3D72FD] ml-[8rpx] font-600">创建志愿表</view>
</view> </view>
</view> </view>
<!-- 咨询 --> <!-- 咨询 -->
<view class="grid grid-cols-2 mx-[32rpx] gap-[24rpx] mt-[60rpx]"> <view class="grid grid-cols-2 mx-[32rpx] mt-[60rpx] gap-[24rpx]">
<view class="rounded-[24rpx] bg-[#F3F4F8] py-[20rpx] pl-[30rpx] pr-[24rpx] flex items-center justify-between" <view
v-for="(item, index) in subMenus" :key="index" @click="navigateToSubPage(item.url)"> v-for="(item, index) in subMenus"
:key="index" class="flex items-center justify-between rounded-[24rpx] bg-[#F3F4F8] py-[20rpx] pl-[30rpx] pr-[24rpx]" @click="navigateToSubPage(item.url)"
>
<text class="text-[34rpx] text-[#333] font-600">{{ item.name }}</text> <text class="text-[34rpx] text-[#333] font-600">{{ item.name }}</text>
<view class="w-[108rpx] h-[108rpx]"> <view class="h-[108rpx] w-[108rpx]">
<image :src="item.imgPath" mode="scaleToFill" class="w-[108rpx] h-[108rpx]" /> <image :src="item.imgPath" mode="scaleToFill" class="h-[108rpx] w-[108rpx]" />
</view> </view>
</view> </view>
</view> </view>
<view class="mx-[30rpx] mt-[60rpx]"> <view class="mx-[30rpx] mt-[60rpx]">
<view class="h-[216rpx]"> <view class="mb-[30rpx] flex items-center">
<image src="https://lwzk.ycymedu.com/img/home/sy_banner.png" mode="scaleToFill" <view class="mr-[12rpx] h-[48rpx] w-[48rpx]">
class="h-[216rpx]" /> <image
src="https://lw-zk.oss-cn-hangzhou.aliyuncs.com/img/home/sy_zhibo.png" mode="scaleToFill"
class="h-[48rpx] w-[48rpx]"
/>
</view>
<text class="text-[40rpx] text-black font-600">最新直播</text>
</view> </view>
<scroll-view scroll-x class="w-full">
<view class="w-full flex gap-[30rpx] whitespace-nowrap" v-if="channelVideoList.length > 0">
<view
v-for="item in channelVideoList"
:key="item.id"
class="h-[430rpx] min-h-[430rpx] min-w-[440rpx] w-[440rpx] rounded-[18rpx] bg-[#F3F4F8]"
>
<view class="relative h-[280rpx] rounded-[18rpx]">
<image
:src="item.coverUrl"
mode="scaleToFill"
class="h-full w-full"
/>
<view v-if="item.type === 'live'" class="absolute bottom-0 h-[48rpx] w-full flex items-center justify-between rounded-[0_0_18rpx_18rpx] bg-black bg-opacity-50">
<view class="ml-[20rpx] flex items-center">
<view class="mr-[8rpx] h-[12rpx] w-[12rpx] rounded-full bg-white" />
<view class="text-[24rpx] text-white">
{{ item.statusText }}
</view>
</view>
<view v-if="item.nickname" class="mr-[20rpx] flex items-center">
<view class="mr-[10rpx] h-[20rpx] w-[30rpx] flex items-center">
<image
src="https://lw-zk.oss-cn-hangzhou.aliyuncs.com/img/home/sy_liulan.png"
mode="widthFix"
/>
</view>
<view class="text-[24rpx] text-white">
{{ item.nickname }}
</view>
</view>
</view>
<view v-else class="absolute bottom-0 h-[48rpx] w-full flex items-center justify-between rounded-[0_0_18rpx_18rpx] bg-black bg-opacity-50">
<view class="ml-[20rpx] text-[24rpx] text-white">
{{ item.startTimeText }}
</view>
</view>
</view>
<view class="ml-[20rpx] mt-[20rpx] text-[34rpx] text-[#333] font-500">
{{ item.title }}
</view>
<view
class="ml-[20rpx] mt-[10rpx] h-[52rpx] w-[148rpx] flex items-center justify-center rounded-[376rpx] text-[26rpx]"
:class="item.type === 'live' ? 'text-white bg-[#3D72FD] border-solid border-[2rpx] border-[#3D72FD]' : 'bg-[#E0E6F8] text-[#3D72FD]'"
@click="handleChannelVideoAction(item)"
>
{{ item.buttonText }}
</view>
</view>
</view>
<view class="w-full flex flex-col items-center justify-center" v-if="channelVideoList.length === 0">
<view class="w-[236rpx] h-[236rpx]">
<image
src="https://lw-zk.oss-cn-hangzhou.aliyuncs.com/img/home/sy_zanweikaobo.png"
mode="scaleToFill"
/>
</view>
<view @click="goToLiveHomePage()" class="mt-[20rpx] flex items-center justify-center rounded-[376rpx] text-[26rpx] text-white bg-[#3D72FD] border-solid border-[2rpx] border-[#3D72FD] px-[40rpx] py-[12rpx]"> 关注官方视频号 </view>
</view>
</scroll-view>
</view> </view>
<view class="mx-[30rpx] mt-[60rpx]"> <view class="mx-[30rpx] mt-[60rpx]">
<view class="flex items-center mb-[30rpx]"> <view class="mb-[30rpx] flex items-center">
<view class="h-[48rpx] w-[48rpx] mr-[12rpx]"> <view class="mr-[12rpx] h-[48rpx] w-[48rpx]">
<image src="https://lwzk.ycymedu.com/img/home/sy_zixuntun.png" mode="scaleToFill" <image
class="h-[48rpx] w-[48rpx]" /> src="https://lwzk.ycymedu.com/img/home/sy_zixuntun.png" mode="scaleToFill"
class="h-[48rpx] w-[48rpx]"
/>
</view> </view>
<text class="text-black text-[40rpx] font-600">最新资讯</text> <text class="text-[40rpx] text-black font-600">最新资讯</text>
<view class="text-[30rpx] text-[#A6A6A6] ml-auto" @click="navigateToNewsPage"></view> <view class="ml-auto text-[30rpx] text-[#A6A6A6]" @click="navigateToNewsPage">
<view class="w-[14rpx] h-[30rpx] flex items-center"> 更多
<image class="w-[14rpx] h-[30rpx]" </view>
src="https://lwzk.ycymedu.com/img/home/sy_zixunjiantou.png" mode="scaleToFill" /> <view class="h-[30rpx] w-[14rpx] flex items-center">
<image
class="h-[30rpx] w-[14rpx]"
src="https://lwzk.ycymedu.com/img/home/sy_zixunjiantou.png" mode="scaleToFill"
/>
</view> </view>
</view> </view>
<view class="flex flex-col border-b-1 border-b-solid border-[#eee] pb-[22rpx] mb-[28rpx]" <view
v-for="(value, index) in notifies" :key="index" @click="navigateToNewsDetail(value.id)"> v-for="(value, index) in notifies"
<text class="text-[#333] text-[32rpx] font-400">{{ value.title }}</text> :key="index" class="mb-[28rpx] flex flex-col border-b-1 border-[#eee] border-b-solid pb-[22rpx]" @click="navigateToNewsDetail(value.id)"
<view class="flex items-center justify-between text-[#999] mt-[20rpx]"> >
<text class="text-[32rpx] text-[#333] font-400">{{ value.title }}</text>
<view class="mt-[20rpx] flex items-center justify-between text-[#999]">
<view class="flex items-center"> <view class="flex items-center">
<view class="w-[28rpx] h-[28rpx] mr-[10rpx] flex items-center"> <view class="mr-[10rpx] h-[28rpx] w-[28rpx] flex items-center">
<image src="https://lwzk.ycymedu.com/img/home/sy_shijian.png" mode="scaleToFill" <image
class="w-[28rpx] h-[28rpx]" /> src="https://lwzk.ycymedu.com/img/home/sy_shijian.png" mode="scaleToFill"
class="h-[28rpx] w-[28rpx]"
/>
</view> </view>
<text class="text-[26rpx]">{{ value.pubtime }}</text> <text class="text-[26rpx]">{{ value.pubtime }}</text>
</view> </view>
<view class="flex items-center"> <view class="flex items-center">
<view class="w-[28rpx] h-[28rpx] mr-[10rpx] flex items-center"> <view class="mr-[10rpx] h-[28rpx] w-[28rpx] flex items-center">
<image src="https://lwzk.ycymedu.com/img/home/sy_chakan.png" mode="scaleToFill" <image
class="w-[28rpx] h-[28rpx]" /> src="https://lwzk.ycymedu.com/img/home/sy_chakan.png" mode="scaleToFill"
class="h-[28rpx] w-[28rpx]"
/>
</view> </view>
<text class="text-[26rpx]">{{ value.viewcount }}</text> <text class="text-[26rpx]">{{ value.viewcount }}</text>
</view> </view>
@ -279,9 +397,11 @@ onShow(() => {
</view> </view>
</view> </view>
<view class="fixed bottom-[200rpx] right-[60rpx]" @click="navigateToCustom"> <view class="fixed bottom-[200rpx] right-[60rpx]" @click="navigateToCustom">
<view class="w-[160rpx] h-[160rpx]"> <view class="h-[160rpx] w-[160rpx]">
<image class="w-[160rpx] h-[160rpx]" src="https://lwzk.ycymedu.com/img/home/sy_kefu.png" <image
mode="scaleToFill" /> class="h-[160rpx] w-[160rpx]" src="https://lwzk.ycymedu.com/img/home/sy_kefu.png"
mode="scaleToFill"
/>
</view> </view>
</view> </view>
</scroll-view> </scroll-view>
@ -290,7 +410,7 @@ onShow(() => {
<style scoped> <style scoped>
.custom-background { .custom-background {
background: linear-gradient(180deg, #C7E0FF 0%, rgba(199, 224, 255, 0) 530rpx); background: linear-gradient(180deg, #c7e0ff 0%, rgba(199, 224, 255, 0) 530rpx);
background-position: 50% 50%; background-position: 50% 50%;
background-origin: padding-box; background-origin: padding-box;
background-clip: border-box; background-clip: border-box;

View File

@ -1,9 +1,9 @@
<script lang="ts" setup> <script lang="ts" setup>
import { safeAreaInsets,systemInfo } from '@/utils/systemInfo'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { useUserStore } from '@/store' import { useUserStore } from '@/store'
import { useTokenStore } from '@/store/token' import { useTokenStore } from '@/store/token'
import { safeAreaInsets, systemInfo } from '@/utils/systemInfo'
// #ifdef MP-WEIXIN // #ifdef MP-WEIXIN
definePage({ definePage({
@ -19,7 +19,7 @@ definePage({
style: { style: {
navigationStyle: 'custom', navigationStyle: 'custom',
transparentTitle: 'always', transparentTitle: 'always',
navigationBarTitleText: '' navigationBarTitleText: '',
}, },
excludeLoginPath: true, excludeLoginPath: true,
}) })
@ -30,88 +30,107 @@ const tokenStore = useTokenStore()
// 使storeToRefsuserInfo // 使storeToRefsuserInfo
const { userInfo } = storeToRefs(userStore) const { userInfo } = storeToRefs(userStore)
const topMenuList = [{ name: '我的志愿表', img: 'https://lwzk.ycymedu.com/img/qt/wd_biao.png', url: '/pages-sub/me/wishlist' }, { name: '我的测评', img: 'https://lwzk.ycymedu.com/img/qt/wd_cp.png', url: '/pages-sub/me/evaluation' }]
const bottomMenuList = [
{ name: '收藏院校', img: 'https://lwzk.ycymedu.com/img/qt/wd_shoucang.png', url: '/pages-sub/me/starSchool' },
{ name: '联系客服', img: 'https://lwzk.ycymedu.com/img/qt/wd_kefu.png', url: '/pages-sub/about/onlineCustom' },
{ name: '关于我们', img: 'https://lwzk.ycymedu.com/img/qt/wd_guanyu.png', url: '/pages-sub/about/about' },
{ name: '直播码注册', img: 'https://lw-zk.oss-cn-hangzhou.aliyuncs.com/liebian/wd_liebian.png', url: '/pages-sub/invite/login' },
]
function navigateToUrl(url: string) {
const topMenuList = [{ name: '我的志愿表', img: 'https://lwzk.ycymedu.com/img/qt/wd_biao.png',url:'/pages-sub/me/wishlist' }, { name: "我的测评", img: 'https://lwzk.ycymedu.com/img/qt/wd_cp.png',url:'/pages-sub/me/evaluation' }] uni.navigateTo({ url })
const bottomMenuList = [{ name: '收藏院校', img: 'https://lwzk.ycymedu.com/img/qt/wd_shoucang.png',url:"/pages-sub/me/starSchool" }, { name: '联系客服', img: 'https://lwzk.ycymedu.com/img/qt/wd_kefu.png', url:"/pages-sub/about/onlineCustom" }, { name: '关于我们', img: 'https://lwzk.ycymedu.com/img/qt/wd_guanyu.png',url:'/pages-sub/about/about' }]
const navigateToUrl = (url:string) => {
uni.navigateTo({url})
} }
const navigateToUserInfo = () => { function navigateToUserInfo() {
uni.navigateTo({url:"/pages-sub/about/userInfo"}) uni.navigateTo({ url: '/pages-sub/about/userInfo' })
} }
</script> </script>
<template> <template>
<view :class="`custom-background`" :style="{height:`calc(100vh - 50px - ${safeAreaInsets.bottom}px)`}"> <view class="custom-background" :style="{ height: `calc(100vh - 50px - ${safeAreaInsets.bottom}px)` }">
<sar-navbar :fixed="true" fixation-style="top:unset;" <sar-navbar
:root-style="{ '--sar-navbar-bg': `transparent`, '--sar-navbar-height': `${systemInfo?.statusBarHeight + 44}px` }"> :fixed="true" fixation-style="top:unset;"
:root-style="{ '--sar-navbar-bg': `transparent`, '--sar-navbar-height': `${systemInfo?.statusBarHeight + 44}px` }"
>
<template #title> <template #title>
<view :style="{ 'padding-top': `${systemInfo?.statusBarHeight}px` }" class="flex justify-center"> <view :style="{ 'padding-top': `${systemInfo?.statusBarHeight}px` }" class="flex justify-center">
我的 我的
</view> </view>
</template> </template>
</sar-navbar> </sar-navbar>
<view class="flex items-center justify-between px-[50rpx] my-[50rpx]"> <view class="my-[50rpx] flex items-center justify-between px-[50rpx]">
<view class="flex"> <view class="flex">
<view class="w-[132rpx] h-[132rpx]"> <view class="h-[132rpx] w-[132rpx]">
<image :src="`${userInfo.avatar}`" mode="scaleToFill" <image
class="w-[132rpx] h-[132rpx] rounded-full" /> :src="`${userInfo.avatar}`" mode="scaleToFill"
class="h-[132rpx] w-[132rpx] rounded-full"
/>
</view> </view>
<view class="ml-[32rpx]"> <view class="ml-[32rpx]">
<view class="text-[44rpx] font-500 mb-[10rpx]">{{ userInfo.nickName }}</view> <view class="mb-[10rpx] text-[44rpx] font-500">
<view class="text-[#666] text-[32rpx]">{{ userInfo.mobile }}</view> {{ userInfo.nickName }}
</view>
<view class="text-[32rpx] text-[#666]">
{{ userInfo.mobile }}
</view>
</view> </view>
</view> </view>
<view class="flex flex-col items-center" @click="navigateToUserInfo"> <view class="flex flex-col items-center" @click="navigateToUserInfo">
<view class="w-[48rpx] h-[48rpx]"> <view class="h-[48rpx] w-[48rpx]">
<image src="https://lwzk.ycymedu.com/img/qt/wd_bianji.png" mode="scaleToFill" <image
class="w-[48rpx] h-[48rpx]" /> src="https://lwzk.ycymedu.com/img/qt/wd_bianji.png" mode="scaleToFill"
class="h-[48rpx] w-[48rpx]"
/>
</view>
<view class="text-[30rpx] text-[#333]">
编辑
</view> </view>
<view class="text-[30rpx] text-[#333]">编辑</view>
</view> </view>
</view> </view>
<view class="grid grid-cols-2 gap-[28rpx] items-center justify-between px-[30rpx]"> <view class="grid grid-cols-2 items-center justify-between gap-[28rpx] px-[30rpx]">
<view <view
class="rounded-[20rpx] flex items-center px-[50rpx] py-[44rpx] bg-[#E4EEFF] border-[2rpx] border-solid border-white" v-for="(value, index) in topMenuList"
v-for="(value, index) in topMenuList" :key="index" @click="navigateToUrl(value.url)"> :key="index" class="flex items-center border-[2rpx] border-white rounded-[20rpx] border-solid bg-[#E4EEFF] px-[50rpx] py-[44rpx]" @click="navigateToUrl(value.url)"
<view class="w-[48rpx] h-[48rpx] mr-[10rpx]"> >
<image :src="value.img" mode="scaleToFill" class="w-[48rpx] h-[48rpx]" /> <view class="mr-[10rpx] h-[48rpx] w-[48rpx]">
<image :src="value.img" mode="scaleToFill" class="h-[48rpx] w-[48rpx]" />
</view> </view>
<view>{{ value.name }}</view> <view>{{ value.name }}</view>
</view> </view>
</view> </view>
<view class="px-[30rpx] pt-[30rpx] mx-[30rpx] rounded-[20rpx] bg-white mt-[30rpx]"> <view class="mx-[30rpx] mt-[30rpx] rounded-[20rpx] bg-white px-[30rpx] pt-[30rpx]">
<view class="text-[34rpx] font-500">其他功能</view> <view class="text-[34rpx] font-500">
<view class="flex items-center py-[34rpx] not-last:border-b-[2rpx] not-last:border-b-solid not-last:border-b-[#EDEDED]" v-for="(value,index) in bottomMenuList" :key="index" @click="navigateToUrl(value.url)"> 其他功能
<view class="w-[44rpx] h-[44rpx] flex items-center"> </view>
<view v-for="(value, index) in bottomMenuList" :key="index" class="flex items-center py-[34rpx] not-last:border-b-[2rpx] not-last:border-b-[#EDEDED] not-last:border-b-solid" @click="navigateToUrl(value.url)">
<view class="h-[44rpx] w-[44rpx] flex items-center">
<image <image
:src="value.img" :src="value.img"
mode="scaleToFill" mode="scaleToFill"
class="w-[44rpx] h-[44rpx]" class="h-[44rpx] w-[44rpx]"
/> />
</view> </view>
<view class="text-[32rpx] ml-[16rpx]">{{ value.name }}</view> <view class="ml-[16rpx] text-[32rpx]">
<view class="w-[18rpx] h-[36rpx] ml-auto flex items-center"> {{ value.name }}
</view>
<view class="ml-auto h-[36rpx] w-[18rpx] flex items-center">
<image <image
src="https://lwzk.ycymedu.com/img/qt/tb_jiantou.png" src="https://lwzk.ycymedu.com/img/qt/tb_jiantou.png"
mode="scaleToFill" mode="scaleToFill"
class="w-[18rpx] h-[36rpx]" class="h-[36rpx] w-[18rpx]"
/> />
</view> </view>
</view> </view>
</view> </view>
</view> </view>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.custom-background { .custom-background {
background: linear-gradient(180deg, #C7E0FF 0%, #f8f8f8 530rpx); background: linear-gradient(180deg, #c7e0ff 0%, #f8f8f8 530rpx);
background-position: 50% 50%; background-position: 50% 50%;
background-origin: padding-box; background-origin: padding-box;
background-clip: border-box; background-clip: border-box;

View File

@ -6,3 +6,4 @@ export * from './listAll';
export * from './info'; export * from './info';
export * from "./requestApi" export * from "./requestApi"
export * from './invite'

116
src/service/invite.ts Normal file
View File

@ -0,0 +1,116 @@
export interface CreatePromoterParams {
name: string
phone: string
referralCode?: string
}
export interface Promoter {
_id?: string
id: string
name: string
phone: string
referralCode: string
promotionCount: number
promotionUrl: string
createdAt: string
updatedAt: string
}
type InviteErrorData = {
message?: string
msg?: string
error?: string
}
const INVITE_API_URL = 'https://liveroom.ycymedu.com/api/h5/promoters'
const INVITE_API_KEY = '28fd3c9a8739424ff5f38'
export const createPromoter = (params: CreatePromoterParams) => {
return new Promise<Promoter>((resolve, reject) => {
uni.request({
url: INVITE_API_URL,
method: 'POST',
data: params,
header: {
'Content-Type': 'application/json',
'x-api-key': INVITE_API_KEY,
},
success: (res) => {
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(res.data as Promoter)
return
}
reject(new Error(getInviteErrorMessage(res.data)))
},
fail: () => {
reject(new Error('提交失败,请稍后重试'))
},
})
})
}
const getInviteErrorMessage = (data: unknown) => {
const errorData = data as InviteErrorData | undefined
return errorData?.message || errorData?.msg || errorData?.error || '提交失败,请稍后重试'
}
const REDIRECT_API_URL = 'https://liveroom.ycymedu.com/api/h5/promotion-visits'
export const trackPromoterRedirect = (referralCode: string) => {
return new Promise<unknown>((resolve, reject) => {
uni.request({
url: REDIRECT_API_URL,
method: 'POST',
data: { from: referralCode },
header: {
'Content-Type': 'application/json',
'x-api-key': INVITE_API_KEY,
},
success: (res) => {
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(res.data)
return
}
reject(new Error(getInviteErrorMessage(res.data)))
},
fail: () => {
reject(new Error('请求失败,请稍后重试'))
},
})
})
}
export interface LiveLinks {
id: string
wechatLiveUrl: string
douyinLiveUrl: string
updatedAt: string
}
const LIVE_LINKS_API_URL = 'https://liveroom.ycymedu.com/api/h5/live-links'
export const getLiveLinks = () => {
return new Promise<LiveLinks>((resolve, reject) => {
uni.request({
url: LIVE_LINKS_API_URL,
method: 'GET',
header: {
'x-api-key': INVITE_API_KEY,
},
success: (res) => {
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(res.data as LiveLinks)
return
}
reject(new Error(getInviteErrorMessage(res.data)))
},
fail: () => {
reject(new Error('请求失败,请稍后重试'))
},
})
})
}

View File

@ -157,6 +157,20 @@ export const getSchoolHistoricalScores = (options: { query: any }) => {
}); });
} }
export const getBatchLine = (options: { query: any }) => {
return request<API.Response>('/api/busBatchLine/h5List', {
method: 'GET',
...options,
});
}
export const getRankTable = (options: { query: any }) => {
return request<API.Response>('/api/busScoreSection/h5List', {
method: 'GET',
...options,
});
}
export const getSchoolNature = () => { export const getSchoolNature = () => {
return request<API.Response>('/api/zhiYuan/naturelist', { return request<API.Response>('/api/zhiYuan/naturelist', {
method: 'GET', method: 'GET',
@ -244,6 +258,12 @@ export const getMyScore = () => {
}); });
} }
export const getShowAiStatus = () => {
return request<API.Response>('/api/sysDictData/detail?Status=1&Id=812248362119237 ', {
method: 'GET',
});
}
export const saveMyScore = (options: { data: any }) => { export const saveMyScore = (options: { data: any }) => {
return request<API.Response>('/api/busMiddleSchoolApply/add', { return request<API.Response>('/api/busMiddleSchoolApply/add', {
method: "POST", method: "POST",
@ -299,4 +319,11 @@ export const getHistoryYearList = () => {
return request<API.Response>('/api/busSchoolAdmission/historicalYears', { return request<API.Response>('/api/busSchoolAdmission/historicalYears', {
method: 'GET', method: 'GET',
}); });
}
export const saveUserAreaScore = (options: { data: any }) => {
return request<API.Response>('/api/busMiddleSchoolApply/add', {
method: "POST",
...options
})
} }

BIN
src/static/tabbar/ai.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

79
src/store/ai.ts Normal file
View File

@ -0,0 +1,79 @@
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
export type AiRole = 'user' | 'assistant' | 'system'
export interface AiMessage {
id: string
role: AiRole
content: string
/** 是否仍在流式生成中,用于 UI 展示打字光标 */
pending?: boolean
/** 标记本条消息是否出错UI 显示重试按钮 */
error?: boolean
/** 仅 assistant 消息使用:本轮回答结束后由大模型给出的追问建议 */
recommendQuestions?: string[]
createdAt: number
}
export const useAiStore = defineStore(
'ai',
() => {
const messages = ref<AiMessage[]>([])
/** 一组对话共用同一个 thread跨多轮维持上下文 */
const threadId = ref('')
const messageCount = computed(() => messages.value.length)
function ensureThreadId() {
if (!threadId.value) {
threadId.value = `thread_id_${Date.now()}${Math.floor(Math.random() * 10000)}`
}
return threadId.value
}
function addMessage(message: AiMessage) {
messages.value.push(message)
}
function updateMessage(id: string, patch: Partial<AiMessage>) {
const target = messages.value.find(m => m.id === id)
if (!target) {
return
}
Object.assign(target, patch)
}
function appendDelta(id: string, delta: string) {
const target = messages.value.find(m => m.id === id)
if (!target) {
return
}
target.content += delta
}
function removeMessage(id: string) {
messages.value = messages.value.filter(m => m.id !== id)
}
function clear() {
messages.value = []
threadId.value = ''
}
return {
messages,
threadId,
messageCount,
ensureThreadId,
addMessage,
updateMessage,
appendDelta,
removeMessage,
clear,
}
},
{
persist: true,
},
)

View File

@ -16,6 +16,8 @@ setActivePinia(store)
export default store export default store
// 模块统一导出 // 模块统一导出
export * from './ai'
export * from './invite'
export * from './token' export * from './token'
export * from './user' export * from './user'
export * from "./wishlist" export * from './wishlist'

30
src/store/invite.ts Normal file
View File

@ -0,0 +1,30 @@
import type { Promoter } from '@/service/invite'
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
export const useInviteStore = defineStore(
'invite',
() => {
const promoter = ref<Promoter | null>(null)
const currentPromoter = computed(() => promoter.value)
const setPromoter = (val: Promoter) => {
promoter.value = val
}
const clearPromoter = () => {
promoter.value = null
uni.removeStorageSync('invite')
}
return {
promoter,
currentPromoter,
setPromoter,
clearPromoter,
}
},
{
persist: true,
},
)

View File

@ -61,6 +61,7 @@ export interface CustomTabBarItem {
iconType: 'uiLib' | 'unocss' | 'iconfont' | 'image' // 不建议用 image 模式需要配置2张图 iconType: 'uiLib' | 'unocss' | 'iconfont' | 'image' // 不建议用 image 模式需要配置2张图
icon: any // 其实是 string 类型,这里是为了避免 ts 报错 (tabbar/index.vue 里面 uni-icons 那行) icon: any // 其实是 string 类型,这里是为了避免 ts 报错 (tabbar/index.vue 里面 uni-icons 那行)
iconActive?: string // 只有在 image 模式下才需要传递的是高亮的图片PS 不建议用 image 模式) iconActive?: string // 只有在 image 模式下才需要传递的是高亮的图片PS 不建议用 image 模式)
openType?: 'switchTab' | 'navigateTo'
badge?: CustomTabBarItemBadge badge?: CustomTabBarItemBadge
isBulge?: boolean // 是否是中间的鼓包tabbarItem isBulge?: boolean // 是否是中间的鼓包tabbarItem
} }
@ -89,6 +90,18 @@ export const customTabbarList: CustomTabBarItem[] = [
iconActive: '/static/tabbar/talented-active.png', iconActive: '/static/tabbar/talented-active.png',
// badge: 'dot', // badge: 'dot',
}, },
{
text: '',
pagePath: 'pages-ai/ai/index',
// 注意 unocss 图标需要如下处理:(二选一)
// 1在fg-tabbar.vue页面上引入一下并注释掉见tabbar/index.vue代码第2行
// 2配置到 unocss.config.ts 的 safelist 中
iconType: 'image',
icon: '/static/tabbar/ai.png',
iconActive: '/static/tabbar/ai.png',
openType: 'navigateTo',
// badge: 'dot',
},
{ {
text: '测评', text: '测评',
pagePath: 'pages/evaluation/index', pagePath: 'pages/evaluation/index',
@ -157,7 +170,11 @@ export const customTabbarEnable
*/ */
export const needHideNativeTabbar = selectedTabbarStrategy === TABBAR_STRATEGY_MAP.CUSTOM_TABBAR_WITH_CACHE export const needHideNativeTabbar = selectedTabbarStrategy === TABBAR_STRATEGY_MAP.CUSTOM_TABBAR_WITH_CACHE
const _tabbarList = customTabbarEnable ? customTabbarList.map(item => ({ text: item.text, pagePath: item.pagePath })) : nativeTabbarList const _tabbarList = customTabbarEnable
? customTabbarList
.filter(item => item.openType !== 'navigateTo')
.map(item => ({ text: item.text, pagePath: item.pagePath }))
: nativeTabbarList
export const tabbarList = customTabbarEnable ? customTabbarList : nativeTabbarList export const tabbarList = customTabbarEnable ? customTabbarList : nativeTabbarList
const _tabbar: TabBar = { const _tabbar: TabBar = {

View File

@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
// i-carbon-code // i-carbon-code
import type { CustomTabBarItem } from './config' import type { CustomTabBarItem } from './config'
import { getShowAiStatus } from '@/service'
import { customTabbarEnable, needHideNativeTabbar, tabbarCacheEnable } from './config' import { customTabbarEnable, needHideNativeTabbar, tabbarCacheEnable } from './config'
import { tabbarList, tabbarStore } from './store' import { tabbarList, tabbarStore } from './store'
@ -22,17 +23,23 @@ function handleClickBulge() {
} }
function handleClick(index: number) { function handleClick(index: number) {
const item = tabbarList[index]
//
if (index === tabbarStore.curIdx) { if (item.isBulge) {
return
}
if (tabbarList[index].isBulge) {
handleClickBulge() handleClickBulge()
return return
} }
const url = tabbarList[index].pagePath //
if (item.openType !== 'navigateTo' && index === tabbarStore.curIdx) {
return
}
const url = item.pagePath
if (item.openType === 'navigateTo') {
uni.navigateTo({ url })
return
}
tabbarStore.setCurIdx(index) tabbarStore.setCurIdx(index)
if (tabbarCacheEnable) { if (tabbarCacheEnable) {
uni.switchTab({ url }) uni.switchTab({ url })
@ -41,6 +48,26 @@ function handleClick(index: number) {
uni.navigateTo({ url }) uni.navigateTo({ url })
} }
} }
const aiShowStatus = ref("0")
function removeNavigateToItem() {
const index = tabbarList.findIndex(item => item.openType === 'navigateTo')
if (index !== -1) {
tabbarList.splice(index, 1)
}
}
function aiShowStatusFn() {
getShowAiStatus().then((resp) => {
if (resp.code === 200) {
aiShowStatus.value = resp.result.value
if (aiShowStatus.value === "0") {
removeNavigateToItem()
}
}
})
}
// #ifndef MP-WEIXIN // #ifndef MP-WEIXIN
// custom:true hide // custom:true hide
onLoad(() => { onLoad(() => {
@ -69,6 +96,10 @@ function getImageByIndex(index: number, item: CustomTabBarItem) {
} }
return tabbarStore.curIdx === index ? item.iconActive : item.icon return tabbarStore.curIdx === index ? item.iconActive : item.icon
} }
onBeforeMount(() => {
aiShowStatusFn()
})
</script> </script>
<template> <template>
@ -89,7 +120,7 @@ function getImageByIndex(index: number, item: CustomTabBarItem) {
<image class="mt-6rpx h-200rpx w-200rpx" src="/static/tabbar/scan.png" /> <image class="mt-6rpx h-200rpx w-200rpx" src="/static/tabbar/scan.png" />
</view> </view>
</view> </view>
<view v-else class="relative px-3 flex flex-col justify-center items-center"> <view v-else class="relative flex flex-col items-center justify-center px-3">
<template v-if="item.iconType === 'uiLib'"> <template v-if="item.iconType === 'uiLib'">
<!-- TODO: 以下内容请根据选择的UI库自行替换 --> <!-- TODO: 以下内容请根据选择的UI库自行替换 -->
<!-- <wd-icon name="home" /> (https://wot-design-uni.cn/component/icon.html) --> <!-- <wd-icon name="home" /> (https://wot-design-uni.cn/component/icon.html) -->
@ -101,11 +132,14 @@ function getImageByIndex(index: number, item: CustomTabBarItem) {
<view :class="item.icon" class="text-20px" /> <view :class="item.icon" class="text-20px" />
</template> </template>
<template v-if="item.iconType === 'image'"> <template v-if="item.iconType === 'image'">
<view class="h-24px w-24px"> <view v-if="item.openType !== 'navigateTo'" class="h-24px w-24px">
<image :src="getImageByIndex(index, item)" mode="scaleToFill" class="h-24px w-24px" /> <image :src="getImageByIndex(index, item)" mode="scaleToFill" class="h-24px w-24px" />
</view> </view>
<view v-if="item.openType === 'navigateTo' && aiShowStatus" class="h-48px w-42px">
<image :src="getImageByIndex(index, item)" mode="scaleToFill" class="h-48px w-42px" />
</view>
</template> </template>
<view class="mt-2px text-14px"> <view v-if="item.text" class="mt-2px text-14px">
{{ item.text }} {{ item.text }}
</view> </view>
<!-- 角标显示 --> <!-- 角标显示 -->

View File

@ -26,7 +26,7 @@ if (customTabbarEnable && BULGE_ENABLE) {
export function isPageTabbar(path: string) { export function isPageTabbar(path: string) {
const _path = path.split('?')[0] const _path = path.split('?')[0]
return tabbarList.some(item => item.pagePath === _path) return tabbarList.some(item => item.openType !== 'navigateTo' && item.pagePath === _path)
} }
/** /**
@ -56,13 +56,13 @@ const tabbarStore = reactive({
this.setCurIdx(0) this.setCurIdx(0)
return return
} }
const index = tabbarList.findIndex(item => item.pagePath === path) const index = tabbarList.findIndex(item => item.openType !== 'navigateTo' && item.pagePath === path)
FG_LOG_ENABLE && console.log('index:', index, path) FG_LOG_ENABLE && console.log('index:', index, path)
// console.log('tabbarList:', tabbarList) // console.log('tabbarList:', tabbarList)
if (index === -1) { if (index === -1) {
const pagesPathList = getCurrentPages().map(item => item.route.startsWith('/') ? item.route : `/${item.route}`) const pagesPathList = getCurrentPages().map(item => item.route.startsWith('/') ? item.route : `/${item.route}`)
// console.log(pagesPathList) // console.log(pagesPathList)
const flag = tabbarList.some(item => pagesPathList.includes(item.pagePath)) const flag = tabbarList.some(item => item.openType !== 'navigateTo' && pagesPathList.includes(item.pagePath))
if (!flag) { if (!flag) {
this.setCurIdx(0) this.setCurIdx(0)
return return

View File

@ -108,8 +108,6 @@ export function getCurrentPageI18nKey() {
console.warn('路由不正确') console.warn('路由不正确')
return '' return ''
} }
console.log(currPage)
console.log(currPage.style.navigationBarTitleText)
return currPage.style?.navigationBarTitleText || '' return currPage.style?.navigationBarTitleText || ''
} }
@ -120,10 +118,13 @@ export function getEnvBaseUrl() {
// 请求基准地址 // 请求基准地址
let baseUrl = import.meta.env.VITE_SERVER_BASEURL let baseUrl = import.meta.env.VITE_SERVER_BASEURL
// https://senior.ycymedu.com 六纬中考通
// https://liebian.ycymedu.com 智能中专学校的
// https://xqwgy.ycymedu.com/ 深泉外国语学院
// # 有些同学可能需要在微信小程序里面根据 develop、trial、release 分别设置上传地址,参考代码如下。 // # 有些同学可能需要在微信小程序里面根据 develop、trial、release 分别设置上传地址,参考代码如下。
const VITE_SERVER_BASEURL__WEIXIN_DEVELOP = 'https://senior.ycymedu.com' const VITE_SERVER_BASEURL__WEIXIN_DEVELOP = 'https://xqwgy.ycymedu.com'
const VITE_SERVER_BASEURL__WEIXIN_TRIAL = 'https://senior.ycymedu.com' const VITE_SERVER_BASEURL__WEIXIN_TRIAL = 'https://xqwgy.ycymedu.com'
const VITE_SERVER_BASEURL__WEIXIN_RELEASE = 'https://senior.ycymedu.com' const VITE_SERVER_BASEURL__WEIXIN_RELEASE = 'https://xqwgy.ycymedu.com'
// 微信小程序端环境区分 // 微信小程序端环境区分
if (isMpWeixin) { if (isMpWeixin) {
@ -158,9 +159,9 @@ export const isDoubleTokenMode = import.meta.env.VITE_AUTH_MODE === 'double'
*/ */
export const HOME_PAGE = `/${(pages as PageMetaDatum[]).find(page => page.type === 'home')?.path || (pages as PageMetaDatum[])[0].path}` export const HOME_PAGE = `/${(pages as PageMetaDatum[]).find(page => page.type === 'home')?.path || (pages as PageMetaDatum[])[0].path}`
export const checkEmptyValues=(obj: Record<string, any>, keys: string[]): boolean=> { export function checkEmptyValues(obj: Record<string, any>, keys: string[]): boolean {
return keys.some(key => { return keys.some((key) => {
const value = obj[key]; const value = obj[key]
return value === '' || value === null || value === undefined; return value === '' || value === null || value === undefined
}); })
} }

View File

@ -1,7 +1,7 @@
/* eslint-disable import/no-mutable-exports */ /* eslint-disable import/no-mutable-exports */
// 获取屏幕边界到安全区域距离 // 获取屏幕边界到安全区域距离
let systemInfo let systemInfo: any
let safeAreaInsets let safeAreaInsets: any
let headerBarHeight = 0 let headerBarHeight = 0
// #ifdef MP-WEIXIN // #ifdef MP-WEIXIN

View File

@ -0,0 +1,231 @@
// components/agent-ui-new/chatFIle/chatFile.js
import { getCloudInstance, compareVersions, commonRequest } from "../tools";
Component({
lifetimes: {
attached: async function () {
console.log("enableDel", this.data.enableDel);
const { tempFileName, rawFileName, rawType, tempPath, fileId, botId, status } = this.data.fileData;
const type = this.getFileType(rawFileName || tempFileName);
console.log("type", type);
if (!fileId) {
this.setData({
iconPath: "../imgs/" + type + ".svg",
});
this.triggerEvent("changeChild", { tempId: this.data.fileData.tempId, status: "uploading" });
}
if (fileId && status === "parsed") {
this.setData({
iconPath: "../imgs/" + type + ".svg",
});
return;
}
const cloudInstance = await getCloudInstance();
// console.log('file', cloudInstance)
// 上传云存储获取 fileId
// console.log('rawFileName tempFileName tempPath', rawFileName, tempFileName, tempPath)
cloudInstance.uploadFile({
cloudPath: this.generateCosUploadPath(
botId,
rawFileName ? rawFileName.split(".")[0] + "-" + tempFileName : tempFileName
), // 云上文件路径
filePath: tempPath,
success: async (res) => {
const appBaseInfo = wx.getAppBaseInfo();
const fileId = res.fileID;
console.log("当前版本", appBaseInfo.SDKVersion);
if (botId.startsWith("ibot")) {
this.triggerEvent("changeChild", { tempId: this.data.fileData.tempId, fileId, status: "parsed" });
} else {
this.triggerEvent("changeChild", { tempId: this.data.fileData.tempId, status: "parsing" });
commonRequest({
path: `bots/${botId}/files`,
data: {
fileList: [
{
fileName: rawFileName || tempFileName,
fileId,
type: rawType,
},
],
}, // any
method: "POST",
timeout: 60000,
success: (res) => {
console.log("resolve agent file res", res);
this.triggerEvent("changeChild", { tempId: this.data.fileData.tempId, fileId, status: "parsed" });
},
fail: (e) => {
console.log("e", e);
this.triggerEvent("changeChild", { tempId: this.data.fileData.tempId, fileId, status: "parseFailed" });
},
complete: () => {},
header: {},
});
}
},
fail: (err) => {
console.error("上传失败:", err);
},
});
},
},
observers: {
"fileData.status": function (status) {
this.setData({
statusTxt: this.getFormatStatusText(status),
});
},
},
/**
* 组件的属性列表
*/
properties: {
enableDel: {
type: Boolean,
value: false,
},
fileData: {
type: Object,
value: {
tempId: "",
rawType: "",
tempFileName: "",
rawFileName: "",
tempPath: "",
fileSize: 0,
fileUrl: "",
fileId: "",
status: "",
},
},
},
/**
* 组件的初始数据
*/
data: {
formatSize: "",
iconPath: "../imgs/file.svg",
statusTextMap: {
uploading: "上传中",
parsing: "解析中",
parseFailed: "解析失败",
},
statusTxt: "",
},
/**
* 组件的方法列表
*/
methods: {
getFormatStatusText: function (status) {
if (status === "parsed") {
return this.transformSize(this.data.fileData.fileSize);
}
return this.data.statusTextMap[status] || "";
},
generateCosUploadPath: function (botId, fileName) {
return `agent_file/${botId}/${fileName}`;
},
// 提取文件后缀
getFileType: function (fileName) {
let index = fileName.lastIndexOf(".");
const fileExt = fileName.substring(index + 1);
if (fileExt === "docx" || fileExt === "doc") {
return "word";
}
if (fileExt === "xlsx" || fileExt === "xls" || fileExt === "csv") {
return "excel";
}
if (fileExt === "png" || fileExt === "jpg" || fileExt === "jpeg" || fileExt === "svg") {
return "image";
}
if (fileExt === "ppt" || fileExt === "pptx") {
return "ppt";
}
if (fileExt === "pdf") {
return "pdf";
}
return "file";
},
// 转换文件大小原始单位为B
transformSize: function (size) {
if (size < 1024) {
return size + "B";
} else if (size < 1024 * 1024) {
return (size / 1024).toFixed(2) + "KB";
} else {
return (size / 1024 / 1024).toFixed(2) + "MB";
}
},
removeFileFromParents: function () {
console.log("remove", this.data.fileData);
this.triggerEvent("removeChild", { tempId: this.data.fileData.tempId });
},
openFileByWx: function (tempPath) {
const fileExt = tempPath.split(".")[1];
if (["doc", "docx", "xls", "xlsx", "ppt", "pptx", "pdf"].includes(fileExt)) {
wx.openDocument({
filePath: tempPath,
success: function (res) {
console.log("打开文档成功");
},
fail: function (err) {
console.log("打开文档失败", err);
},
});
} else {
wx.showModal({
content: "当前支持预览文件类型为 pdf、doc、docx、ppt、pptx、xls、xlsx",
showCancel: false,
confirmText: "确定",
});
}
},
previewImageByWx: function (fileId) {
wx.previewImage({
urls: [fileId],
showmenu: true,
success: function (res) {
console.log("previewImage res", res);
},
fail: function (e) {
console.log("previewImage e", e);
},
});
},
openFile: async function () {
if (this.data.fileData.tempPath) {
// 本地上传的文件
if (this.data.fileData.rawType === "file") {
this.openFileByWx(this.data.fileData.tempPath);
} else {
console.log("fileId", this.data.fileData.fileId);
if (this.data.fileData.fileId) {
this.previewImageByWx(this.data.fileData.fileId);
}
}
} else if (this.data.fileData.fileId) {
// 针对历史记录中带cloudID的处理历史记录中附带的文件
const cloudInsatnce = await getCloudInstance();
cloudInsatnce.downloadFile({
fileID: this.data.fileData.fileId,
success: (res) => {
console.log("download res", res);
if (this.data.fileData.rawType === "file") {
this.openFileByWx(res.tempFilePath);
} else {
this.previewImageByWx(this.data.fileData.fileId);
}
},
fail: (err) => {
console.log("download err", err);
},
});
}
},
},
});

View File

@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}

View File

@ -0,0 +1,14 @@
<!--components/agent-ui-new/chatFIle/chatFile.wxml-->
<!-- <text>components/agent-ui-new/chatFIle/chatFile.wxml</text> -->
<view class="chat_file" bind:tap="openFile">
<view class="chat_file__content">
<image class="chat_file__icon" src="{{iconPath}}" />
<view class="chat_file__info">
<view class="chat_file__name">{{fileData.rawFileName || fileData.tempFileName}}</view>
<view class="chat_file__size">{{statusTxt}}
<image wx:if="{{fileData.status === 'uploading' || fileData.status === 'parsing'}}" style="width: 15px;height:15px;margin-left: 5px" src="../imgs/loading.svg" mode=""/>
</view>
</view>
</view>
<image wx:if="{{enableDel}}" bind:tap="removeFileFromParents" style="width: 15px;height: 15px;position: absolute;top: -7px;right: -7px" src="../imgs/close-filled.png" mode="aspectFill"/>
</view>

View File

@ -0,0 +1,49 @@
/* components/agent-ui-new/chatFIle/chatFile.wxss */
.chat_file {
padding: 16rpx 24rpx;
background: #ffffff;
border-radius: 12rpx;
/* box-shadow: 0 1px 8px rgba(0, 0, 0, 0.253); */
/* margin: 0rpx 5rpx; */
/* max-width: 110px; */
width: 110px;
position: relative;
background-color: #f3f4f6;
}
.chat_file:active {
background: #f9f9f9;
}
.chat_file__content {
display: flex;
align-items: center;
gap: 24rpx;
}
.chat_file__icon {
width: 60rpx;
height: 60rpx;
flex-shrink: 0;
}
.chat_file__info {
flex: 1;
min-width: 0;
}
.chat_file__name {
font-size: 28rpx;
color: #333333;
margin-bottom: 8rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.chat_file__size {
font-size: 24rpx;
color: #999999;
display: flex;
align-items: center;
}

View File

@ -0,0 +1,44 @@
// components/agent-ui/collapsibleCard/index.js
Component({
/**
* 组件的属性列表
*/
properties: {
initStatus: {
type: Boolean,
value: false
},
showBgColor:{
type: Boolean,
value: false
},
showExpandIcon: {
type: Boolean,
value: true
}
},
/**
* 组件的初始数据
*/
data: {
collapsedStatus: false
},
lifetimes: {
attached() {
this.setData({ collapsedStatus: this.properties.initStatus })
}
},
/**
* 组件的方法列表
*/
methods: {
changeCollapsedStatus: function () {
this.setData({ collapsedStatus: !this.data.collapsedStatus })
}
},
options: {
multipleSlots: true
}
})

View File

@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}

View File

@ -0,0 +1,10 @@
<!--components/agent-ui/collapsibleCard/index.wxml-->
<view class="collapse" style="{{collapsedStatus&&showBgColor?'background-color: #f5f5f5;':''}}">
<view class="collapse-header" bind:tap="changeCollapsedStatus">
<image wx:if="{{showExpandIcon}}" src="../imgs/arrow.svg" mode="aspectFill" style="width: 16px;height: 16px;transform: rotate({{collapsedStatus?360:270}}deg);" />
<slot name="title"></slot>
</view>
<block wx:if="{{collapsedStatus}}">
<slot name="content"></slot>
</block>
</view>

View File

@ -0,0 +1,14 @@
/* components/agent-ui/collapsibleCard/index.wxss */
.collapse{
border-radius: 8px;
margin-bottom: 12px;
}
.collapse-header {
display: inline-flex;
align-items: center;
gap: 8px;
background-color: #f5f5f5;
justify-content: space-between;
padding: 18rpx 26rpx;
border-radius: 8px;
}

View File

@ -0,0 +1,18 @@
Component({
properties: {
name: {
type: String,
value: "",
},
toolParams: {
type: Object,
value: {},
},
toolData: {
type: Object,
value: {},
},
},
data: {},
lifetimes: {},
});

View File

@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}

View File

@ -0,0 +1,27 @@
<!--components/agent-ui-new/chatFIle/chatFile.wxml-->
<!-- <text>components/agent-ui-new/chatFIle/chatFile.wxml</text> -->
<!-- <block>
<view wx:if="{{name === 'maps_geo' || name === 'maps_direction_driving'}}">
<custom-map name="{{name}}" toolData="{{toolData}}"></custom-map>
</view>
<view wx:if="{{name === 'maps_weather'}}">
<custom-weather name="{{name}}" toolData="{{toolData}}"></custom-weather>
</view>
<view wx:if="{{name === 'map_search_places'}}">
<custom-business-list name="{{name}}" toolData="{{toolData}}"></custom-business-list>
<custom-food-list name="{{name}}" toolData="{{toolData}}"></custom-food-list>
</view>
</block> -->
<block>
<view class="customCard">
<custom-map wx:if="{{name === 'geocoder' || name === 'placeSearchNearby' || 'directionDriving'}}" name="{{name}}" toolParams="{{toolParams}}" toolData="{{toolData}}"></custom-map>
</view>
<view class="customCard">
<custom-weather wx:if="{{name === 'weather'}}" name="{{name}}" toolData="{{toolData}}"></custom-weather>
</view>
<view class="customCard" wx:if="{{name === 'placeSearchNearby'}}">
<custom-business-list name="{{name}}" toolData="{{toolData}}"></custom-business-list>
</view>
<!-- 用户可类似添加自定义组件 -->
</block>

View File

@ -0,0 +1,3 @@
.customCard {
margin: 15px 0px;
}

View File

@ -0,0 +1,2 @@
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12.0003 0.630371L14.9029 8.98093L23.7417 9.16105L16.6969 14.5021L19.2569 22.964L12.0003 17.9144L4.74363 22.964L7.30367 14.5021L0.258789 9.16105L9.09761 8.98093L12.0003 0.630371ZM12.0003 6.72181L10.5298 10.9522L6.05209 11.0434L9.62099 13.7492L8.32409 18.0359L12.0003 15.4778L15.6764 18.0359L14.3795 13.7492L17.9484 11.0434L13.4707 10.9522L12.0003 6.72181Z" fill="#f2db4a" />
</svg>

After

Width:  |  Height:  |  Size: 473 B

View File

@ -0,0 +1,2 @@
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12.0003 0.630371L14.9029 8.98093L23.7417 9.16105L16.6969 14.5021L19.2569 22.964L12.0003 17.9144L4.74363 22.964L7.30367 14.5021L0.258789 9.16105L9.09761 8.98093L12.0003 0.630371ZM12.0003 6.72181L10.5298 10.9522L6.05209 11.0434L9.62099 13.7492L8.32409 18.0359L12.0003 15.4778L15.6764 18.0359L14.3795 13.7492L17.9484 11.0434L13.4707 10.9522L12.0003 6.72181Z" fill="black" />
</svg>

After

Width:  |  Height:  |  Size: 471 B

View File

@ -0,0 +1,277 @@
// pages/components/feedback/index.js
Component({
/**
* 组件的属性列表
*/
properties: {
isShowFeedback: {
type: Boolean,
value: false
},
feedbackRecordId: {
type: String,
value: ''
},
feedbackType: {
type: String,
value: ''
},
botId: {
type: String,
value: ''
},
input: {
type: String,
value: ""
},
aiAnswer: {
type: String,
value: ''
}
},
/**
* 组件的初始数据
*/
data: {
upVote: [
{
"selected": false,
"value": "准确有效"
},
{
"selected": false,
"value": "回答全面"
},
{
"selected": false,
"value": "立场正确"
},
{
"selected": false,
"value": "格式规范"
},
{
"selected": false,
"value": "专业性强"
},
{
"selected": false,
"value": "富有创意"
},
{
"selected": false,
"value": "表达清晰"
},
{
"selected": false,
"value": "值得信赖"
},
{
"selected": false,
"value": "高效"
},
{
"selected": false,
"value": "满意"
}
],
downVote: [
{
"selected": false,
"value": "理解错误"
},
{
"selected": false,
"value": "未识别问题"
},
{
"selected": false,
"value": "事实错误"
},
{
"selected": false,
"value": "推理错误"
},
{
"selected": false,
"value": "内容不完整"
},
{
"selected": false,
"value": "不专业"
},
{
"selected": false,
"value": "违法有害"
},
{
"selected": false,
"value": "格式错误"
},
{
"selected": false,
"value": "乱码"
},
{
"selected": false,
"value": "内容重复"
}
],
score: 5,
message: ""
},
observers:{
"feedbackType":function (value) {
this.setData({score:value==='upvote'?5:1})
}
},
/**
* 组件的方法列表
*/
methods: {
reset: function () {
this.setData({
upVote: [
{
"selected": false,
"value": "准确有效"
},
{
"selected": false,
"value": "回答全面"
},
{
"selected": false,
"value": "立场正确"
},
{
"selected": false,
"value": "格式规范"
},
{
"selected": false,
"value": "专业性强"
},
{
"selected": false,
"value": "富有创意"
},
{
"selected": false,
"value": "表达清晰"
},
{
"selected": false,
"value": "值得信赖"
},
{
"selected": false,
"value": "高效"
},
{
"selected": false,
"value": "满意"
}
],
downVote: [
{
"selected": false,
"value": "理解错误"
},
{
"selected": false,
"value": "未识别问题"
},
{
"selected": false,
"value": "事实错误"
},
{
"selected": false,
"value": "推理错误"
},
{
"selected": false,
"value": "内容不完整"
},
{
"selected": false,
"value": "不专业"
},
{
"selected": false,
"value": "违法有害"
},
{
"selected": false,
"value": "格式错误"
},
{
"selected": false,
"value": "乱码"
},
{
"selected": false,
"value": "内容重复"
}
],
score: 5,
message: ""
})
},
onChangeScore: function (e) {
const { score } = e.currentTarget.dataset
this.setData({ score })
},
onSelect: function (e) {
const { item } = e.currentTarget.dataset
const newArr = [...this.data.feedbackType === 'upvote' ? this.data.upVote : this.data.downVote]
const [selectedItem] = newArr.filter(i => i.value === item.value)
selectedItem.selected = !selectedItem.selected
if (this.data.feedbackType === 'upvote') {
this.setData({ upVote: newArr })
} else {
this.setData({ downVote: newArr })
}
},
inputChange: function (e) {
const value = e.detail.value
this.setData({ message: value })
},
closeShowFeedback: function () {
this.triggerEvent('close')
},
submitFeedback: async function () {
const res = await wx.cloud.extend.AI.bot.sendFeedback({
userFeedback: {
botId: this.data.botId,
recordId: this.data.feedbackRecordId,
comment: this.data.message,
rating: this.data.score,
tags: this.data.feedbackType === 'upvote' ? this.data.upVote.filter(item => item.selected).map(item => item.value) : this.data.downVote.filter(item => item.selected).map(item => item.value),
aiAnswer: this.data.aiAnswer,
input: this.data.input,
type: this.data.feedbackType === 'upvote' ? "upvote" : 'downvote',
},
botId: this.data.botId
});
if (res.status === 'success') {
wx.showToast({
title: "感谢反馈",
icon: "success",
});
} else {
wx.showToast({
title: "反馈失败",
icon: "fail",
});
}
this.reset();
// console.log(res)
this.triggerEvent("close")
}
}
})

View File

@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}

View File

@ -0,0 +1,35 @@
<view class="feedback-modal" wx:if="{{isShowFeedback}}">
<view class="feedback">
<view class="feedback-header">
感谢您的宝贵反馈,我们会不断改进服务
</view>
<view class="feedback-body">
<view class="item-box">
<view class="item-title">评分</view>
<view style="display: flex; gap: 14rpx;">
<block wx:for="{{[1,2,3,4,5]}}" wx:key="*this">
<image src="{{item<=score?'./imgs/star-highlight.svg':'./imgs/star.svg'}}" mode="aspectFill" class="star" bind:touchend="onChangeScore" data-score="{{item}}" />
</block>
</view>
</view>
<view class="item-box">
<view class="item-title">回答内容</view>
<view>
<block wx:for="{{feedbackType==='upvote'?upVote:downVote}}" wx:key="value">
<view class="{{item.selected?'vote-item-highlight':'vote-item-normal'}}" bind:tap="onSelect" data-item="{{item}}">{{item.value}}</view>
</block>
</view>
</view>
<view class="item-box">
<view class="item-title">反馈建议</view>
<view>
<textarea value="{{message}}" class="feedback-textarea" maxlength="140" bindinput="inputChange"/>
</view>
</view>
</view>
<view class="feedback-footer">
<view class="btn-cancel" bind:tap="closeShowFeedback">取消</view>
<view class="btn-submit" bind:tap="submitFeedback">提交反馈</view>
</view>
</view>
</view>

View File

@ -0,0 +1,91 @@
/* pages/components/feedback/index.wxss */
.feedback-modal {
position: fixed;
top: 0px;
left: 0px;
width: 750rpx;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
}
.feedback {
width: 520rpx;
max-height: 70%;
overflow-y: auto;
background-color: #fff;
border-radius: 24rpx;
padding: 40rpx;
}
.feedback-header{
font-weight: 500;
font-size: 32rpx;
text-align: justify;
}
.feedback-body{
font-size: 28rpx;
}
.item-box{
padding: 28rpx 0px;
}
.item-title{
font-size: 28rpx;
padding-bottom: 28rpx;
}
.star{
width: 40rpx;
height: 40rpx;
}
.vote-item-normal{
display: inline-block;
background-color: rgba(243, 243, 243, 1);
height: 48rpx;
line-height: 48rpx;
border-radius: 24rpx;
padding: 4rpx 24rpx;
margin-right: 16rpx;
margin-bottom: 16rpx;
}
.vote-item-highlight{
display: inline-block;
background-color: rgba(0, 82, 217, 0.1);
height: 48rpx;
line-height: 48rpx;
border-radius: 24rpx;
padding: 4rpx 24rpx;
margin-right: 16rpx;
margin-bottom: 16rpx;
color: rgb(0, 82, 217);
}
.feedback-textarea{
width: 100%;
height: 150rpx;
border-radius: 16rpx;
border: 1px solid #ccc;
box-sizing: border-box;
padding: 16rpx;
}
.feedback-footer{
font-size: 28rpx;
display: flex;
justify-content: flex-end;
gap: 24rpx;
}
.btn-cancel{
box-sizing: border-box;
background-color: #fff;
border: 1px solid #eee;
padding: 6rpx 24rpx;
border-radius: 6rpx;
line-height: 48rpx;
}
.btn-submit{
box-sizing: border-box;
padding: 6rpx 24rpx;
border-radius: 6rpx;
color: #fff;
background-color: #0052d9;
line-height: 48rpx;
}

View File

@ -0,0 +1,2 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M17.5001 8.08575L12.0002 13.5858L6.50015 8.08576L5.08594 9.49997L12.0002 16.4142L18.9144 9.49997L17.5001 8.08575Z" fill="black" />
</svg>

After

Width:  |  Height:  |  Size: 244 B

View File

@ -0,0 +1,2 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M7.88197 1.99988H16.118L17.618 4.99988H23V20.9999H1V4.99988H6.38197L7.88197 1.99988ZM9.11803 3.99988L7.61803 6.99988H3V18.9999H21V6.99988H16.382L14.882 3.99988H9.11803ZM12 9.49988C10.3431 9.49988 9 10.843 9 12.4999C9 14.1567 10.3431 15.4999 12 15.4999C13.6569 15.4999 15 14.1567 15 12.4999C15 10.843 13.6569 9.49988 12 9.49988ZM7 12.4999C7 9.73845 9.23858 7.49988 12 7.49988C14.7614 7.49988 17 9.73845 17 12.4999C17 15.2613 14.7614 17.4999 12 17.4999C9.23858 17.4999 7 15.2613 7 12.4999Z" fill="rgb(95, 114, 146)" />
</svg>

After

Width:  |  Height:  |  Size: 630 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 24 24"><rect width="2.8" height="12" x="1" y="6" fill="#e84f50"><animate attributeName="y" begin="svgSpinnersBarsScaleMiddle0.begin+0.4s" calcMode="spline" dur="0.6s" keySplines=".14,.73,.34,1;.65,.26,.82,.45" values="6;1;6"/><animate attributeName="height" begin="svgSpinnersBarsScaleMiddle0.begin+0.4s" calcMode="spline" dur="0.6s" keySplines=".14,.73,.34,1;.65,.26,.82,.45" values="12;22;12"/></rect><rect width="2.8" height="12" x="5.8" y="6" fill="#e84f50"><animate attributeName="y" begin="svgSpinnersBarsScaleMiddle0.begin+0.2s" calcMode="spline" dur="0.6s" keySplines=".14,.73,.34,1;.65,.26,.82,.45" values="6;1;6"/><animate attributeName="height" begin="svgSpinnersBarsScaleMiddle0.begin+0.2s" calcMode="spline" dur="0.6s" keySplines=".14,.73,.34,1;.65,.26,.82,.45" values="12;22;12"/></rect><rect width="2.8" height="12" x="10.6" y="6" fill="#e84f50"><animate id="svgSpinnersBarsScaleMiddle0" attributeName="y" begin="0;svgSpinnersBarsScaleMiddle1.end-0.1s" calcMode="spline" dur="0.6s" keySplines=".14,.73,.34,1;.65,.26,.82,.45" values="6;1;6"/><animate attributeName="height" begin="0;svgSpinnersBarsScaleMiddle1.end-0.1s" calcMode="spline" dur="0.6s" keySplines=".14,.73,.34,1;.65,.26,.82,.45" values="12;22;12"/></rect><rect width="2.8" height="12" x="15.4" y="6" fill="#e84f50"><animate attributeName="y" begin="svgSpinnersBarsScaleMiddle0.begin+0.2s" calcMode="spline" dur="0.6s" keySplines=".14,.73,.34,1;.65,.26,.82,.45" values="6;1;6"/><animate attributeName="height" begin="svgSpinnersBarsScaleMiddle0.begin+0.2s" calcMode="spline" dur="0.6s" keySplines=".14,.73,.34,1;.65,.26,.82,.45" values="12;22;12"/></rect><rect width="2.8" height="12" x="20.2" y="6" fill="#e84f50"><animate id="svgSpinnersBarsScaleMiddle1" attributeName="y" begin="svgSpinnersBarsScaleMiddle0.begin+0.4s" calcMode="spline" dur="0.6s" keySplines=".14,.73,.34,1;.65,.26,.82,.45" values="6;1;6"/><animate attributeName="height" begin="svgSpinnersBarsScaleMiddle0.begin+0.4s" calcMode="spline" dur="0.6s" keySplines=".14,.73,.34,1;.65,.26,.82,.45" values="12;22;12"/></rect></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -0,0 +1,2 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 3C7.02944 3 3 7.02944 3 12C3 14.3966 3.93542 16.5725 5.46305 18.1862L5.96701 18.7185L4.69951 21H12C16.9706 21 21 16.9706 21 12C21 7.02944 16.9706 3 12 3ZM1 12C1 5.92487 5.92487 1 12 1C18.0751 1 23 5.92487 23 12C23 18.0751 18.0751 23 12 23H1.30049L3.51936 19.006C1.94632 17.1038 1 14.6615 1 12ZM13 8V11H16V13H13V16H11V13H8V11H11V8H13Z" fill="#4d6bfe" />
</svg>

After

Width:  |  Height:  |  Size: 470 B

View File

@ -0,0 +1,2 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 3C7.02944 3 3 7.02944 3 12C3 14.3966 3.93542 16.5725 5.46305 18.1862L5.96701 18.7185L4.69951 21H12C16.9706 21 21 16.9706 21 12C21 7.02944 16.9706 3 12 3ZM1 12C1 5.92487 5.92487 1 12 1C18.0751 1 23 5.92487 23 12C23 18.0751 18.0751 23 12 23H1.30049L3.51936 19.006C1.94632 17.1038 1 14.6615 1 12ZM13 8V11H16V13H13V16H11V13H8V11H11V8H13Z" fill="rgb(95, 114, 146)" />
</svg>

After

Width:  |  Height:  |  Size: 480 B

View File

@ -0,0 +1,2 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 3C7.02944 3 3 7.02944 3 12C3 14.3966 3.93542 16.5725 5.46305 18.1862L5.96701 18.7185L4.69951 21H12C16.9706 21 21 16.9706 21 12C21 7.02944 16.9706 3 12 3ZM1 12C1 5.92487 5.92487 1 12 1C18.0751 1 23 5.92487 23 12C23 18.0751 18.0751 23 12 23H1.30049L3.51936 19.006C1.94632 17.1038 1 14.6615 1 12ZM13 5.5V11.5858L16.4142 15L15 16.4142L11 12.4142V5.5H13Z" fill="rgb(160, 160, 160)" />
</svg>

After

Width:  |  Height:  |  Size: 497 B

View File

@ -0,0 +1,2 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M20.985 7.37845L10.3784 17.985L4.0144 11.6211L5.42862 10.2069L10.3784 15.1566L19.5708 5.96423L20.985 7.37845Z" fill="green" />
</svg>

After

Width:  |  Height:  |  Size: 237 B

View File

@ -0,0 +1,2 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M9 1L15 1L15 9.5L21 9.5L21 23L3 23L3 9.5L9 9.5L9 1ZM11 3L11 11.5L5 11.5V14L19 14L19 11.5H13L13 3L11 3ZM19 16L5 16L5 21H14V18L16 18V21H19V16Z" fill="rgb(95, 114, 146)" />
</svg>

After

Width:  |  Height:  |  Size: 283 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 B

View File

@ -0,0 +1,2 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M7.0502 5.63611L11.9999 10.5859L16.9497 5.63611L18.3639 7.05032L13.4142 12.0001L18.3639 16.9498L16.9497 18.364L11.9999 13.4143L7.0502 18.364L5.63599 16.9498L10.5857 12.0001L5.63599 7.05032L7.0502 5.63611Z" fill="red" />
</svg>

After

Width:  |  Height:  |  Size: 333 B

View File

@ -0,0 +1,2 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M7.0502 5.63611L11.9999 10.5859L16.9497 5.63611L18.3639 7.05032L13.4142 12.0001L18.3639 16.9498L16.9497 18.364L11.9999 13.4143L7.0502 18.364L5.63599 16.9498L10.5857 12.0001L5.63599 7.05032L7.0502 5.63611Z" fill="black" />
</svg>

After

Width:  |  Height:  |  Size: 335 B

View File

@ -0,0 +1,2 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M2 2H15V7.5H13V4H4V13H7.5V15H2V2ZM9 9H22V22H9V9ZM11 11V20H20V11H11Z" fill="#8b8b8b" />
</svg>

After

Width:  |  Height:  |  Size: 197 B

View File

@ -0,0 +1,2 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M16.4278 1.96289L22.0381 7.57319L7.61066 22.0007L2.00037 22.0007L2.00037 16.3904L16.4278 1.96289ZM16.4278 4.79132L13.646 7.57319L16.4278 10.3551L19.2097 7.57319L16.4278 4.79132ZM15.0136 11.7693L12.2318 8.9874L4.00037 17.2188L4.00037 20.0007H6.78224L15.0136 11.7693ZM22.2252 22.0007H12.6821V20.0007L22.2252 20.0007V22.0007Z" fill="black" />
</svg>

After

Width:  |  Height:  |  Size: 453 B

View File

@ -0,0 +1,2 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12.0001 3C7.02956 3 3.00012 7.02944 3.00012 12C3.00012 16.9706 7.02956 21 12.0001 21C16.9707 21 21.0001 16.9706 21.0001 12C21.0001 7.02944 16.9707 3 12.0001 3ZM1.00012 12C1.00012 5.92487 5.92499 1 12.0001 1C18.0753 1 23.0001 5.92487 23.0001 12C23.0001 18.0751 18.0753 23 12.0001 23C5.92499 23 1.00012 18.0751 1.00012 12ZM13.0001 6.5V14H11.0001V6.5H13.0001ZM11.0001 15.5H13.004V17.5039H11.0001V15.5Z" fill="#8b8b8b" />
</svg>

After

Width:  |  Height:  |  Size: 532 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 32 32"><path fill="#20744a" fill-rule="evenodd" d="M28.781 4.405h-10.13V2.018L2 4.588v22.527l16.651 2.868v-3.538h10.13A1.16 1.16 0 0 0 30 25.349V5.5a1.16 1.16 0 0 0-1.219-1.095m.16 21.126H18.617l-.017-1.889h2.487v-2.2h-2.506l-.012-1.3h2.518v-2.2H18.55l-.012-1.3h2.549v-2.2H18.53v-1.3h2.557v-2.2H18.53v-1.3h2.557v-2.2H18.53v-2h10.411Z"/><path fill="#20744a" d="M22.487 7.439h4.323v2.2h-4.323zm0 3.501h4.323v2.2h-4.323zm0 3.501h4.323v2.2h-4.323zm0 3.501h4.323v2.2h-4.323zm0 3.501h4.323v2.2h-4.323z"/><path fill="#fff" fill-rule="evenodd" d="m6.347 10.673l2.146-.123l1.349 3.709l1.594-3.862l2.146-.123l-2.606 5.266l2.606 5.279l-2.269-.153l-1.532-4.024l-1.533 3.871l-2.085-.184l2.422-4.663z"/></svg>

After

Width:  |  Height:  |  Size: 773 B

View File

@ -0,0 +1,3 @@
<svg width="20px" height="20px" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 4C4 2.89543 4.89543 2 6 2H10.5858C11.1162 2 11.6249 2.21071 12 2.58579L15.4142 6C15.7893 6.37507 16 6.88378 16 7.41421V16C16 17.1046 15.1046 18 14 18H6C4.89543 18 4 17.1046 4 16V4ZM6 10C6 9.44772 6.44772 9 7 9H13C13.5523 9 14 9.44772 14 10C14 10.5523 13.5523 11 13 11H7C6.44772 11 6 10.5523 6 10ZM7 13C6.44772 13 6 13.4477 6 14C6 14.5523 6.44772 15 7 15H13C13.5523 15 14 14.5523 14 14C14 13.4477 13.5523 13 13 13H7Z" fill="#3895FB"/>
</svg>

After

Width:  |  Height:  |  Size: 598 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 16 16"><g fill="none"><path fill="url(#fluentColorImage162)" d="M2 4.5A2.5 2.5 0 0 1 4.5 2h7A2.5 2.5 0 0 1 14 4.5v7a2.5 2.5 0 0 1-2.5 2.5h-7A2.5 2.5 0 0 1 2 11.5z"/><path fill="url(#fluentColorImage160)" d="M13.586 12.879A2.5 2.5 0 0 1 11.5 14h-7a2.5 2.5 0 0 1-2.086-1.121l4.384-4.384a1.7 1.7 0 0 1 2.404 0z"/><path fill="url(#fluentColorImage161)" d="M11.5 5.502a1.002 1.002 0 1 1-2.004 0a1.002 1.002 0 0 1 2.004 0"/><defs><linearGradient id="fluentColorImage160" x1="6.286" x2="7.572" y1="7.997" y2="14.347" gradientUnits="userSpaceOnUse"><stop stop-color="#b3e0ff"/><stop offset="1" stop-color="#8cd0ff"/></linearGradient><linearGradient id="fluentColorImage161" x1="10.097" x2="10.829" y1="4.277" y2="6.913" gradientUnits="userSpaceOnUse"><stop stop-color="#fdfdfd"/><stop offset="1" stop-color="#b3e0ff"/></linearGradient><radialGradient id="fluentColorImage162" cx="0" cy="0" r="1" gradientTransform="matrix(20.57146 26.03575 -23.68122 18.71109 -2.714 -4.75)" gradientUnits="userSpaceOnUse"><stop offset=".338" stop-color="#0fafff"/><stop offset=".529" stop-color="#367af2"/></radialGradient></defs></g></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,2 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M2.00003 4V6H22V4H2.00003Z" fill="black" /><path d="M8.00003 11V13H22V11H8.00003Z" fill="black" /><path d="M2.00003 18H22V20H2.00003V18Z" fill="black" /><path d="M1.58582 11.9998L4.7678 15.1818L6.18201 13.7676L4.41424 11.9998L6.18201 10.232L4.7678 8.81783L1.58582 11.9998Z" fill="rgb(95, 114, 146)" />
</svg>

After

Width:  |  Height:  |  Size: 415 B

View File

@ -0,0 +1,2 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M2.99997 4H1.99997V6H2.99997H21H22V4H21H2.99997ZM8.99997 11H7.99997V13H8.99997H21H22V11H21H8.99997ZM1.99997 18H2.99997H21H22V20H21H2.99997H1.99997V18ZM5.80474 12.7073L6.51184 12.0002L5.80474 11.2931L4.03697 9.52532L3.32986 8.81821L1.91565 10.2324L2.62276 10.9395L3.68342 12.0002L2.62276 13.0608L1.91565 13.768L3.32986 15.1822L4.03697 14.4751L5.80474 12.7073Z" fill="rgb(95, 114, 146)" />
</svg>

After

Width:  |  Height:  |  Size: 501 B

View File

@ -0,0 +1,2 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3.05493 11C3.45261 7.40254 5.97062 4.44413 9.33156 3.40217C8.00851 5.65483 7.1892 8.23957 7.02895 11H3.05493ZM10.9922 1.04555C5.38944 1.55442 1 6.26461 1 12C1 17.7354 5.3894 22.4455 10.9922 22.9545L11 22.9643L11.4254 22.9853C11.4932 22.9887 11.5611 22.9916 11.6292 22.9939C11.7523 22.9979 11.8759 23 12 23C12.1187 23 12.2369 22.9981 12.3546 22.9944C12.4281 22.9921 12.5015 22.989 12.5746 22.9853L13 22.9643L13.0078 22.9545C18.6106 22.4455 23 17.7354 23 12C23 6.26461 18.6106 1.55443 13.0078 1.04555L12.9999 1.03571L12.5736 1.0147C12.4767 1.00972 12.3795 1.006 12.2819 1.00354C12.1882 1.00119 12.0942 1 12 1C11.9051 1 11.8104 1.0012 11.7161 1.00359C11.6192 1.00605 11.5226 1.00976 11.4263 1.0147L11.0001 1.03571L10.9922 1.04555ZM12.0011 3C13.6972 5.25767 14.7704 8.00828 14.9672 11H9.03278C9.22955 8.00828 10.3028 5.25767 11.9989 3C11.9992 3 11.9996 3 12 3C12.0004 3 12.0008 3 12.0011 3ZM7.02894 13C7.18917 15.7604 8.00847 18.3452 9.3315 20.5978C5.97059 19.5558 3.45261 16.5974 3.05493 13H7.02894ZM11.9988 21C10.3028 18.7423 9.22953 15.9917 9.03277 13H14.9672C14.7705 15.9917 13.6972 18.7423 12.0012 21C12.0008 21 12.0004 21 12 21C11.9996 21 11.9992 21 11.9988 21ZM14.6685 20.5978C15.9915 18.3452 16.8108 15.7604 16.9711 13H20.9451C20.5474 16.5974 18.0294 19.5558 14.6685 20.5978ZM16.9711 11C16.8108 8.23957 15.9915 5.65483 14.6684 3.40217C18.0294 4.44413 20.5474 7.40254 20.9451 11H16.9711Z" fill="rgb(95, 114, 146)" />
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,2 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="rgb(77, 107, 254)" xmlns="http://www.w3.org/2000/svg"><path d="M3.05493 11C3.45261 7.40254 5.97062 4.44413 9.33156 3.40217C8.00851 5.65483 7.1892 8.23957 7.02895 11H3.05493ZM10.9922 1.04555C5.38944 1.55442 1 6.26461 1 12C1 17.7354 5.3894 22.4455 10.9922 22.9545L11 22.9643L11.4254 22.9853C11.4932 22.9887 11.5611 22.9916 11.6292 22.9939C11.7523 22.9979 11.8759 23 12 23C12.1187 23 12.2369 22.9981 12.3546 22.9944C12.4281 22.9921 12.5015 22.989 12.5746 22.9853L13 22.9643L13.0078 22.9545C18.6106 22.4455 23 17.7354 23 12C23 6.26461 18.6106 1.55443 13.0078 1.04555L12.9999 1.03571L12.5736 1.0147C12.4767 1.00972 12.3795 1.006 12.2819 1.00354C12.1882 1.00119 12.0942 1 12 1C11.9051 1 11.8104 1.0012 11.7161 1.00359C11.6192 1.00605 11.5226 1.00976 11.4263 1.0147L11.0001 1.03571L10.9922 1.04555ZM12.0011 3C13.6972 5.25767 14.7704 8.00828 14.9672 11H9.03278C9.22955 8.00828 10.3028 5.25767 11.9989 3C11.9992 3 11.9996 3 12 3C12.0004 3 12.0008 3 12.0011 3ZM7.02894 13C7.18917 15.7604 8.00847 18.3452 9.3315 20.5978C5.97059 19.5558 3.45261 16.5974 3.05493 13H7.02894ZM11.9988 21C10.3028 18.7423 9.22953 15.9917 9.03277 13H14.9672C14.7705 15.9917 13.6972 18.7423 12.0012 21C12.0008 21 12.0004 21 12 21C11.9996 21 11.9992 21 11.9988 21ZM14.6685 20.5978C15.9915 18.3452 16.8108 15.7604 16.9711 13H20.9451C20.5474 16.5974 18.0294 19.5558 14.6685 20.5978ZM16.9711 11C16.8108 8.23957 15.9915 5.65483 14.6684 3.40217C18.0294 4.44413 20.5474 7.40254 20.9451 11H16.9711Z" fill="rgb(77, 107, 254)" />
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,2 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1 3H23V21H1V3ZM3 5V19H21V5H3ZM4.99609 7.5H7V9.50391H4.99609V7.5ZM8.99609 7.5H11V9.50391H8.99609V7.5ZM12.9961 7.5H15V9.50391H12.9961V7.5ZM16.9961 7.5H19V9.50391H16.9961V7.5ZM4.99609 10.5H7V12.5039H4.99609V10.5ZM8.99609 10.5H11V12.5039H8.99609V10.5ZM12.9961 10.5H15V12.5039H12.9961V10.5ZM16.9961 10.5H19V12.5039H16.9961V10.5ZM5 15H19V17H5V15Z" fill="black" />
</svg>

After

Width:  |  Height:  |  Size: 472 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 24 24"><path fill="rgb(95, 114, 146)" d="M12,4a8,8,0,0,1,7.89,6.7A1.53,1.53,0,0,0,21.38,12h0a1.5,1.5,0,0,0,1.48-1.75,11,11,0,0,0-21.72,0A1.5,1.5,0,0,0,2.62,12h0a1.53,1.53,0,0,0,1.49-1.3A8,8,0,0,1,12,4Z"><animateTransform attributeName="transform" dur="0.75s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/></path></svg>

After

Width:  |  Height:  |  Size: 417 B

View File

@ -0,0 +1,2 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21C16.9706 21 21 16.9706 21 12C21 7.02944 16.9706 3 12 3ZM1 12C1 5.92487 5.92487 1 12 1C18.0751 1 23 5.92487 23 12C23 18.0751 18.0751 23 12 23C5.92487 23 1 18.0751 1 12ZM10.5 7V17H8.5V7H10.5ZM15.5 7V17H13.5V7H15.5Z" fill="#8b8b8b" />
</svg>

After

Width:  |  Height:  |  Size: 402 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="192" height="192" viewBox="0 0 32 32"><path fill="#909090" d="m24.1 2.072l5.564 5.8v22.056H8.879V30h20.856V7.945z"/><path fill="#f4f4f4" d="M24.031 2H8.808v27.928h20.856V7.873z"/><path fill="#7a7b7c" d="M8.655 3.5h-6.39v6.827h20.1V3.5z"/><path fill="#dd2025" d="M22.472 10.211H2.395V3.379h20.077z"/><path fill="#464648" d="M9.052 4.534H7.745v4.8h1.028V7.715L9 7.728a2 2 0 0 0 .647-.117a1.4 1.4 0 0 0 .493-.291a1.2 1.2 0 0 0 .335-.454a2.1 2.1 0 0 0 .105-.908a2.2 2.2 0 0 0-.114-.644a1.17 1.17 0 0 0-.687-.65a2 2 0 0 0-.409-.104a2 2 0 0 0-.319-.026m-.189 2.294h-.089v-1.48h.193a.57.57 0 0 1 .459.181a.92.92 0 0 1 .183.558c0 .246 0 .469-.222.626a.94.94 0 0 1-.524.114m3.671-2.306c-.111 0-.219.008-.295.011L12 4.538h-.78v4.8h.918a2.7 2.7 0 0 0 1.028-.175a1.7 1.7 0 0 0 .68-.491a1.9 1.9 0 0 0 .373-.749a3.7 3.7 0 0 0 .114-.949a4.4 4.4 0 0 0-.087-1.127a1.8 1.8 0 0 0-.4-.733a1.6 1.6 0 0 0-.535-.4a2.4 2.4 0 0 0-.549-.178a1.3 1.3 0 0 0-.228-.017m-.182 3.937h-.1V5.392h.013a1.06 1.06 0 0 1 .6.107a1.2 1.2 0 0 1 .324.4a1.3 1.3 0 0 1 .142.526c.009.22 0 .4 0 .549a3 3 0 0 1-.033.513a1.8 1.8 0 0 1-.169.5a1.1 1.1 0 0 1-.363.36a.67.67 0 0 1-.416.106m5.08-3.915H15v4.8h1.028V7.434h1.3v-.892h-1.3V5.43h1.4v-.892"/><path fill="#dd2025" d="M21.781 20.255s3.188-.578 3.188.511s-1.975.646-3.188-.511m-2.357.083a7.5 7.5 0 0 0-1.473.489l.4-.9c.4-.9.815-2.127.815-2.127a14 14 0 0 0 1.658 2.252a13 13 0 0 0-1.4.288Zm-1.262-6.5c0-.949.307-1.208.546-1.208s.508.115.517.939a10.8 10.8 0 0 1-.517 2.434a4.4 4.4 0 0 1-.547-2.162Zm-4.649 10.516c-.978-.585 2.051-2.386 2.6-2.444c-.003.001-1.576 3.056-2.6 2.444M25.9 20.895c-.01-.1-.1-1.207-2.07-1.16a14 14 0 0 0-2.453.173a12.5 12.5 0 0 1-2.012-2.655a11.8 11.8 0 0 0 .623-3.1c-.029-1.2-.316-1.888-1.236-1.878s-1.054.815-.933 2.013a9.3 9.3 0 0 0 .665 2.338s-.425 1.323-.987 2.639s-.946 2.006-.946 2.006a9.6 9.6 0 0 0-2.725 1.4c-.824.767-1.159 1.356-.725 1.945c.374.508 1.683.623 2.853-.91a23 23 0 0 0 1.7-2.492s1.784-.489 2.339-.623s1.226-.24 1.226-.24s1.629 1.639 3.2 1.581s1.495-.939 1.485-1.035"/><path fill="#909090" d="M23.954 2.077V7.95h5.633z"/><path fill="#f4f4f4" d="M24.031 2v5.873h5.633z"/><path fill="#fff" d="M8.975 4.457H7.668v4.8H8.7V7.639l.228.013a2 2 0 0 0 .647-.117a1.4 1.4 0 0 0 .493-.291a1.2 1.2 0 0 0 .332-.454a2.1 2.1 0 0 0 .105-.908a2.2 2.2 0 0 0-.114-.644a1.17 1.17 0 0 0-.687-.65a2 2 0 0 0-.411-.105a2 2 0 0 0-.319-.026m-.189 2.294h-.089v-1.48h.194a.57.57 0 0 1 .459.181a.92.92 0 0 1 .183.558c0 .246 0 .469-.222.626a.94.94 0 0 1-.524.114m3.67-2.306c-.111 0-.219.008-.295.011l-.235.006h-.78v4.8h.918a2.7 2.7 0 0 0 1.028-.175a1.7 1.7 0 0 0 .68-.491a1.9 1.9 0 0 0 .373-.749a3.7 3.7 0 0 0 .114-.949a4.4 4.4 0 0 0-.087-1.127a1.8 1.8 0 0 0-.4-.733a1.6 1.6 0 0 0-.535-.4a2.4 2.4 0 0 0-.549-.178a1.3 1.3 0 0 0-.228-.017m-.182 3.937h-.1V5.315h.013a1.06 1.06 0 0 1 .6.107a1.2 1.2 0 0 1 .324.4a1.3 1.3 0 0 1 .142.526c.009.22 0 .4 0 .549a3 3 0 0 1-.033.513a1.8 1.8 0 0 1-.169.5a1.1 1.1 0 0 1-.363.36a.67.67 0 0 1-.416.106m5.077-3.915h-2.43v4.8h1.028V7.357h1.3v-.892h-1.3V5.353h1.4v-.892"/></svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -0,0 +1,2 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><g opacity="0.9"> <path d="M12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21C16.9706 21 21 16.9706 21 12C21 7.02944 16.9706 3 12 3ZM1 12C1 5.92487 5.92487 1 12 1C18.0751 1 23 5.92487 23 12C23 18.0751 18.0751 23 12 23C5.92487 23 1 18.0751 1 12ZM8.5 6.37083L18.25 12L8.5 17.6292L8.5 6.37083ZM10.5 9.83494L10.5 14.1651L14.25 12L10.5 9.83494Z" fill="#8b8b8b" /></g>
</svg>

After

Width:  |  Height:  |  Size: 485 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 24 24"><rect width="2.8" height="12" x="1" y="6" fill="#8b8b8b"><animate id="svgSpinnersBarsScale0" attributeName="y" begin="0;svgSpinnersBarsScale1.end-0.1s" calcMode="spline" dur="0.6s" keySplines=".36,.61,.3,.98;.36,.61,.3,.98" values="6;1;6"/><animate attributeName="height" begin="0;svgSpinnersBarsScale1.end-0.1s" calcMode="spline" dur="0.6s" keySplines=".36,.61,.3,.98;.36,.61,.3,.98" values="12;22;12"/></rect><rect width="2.8" height="12" x="5.8" y="6" fill="#8b8b8b"><animate attributeName="y" begin="svgSpinnersBarsScale0.begin+0.1s" calcMode="spline" dur="0.6s" keySplines=".36,.61,.3,.98;.36,.61,.3,.98" values="6;1;6"/><animate attributeName="height" begin="svgSpinnersBarsScale0.begin+0.1s" calcMode="spline" dur="0.6s" keySplines=".36,.61,.3,.98;.36,.61,.3,.98" values="12;22;12"/></rect><rect width="2.8" height="12" x="10.6" y="6" fill="#8b8b8b"><animate attributeName="y" begin="svgSpinnersBarsScale0.begin+0.2s" calcMode="spline" dur="0.6s" keySplines=".36,.61,.3,.98;.36,.61,.3,.98" values="6;1;6"/><animate attributeName="height" begin="svgSpinnersBarsScale0.begin+0.2s" calcMode="spline" dur="0.6s" keySplines=".36,.61,.3,.98;.36,.61,.3,.98" values="12;22;12"/></rect><rect width="2.8" height="12" x="15.4" y="6" fill="#8b8b8b"><animate attributeName="y" begin="svgSpinnersBarsScale0.begin+0.3s" calcMode="spline" dur="0.6s" keySplines=".36,.61,.3,.98;.36,.61,.3,.98" values="6;1;6"/><animate attributeName="height" begin="svgSpinnersBarsScale0.begin+0.3s" calcMode="spline" dur="0.6s" keySplines=".36,.61,.3,.98;.36,.61,.3,.98" values="12;22;12"/></rect><rect width="2.8" height="12" x="20.2" y="6" fill="#8b8b8b"><animate id="svgSpinnersBarsScale1" attributeName="y" begin="svgSpinnersBarsScale0.begin+0.4s" calcMode="spline" dur="0.6s" keySplines=".36,.61,.3,.98;.36,.61,.3,.98" values="6;1;6"/><animate attributeName="height" begin="svgSpinnersBarsScale0.begin+0.4s" calcMode="spline" dur="0.6s" keySplines=".36,.61,.3,.98;.36,.61,.3,.98" values="12;22;12"/></rect></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 32 32"><path fill="#d33922" d="M18.536 2.321v2.863c3.4.019 7.357-.035 10.754.016c.642 0 .67.568.678 1.064c.054 5.942-.013 12.055.032 18c-.012.234-.006 1.1-.013 1.346c-.022.823-.434.859-1.257.884c-.132 0-.52.006-.648.012c-3.181-.016-6.362-.009-9.546-.009v3.182L2 27.134V4.873z"/><path fill="#fff" d="M18.536 6.138h10.5v19.4h-10.5V23h7.634v-1.275h-7.634v-1.59h7.634v-1.272h-7.631q.002-.936-.006-1.87a4.47 4.47 0 0 0 3.82-.375a4.35 4.35 0 0 0 1.959-3.474c-1.4-.01-2.793-.006-4.186-.006c0-1.384.016-2.767-.029-4.148c-.522.1-1.043.21-1.562.321V6.139"/><path fill="#d33922" d="M20.766 8.324a4.476 4.476 0 0 1 4.186 4.167c-1.4.016-2.793.01-4.189.01V8.324"/><path fill="#fff" d="M7.1 10.726c1.727.083 3.82-.684 5.252.611c1.371 1.664 1.008 4.724-1.024 5.719A4.7 4.7 0 0 1 9 17.348c0 1.244-.006 2.488 0 3.731q-.947-.082-1.893-.159c-.029-3.4-.035-6.8 0-10.2"/><path fill="#d33922" d="M8.993 12.446c.627-.029 1.4-.143 1.826.445a2.3 2.3 0 0 1 .041 2.087c-.363.655-1.183.592-1.816.668c-.067-1.066-.06-2.131-.051-3.2"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Some files were not shown because too many files have changed in this diff Show More