mirror of
https://github.com/ialley-workshop-open/uni-halo.git
synced 2026-07-26 20:10:51 +08:00
Compare commits
7 Commits
c847dfb1c4
...
759373e327
| Author | SHA1 | Date | |
|---|---|---|---|
| 759373e327 | |||
| 7d74a40f51 | |||
| 50cad268d3 | |||
| 8510b71ff6 | |||
| a012a57cf0 | |||
| d8ad0772bf | |||
| f105dedc23 |
@@ -1,7 +1,7 @@
|
||||
---
|
||||
alwaysApply: true
|
||||
---
|
||||
# unibest 项目概览
|
||||
# 项目概览
|
||||
|
||||
这是一个基于 uniapp + Vue3 + TypeScript + Vite5 + UnoCSS 的跨平台开发框架。
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
alwaysApply: true
|
||||
scene: git_message
|
||||
---
|
||||
|
||||
在此处编写规则,自定义 AI 生成提交信息的风格。
|
||||
|
||||
使用中文编写提交信息
|
||||
Vendored
+2
-1
@@ -102,5 +102,6 @@
|
||||
"i18n-ally.localesPaths": [
|
||||
"src/locale",
|
||||
"src/pages/i18n"
|
||||
]
|
||||
],
|
||||
"i18n-ally.keystyle": "nested"
|
||||
}
|
||||
|
||||
@@ -19,14 +19,14 @@ VITE_APP_PUBLIC_BASE=/
|
||||
# 默认后台请求地址
|
||||
# 不同命令会按 mode 叠加读取 .env.development / .env.test / .env.production。
|
||||
# 微信小程序如果没有配置下面的专用地址,也会回退使用这个值。
|
||||
VITE_SERVER_BASEURL=''
|
||||
VITE_UNI_HALO_BASEURL=''
|
||||
# 备注:如果后台带统一前缀,则也要加到后面,eg: https://blog.xiaoxiaomo.cn/api
|
||||
|
||||
# 微信小程序专用后台请求地址,按微信开发者工具 envVersion 区分。
|
||||
# 不配置时会回退使用 VITE_SERVER_BASEURL。
|
||||
# VITE_SERVER_BASEURL__WEIXIN_DEVELOP = 'https://dev.xxx.com'
|
||||
# VITE_SERVER_BASEURL__WEIXIN_TRIAL = 'https://trial.xxx.com'
|
||||
# VITE_SERVER_BASEURL__WEIXIN_RELEASE = 'https://prod.xxx.com'
|
||||
# 不配置时会回退使用 VITE_UNI_HALO_BASEURL。
|
||||
# VITE_UNI_HALO_BASEURL__WEIXIN_DEVELOP = 'https://dev.xxx.com'
|
||||
# VITE_UNI_HALO_BASEURL__WEIXIN_TRIAL = 'https://trial.xxx.com'
|
||||
# VITE_UNI_HALO_BASEURL__WEIXIN_RELEASE = 'https://prod.xxx.com'
|
||||
|
||||
# h5是否需要配置代理
|
||||
VITE_APP_PROXY_ENABLE=false
|
||||
@@ -34,7 +34,7 @@ VITE_APP_PROXY_ENABLE=false
|
||||
VITE_APP_PROXY_PREFIX='/dev-api'
|
||||
|
||||
# 第二个请求地址 (目前alova中可以使用)
|
||||
VITE_SERVER_BASEURL_SECONDARY='https://blog.xiaoxiaomo.cn'
|
||||
VITE_UNI_HALO_BASEURL_SECONDARY='https://blog.xiaoxiaomo.cn'
|
||||
|
||||
# 认证模式,'single' | 'double' ==> 单token | 双token
|
||||
VITE_AUTH_MODE='single'
|
||||
|
||||
Vendored
+2
-1
@@ -6,4 +6,5 @@ VITE_DELETE_CONSOLE=false
|
||||
VITE_SHOW_SOURCEMAP=false
|
||||
|
||||
# development mode 后台请求地址
|
||||
VITE_SERVER_BASEURL='https://www.yijunzhao.cn'
|
||||
# VITE_UNI_HALO_BASEURL='https://www.yijunzhao.cn'
|
||||
VITE_UNI_HALO_BASEURL='https://www.xhhao.com'
|
||||
|
||||
Vendored
+1
-1
@@ -6,4 +6,4 @@ VITE_DELETE_CONSOLE=true
|
||||
VITE_SHOW_SOURCEMAP=false
|
||||
|
||||
# production mode 后台请求地址
|
||||
# VITE_SERVER_BASEURL = 'https://prod.xxx.com'
|
||||
# VITE_UNI_HALO_BASEURL = 'https://prod.xxx.com'
|
||||
|
||||
Vendored
+1
-1
@@ -6,4 +6,4 @@ VITE_DELETE_CONSOLE=false
|
||||
VITE_SHOW_SOURCEMAP=false
|
||||
|
||||
# test mode 后台请求地址
|
||||
# VITE_SERVER_BASEURL = 'https://test.xxx.com'
|
||||
# VITE_UNI_HALO_BASEURL = 'https://test.xxx.com'
|
||||
|
||||
@@ -2,7 +2,7 @@ import { defineConfig } from 'openapi-ts-request'
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
describe: 'unibest-openapi-test',
|
||||
describe: 'uni-openapi-test',
|
||||
schemaPath: 'https://ukw0y1.laf.run/unibest-opapi-test.json',
|
||||
serversPath: './src/service',
|
||||
requestLibPath: `import request from '@/http/vue-query';\n import { CustomRequestOptions_ } from '@/http/types';`,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { GLOBAL_SEARCH_EVENT_NAME } from '@/constants/events'
|
||||
import FgTabbar from '@/tabbar/index.vue'
|
||||
import { isPageTabbar } from './tabbar/store'
|
||||
import { currRoute } from './utils'
|
||||
@@ -21,6 +22,17 @@ const helloKuRoot = ref('Hello AppKuVue')
|
||||
|
||||
const exposeRef = ref('this is form app.Ku.vue')
|
||||
|
||||
const isShowGlobalSearch = ref(false)
|
||||
function onShowGlobalSearch() {
|
||||
isShowGlobalSearch.value = !isShowGlobalSearch.value
|
||||
console.log('isShowGlobalSearch', isShowGlobalSearch.value)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
uni.$off(GLOBAL_SEARCH_EVENT_NAME, onShowGlobalSearch)
|
||||
uni.$on(GLOBAL_SEARCH_EVENT_NAME, onShowGlobalSearch)
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
exposeRef,
|
||||
})
|
||||
@@ -35,6 +47,9 @@ defineExpose({
|
||||
|
||||
<KuRootView />
|
||||
|
||||
<!-- 全局搜索弹窗 -->
|
||||
<uh-global-search v-if="isShowGlobalSearch" />
|
||||
|
||||
<FgTabbar v-if="isCurrentPageTabbar" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
+13
-5
@@ -4,14 +4,26 @@ import { getCurrentInstance, onMounted, onUnmounted } from 'vue'
|
||||
import { navigateToInterceptor } from '@/router/interceptor'
|
||||
import { tabbarStore } from '@/tabbar/store'
|
||||
import { permission } from '@/router/permission'
|
||||
import { useUniHaloConfigStore } from './store/config'
|
||||
|
||||
const { proxy } = (getCurrentInstance() || {}) as any
|
||||
const router = proxy?.$router
|
||||
|
||||
router && permission.install(router)
|
||||
|
||||
onLaunch((options) => {
|
||||
async function init() {
|
||||
try {
|
||||
const uniHaloConfigStore = useUniHaloConfigStore()
|
||||
await uniHaloConfigStore.init()
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
onLaunch(async (options) => {
|
||||
console.log('App.vue onLaunch', options)
|
||||
await init()
|
||||
})
|
||||
onShow((options) => {
|
||||
console.log('App.vue onShow', options)
|
||||
@@ -46,7 +58,3 @@ onUnmounted(() => {
|
||||
})
|
||||
// #endif
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { http } from '@/http/alova'
|
||||
import type { IndexV1alpha1PublicApiIndicesSearchRequest, SearchResult } from '@halo-dev/api-client'
|
||||
import type { SearchOption, SearchResult } from '@halo-dev/api-client'
|
||||
|
||||
/**
|
||||
* 检查插件是否可用
|
||||
@@ -17,7 +17,7 @@ export function checkPluginAvailable(pluginId: string) {
|
||||
* @param data 搜索参数
|
||||
* @returns 搜索结果
|
||||
*/
|
||||
export function indicesSearch(data: IndexV1alpha1PublicApiIndicesSearchRequest) {
|
||||
export function indicesSearch(data: SearchOption) {
|
||||
return http.Post<SearchResult>(`/apis/api.halo.run/v1alpha1/indices/-/search`, data, {
|
||||
meta: { isHalo: true },
|
||||
})
|
||||
|
||||
@@ -12,7 +12,7 @@ export interface SiteStatsVo {
|
||||
* 获取站点统计信息
|
||||
* @returns 站点统计信息
|
||||
*/
|
||||
export function getSiteStats() {
|
||||
export function querySiteStats() {
|
||||
return http.Get<SiteStatsVo>(`/apis/api.halo.run/v1alpha1/stats/-`, {
|
||||
meta: { isHalo: true },
|
||||
})
|
||||
|
||||
@@ -6,7 +6,10 @@ import { http } from '@/http/alova'
|
||||
* @returns 获取链接分分组组列表响应参数
|
||||
*/
|
||||
export function queryLinkGroups(params: any) {
|
||||
return http.Get<any>(`/apis/api.link.halo.run/v1alpha1/linkgroups`, { params })
|
||||
return http.Get<any>(`/apis/api.link.halo.run/v1alpha1/linkgroups`, {
|
||||
params,
|
||||
meta: { isHalo: true },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -15,5 +18,8 @@ export function queryLinkGroups(params: any) {
|
||||
* @returns 获取链接列表响应参数
|
||||
*/
|
||||
export function queryLinks(params: any) {
|
||||
return http.Get<any>(`/apis/api.link.halo.run/v1alpha1/links`, { params })
|
||||
return http.Get<any>(`/apis/api.link.halo.run/v1alpha1/links`, {
|
||||
params,
|
||||
meta: { isHalo: true },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -10,6 +10,9 @@ const EndpointBase = '/apis/api.moment.halo.run/v1alpha1/moments'
|
||||
export function queryMoments(params: any) {
|
||||
return http.Get<any>(`${EndpointBase}`, {
|
||||
params,
|
||||
meta: {
|
||||
isHalo: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -19,5 +22,9 @@ export function queryMoments(params: any) {
|
||||
* @returns 获取瞬间详情响应参数
|
||||
*/
|
||||
export function queryMomentByName(name: string) {
|
||||
return http.Get<any>(`${EndpointBase}/${name}`, {})
|
||||
return http.Get<any>(`${EndpointBase}/${name}`, {
|
||||
meta: {
|
||||
isHalo: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,11 +1,45 @@
|
||||
import { http } from '@/http/alova'
|
||||
import type { IHaloListResponseBase } from '@/api/types/halo'
|
||||
|
||||
export interface IQueryPhotoGroupsRequest {
|
||||
page?: number
|
||||
size?: number
|
||||
}
|
||||
|
||||
export interface IQueryPhotosRequest {
|
||||
group?: string
|
||||
ungrouped?: boolean
|
||||
tag?: string
|
||||
keyword?: string
|
||||
labelSelector?: string
|
||||
fieldSelector?: string
|
||||
sort?: string
|
||||
page?: number
|
||||
size?: number
|
||||
}
|
||||
|
||||
export interface IPhoto {
|
||||
metadata: {
|
||||
creationTimestamp: string
|
||||
generateName: string
|
||||
name: string
|
||||
version: string
|
||||
}
|
||||
permalink: string
|
||||
spec: {
|
||||
cover: string
|
||||
displayName: string
|
||||
groupName: string
|
||||
url: string
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图库分组列表
|
||||
* @param params 获取图库分组列表请求参数
|
||||
* @returns 获取图库分组列表响应参数
|
||||
*/
|
||||
export function queryPhotoGroups(params: any) {
|
||||
export function queryPhotoGroups(params: IQueryPhotoGroupsRequest) {
|
||||
return http.Get<any>(`/apis/api.photo.halo.run/v1alpha1/photogroups`, {
|
||||
params,
|
||||
meta: {
|
||||
@@ -33,8 +67,8 @@ export function queryPhotoTags(params: any) {
|
||||
* @param params 获取图库列表请求参数
|
||||
* @returns 获取图库列表响应参数
|
||||
*/
|
||||
export function queryPhotos(params: any) {
|
||||
return http.Get<any>(`/apis/api.photo.halo.run/v1alpha1/photos`, {
|
||||
export function queryPhotos(params: IQueryPhotosRequest) {
|
||||
return http.Get<IHaloListResponseBase<IPhoto>>(`/apis/api.photo.halo.run/v1alpha1/photos`, {
|
||||
params,
|
||||
meta: {
|
||||
isHalo: true,
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { http } from '@/http/alova'
|
||||
|
||||
export interface IUniHaloConfig {
|
||||
baseURL: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 uni-halo 插件配置
|
||||
* @returns uni-halo 插件配置响应参数
|
||||
*/
|
||||
export function queryUniHaloPluginConfigs() {
|
||||
return http.Get(`/apis/api.uni.uhalo.pro/v1alpha1/plugins/plugin-uni-halo/getConfigs`, {
|
||||
return http.Get<IUniHaloConfig>(`/apis/api.uni.uhalo.pro/v1alpha1/plugins/plugin-uni-halo/getConfigs`, {
|
||||
meta: {
|
||||
isHalo: true,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Halo 列表响应基础结构
|
||||
* @template T 列表项类型
|
||||
*/
|
||||
export interface IHaloListResponseBase<T> {
|
||||
page: number
|
||||
size: number
|
||||
total: number
|
||||
items: T[]
|
||||
first: boolean
|
||||
hasNext: boolean
|
||||
hasPrevious: boolean
|
||||
last: boolean
|
||||
totalPages: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Halo 列表请求基础结构
|
||||
*/
|
||||
export interface IHaloListRequestBase {
|
||||
page?: number
|
||||
size?: number
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view>
|
||||
内容
|
||||
</view>
|
||||
</template>
|
||||
@@ -0,0 +1,72 @@
|
||||
<script setup lang="ts">
|
||||
import { querySiteStats } from '@/api/halo-base/stats'
|
||||
import { formatNumberUnit } from '@/utils/base/digital'
|
||||
import type { SiteStatsVo } from '@halo-dev/api-client'
|
||||
|
||||
const { loading, error, data, run } = useRequest<SiteStatsVo>(querySiteStats, { loadingToast: false, loadingToastContent: '加载中...', loadingToastMask: true })
|
||||
|
||||
const stats = computed(() => {
|
||||
return {
|
||||
post: formatNumberUnit(data.value?.post || 0),
|
||||
comment: formatNumberUnit(data.value?.comment || 0),
|
||||
upvote: formatNumberUnit(data.value?.upvote || 0),
|
||||
visit: formatNumberUnit(data.value?.visit || 0),
|
||||
category: formatNumberUnit(data.value?.category || 0),
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
run()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="uh-shadow-xs box-border flex flex-col items-center gap-y-2 rounded-2xl bg-white p-4">
|
||||
<view class="w-full flex items-center justify-between gap-x-2">
|
||||
<image
|
||||
class="uh-shadow-xs h-11 w-11 shrink-0 border-2 border-white rounded-full border-solid"
|
||||
src="https://www.xiaoxiaomo.cn/logo.jpg" mode="scaleToFill"
|
||||
/>
|
||||
<view class="h-full flex flex-1 flex-col justify-between gap-y-1">
|
||||
<view class="text-sm text-gray-900 font-bold">
|
||||
小莫唐尼
|
||||
</view>
|
||||
<view class="line-clamp-1 text-xs text-gray-500">
|
||||
一个爱凑热闹,喜欢捣鼓前端的码农。
|
||||
</view>
|
||||
</view>
|
||||
<view class="h-full shrink-0 rounded-3xl bg-[#1A1A1A] px-4 py-2 text-xs text-white">
|
||||
联系我
|
||||
</view>
|
||||
</view>
|
||||
<view class="my-1 h-1px w-full bg-gray-50" />
|
||||
<view class="grid grid-cols-5 w-full gap-x-4">
|
||||
<view class="flex flex-col items-center justify-center gap-y-0.5">
|
||||
<text class="text-sm text-gray-900 font-bold">{{ stats.post }}</text>
|
||||
<text class="text-xs text-gray-600">笔记</text>
|
||||
</view>
|
||||
<view class="flex flex-col items-center justify-center gap-y-0.5">
|
||||
<text class="text-sm text-gray-900 font-bold">{{ stats.visit }}</text>
|
||||
<text class="text-xs text-gray-600">访客</text>
|
||||
</view>
|
||||
<view class="flex flex-col items-center justify-center gap-y-0.5">
|
||||
<text class="text-sm text-gray-900 font-bold">{{ stats.category }}</text>
|
||||
<text class="text-xs text-gray-600">分类</text>
|
||||
</view>
|
||||
<view class="flex flex-col items-center justify-center gap-y-0.5">
|
||||
<text class="text-sm text-gray-900 font-bold">{{ stats.comment }}</text>
|
||||
<text class="text-xs text-gray-600">评论</text>
|
||||
</view>
|
||||
<view class="flex flex-col items-center justify-center gap-y-0.5">
|
||||
<text class="text-sm text-gray-900 font-bold">{{ stats.upvote }}</text>
|
||||
<text class="text-xs text-gray-600">点赞</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.uh-shadow-xs {
|
||||
box-shadow: 0 0 24rpx rgba(0, 0, 0, 0.035);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,76 @@
|
||||
<script setup lang="ts">
|
||||
import { cover } from '@/utils/imageHelper'
|
||||
import { queryCategories } from '@/api/halo-base/category'
|
||||
import type { CategoryV1alpha1PublicApiQueryCategoriesRequest, CategoryVoList } from '@halo-dev/api-client'
|
||||
|
||||
const { loading, error, data, run } = useRequest<CategoryVoList, CategoryV1alpha1PublicApiQueryCategoriesRequest>(queryCategories, { loadingToast: false, loadingToastContent: '加载中...', loadingToastMask: true })
|
||||
|
||||
const isVisible = computed(() => {
|
||||
return !loading.value && !error.value && data.value?.items.length > 0
|
||||
})
|
||||
|
||||
const firstCateItem = computed(() => {
|
||||
const item = data.value?.items[0]
|
||||
return {
|
||||
name: item.spec.displayName,
|
||||
count: item.status.postCount,
|
||||
cover: cover(item.spec.cover),
|
||||
}
|
||||
})
|
||||
|
||||
const otherCateItems = computed(() => {
|
||||
const items = data.value?.items.slice(1) || []
|
||||
return items.map(item => ({
|
||||
name: item.spec.displayName,
|
||||
count: item.status.postCount,
|
||||
cover: cover(item.spec.cover),
|
||||
}))
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
console.log('分类推荐 onMounted')
|
||||
run({
|
||||
page: 1,
|
||||
size: 3,
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view v-if="isVisible" class="mt-6 box-border px-4">
|
||||
<view class="mb-4 box-border flex items-center justify-between">
|
||||
<text class="text-sm text-gray-900 font-bold">热门分类</text>
|
||||
</view>
|
||||
<view class="box-border h-36 flex items-center gap-2 overflow-hidden">
|
||||
<view
|
||||
class="relative box-border h-full flex-1 overflow-hidden border-2 border-white rounded-xl border-solid bg-white"
|
||||
>
|
||||
<image :src="firstCateItem.cover" mode="heightFix" class="h-full w-full rounded-xl" />
|
||||
<view
|
||||
class="absolute bottom-0 left-0 right-0 z-10 box-border box-border w-full flex flex-col gap-y-1 from-black/0 via-black/50 to-black/90 bg-gradient-to-b p-2"
|
||||
>
|
||||
<text class="line-clamp-1 text-xs text-white font-bold">{{ firstCateItem.name }}</text>
|
||||
</view>
|
||||
<view class="absolute right-2 top-2 z-10 box-border rounded-md bg-black/20 px-1.5">
|
||||
<text class="text-[12px] text-white">{{ firstCateItem.count }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="h-full flex flex-1 flex-col gap-2">
|
||||
<view
|
||||
v-for="cate in otherCateItems" :key="cate.name"
|
||||
class="relative box-border flex-1 overflow-hidden border-2 border-white rounded-xl border-solid bg-white"
|
||||
>
|
||||
<image :src="cate.cover" mode="aspectFill" class="w-full rounded-xl" />
|
||||
<view
|
||||
class="absolute bottom-0 left-0 right-0 z-10 box-border box-border w-full flex flex-col gap-y-1 from-black/0 via-black/50 to-black/90 bg-gradient-to-b p-2"
|
||||
>
|
||||
<text class="line-clamp-1 text-xs text-white font-bold">{{ cate.name }}</text>
|
||||
</view>
|
||||
<view class="absolute right-2 top-2 z-10 box-border rounded-md bg-black/20 px-1.5 py-0.5">
|
||||
<text class="text-[12px] text-white">{{ cate.count }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -0,0 +1,11 @@
|
||||
// 以下项目可以删减或更换顺序,但不能添加或更改名字
|
||||
export default {
|
||||
// 普通标签的菜单项
|
||||
node: ['大小', '斜体', '粗体', '下划线', '居中', '缩进', '上移', '下移', '删除'],
|
||||
// 图片的菜单项
|
||||
img: ['换图', '宽度', '超链接', '预览图', '禁用预览', '上移', '下移', '删除'],
|
||||
// 链接的菜单项
|
||||
link: ['更换链接', '上移', '下移', '删除'],
|
||||
// 音视频的菜单项
|
||||
media: ['封面', '循环', '自动播放', '上移', '下移', '删除']
|
||||
}
|
||||
@@ -0,0 +1,532 @@
|
||||
/**
|
||||
* @fileoverview editable 插件
|
||||
*/
|
||||
import config from './config'
|
||||
import Parser from '../parser'
|
||||
|
||||
function Editable (vm) {
|
||||
this.vm = vm
|
||||
this.editHistory = [] // 历史记录
|
||||
this.editI = -1 // 历史记录指针
|
||||
vm._mask = [] // 蒙版被点击时进行的操作
|
||||
|
||||
vm._setData = function (path, val) {
|
||||
const paths = path.split('.')
|
||||
let target = vm
|
||||
for (let i = 0; i < paths.length - 1; i++) {
|
||||
target = target[paths[i]]
|
||||
}
|
||||
vm.$set(target, paths.pop(), val)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 移动历史记录指针
|
||||
* @param {Number} num 移动距离
|
||||
*/
|
||||
const move = num => {
|
||||
setTimeout(() => {
|
||||
const item = this.editHistory[this.editI + num]
|
||||
if (item) {
|
||||
this.editI += num
|
||||
vm._setData(item.key, item.value)
|
||||
}
|
||||
}, 200)
|
||||
}
|
||||
vm.undo = () => move(-1) // 撤销
|
||||
vm.redo = () => move(1) // 重做
|
||||
|
||||
/**
|
||||
* @description 更新记录
|
||||
* @param {String} path 更新内容路径
|
||||
* @param {*} oldVal 旧值
|
||||
* @param {*} newVal 新值
|
||||
* @param {Boolean} set 是否更新到视图
|
||||
* @private
|
||||
*/
|
||||
vm._editVal = (path, oldVal, newVal, set) => {
|
||||
// 当前指针后的内容去除
|
||||
while (this.editI < this.editHistory.length - 1) {
|
||||
this.editHistory.pop()
|
||||
}
|
||||
|
||||
// 最多存储 30 条操作记录
|
||||
while (this.editHistory.length > 30) {
|
||||
this.editHistory.pop()
|
||||
this.editI--
|
||||
}
|
||||
|
||||
const last = this.editHistory[this.editHistory.length - 1]
|
||||
if (!last || last.key !== path) {
|
||||
if (last) {
|
||||
// 去掉上一次的新值
|
||||
this.editHistory.pop()
|
||||
this.editI--
|
||||
}
|
||||
// 存入这一次的旧值
|
||||
this.editHistory.push({
|
||||
key: path,
|
||||
value: oldVal
|
||||
})
|
||||
this.editI++
|
||||
}
|
||||
|
||||
// 存入本次的新值
|
||||
this.editHistory.push({
|
||||
key: path,
|
||||
value: newVal
|
||||
})
|
||||
this.editI++
|
||||
|
||||
// 更新到视图
|
||||
if (set) {
|
||||
vm._setData(path, newVal)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 获取菜单项
|
||||
* @private
|
||||
*/
|
||||
vm._getItem = function (node, up, down) {
|
||||
let items
|
||||
let i
|
||||
if (node.name === 'img') {
|
||||
items = config.img.slice(0)
|
||||
if (!vm.getSrc) {
|
||||
i = items.indexOf('换图')
|
||||
if (i !== -1) {
|
||||
items.splice(i, 1)
|
||||
}
|
||||
i = items.indexOf('超链接')
|
||||
if (i !== -1) {
|
||||
items.splice(i, 1)
|
||||
}
|
||||
i = items.indexOf('预览图')
|
||||
if (i !== -1) {
|
||||
items.splice(i, 1)
|
||||
}
|
||||
}
|
||||
i = items.indexOf('禁用预览')
|
||||
if (i !== -1 && node.attrs.ignore) {
|
||||
items[i] = '启用预览'
|
||||
}
|
||||
} else if (node.name === 'a') {
|
||||
items = config.link.slice(0)
|
||||
if (!vm.getSrc) {
|
||||
i = items.indexOf('更换链接')
|
||||
if (i !== -1) {
|
||||
items.splice(i, 1)
|
||||
}
|
||||
}
|
||||
} else if (node.name === 'video' || node.name === 'audio') {
|
||||
items = config.media.slice(0)
|
||||
i = items.indexOf('封面')
|
||||
if (!vm.getSrc && i !== -1) {
|
||||
items.splice(i, 1)
|
||||
}
|
||||
i = items.indexOf('循环')
|
||||
if (node.attrs.loop && i !== -1) {
|
||||
items[i] = '不循环'
|
||||
}
|
||||
i = items.indexOf('自动播放')
|
||||
if (node.attrs.autoplay && i !== -1) {
|
||||
items[i] = '不自动播放'
|
||||
}
|
||||
} else {
|
||||
items = config.node.slice(0)
|
||||
}
|
||||
if (!up) {
|
||||
i = items.indexOf('上移')
|
||||
if (i !== -1) {
|
||||
items.splice(i, 1)
|
||||
}
|
||||
}
|
||||
if (!down) {
|
||||
i = items.indexOf('下移')
|
||||
if (i !== -1) {
|
||||
items.splice(i, 1)
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 显示 tooltip
|
||||
* @param {object} obj
|
||||
* @private
|
||||
*/
|
||||
vm._tooltip = function (obj) {
|
||||
vm.$set(vm, 'tooltip', {
|
||||
top: obj.top,
|
||||
items: obj.items
|
||||
})
|
||||
vm._tooltipcb = obj.success
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 显示滚动条
|
||||
* @param {object} obj
|
||||
* @private
|
||||
*/
|
||||
vm._slider = function (obj) {
|
||||
vm.$set(vm, 'slider', {
|
||||
min: obj.min,
|
||||
max: obj.max,
|
||||
value: obj.value,
|
||||
top: obj.top
|
||||
})
|
||||
vm._slideringcb = obj.changing
|
||||
vm._slidercb = obj.change
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 点击蒙版
|
||||
* @private
|
||||
*/
|
||||
vm._maskTap = function () {
|
||||
// 隐藏所有悬浮窗
|
||||
while (vm._mask.length) {
|
||||
(vm._mask.pop())()
|
||||
}
|
||||
if (vm.tooltip) {
|
||||
vm.$set(vm, 'tooltip', null)
|
||||
}
|
||||
if (vm.slider) {
|
||||
vm.$set(vm, 'slider', null)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 插入节点
|
||||
* @param {Object} node
|
||||
*/
|
||||
function insert (node) {
|
||||
if (vm._edit) {
|
||||
vm._edit.insert(node)
|
||||
} else {
|
||||
const nodes = vm.nodes.slice(0)
|
||||
nodes.push(node)
|
||||
vm._editVal('nodes', vm.nodes, nodes, true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 在光标处插入指定 html 内容
|
||||
* @param {String} html 内容
|
||||
*/
|
||||
vm.insertHtml = html => {
|
||||
this.inserting = true
|
||||
const arr = new Parser(vm).parse(html)
|
||||
this.inserting = undefined
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
insert(arr[i])
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 在光标处插入图片
|
||||
*/
|
||||
vm.insertImg = function () {
|
||||
vm.getSrc && vm.getSrc('img').then(src => {
|
||||
if (typeof src === 'string') {
|
||||
src = [src]
|
||||
}
|
||||
const parser = new Parser(vm)
|
||||
for (let i = 0; i < src.length; i++) {
|
||||
insert({
|
||||
name: 'img',
|
||||
attrs: {
|
||||
src: parser.getUrl(src[i])
|
||||
}
|
||||
})
|
||||
}
|
||||
}).catch(() => { })
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 在光标处插入一个链接
|
||||
*/
|
||||
vm.insertLink = function () {
|
||||
vm.getSrc && vm.getSrc('link').then(url => {
|
||||
insert({
|
||||
name: 'a',
|
||||
attrs: {
|
||||
href: url
|
||||
},
|
||||
children: [{
|
||||
type: 'text',
|
||||
text: url
|
||||
}]
|
||||
})
|
||||
}).catch(() => { })
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 在光标处插入一个表格
|
||||
* @param {Number} rows 行数
|
||||
* @param {Number} cols 列数
|
||||
*/
|
||||
vm.insertTable = function (rows, cols) {
|
||||
const table = {
|
||||
name: 'table',
|
||||
attrs: {
|
||||
style: 'display:table;width:100%;margin:10px 0;text-align:center;border-spacing:0;border-collapse:collapse;border:1px solid gray'
|
||||
},
|
||||
children: []
|
||||
}
|
||||
for (let i = 0; i < rows; i++) {
|
||||
const tr = {
|
||||
name: 'tr',
|
||||
attrs: {},
|
||||
children: []
|
||||
}
|
||||
for (let j = 0; j < cols; j++) {
|
||||
tr.children.push({
|
||||
name: 'td',
|
||||
attrs: {
|
||||
style: 'padding:2px;border:1px solid gray'
|
||||
},
|
||||
children: [{
|
||||
type: 'text',
|
||||
text: ''
|
||||
}]
|
||||
})
|
||||
}
|
||||
table.children.push(tr)
|
||||
}
|
||||
insert(table)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 插入视频/音频
|
||||
* @param {Object} node
|
||||
*/
|
||||
function insertMedia (node) {
|
||||
if (typeof node.src === 'string') {
|
||||
node.src = [node.src]
|
||||
}
|
||||
const parser = new Parser(vm)
|
||||
// 拼接主域名
|
||||
for (let i = 0; i < node.src.length; i++) {
|
||||
node.src[i] = parser.getUrl(node.src[i])
|
||||
}
|
||||
insert({
|
||||
name: 'div',
|
||||
attrs: {
|
||||
style: 'text-align:center'
|
||||
},
|
||||
children: [node]
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 在光标处插入一个视频
|
||||
*/
|
||||
vm.insertVideo = function () {
|
||||
vm.getSrc && vm.getSrc('video').then(src => {
|
||||
insertMedia({
|
||||
name: 'video',
|
||||
attrs: {
|
||||
controls: 'T'
|
||||
},
|
||||
children: [],
|
||||
src,
|
||||
// #ifdef APP-PLUS
|
||||
html: `<video src="${src}" style="width:100%;height:100%"></video>`
|
||||
// #endif
|
||||
})
|
||||
}).catch(() => { })
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 在光标处插入一个音频
|
||||
*/
|
||||
vm.insertAudio = function () {
|
||||
vm.getSrc && vm.getSrc('audio').then(attrs => {
|
||||
let src
|
||||
if (attrs.src) {
|
||||
src = attrs.src
|
||||
attrs.src = undefined
|
||||
} else {
|
||||
src = attrs
|
||||
attrs = {}
|
||||
}
|
||||
attrs.controls = 'T'
|
||||
insertMedia({
|
||||
name: 'audio',
|
||||
attrs,
|
||||
children: [],
|
||||
src
|
||||
})
|
||||
}).catch(() => { })
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 在光标处插入一段文本
|
||||
*/
|
||||
vm.insertText = function () {
|
||||
insert({
|
||||
name: 'p',
|
||||
attrs: {},
|
||||
children: [{
|
||||
type: 'text',
|
||||
text: ''
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 清空内容
|
||||
*/
|
||||
vm.clear = function () {
|
||||
vm._maskTap()
|
||||
vm._edit = undefined
|
||||
vm.$set(vm, 'nodes', [{
|
||||
name: 'p',
|
||||
attrs: {},
|
||||
children: [{
|
||||
type: 'text',
|
||||
text: ''
|
||||
}]
|
||||
}])
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 获取编辑后的 html
|
||||
*/
|
||||
vm.getContent = function () {
|
||||
let html = '';
|
||||
// 递归遍历获取
|
||||
(function traversal (nodes, table) {
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
let item = nodes[i]
|
||||
if (item.type === 'text') {
|
||||
html += item.text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/\n/g, '<br>').replace(/\xa0/g, ' ') // 编码实体
|
||||
} else {
|
||||
if (item.name === 'img') {
|
||||
item.attrs.i = ''
|
||||
// 还原被转换的 svg
|
||||
if ((item.attrs.src || '').includes('data:image/svg+xml;utf8,')) {
|
||||
html += item.attrs.src.substr(24).replace(/%23/g, '#').replace('<svg', '<svg style="' + (item.attrs.style || '') + '"')
|
||||
continue
|
||||
}
|
||||
} else if (item.name === 'video' || item.name === 'audio') {
|
||||
// 还原 video 和 audio 的 source
|
||||
item = JSON.parse(JSON.stringify(item))
|
||||
if (item.src.length > 1) {
|
||||
item.children = []
|
||||
for (let j = 0; j < item.src.length; j++) {
|
||||
item.children.push({
|
||||
name: 'source',
|
||||
attrs: {
|
||||
src: item.src[j]
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
item.attrs.src = item.src[0]
|
||||
}
|
||||
} else if (item.name === 'div' && (item.attrs.style || '').includes('overflow:auto') && (item.children[0] || {}).name === 'table') {
|
||||
// 还原滚动层
|
||||
item = item.children[0]
|
||||
}
|
||||
// 还原 table
|
||||
if (item.name === 'table') {
|
||||
item = JSON.parse(JSON.stringify(item))
|
||||
table = item.attrs
|
||||
if ((item.attrs.style || '').includes('display:grid')) {
|
||||
item.attrs.style = item.attrs.style.split('display:grid')[0]
|
||||
const children = [{
|
||||
name: 'tr',
|
||||
attrs: {},
|
||||
children: []
|
||||
}]
|
||||
for (let j = 0; j < item.children.length; j++) {
|
||||
item.children[j].attrs.style = item.children[j].attrs.style.replace(/grid-[^;]+;*/g, '')
|
||||
if (item.children[j].r !== children.length) {
|
||||
children.push({
|
||||
name: 'tr',
|
||||
attrs: {},
|
||||
children: [item.children[j]]
|
||||
})
|
||||
} else {
|
||||
children[children.length - 1].children.push(item.children[j])
|
||||
}
|
||||
}
|
||||
item.children = children
|
||||
}
|
||||
}
|
||||
html += '<' + item.name
|
||||
for (const attr in item.attrs) {
|
||||
let val = item.attrs[attr]
|
||||
if (!val) continue
|
||||
if (val === 'T' || val === true) {
|
||||
// bool 型省略值
|
||||
html += ' ' + attr
|
||||
continue
|
||||
} else if (item.name[0] === 't' && attr === 'style' && table) {
|
||||
// 取消为了显示 table 添加的 style
|
||||
val = val.replace(/;*display:table[^;]*/, '')
|
||||
if (table.border) {
|
||||
val = val.replace(/border[^;]+;*/g, $ => $.includes('collapse') ? $ : '')
|
||||
}
|
||||
if (table.cellpadding) {
|
||||
val = val.replace(/padding[^;]+;*/g, '')
|
||||
}
|
||||
if (!val) continue
|
||||
}
|
||||
html += ' ' + attr + '="' + val.replace(/"/g, '"') + '"'
|
||||
}
|
||||
html += '>'
|
||||
if (item.children) {
|
||||
traversal(item.children, table)
|
||||
html += '</' + item.name + '>'
|
||||
}
|
||||
}
|
||||
}
|
||||
})(vm.nodes)
|
||||
|
||||
// 其他插件处理
|
||||
for (let i = vm.plugins.length; i--;) {
|
||||
if (vm.plugins[i].onGetContent) {
|
||||
html = vm.plugins[i].onGetContent(html) || html
|
||||
}
|
||||
}
|
||||
|
||||
return html
|
||||
}
|
||||
}
|
||||
|
||||
Editable.prototype.onUpdate = function (content, config) {
|
||||
if (this.vm.editable) {
|
||||
this.vm._maskTap()
|
||||
config.entities.amp = '&'
|
||||
if (!this.inserting) {
|
||||
this.vm._edit = undefined
|
||||
if (!content) {
|
||||
setTimeout(() => {
|
||||
this.vm.$set(this.vm, 'nodes', [{
|
||||
name: 'p',
|
||||
attrs: {},
|
||||
children: [{
|
||||
type: 'text',
|
||||
text: ''
|
||||
}]
|
||||
}])
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Editable.prototype.onParse = function (node) {
|
||||
// 空白单元格可编辑
|
||||
if (this.vm.editable && (node.name === 'td' || node.name === 'th') && !this.vm.getText(node.children)) {
|
||||
node.children.push({
|
||||
type: 'text',
|
||||
text: ''
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default Editable
|
||||
@@ -0,0 +1,203 @@
|
||||
/**
|
||||
* @fileoverview emoji 插件
|
||||
*/
|
||||
const reg = /\[(\S+?)\]/g
|
||||
const data = {
|
||||
笑脸: '😄',
|
||||
生病: '😷',
|
||||
破涕为笑: '😂',
|
||||
吐舌: '😝',
|
||||
脸红: '😳',
|
||||
恐惧: '😱',
|
||||
失望: '😔',
|
||||
无语: '😒',
|
||||
眨眼: '😉',
|
||||
酷: '😎',
|
||||
哭: '😭',
|
||||
痴迷: '😍',
|
||||
吻: '😘',
|
||||
思考: '🤔',
|
||||
困惑: '😕',
|
||||
颠倒: '🙃',
|
||||
钱: '🤑',
|
||||
惊讶: '😲',
|
||||
白眼: '🙄',
|
||||
叹气: '😤',
|
||||
睡觉: '😴',
|
||||
书呆子: '🤓',
|
||||
愤怒: '😡',
|
||||
面无表情: '😑',
|
||||
张嘴: '😮',
|
||||
量体温: '🤒',
|
||||
呕吐: '🤮',
|
||||
光环: '😇',
|
||||
幽灵: '👻',
|
||||
外星人: '👽',
|
||||
机器人: '🤖',
|
||||
捂眼镜: '🙈',
|
||||
捂耳朵: '🙉',
|
||||
捂嘴: '🙊',
|
||||
婴儿: '👶',
|
||||
男孩: '👦',
|
||||
女孩: '👧',
|
||||
男人: '👨',
|
||||
女人: '👩',
|
||||
老人: '👴',
|
||||
老妇人: '👵',
|
||||
警察: '👮',
|
||||
王子: '🤴',
|
||||
公主: '🤴',
|
||||
举手: '🙋',
|
||||
跑步: '🏃',
|
||||
家庭: '👪',
|
||||
眼睛: '👀',
|
||||
鼻子: '👃',
|
||||
耳朵: '👂',
|
||||
舌头: '👅',
|
||||
嘴: '👄',
|
||||
心: '❤️',
|
||||
心碎: '💔',
|
||||
雪人: '☃️',
|
||||
情书: '💌',
|
||||
大便: '💩',
|
||||
闹钟: '⏰',
|
||||
眼镜: '👓',
|
||||
雨伞: '☂️',
|
||||
音乐: '🎵',
|
||||
话筒: '🎤',
|
||||
游戏机: '🎮',
|
||||
喇叭: '📢',
|
||||
耳机: '🎧',
|
||||
礼物: '🎁',
|
||||
电话: '📞',
|
||||
电脑: '💻',
|
||||
打印机: '🖨️',
|
||||
手电筒: '🔦',
|
||||
灯泡: '💡',
|
||||
书本: '📖',
|
||||
信封: '✉️',
|
||||
药丸: '💊',
|
||||
口红: '💄',
|
||||
手机: '📱',
|
||||
相机: '📷',
|
||||
电视: '📺',
|
||||
中: '🀄',
|
||||
垃圾桶: '🚮',
|
||||
厕所: '🚾',
|
||||
感叹号: '❗',
|
||||
禁: '🈲',
|
||||
可: '🉑',
|
||||
彩虹: '🌈',
|
||||
旋风: '🌀',
|
||||
雷电: '⚡',
|
||||
雪花: '❄️',
|
||||
星星: '⭐',
|
||||
水滴: '💧',
|
||||
玫瑰: '🌹',
|
||||
加油: '💪',
|
||||
左: '👈',
|
||||
右: '👉',
|
||||
上: '👆',
|
||||
下: '👇',
|
||||
手掌: '🖐️',
|
||||
好的: '👌',
|
||||
好: '👍',
|
||||
差: '👎',
|
||||
胜利: '✌',
|
||||
拳头: '👊',
|
||||
挥手: '👋',
|
||||
鼓掌: '👏',
|
||||
猴子: '🐒',
|
||||
狗: '🐶',
|
||||
狼: '🐺',
|
||||
猫: '🐱',
|
||||
老虎: '🐯',
|
||||
马: '🐎',
|
||||
独角兽: '🦄',
|
||||
斑马: '🦓',
|
||||
鹿: '🦌',
|
||||
牛: '🐮',
|
||||
猪: '🐷',
|
||||
羊: '🐏',
|
||||
长颈鹿: '🦒',
|
||||
大象: '🐘',
|
||||
老鼠: '🐭',
|
||||
蝙蝠: '🦇',
|
||||
刺猬: '🦔',
|
||||
熊猫: '🐼',
|
||||
鸽子: '🕊️',
|
||||
鸭子: '🦆',
|
||||
兔子: '🐇',
|
||||
老鹰: '🦅',
|
||||
青蛙: '🐸',
|
||||
蛇: '🐍',
|
||||
龙: '🐉',
|
||||
鲸鱼: '🐳',
|
||||
海豚: '🐬',
|
||||
足球: '⚽',
|
||||
棒球: '⚾',
|
||||
篮球: '🏀',
|
||||
排球: '🏐',
|
||||
橄榄球: '🏉',
|
||||
网球: '🎾',
|
||||
骰子: '🎲',
|
||||
鸡腿: '🍗',
|
||||
蛋糕: '🎂',
|
||||
啤酒: '🍺',
|
||||
饺子: '🥟',
|
||||
汉堡: '🍔',
|
||||
薯条: '🍟',
|
||||
意大利面: '🍝',
|
||||
干杯: '🥂',
|
||||
筷子: '🥢',
|
||||
糖果: '🍬',
|
||||
奶瓶: '🍼',
|
||||
爆米花: '🍿',
|
||||
邮局: '🏤',
|
||||
医院: '🏥',
|
||||
银行: '🏦',
|
||||
酒店: '🏨',
|
||||
学校: '🏫',
|
||||
城堡: '🏰',
|
||||
火车: '🚂',
|
||||
高铁: '🚄',
|
||||
地铁: '🚇',
|
||||
公交: '🚌',
|
||||
救护车: '🚑',
|
||||
消防车: '🚒',
|
||||
警车: '🚓',
|
||||
出租车: '🚕',
|
||||
汽车: '🚗',
|
||||
货车: '🚛',
|
||||
自行车: '🚲',
|
||||
摩托: '🛵',
|
||||
红绿灯: '🚥',
|
||||
帆船: '⛵',
|
||||
游轮: '🛳️',
|
||||
轮船: '⛴️',
|
||||
飞机: '✈️',
|
||||
直升机: '🚁',
|
||||
缆车: '🚠',
|
||||
警告: '⚠️',
|
||||
禁止: '⛔'
|
||||
}
|
||||
|
||||
function Emoji () {
|
||||
|
||||
}
|
||||
|
||||
Emoji.prototype.onUpdate = function (content) {
|
||||
return content.replace(reg, ($, $1) => {
|
||||
if (data[$1]) return data[$1]
|
||||
return $
|
||||
})
|
||||
}
|
||||
|
||||
Emoji.prototype.onGetContent = function (content) {
|
||||
for (const item in data) {
|
||||
content = content.replace(new RegExp(data[item], 'g'), '[' + item + ']')
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
export default Emoji
|
||||
@@ -0,0 +1,5 @@
|
||||
export default {
|
||||
copyByLongPress: true, // 是否需要长按代码块时显示复制代码内容菜单
|
||||
showLanguageName: true, // 是否在代码块右上角显示语言的名称
|
||||
showLineNumber: true // 是否显示行号
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* @fileoverview highlight 插件
|
||||
* Include prismjs (https://prismjs.com)
|
||||
*/
|
||||
import prism from './prism.min'
|
||||
import config from './config'
|
||||
import Parser from '../parser'
|
||||
|
||||
function Highlight (vm) {
|
||||
this.vm = vm
|
||||
}
|
||||
|
||||
Highlight.prototype.onParse = function (node, vm) {
|
||||
if (node.name === 'pre') {
|
||||
if (vm.options.editable) {
|
||||
node.attrs.class = (node.attrs.class || '') + ' hl-pre'
|
||||
return
|
||||
}
|
||||
let i
|
||||
for (i = node.children.length; i--;) {
|
||||
if (node.children[i].name === 'code') break
|
||||
}
|
||||
if (i === -1) return
|
||||
const code = node.children[i]
|
||||
let className = code.attrs.class + ' ' + node.attrs.class
|
||||
i = className.indexOf('language-')
|
||||
if (i === -1) {
|
||||
i = className.indexOf('lang-')
|
||||
if (i === -1) {
|
||||
className = 'language-text'
|
||||
i = 9
|
||||
} else {
|
||||
i += 5
|
||||
}
|
||||
} else {
|
||||
i += 9
|
||||
}
|
||||
let j
|
||||
for (j = i; j < className.length; j++) {
|
||||
if (className[j] === ' ') break
|
||||
}
|
||||
const lang = className.substring(i, j)
|
||||
if (code.children.length) {
|
||||
const text = this.vm.getText(code.children).replace(/&/g, '&')
|
||||
if (!text) return
|
||||
if (node.c) {
|
||||
node.c = undefined
|
||||
}
|
||||
if (prism.languages[lang]) {
|
||||
code.children = (new Parser(this.vm).parse(
|
||||
// 加一层 pre 保留空白符
|
||||
'<pre>' + prism.highlight(text, prism.languages[lang], lang).replace(/token /g, 'hl-') + '</pre>'))[0].children
|
||||
}
|
||||
node.attrs.class = 'hl-pre'
|
||||
code.attrs.class = 'hl-code'
|
||||
if (config.showLanguageName) {
|
||||
node.children.push({
|
||||
name: 'div',
|
||||
attrs: {
|
||||
class: 'hl-language',
|
||||
style: 'user-select:none'
|
||||
},
|
||||
children: [{
|
||||
type: 'text',
|
||||
text: lang
|
||||
}]
|
||||
})
|
||||
}
|
||||
if (config.copyByLongPress) {
|
||||
node.attrs.style += (node.attrs.style || '') + ';user-select:none'
|
||||
node.attrs['data-content'] = text
|
||||
vm.expose()
|
||||
}
|
||||
if (config.showLineNumber) {
|
||||
const line = text.split('\n').length; const children = []
|
||||
for (let k = line; k--;) {
|
||||
children.push({
|
||||
name: 'span',
|
||||
attrs: {
|
||||
class: 'span'
|
||||
}
|
||||
})
|
||||
}
|
||||
node.children.push({
|
||||
name: 'span',
|
||||
attrs: {
|
||||
class: 'line-numbers-rows'
|
||||
},
|
||||
children
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Highlight
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,138 @@
|
||||
const data = {
|
||||
name: 'imgcache',
|
||||
prefix: 'imgcache_'
|
||||
}
|
||||
function ImgCache (vm) {
|
||||
this.vm = vm // 保存实例在其他周期使用
|
||||
this.i = 0 // 用于标记第几张图
|
||||
vm.imgCache = {
|
||||
get list () {
|
||||
return uni
|
||||
.getStorageInfoSync()
|
||||
.keys.filter((key) => key.startsWith(data.prefix))
|
||||
.map((key) => key.split(data.prefix)[1])
|
||||
},
|
||||
get (url) {
|
||||
return uni.getStorageSync(data.prefix + url)
|
||||
},
|
||||
delete (url) {
|
||||
const path = uni.getStorageSync(data.prefix + url)
|
||||
if (!path) return false
|
||||
plus.io.resolveLocalFileSystemURL(path, (entry) => {
|
||||
entry.remove()
|
||||
})
|
||||
uni.removeStorageSync(data.prefix + url)
|
||||
return true
|
||||
},
|
||||
async add (url) {
|
||||
const filename = await download(url)
|
||||
if (filename) {
|
||||
uni.setStorageSync(data.prefix + url, filename)
|
||||
return 'file://' + plus.io.convertLocalFileSystemURL(filename)
|
||||
}
|
||||
return null
|
||||
},
|
||||
clear () {
|
||||
uni
|
||||
.getStorageInfoSync()
|
||||
.keys.filter((key) => key.startsWith(data.prefix))
|
||||
.forEach((key) => {
|
||||
uni.removeStorageSync(key)
|
||||
})
|
||||
|
||||
plus.io.resolveLocalFileSystemURL(`_doc/${data.name}/`, (entry) => {
|
||||
entry.removeRecursively(
|
||||
(entry) => {
|
||||
console.log(`${data.name}缓存删除成功`, entry)
|
||||
},
|
||||
(e) => {
|
||||
console.log(`${data.name}缓存删除失败`, e)
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// #ifdef APP-PLUS
|
||||
ImgCache.prototype.onParse = function (node, parser) {
|
||||
// 启用本插件 && 解析图片标签 && 拥有src属性 && 是网络图片
|
||||
if (
|
||||
this.vm.ImgCache &&
|
||||
node.name === 'img' &&
|
||||
node.attrs.src &&
|
||||
/^https?:\/\//.test(node.attrs.src)
|
||||
) {
|
||||
const src = node.attrs.src
|
||||
node.attrs.src = ''
|
||||
node.attrs.i = this.vm.imgList.length + this.i++
|
||||
parser.expose()
|
||||
|
||||
async function getUrl (path) {
|
||||
if (await resolveFile(path)) return path
|
||||
const filename = await download(src)
|
||||
filename && uni.setStorageSync(data.prefix + src, filename)
|
||||
return filename
|
||||
}
|
||||
|
||||
uni.getStorage({
|
||||
key: data.prefix + src,
|
||||
success: async (res) => {
|
||||
const path = await getUrl(res.data)
|
||||
const url = path
|
||||
? 'file://' + plus.io.convertLocalFileSystemURL(path)
|
||||
: src
|
||||
node.attrs.src = url
|
||||
this.vm.imgList[node.attrs.i] = path || src
|
||||
},
|
||||
fail: async () => {
|
||||
const path = await getUrl()
|
||||
const url = path
|
||||
? 'file://' + plus.io.convertLocalFileSystemURL(path)
|
||||
: src
|
||||
node.attrs.src = url
|
||||
this.vm.imgList[node.attrs.i] = path || src
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const taskQueue = new Set()
|
||||
|
||||
function download (url) {
|
||||
return new Promise((resolve) => {
|
||||
if (taskQueue.has(url)) return
|
||||
taskQueue.add(url)
|
||||
const suffix = /.+\.(jpg|jpeg|png|bmp|gif|webp)/.exec(url)
|
||||
const name = `${makeid(8)}_${Date.now()}${suffix ? '.' + suffix[1] : ''}`
|
||||
const task = plus.downloader.createDownload(
|
||||
url,
|
||||
{ filename: `_doc/${data.name}/${name}` },
|
||||
(download, status) => {
|
||||
taskQueue.delete(url)
|
||||
resolve(status === 200 ? download.filename : null)
|
||||
}
|
||||
)
|
||||
task.start()
|
||||
})
|
||||
}
|
||||
|
||||
// 判断文件存在
|
||||
function resolveFile (url) {
|
||||
return new Promise((resolve) => {
|
||||
plus.io.resolveLocalFileSystemURL(url, resolve, () => resolve(null))
|
||||
})
|
||||
}
|
||||
|
||||
// 生成uuid
|
||||
function makeid (length) {
|
||||
let result = ''
|
||||
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += characters.charAt(Math.floor(Math.random() * characters.length))
|
||||
}
|
||||
return result
|
||||
}
|
||||
// #endif
|
||||
|
||||
export default ImgCache
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* @fileoverview markdown 插件
|
||||
* Include marked (https://github.com/markedjs/marked)
|
||||
* Include github-markdown-css (https://github.com/sindresorhus/github-markdown-css)
|
||||
*/
|
||||
import marked from './marked.min'
|
||||
let index = 0
|
||||
|
||||
function Markdown (vm) {
|
||||
this.vm = vm
|
||||
vm._ids = {}
|
||||
}
|
||||
|
||||
Markdown.prototype.onUpdate = function (content) {
|
||||
if (this.vm.markdown) {
|
||||
return marked(content)
|
||||
}
|
||||
}
|
||||
|
||||
Markdown.prototype.onParse = function (node, vm) {
|
||||
if (vm.options.markdown) {
|
||||
// 中文 id 需要转换,否则无法跳转
|
||||
if (vm.options.useAnchor && node.attrs && /[\u4e00-\u9fa5]/.test(node.attrs.id)) {
|
||||
const id = 't' + index++
|
||||
this.vm._ids[node.attrs.id] = id
|
||||
node.attrs.id = id
|
||||
}
|
||||
if (node.name === 'p' || node.name === 'table' || node.name === 'tr' || node.name === 'th' || node.name === 'td' || node.name === 'blockquote' || node.name === 'pre' || node.name === 'code') {
|
||||
node.attrs.class = `md-${node.name} ${node.attrs.class || ''}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Markdown
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,579 @@
|
||||
<template>
|
||||
<view id="_root" :class="(selectable?'_select ':'')+'_root'" :style="(editable?'min-height:200px;':'')+containerStyle" @tap="_containTap">
|
||||
<slot v-if="!nodes[0]" />
|
||||
<!-- #ifndef APP-PLUS-NVUE -->
|
||||
<node v-else :childs="nodes" :opts="[lazyLoad,loadingImg,errorImg,showImgMenu,selectable,editable,placeholder,'nodes']" name="span" />
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef APP-PLUS-NVUE -->
|
||||
<web-view ref="web" src="/static/app-plus/mp-html/local.html" :style="'margin-top:-2px;height:' + height + 'px'" @onPostMessage="_onMessage" />
|
||||
<!-- #endif -->
|
||||
<view v-if="tooltip" class="_tooltip_contain" :style="'top:'+tooltip.top+'px'">
|
||||
<view class="_tooltip">
|
||||
<view v-for="(item, index) in tooltip.items" v-bind:key="index" class="_tooltip_item" :data-i="index" @tap="_tooltipTap">{{item}}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="slider" class="_slider" :style="'top:'+slider.top+'px'">
|
||||
<slider :value="slider.value" :min="slider.min" :max="slider.max" handle-size="14" block-size="14" show-value activeColor="white" style="padding:3px" @changing="_sliderChanging" @change="_sliderChange" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* mp-html v2.4.0
|
||||
* @description 富文本组件
|
||||
* @tutorial https://github.com/jin-yufeng/mp-html
|
||||
* @property {String} container-style 容器的样式
|
||||
* @property {String} content 用于渲染的 html 字符串
|
||||
* @property {Boolean} copy-link 是否允许外部链接被点击时自动复制
|
||||
* @property {String} domain 主域名,用于拼接链接
|
||||
* @property {String} error-img 图片出错时的占位图链接
|
||||
* @property {Boolean} lazy-load 是否开启图片懒加载
|
||||
* @property {string} loading-img 图片加载过程中的占位图链接
|
||||
* @property {Boolean} pause-video 是否在播放一个视频时自动暂停其他视频
|
||||
* @property {Boolean} preview-img 是否允许图片被点击时自动预览
|
||||
* @property {Boolean} scroll-table 是否给每个表格添加一个滚动层使其能单独横向滚动
|
||||
* @property {Boolean | String} selectable 是否开启长按复制
|
||||
* @property {Boolean} set-title 是否将 title 标签的内容设置到页面标题
|
||||
* @property {Boolean} show-img-menu 是否允许图片被长按时显示菜单
|
||||
* @property {Object} tag-style 标签的默认样式
|
||||
* @property {Boolean | Number} use-anchor 是否使用锚点链接
|
||||
* @event {Function} load dom 结构加载完毕时触发
|
||||
* @event {Function} ready 所有图片加载完毕时触发
|
||||
* @event {Function} imgtap 图片被点击时触发
|
||||
* @event {Function} linktap 链接被点击时触发
|
||||
* @event {Function} play 音视频播放时触发
|
||||
* @event {Function} error 媒体加载出错时触发
|
||||
*/
|
||||
// #ifndef APP-PLUS-NVUE
|
||||
import node from './node/node'
|
||||
// #endif
|
||||
import Parser from './parser'
|
||||
import markdown from './markdown/index.js'
|
||||
import emoji from './emoji/index.js'
|
||||
import highlight from './highlight/index.js'
|
||||
import style from './style/index.js'
|
||||
import imgCache from './img-cache/index.js'
|
||||
import editable from './editable/index.js'
|
||||
const plugins=[markdown,emoji,highlight,style,imgCache,editable,]
|
||||
// #ifdef APP-PLUS-NVUE
|
||||
const dom = weex.requireModule('dom')
|
||||
// #endif
|
||||
export default {
|
||||
name: 'mp-html',
|
||||
data() {
|
||||
return {
|
||||
tooltip: null,
|
||||
slider: null,
|
||||
nodes: [],
|
||||
// #ifdef APP-PLUS-NVUE
|
||||
height: 3
|
||||
// #endif
|
||||
}
|
||||
},
|
||||
props: {
|
||||
editable: Boolean,
|
||||
placeholder: String,
|
||||
ImgCache: Boolean,
|
||||
markdown: Boolean,
|
||||
containerStyle: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
content: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
copyLink: {
|
||||
type: [Boolean, String],
|
||||
default: true
|
||||
},
|
||||
domain: String,
|
||||
errorImg: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
lazyLoad: {
|
||||
type: [Boolean, String],
|
||||
default: false
|
||||
},
|
||||
loadingImg: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
pauseVideo: {
|
||||
type: [Boolean, String],
|
||||
default: true
|
||||
},
|
||||
previewImg: {
|
||||
type: [Boolean, String],
|
||||
default: true
|
||||
},
|
||||
scrollTable: [Boolean, String],
|
||||
selectable: [Boolean, String],
|
||||
setTitle: {
|
||||
type: [Boolean, String],
|
||||
default: true
|
||||
},
|
||||
showImgMenu: {
|
||||
type: [Boolean, String],
|
||||
default: true
|
||||
},
|
||||
tagStyle: Object,
|
||||
useAnchor: [Boolean, Number]
|
||||
},
|
||||
// #ifdef VUE3
|
||||
emits: ['load', 'ready', 'imgtap', 'linktap', 'play', 'error'],
|
||||
// #endif
|
||||
// #ifndef APP-PLUS-NVUE
|
||||
components: {
|
||||
node
|
||||
},
|
||||
// #endif
|
||||
watch: {
|
||||
editable(val) {
|
||||
this.setContent(val ? this.content : this.getContent())
|
||||
if (!val)
|
||||
this._maskTap()
|
||||
},
|
||||
content (content) {
|
||||
this.setContent(content)
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.plugins = []
|
||||
for (let i = plugins.length; i--;) {
|
||||
this.plugins.push(new plugins[i](this))
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
if ((this.content || this.editable) && !this.nodes.length) {
|
||||
this.setContent(this.content)
|
||||
}
|
||||
},
|
||||
beforeDestroy () {
|
||||
this._hook('onDetached')
|
||||
},
|
||||
methods: {
|
||||
_containTap() {
|
||||
if (!this._lock && !this.slider) {
|
||||
this._edit = undefined
|
||||
this._maskTap()
|
||||
}
|
||||
},
|
||||
_tooltipTap(e) {
|
||||
this._tooltipcb(e.currentTarget.dataset.i)
|
||||
this.$set(this, 'tooltip', null)
|
||||
},
|
||||
_sliderChanging(e) {
|
||||
this._slideringcb(e.detail.value)
|
||||
},
|
||||
_sliderChange(e) {
|
||||
this._slidercb(e.detail.value)
|
||||
},
|
||||
/**
|
||||
* @description 将锚点跳转的范围限定在一个 scroll-view 内
|
||||
* @param {Object} page scroll-view 所在页面的示例
|
||||
* @param {String} selector scroll-view 的选择器
|
||||
* @param {String} scrollTop scroll-view scroll-top 属性绑定的变量名
|
||||
*/
|
||||
in (page, selector, scrollTop) {
|
||||
// #ifndef APP-PLUS-NVUE
|
||||
if (page && selector && scrollTop) {
|
||||
this._in = {
|
||||
page,
|
||||
selector,
|
||||
scrollTop
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 锚点跳转
|
||||
* @param {String} id 要跳转的锚点 id
|
||||
* @param {Number} offset 跳转位置的偏移量
|
||||
* @returns {Promise}
|
||||
*/
|
||||
navigateTo (id, offset) {
|
||||
id = this._ids[decodeURI(id)] || id
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.useAnchor) {
|
||||
reject(Error('Anchor is disabled'))
|
||||
return
|
||||
}
|
||||
offset = offset || parseInt(this.useAnchor) || 0
|
||||
// #ifdef APP-PLUS-NVUE
|
||||
if (!id) {
|
||||
dom.scrollToElement(this.$refs.web, {
|
||||
offset
|
||||
})
|
||||
resolve()
|
||||
} else {
|
||||
this._navigateTo = {
|
||||
resolve,
|
||||
reject,
|
||||
offset
|
||||
}
|
||||
this.$refs.web.evalJs('uni.postMessage({data:{action:"getOffset",offset:(document.getElementById(' + id + ')||{}).offsetTop}})')
|
||||
}
|
||||
// #endif
|
||||
// #ifndef APP-PLUS-NVUE
|
||||
let deep = ' '
|
||||
// #ifdef MP-WEIXIN || MP-QQ || MP-TOUTIAO
|
||||
deep = '>>>'
|
||||
// #endif
|
||||
const selector = uni.createSelectorQuery()
|
||||
// #ifndef MP-ALIPAY
|
||||
.in(this._in ? this._in.page : this)
|
||||
// #endif
|
||||
.select((this._in ? this._in.selector : '._root') + (id ? `${deep}#${id}` : '')).boundingClientRect()
|
||||
if (this._in) {
|
||||
selector.select(this._in.selector).scrollOffset()
|
||||
.select(this._in.selector).boundingClientRect()
|
||||
} else {
|
||||
// 获取 scroll-view 的位置和滚动距离
|
||||
selector.selectViewport().scrollOffset() // 获取窗口的滚动距离
|
||||
}
|
||||
selector.exec(res => {
|
||||
if (!res[0]) {
|
||||
reject(Error('Label not found'))
|
||||
return
|
||||
}
|
||||
const scrollTop = res[1].scrollTop + res[0].top - (res[2] ? res[2].top : 0) + offset
|
||||
if (this._in) {
|
||||
// scroll-view 跳转
|
||||
this._in.page[this._in.scrollTop] = scrollTop
|
||||
} else {
|
||||
// 页面跳转
|
||||
uni.pageScrollTo({
|
||||
scrollTop,
|
||||
duration: 300
|
||||
})
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
// #endif
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 获取文本内容
|
||||
* @return {String}
|
||||
*/
|
||||
getText (nodes) {
|
||||
let text = '';
|
||||
(function traversal (nodes) {
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
const node = nodes[i]
|
||||
if (node.type === 'text') {
|
||||
text += node.text.replace(/&/g, '&')
|
||||
} else if (node.name === 'br') {
|
||||
text += '\n'
|
||||
} else {
|
||||
// 块级标签前后加换行
|
||||
const isBlock = node.name === 'p' || node.name === 'div' || node.name === 'tr' || node.name === 'li' || (node.name[0] === 'h' && node.name[1] > '0' && node.name[1] < '7')
|
||||
if (isBlock && text && text[text.length - 1] !== '\n') {
|
||||
text += '\n'
|
||||
}
|
||||
// 递归获取子节点的文本
|
||||
if (node.children) {
|
||||
traversal(node.children)
|
||||
}
|
||||
if (isBlock && text[text.length - 1] !== '\n') {
|
||||
text += '\n'
|
||||
} else if (node.name === 'td' || node.name === 'th') {
|
||||
text += '\t'
|
||||
}
|
||||
}
|
||||
}
|
||||
})(nodes || this.nodes)
|
||||
return text
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 获取内容大小和位置
|
||||
* @return {Promise}
|
||||
*/
|
||||
getRect () {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.createSelectorQuery()
|
||||
// #ifndef MP-ALIPAY
|
||||
.in(this)
|
||||
// #endif
|
||||
.select('#_root').boundingClientRect().exec(res => res[0] ? resolve(res[0]) : reject(Error('Root label not found')))
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 暂停播放媒体
|
||||
*/
|
||||
pauseMedia () {
|
||||
for (let i = (this._videos || []).length; i--;) {
|
||||
this._videos[i].pause()
|
||||
}
|
||||
// #ifdef APP-PLUS
|
||||
const command = 'for(var e=document.getElementsByTagName("video"),i=e.length;i--;)e[i].pause()'
|
||||
// #ifndef APP-PLUS-NVUE
|
||||
let page = this.$parent
|
||||
while (!page.$scope) page = page.$parent
|
||||
page.$scope.$getAppWebview().evalJS(command)
|
||||
// #endif
|
||||
// #ifdef APP-PLUS-NVUE
|
||||
this.$refs.web.evalJs(command)
|
||||
// #endif
|
||||
// #endif
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 设置媒体播放速率
|
||||
* @param {Number} rate 播放速率
|
||||
*/
|
||||
setPlaybackRate (rate) {
|
||||
this.playbackRate = rate
|
||||
for (let i = (this._videos || []).length; i--;) {
|
||||
this._videos[i].playbackRate(rate)
|
||||
}
|
||||
// #ifdef APP-PLUS
|
||||
const command = 'for(var e=document.getElementsByTagName("video"),i=e.length;i--;)e[i].playbackRate=' + rate
|
||||
// #ifndef APP-PLUS-NVUE
|
||||
let page = this.$parent
|
||||
while (!page.$scope) page = page.$parent
|
||||
page.$scope.$getAppWebview().evalJS(command)
|
||||
// #endif
|
||||
// #ifdef APP-PLUS-NVUE
|
||||
this.$refs.web.evalJs(command)
|
||||
// #endif
|
||||
// #endif
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 设置内容
|
||||
* @param {String} content html 内容
|
||||
* @param {Boolean} append 是否在尾部追加
|
||||
*/
|
||||
setContent (content, append) {
|
||||
if (!append || !this.imgList) {
|
||||
this.imgList = []
|
||||
}
|
||||
const nodes = new Parser(this).parse(content)
|
||||
// #ifdef APP-PLUS-NVUE
|
||||
if (this._ready) {
|
||||
this._set(nodes, append)
|
||||
}
|
||||
// #endif
|
||||
this.$set(this, 'nodes', append ? (this.nodes || []).concat(nodes) : nodes)
|
||||
|
||||
// #ifndef APP-PLUS-NVUE
|
||||
this._videos = []
|
||||
this.$nextTick(() => {
|
||||
this._hook('onLoad')
|
||||
this.$emit('load')
|
||||
})
|
||||
|
||||
if (this.lazyLoad || this.imgList._unloadimgs < this.imgList.length / 2) {
|
||||
// 设置懒加载,每 350ms 获取高度,不变则认为加载完毕
|
||||
let height
|
||||
const callback = rect => {
|
||||
// 350ms 总高度无变化就触发 ready 事件
|
||||
if (rect.height === height) {
|
||||
this.$emit('ready', rect)
|
||||
} else {
|
||||
height = rect.height
|
||||
setTimeout(() => {
|
||||
this.getRect().then(callback)
|
||||
}, 350)
|
||||
}
|
||||
}
|
||||
this.getRect().then(callback)
|
||||
} else {
|
||||
// 未设置懒加载,等待所有图片加载完毕
|
||||
if (!this.imgList._unloadimgs) {
|
||||
this.getRect(rect => {
|
||||
this.$emit('ready', rect)
|
||||
})
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 调用插件钩子函数
|
||||
*/
|
||||
_hook (name) {
|
||||
for (let i = plugins.length; i--;) {
|
||||
if (this.plugins[i][name]) {
|
||||
this.plugins[i][name]()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// #ifdef APP-PLUS-NVUE
|
||||
/**
|
||||
* @description 设置内容
|
||||
*/
|
||||
_set (nodes, append) {
|
||||
this.$refs.web.evalJs('setContent(' + JSON.stringify(nodes) + ',' + JSON.stringify([this.containerStyle.replace(/(?:margin|padding)[^;]+/g, ''), this.errorImg, this.loadingImg, this.pauseVideo, this.scrollTable, this.selectable]) + ',' + append + ')')
|
||||
},
|
||||
|
||||
/**
|
||||
* @description 接收到 web-view 消息
|
||||
*/
|
||||
_onMessage (e) {
|
||||
const message = e.detail.data[0]
|
||||
switch (message.action) {
|
||||
// web-view 初始化完毕
|
||||
case 'onJSBridgeReady':
|
||||
this._ready = true
|
||||
if (this.nodes) {
|
||||
this._set(this.nodes)
|
||||
}
|
||||
break
|
||||
// 内容 dom 加载完毕
|
||||
case 'onLoad':
|
||||
this.height = message.height
|
||||
this._hook('onLoad')
|
||||
this.$emit('load')
|
||||
break
|
||||
// 所有图片加载完毕
|
||||
case 'onReady':
|
||||
this.getRect().then(res => {
|
||||
this.$emit('ready', res)
|
||||
}).catch(() => { })
|
||||
break
|
||||
// 总高度发生变化
|
||||
case 'onHeightChange':
|
||||
this.height = message.height
|
||||
break
|
||||
// 图片点击
|
||||
case 'onImgTap':
|
||||
this.$emit('imgtap', message.attrs)
|
||||
if (this.previewImg) {
|
||||
uni.previewImage({
|
||||
current: parseInt(message.attrs.i),
|
||||
urls: this.imgList
|
||||
})
|
||||
}
|
||||
break
|
||||
// 链接点击
|
||||
case 'onLinkTap': {
|
||||
const href = message.attrs.href
|
||||
this.$emit('linktap', message.attrs)
|
||||
if (href) {
|
||||
// 锚点跳转
|
||||
if (href[0] === '#') {
|
||||
if (this.useAnchor) {
|
||||
dom.scrollToElement(this.$refs.web, {
|
||||
offset: message.offset
|
||||
})
|
||||
}
|
||||
} else if (href.includes('://')) {
|
||||
// 打开外链
|
||||
if (this.copyLink) {
|
||||
plus.runtime.openWeb(href)
|
||||
}
|
||||
} else {
|
||||
uni.navigateTo({
|
||||
url: href,
|
||||
fail () {
|
||||
uni.switchTab({
|
||||
url: href
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'onPlay':
|
||||
this.$emit('play')
|
||||
break
|
||||
// 获取到锚点的偏移量
|
||||
case 'getOffset':
|
||||
if (typeof message.offset === 'number') {
|
||||
dom.scrollToElement(this.$refs.web, {
|
||||
offset: message.offset + this._navigateTo.offset
|
||||
})
|
||||
this._navigateTo.resolve()
|
||||
} else {
|
||||
this._navigateTo.reject(Error('Label not found'))
|
||||
}
|
||||
break
|
||||
// 点击
|
||||
case 'onClick':
|
||||
this.$emit('tap')
|
||||
this.$emit('click')
|
||||
break
|
||||
// 出错
|
||||
case 'onError':
|
||||
this.$emit('error', {
|
||||
source: message.source,
|
||||
attrs: message.attrs
|
||||
})
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* #ifndef APP-PLUS-NVUE */
|
||||
/* 根节点样式 */
|
||||
._root {
|
||||
padding: 1px 0;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* 长按复制 */
|
||||
._select {
|
||||
user-select: text;
|
||||
}
|
||||
/* #endif */
|
||||
|
||||
/* 提示条 */
|
||||
._tooltip_contain {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
left: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
._tooltip {
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
height: 30px;
|
||||
padding: 0 3px;
|
||||
overflow: scroll;
|
||||
font-size: 14px;
|
||||
line-height: 30px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
._tooltip_item {
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
padding: 0 2vw;
|
||||
line-height: 30px;
|
||||
background-color: black;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 图片宽度滚动条 */
|
||||
._slider {
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
._tooltip,
|
||||
._slider {
|
||||
background-color: black;
|
||||
border-radius: 3px;
|
||||
opacity: 0.75;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* @fileoverview style 插件
|
||||
*/
|
||||
// #ifndef APP-PLUS-NVUE
|
||||
import Parser from './parser'
|
||||
// #endif
|
||||
|
||||
function Style () {
|
||||
this.styles = []
|
||||
}
|
||||
|
||||
// #ifndef APP-PLUS-NVUE
|
||||
Style.prototype.onParse = function (node, vm) {
|
||||
// 获取样式
|
||||
if (node.name === 'style' && node.children.length && node.children[0].type === 'text') {
|
||||
this.styles = this.styles.concat(new Parser().parse(node.children[0].text))
|
||||
} else if (node.name) {
|
||||
// 匹配样式(对非文本标签)
|
||||
// 存储不同优先级的样式 name < class < id < 后代
|
||||
let matched = ['', '', '', '']
|
||||
for (let i = 0, len = this.styles.length; i < len; i++) {
|
||||
const item = this.styles[i]
|
||||
let res = match(node, item.key || item.list[item.list.length - 1])
|
||||
let j
|
||||
if (res) {
|
||||
// 后代选择器
|
||||
if (!item.key) {
|
||||
j = item.list.length - 2
|
||||
for (let k = vm.stack.length; j >= 0 && k--;) {
|
||||
// 子选择器
|
||||
if (item.list[j] === '>') {
|
||||
// 错误情况
|
||||
if (j < 1 || j > item.list.length - 2) break
|
||||
if (match(vm.stack[k], item.list[j - 1])) {
|
||||
j -= 2
|
||||
} else {
|
||||
j++
|
||||
}
|
||||
} else if (match(vm.stack[k], item.list[j])) {
|
||||
j--
|
||||
}
|
||||
}
|
||||
res = 4
|
||||
}
|
||||
if (item.key || j < 0) {
|
||||
// 添加伪类
|
||||
if (item.pseudo && node.children) {
|
||||
let text
|
||||
item.style = item.style.replace(/content:([^;]+)/, (_, $1) => {
|
||||
text = $1.replace(/['"]/g, '')
|
||||
// 处理 attr 函数
|
||||
.replace(/attr\((.+?)\)/, (_, $1) => node.attrs[$1.trim()] || '')
|
||||
// 编码 \xxx
|
||||
.replace(/\\(\w{4})/, (_, $1) => String.fromCharCode(parseInt($1, 16)))
|
||||
return ''
|
||||
})
|
||||
const pseudo = {
|
||||
name: 'span',
|
||||
attrs: {
|
||||
style: item.style
|
||||
},
|
||||
children: [{
|
||||
type: 'text',
|
||||
text
|
||||
}]
|
||||
}
|
||||
if (item.pseudo === 'before') {
|
||||
node.children.unshift(pseudo)
|
||||
} else {
|
||||
node.children.push(pseudo)
|
||||
}
|
||||
} else {
|
||||
matched[res - 1] += item.style + (item.style[item.style.length - 1] === ';' ? '' : ';')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
matched = matched.join('')
|
||||
if (matched.length > 2) {
|
||||
node.attrs.style = matched + (node.attrs.style || '')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 匹配样式
|
||||
* @param {object} node 要匹配的标签
|
||||
* @param {string|string[]} keys 选择器
|
||||
* @returns {number} 0:不匹配;1:name 匹配;2:class 匹配;3:id 匹配
|
||||
*/
|
||||
function match (node, keys) {
|
||||
function matchItem (key) {
|
||||
if (key[0] === '#') {
|
||||
// 匹配 id
|
||||
if (node.attrs.id && node.attrs.id.trim() === key.substr(1)) return 3
|
||||
} else if (key[0] === '.') {
|
||||
// 匹配 class
|
||||
key = key.substr(1)
|
||||
const selectors = (node.attrs.class || '').split(' ')
|
||||
for (let i = 0; i < selectors.length; i++) {
|
||||
if (selectors[i].trim() === key) return 2
|
||||
}
|
||||
} else if (node.name === key) {
|
||||
// 匹配 name
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// 多选择器交集
|
||||
if (keys instanceof Array) {
|
||||
let res = 0
|
||||
for (let j = 0; j < keys.length; j++) {
|
||||
const tmp = matchItem(keys[j])
|
||||
// 任意一个不匹配就失败
|
||||
if (!tmp) return 0
|
||||
// 优先级最大的一个作为最终优先级
|
||||
if (tmp > res) {
|
||||
res = tmp
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
return matchItem(keys)
|
||||
}
|
||||
// #endif
|
||||
|
||||
export default Style
|
||||
@@ -0,0 +1,175 @@
|
||||
const blank = {
|
||||
' ': true,
|
||||
'\n': true,
|
||||
'\t': true,
|
||||
'\r': true,
|
||||
'\f': true
|
||||
}
|
||||
|
||||
function Parser () {
|
||||
this.styles = []
|
||||
this.selectors = []
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 解析 css 字符串
|
||||
* @param {string} content css 内容
|
||||
*/
|
||||
Parser.prototype.parse = function (content) {
|
||||
new Lexer(this).parse(content)
|
||||
return this.styles
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 解析到一个选择器
|
||||
* @param {string} name 名称
|
||||
*/
|
||||
Parser.prototype.onSelector = function (name) {
|
||||
// 不支持的选择器
|
||||
if (name.includes('[') || name.includes('*') || name.includes('@')) return
|
||||
const selector = {}
|
||||
// 伪类
|
||||
if (name.includes(':')) {
|
||||
const info = name.split(':')
|
||||
const pseudo = info.pop()
|
||||
if (pseudo === 'before' || pseudo === 'after') {
|
||||
selector.pseudo = pseudo
|
||||
name = info[0]
|
||||
} else return
|
||||
}
|
||||
|
||||
// 分割交集选择器
|
||||
function splitItem (str) {
|
||||
const arr = []
|
||||
let i, start
|
||||
for (i = 1, start = 0; i < str.length; i++) {
|
||||
if (str[i] === '.' || str[i] === '#') {
|
||||
arr.push(str.substring(start, i))
|
||||
start = i
|
||||
}
|
||||
}
|
||||
if (!arr.length) {
|
||||
return str
|
||||
} else {
|
||||
arr.push(str.substring(start, i))
|
||||
return arr
|
||||
}
|
||||
}
|
||||
|
||||
// 后代选择器
|
||||
if (name.includes(' ')) {
|
||||
selector.list = []
|
||||
const list = name.split(' ')
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
if (list[i].length) {
|
||||
// 拆分子选择器
|
||||
const arr = list[i].split('>')
|
||||
for (let j = 0; j < arr.length; j++) {
|
||||
selector.list.push(splitItem(arr[j]))
|
||||
if (j < arr.length - 1) {
|
||||
selector.list.push('>')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
selector.key = splitItem(name)
|
||||
}
|
||||
|
||||
this.selectors.push(selector)
|
||||
}
|
||||
|
||||
/**
|
||||
* @description 解析到选择器内容
|
||||
* @param {string} content 内容
|
||||
*/
|
||||
Parser.prototype.onContent = function (content) {
|
||||
// 并集选择器
|
||||
for (let i = 0; i < this.selectors.length; i++) {
|
||||
this.selectors[i].style = content
|
||||
}
|
||||
this.styles = this.styles.concat(this.selectors)
|
||||
this.selectors = []
|
||||
}
|
||||
|
||||
/**
|
||||
* @description css 词法分析器
|
||||
* @param {object} handler 高层处理器
|
||||
*/
|
||||
function Lexer (handler) {
|
||||
this.selector = ''
|
||||
this.style = ''
|
||||
this.handler = handler
|
||||
}
|
||||
|
||||
Lexer.prototype.parse = function (content) {
|
||||
this.i = 0
|
||||
this.content = content
|
||||
this.state = this.blank
|
||||
for (let len = content.length; this.i < len; this.i++) {
|
||||
this.state(content[this.i])
|
||||
}
|
||||
}
|
||||
|
||||
Lexer.prototype.comment = function () {
|
||||
this.i = this.content.indexOf('*/', this.i) + 1
|
||||
if (!this.i) {
|
||||
this.i = this.content.length
|
||||
}
|
||||
}
|
||||
|
||||
Lexer.prototype.blank = function (c) {
|
||||
if (!blank[c]) {
|
||||
if (c === '/' && this.content[this.i + 1] === '*') {
|
||||
this.comment()
|
||||
return
|
||||
}
|
||||
this.selector += c
|
||||
this.state = this.name
|
||||
}
|
||||
}
|
||||
|
||||
Lexer.prototype.name = function (c) {
|
||||
if (c === '/' && this.content[this.i + 1] === '*') {
|
||||
this.comment()
|
||||
return
|
||||
}
|
||||
if (c === '{' || c === ',' || c === ';') {
|
||||
this.handler.onSelector(this.selector.trimEnd())
|
||||
this.selector = ''
|
||||
if (c !== '{') {
|
||||
while (blank[this.content[++this.i]]);
|
||||
}
|
||||
if (this.content[this.i] === '{') {
|
||||
this.floor = 1
|
||||
this.state = this.val
|
||||
} else {
|
||||
this.selector += this.content[this.i]
|
||||
}
|
||||
} else if (blank[c]) {
|
||||
this.selector += ' '
|
||||
} else {
|
||||
this.selector += c
|
||||
}
|
||||
}
|
||||
|
||||
Lexer.prototype.val = function (c) {
|
||||
if (c === '/' && this.content[this.i + 1] === '*') {
|
||||
this.comment()
|
||||
return
|
||||
}
|
||||
if (c === '{') {
|
||||
this.floor++
|
||||
} else if (c === '}') {
|
||||
this.floor--
|
||||
if (!this.floor) {
|
||||
this.handler.onContent(this.style)
|
||||
this.style = ''
|
||||
this.state = this.blank
|
||||
return
|
||||
}
|
||||
}
|
||||
this.style += c
|
||||
}
|
||||
|
||||
export default Parser
|
||||
@@ -0,0 +1 @@
|
||||
"use strict";function t(t){for(var e=Object.create(null),n=t.attributes.length;n--;)e[t.attributes[n].name]=t.attributes[n].value;return e}function e(){a[1]&&(this.src=a[1],this.onerror=null),this.onclick=null,this.ontouchstart=null,uni.postMessage({data:{action:"onError",source:"img",attrs:t(this)}})}function n(){window.unloadimgs-=1,0===window.unloadimgs&&uni.postMessage({data:{action:"onReady"}})}function o(r,s,c){for(var d=0;d<r.length;d++)!function(d){var u=r[d],l=void 0;if(u.type&&"node"!==u.type)l=document.createTextNode(u.text.replace(/&/g,"&"));else{var g=u.name;"svg"===g&&(c="http://www.w3.org/2000/svg"),"html"!==g&&"body"!==g||(g="div"),l=c?document.createElementNS(c,g):document.createElement(g);for(var p in u.attrs)l.setAttribute(p,u.attrs[p]);if(u.children&&o(u.children,l,c),"img"===g){if(window.unloadimgs+=1,l.onload=n,l.onerror=n,!l.src&&l.getAttribute("data-src")&&(l.src=l.getAttribute("data-src")),u.attrs.ignore||(l.onclick=function(e){e.stopPropagation(),uni.postMessage({data:{action:"onImgTap",attrs:t(this)}})}),a[2]){var h=new Image;h.src=l.src,l.src=a[2],h.onload=function(){l.src=this.src},h.onerror=function(){l.onerror()}}l.onerror=e}else if("a"===g)l.addEventListener("click",function(e){e.stopPropagation(),e.preventDefault();var n,o=this.getAttribute("href");o&&"#"===o[0]&&(n=(document.getElementById(o.substr(1))||{}).offsetTop),uni.postMessage({data:{action:"onLinkTap",attrs:t(this),offset:n}})},!0);else if("video"===g||"audio"===g)i.push(l),u.attrs.autoplay||u.attrs.controls||l.setAttribute("controls","true"),l.onplay=function(){if(uni.postMessage({data:{action:"onPlay"}}),a[3])for(var t=0;t<i.length;t++)i[t]!==this&&i[t].pause()},l.onerror=function(){uni.postMessage({data:{action:"onError",source:g,attrs:t(this)}})};else if("table"===g&&a[4]&&!l.style.cssText.includes("inline")){var f=document.createElement("div");f.style.overflow="auto",f.appendChild(l),l=f}else"svg"===g&&(c=void 0)}s.appendChild(l)}(d)}document.addEventListener("UniAppJSBridgeReady",function(){document.body.onclick=function(){return uni.postMessage({data:{action:"onClick"}})},uni.postMessage({data:{action:"onJSBridgeReady"}})});var a,i=[];window.setContent=function(t,e,n){var r=document.getElementById("content");e[0]&&(document.body.style.cssText=e[0]),e[5]||(r.style.userSelect="none"),n||(r.innerHTML="",i=[]),a=e,window.unloadimgs=0;var s=document.createDocumentFragment();o(t,s),r.appendChild(s);var c=r.scrollHeight;uni.postMessage({data:{action:"onLoad",height:c}}),window.unloadimgs||uni.postMessage({data:{action:"onReady",height:c}}),clearInterval(window.timer),window.timer=setInterval(function(){r.scrollHeight!==c&&(c=r.scrollHeight,uni.postMessage({data:{action:"onHeightChange",height:c}}))},350)},window.onunload=function(){clearInterval(window.timer)};
|
||||
@@ -0,0 +1 @@
|
||||
!function(e,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):(e=e||self).uni=n()}(this,(function(){"use strict";try{var e={};Object.defineProperty(e,"passive",{get:function(){!0}}),window.addEventListener("test-passive",null,e)}catch(e){}var n=Object.prototype.hasOwnProperty;function t(e,t){return n.call(e,t)}var i=[],a=function(e,n){var t={options:{timestamp:+new Date},name:e,arg:n};if(window.__dcloud_weex_postMessage||window.__dcloud_weex_){if("postMessage"===e){var a={data:[n]};return window.__dcloud_weex_postMessage?window.__dcloud_weex_postMessage(a):window.__dcloud_weex_.postMessage(JSON.stringify(a))}var o={type:"WEB_INVOKE_APPSERVICE",args:{data:t,webviewIds:i}};window.__dcloud_weex_postMessage?window.__dcloud_weex_postMessageToService(o):window.__dcloud_weex_.postMessageToService(JSON.stringify(o))}if(!window.plus)return window.parent.postMessage({type:"WEB_INVOKE_APPSERVICE",data:t,pageId:""},"*");if(0===i.length){var r=plus.webview.currentWebview();if(!r)throw new Error("plus.webview.currentWebview() is undefined");var d=r.parent(),s="";s=d?d.id:r.id,i.push(s)}if(plus.webview.getWebviewById("__uniapp__service"))plus.webview.postMessageToUniNView({type:"WEB_INVOKE_APPSERVICE",args:{data:t,webviewIds:i}},"__uniapp__service");else{var w=JSON.stringify(t);plus.webview.getLaunchWebview().evalJS('UniPlusBridge.subscribeHandler("'.concat("WEB_INVOKE_APPSERVICE",'",').concat(w,",").concat(JSON.stringify(i),");"))}},o={navigateTo:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=e.url;a("navigateTo",{url:encodeURI(n)})},navigateBack:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=e.delta;a("navigateBack",{delta:parseInt(n)||1})},switchTab:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=e.url;a("switchTab",{url:encodeURI(n)})},reLaunch:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=e.url;a("reLaunch",{url:encodeURI(n)})},redirectTo:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},n=e.url;a("redirectTo",{url:encodeURI(n)})},getEnv:function(e){window.plus?e({plus:!0}):e({h5:!0})},postMessage:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};a("postMessage",e.data||{})}},r=/uni-app/i.test(navigator.userAgent),d=/Html5Plus/i.test(navigator.userAgent),s=/complete|loaded|interactive/;var w=window.my&&navigator.userAgent.indexOf("AlipayClient")>-1;var u=window.swan&&window.swan.webView&&/swan/i.test(navigator.userAgent);var c=window.qq&&window.qq.miniProgram&&/QQ/i.test(navigator.userAgent)&&/miniProgram/i.test(navigator.userAgent);var g=window.tt&&window.tt.miniProgram&&/toutiaomicroapp/i.test(navigator.userAgent);var v=window.wx&&window.wx.miniProgram&&/micromessenger/i.test(navigator.userAgent)&&/miniProgram/i.test(navigator.userAgent);var p=window.qa&&/quickapp/i.test(navigator.userAgent);for(var l,_=function(){window.UniAppJSBridge=!0,document.dispatchEvent(new CustomEvent("UniAppJSBridgeReady",{bubbles:!0,cancelable:!0}))},f=[function(e){if(r||d)return window.__dcloud_weex_postMessage||window.__dcloud_weex_?document.addEventListener("DOMContentLoaded",e):window.plus&&s.test(document.readyState)?setTimeout(e,0):document.addEventListener("plusready",e),o},function(e){if(v)return window.WeixinJSBridge&&window.WeixinJSBridge.invoke?setTimeout(e,0):document.addEventListener("WeixinJSBridgeReady",e),window.wx.miniProgram},function(e){if(c)return window.QQJSBridge&&window.QQJSBridge.invoke?setTimeout(e,0):document.addEventListener("QQJSBridgeReady",e),window.qq.miniProgram},function(e){if(w){document.addEventListener("DOMContentLoaded",e);var n=window.my;return{navigateTo:n.navigateTo,navigateBack:n.navigateBack,switchTab:n.switchTab,reLaunch:n.reLaunch,redirectTo:n.redirectTo,postMessage:n.postMessage,getEnv:n.getEnv}}},function(e){if(u)return document.addEventListener("DOMContentLoaded",e),window.swan.webView},function(e){if(g)return document.addEventListener("DOMContentLoaded",e),window.tt.miniProgram},function(e){if(p){window.QaJSBridge&&window.QaJSBridge.invoke?setTimeout(e,0):document.addEventListener("QaJSBridgeReady",e);var n=window.qa;return{navigateTo:n.navigateTo,navigateBack:n.navigateBack,switchTab:n.switchTab,reLaunch:n.reLaunch,redirectTo:n.redirectTo,postMessage:n.postMessage,getEnv:n.getEnv}}},function(e){return document.addEventListener("DOMContentLoaded",e),o}],m=0;m<f.length&&!(l=f[m](_));m++);l||(l={});var E="undefined"!=typeof uni?uni:{};if(!E.navigateTo)for(var b in l)t(l,b)&&(E[b]=l[b]);return E.webView=l,E}));
|
||||
@@ -0,0 +1 @@
|
||||
<style>.hl-code,.hl-pre{color:#ccc;background:0 0;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}.hl-pre{padding:1em;margin:.5em 0;overflow:auto}.hl-pre{background:#2d2d2d}.hl-block-comment,.hl-cdata,.hl-comment,.hl-doctype,.hl-prolog{color:#999}.hl-punctuation{color:#ccc}.hl-attr-name,.hl-deleted,.hl-namespace,.hl-tag{color:#e2777a}.hl-function-name{color:#6196cc}.hl-boolean,.hl-function,.hl-number{color:#f08d49}.hl-class-name,.hl-constant,.hl-property,.hl-symbol{color:#f8c555}.hl-atrule,.hl-builtin,.hl-important,.hl-keyword,.hl-selector{color:#cc99cd}.hl-attr-value,.hl-char,.hl-regex,.hl-string,.hl-variable{color:#7ec699}.hl-entity,.hl-operator,.hl-url{color:#67cdcc}.hl-bold,.hl-important{font-weight:700}.hl-italic{font-style:italic}.hl-entity{cursor:help}.hl-inserted{color:green}</style><style>.md-p{margin-block-start:1em;margin-block-end:1em}.md-blockquote,.md-table{margin-bottom:16px}.md-table{box-sizing:border-box;width:100%;overflow:auto;border-spacing:0;border-collapse:collapse}.md-tr{background-color:#fff;border-top:1px solid #c6cbd1}.md-table .md-tr:nth-child(2n){background-color:#f6f8fa}.md-td,.md-th{padding:6px 13px!important;border:1px solid #dfe2e5}.md-th{font-weight:600}.md-blockquote{padding:0 1em;color:#6a737d;border-left:.25em solid #dfe2e5}.md-code{padding:.2em .4em;font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-size:85%;background-color:rgba(27,31,35,.05);border-radius:3px}.md-pre .md-code{padding:0;font-size:100%;background:0 0;border:0}.hl-pre{position:relative}.hl-code{overflow:auto;display:block}.hl-language{font-size:12px;font-weight:600;position:absolute;right:8px;text-align:right;top:3px}.hl-pre{padding-top:1.5em}.hl-pre{font-size:14px;padding-left:3.8em;counter-reset:linenumber}.line-numbers-rows{position:absolute;pointer-events:none;top:1.5em;font-size:100%;left:0;width:3em;letter-spacing:-1px;border-right:1px solid #999;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.line-numbers-rows .span{display:block;counter-increment:linenumber}.line-numbers-rows .span:before{content:counter(linenumber);color:#999;display:block;padding-right:.8em;text-align:right}._address,._article,._aside,._body,._caption,._center,._cite,._footer,._header,._html,._nav,._pre,._section{display:block}._video{width:300px;height:225px;display:inline-block;background-color:#000}</style><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no"><style>body,html{width:100%;height:100%;overflow-x:scroll;overflow-y:hidden}body{margin:0}video{width:300px;height:225px}img{max-width:100%;-webkit-touch-callout:none}</style></head><body><div id="content" style="overflow:hidden"></div><script type="text/javascript" src="./js/uni.webview.min.js"></script><script type="text/javascript" src="./js/handler.js"></script></body>
|
||||
@@ -0,0 +1,100 @@
|
||||
<script setup lang="ts">
|
||||
import { queryPhotos } from '@/api/halo-plugin/photo'
|
||||
import { completeUrl } from '@/utils/url'
|
||||
import { useHaloScroll } from '@/hooks/useHaloScroll'
|
||||
import { random } from '@/utils/base/random'
|
||||
import type { IPhoto, IQueryPhotosRequest } from '@/api/halo-plugin/photo'
|
||||
|
||||
interface IProps {
|
||||
groupName: string
|
||||
}
|
||||
|
||||
const props = defineProps<IProps>()
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: 'close'): void
|
||||
}>()
|
||||
|
||||
const {
|
||||
list, // 响应式的数据列表
|
||||
loading, // 是否加载中
|
||||
finished, // 是否已全部加载
|
||||
error, // 是否加载失败
|
||||
refresh, // 刷新数据的函数
|
||||
loadMore, // 加载更多数据的函数
|
||||
} = useHaloScroll<IPhoto, IQueryPhotosRequest>({
|
||||
fetchData: queryPhotos,
|
||||
params: {
|
||||
page: 1,
|
||||
size: 12,
|
||||
},
|
||||
})
|
||||
|
||||
const imageItemClass = ['-rotate-4', 'rotate-4', 'rotate-3', '-rotate-3', 'rotate-2', '-rotate-2', 'rotate-1']
|
||||
|
||||
function getImageItemClass() {
|
||||
const randomIndex = random(0, imageItemClass.length - 1)
|
||||
return imageItemClass[randomIndex]
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 相册列表:根据分组显示 -->
|
||||
<view class="uh-backdrop-blur-xs fixed inset-0 z-110 flex items-center justify-center bg-black/20">
|
||||
<wd-transition :show="true" :lazy-render="true" :duration="300" name="slide-left">
|
||||
<view class="box-border w-[88vw] rounded-2xl bg-page2 p-4">
|
||||
<view class="box-border w-full flex shrink-0 items-center justify-between gap-x-4">
|
||||
<view class="text-gray-900 font-bold">
|
||||
{{ props.groupName }}
|
||||
</view>
|
||||
<view class="shrink-0">
|
||||
<wd-icon name="close-circle" size="24px" color="#1A1A1A" @click="emits('close')" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mt-4 w-full">
|
||||
<scroll-view
|
||||
v-if="list" class="h-[56vh] w-full" scroll-y :refresher-enabled="true"
|
||||
:refresher-triggered="loading" @scrolltolower="loadMore" @refresherrefresh="refresh"
|
||||
>
|
||||
<view class="w-full flex flex-wrap gap-y-4">
|
||||
<view
|
||||
v-for="(item) in list" :key="item.metadata.name"
|
||||
class="box-border w-1/2 overflow-hidden px-2"
|
||||
:class="[getImageItemClass()]"
|
||||
>
|
||||
<image
|
||||
class="uh-shadow-xs w-full border-2 border-white rounded-xl border-solid bg-white/80"
|
||||
:src="completeUrl(item.spec.url)"
|
||||
mode="widthFix"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view v-else class="h-[56vh] flex items-center justify-center">
|
||||
<wd-empty tip="暂无数据">
|
||||
<template #image>
|
||||
<wd-icon name="empty" color="#1A1A1A" :size="52" />
|
||||
</template>
|
||||
</wd-empty>
|
||||
</view>
|
||||
<view
|
||||
v-if="loading || error || finished"
|
||||
class="mt-4 w-full flex items-center justify-center text-xs text-gray-900"
|
||||
>
|
||||
<wd-loading v-if="loading" text="加载中..." direction="horizontal" color="#1A1A1A" :size="16" />
|
||||
<view
|
||||
v-else-if="error"
|
||||
class="w-full rounded-lg bg-gray-900 py-1.5 text-center text-xs text-white"
|
||||
>
|
||||
<wd-icon name="refresh" :size="14" /> 加载失败,点击刷新试试
|
||||
</view>
|
||||
<view v-else-if="finished" class="text-black/60">
|
||||
数据已全部加载,没有更多了
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</wd-transition>
|
||||
</view>
|
||||
</template>
|
||||
@@ -0,0 +1,176 @@
|
||||
<script setup lang="ts">
|
||||
import { indicesSearch } from '@/api/halo-base/common'
|
||||
import { timeFormat } from '@/utils/base/timeFormat'
|
||||
import { debounce } from '@/utils/base/debounce'
|
||||
import { GLOBAL_SEARCH_EVENT_NAME } from '@/constants/events'
|
||||
import type { HaloDocument, SearchOption, SearchResult } from '@halo-dev/api-client'
|
||||
|
||||
const { loading, error, data, run } = useRequest<SearchResult, SearchOption>(indicesSearch, { loadingToast: false, loadingToastContent: '加载中...', loadingToastMask: true })
|
||||
|
||||
const showTransition = ref(true)
|
||||
const isAutoFocus = ref(false)
|
||||
|
||||
const scroll = reactive({
|
||||
top: 0,
|
||||
})
|
||||
|
||||
const searchParams = ref<SearchOption>({
|
||||
keyword: undefined,
|
||||
limit: 50,
|
||||
})
|
||||
|
||||
const searchResultTypeMap = {
|
||||
'moment.moment.halo.run': 'moment',
|
||||
'post.content.halo.run': 'post',
|
||||
'doc.halo.run': 'doc',
|
||||
}
|
||||
const searchResultTypeCN = {
|
||||
'moment.moment.halo.run': '瞬间',
|
||||
'post.content.halo.run': '文章',
|
||||
'doc.halo.run': '文档',
|
||||
}
|
||||
|
||||
interface SearchResultItem extends HaloDocument {
|
||||
releaseTime: string
|
||||
typeCN: string
|
||||
}
|
||||
|
||||
const searchResult = computed<Array<SearchResultItem>>(() => {
|
||||
if (!data.value || data.value?.hits?.length === 0) {
|
||||
return []
|
||||
}
|
||||
return data.value.hits.map((item) => {
|
||||
return {
|
||||
...item,
|
||||
type: searchResultTypeMap[item.type],
|
||||
typeCN: searchResultTypeCN[item.type],
|
||||
releaseTime: timeFormat(item.updateTimestamp ?? item.creationTimestamp),
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
function handleSearch() {
|
||||
if (!searchParams.value.keyword) {
|
||||
uni.showToast({
|
||||
title: '请输入搜索内容',
|
||||
icon: 'none',
|
||||
})
|
||||
return
|
||||
}
|
||||
scroll.top = 0
|
||||
run(searchParams.value)
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
setTimeout(() => {
|
||||
uni.$emit(GLOBAL_SEARCH_EVENT_NAME)
|
||||
}, 30)
|
||||
}
|
||||
|
||||
const onViewScroll = debounce((e: any) => {
|
||||
scroll.top = e.detail.scrollTop
|
||||
}, 150)
|
||||
|
||||
onMounted(() => {
|
||||
// handleSearch()
|
||||
|
||||
setTimeout(() => {
|
||||
isAutoFocus.value = true
|
||||
}, 350)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view
|
||||
class="uh-backdrop-blur-xs fixed inset-0 z-110 flex items-center justify-center bg-black/20"
|
||||
@click.stop="handleClose"
|
||||
>
|
||||
<wd-transition :show="showTransition" :lazy-render="true" :duration="300" name="fade-down">
|
||||
<!-- 内容区 -->
|
||||
<view class="box-border w-[86vw] flex flex-col overflow-hidden rounded-2xl bg-page2 p-2" @click.stop>
|
||||
<view class="box-border w-full flex shrink-0 items-center justify-between gap-x-4 p-2">
|
||||
<view class="flex flex-1 items-center gap-x-2">
|
||||
<input
|
||||
v-model="searchParams.keyword" :focus="isAutoFocus" :auto-focus="isAutoFocus" confirm-type="search" type="text" placeholder="请输入搜索内容"
|
||||
class="flex-1 border-2 border-gray-900 rounded-md border-solid px-2 py-1 text-xs text-gray-900 placeholder:text-xs"
|
||||
@confirm="handleSearch"
|
||||
>
|
||||
<view
|
||||
class="flex items-center justify-center rounded-lg bg-gray-900 px-3 py-2"
|
||||
@click="handleSearch()"
|
||||
>
|
||||
<wd-icon name="search-line" color="#ffffff" :size="16" />
|
||||
<text class="text-xs text-white">搜索</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="shrink-0">
|
||||
<wd-icon name="close-circle" size="24px" color="#1A1A1A" @click="handleClose" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 搜索结果区 -->
|
||||
<view class="mb-2 box-border flex items-center justify-between px-2">
|
||||
<text class="text-xs text-gray-900 font-bold">搜索结果</text>
|
||||
<text class="text-xs text-gray-600">共 {{ searchResult.length }} 条</text>
|
||||
</view>
|
||||
<scroll-view
|
||||
v-if="searchResult.length !== 0" class="h-[56vh] w-full" :scroll-y="true"
|
||||
:scroll-top="scroll.top" @scroll="onViewScroll"
|
||||
>
|
||||
<view class="box-border flex flex-col gap-y-3 px-2">
|
||||
<!-- 卡片 -->
|
||||
<view
|
||||
v-for="(item, index) in searchResult" :key="item.metadataName"
|
||||
class="uh-shadow-xs box-border w-full flex flex-col gap-y-2 rounded-lg bg-white p-3"
|
||||
>
|
||||
<view class="flex items-center gap-x-2">
|
||||
<view
|
||||
class="flex items-center rounded-md bg-gray-900 px-1.5 py-0.5 text-xs text-white font-bold"
|
||||
>
|
||||
# <text class="pl-0.5 text-[12px]">{{ index + 1 }}</text>
|
||||
</view>
|
||||
<rich-text :nodes="item.title" class="line-clamp-1 text-sm text-gray-900 font-bold" />
|
||||
</view>
|
||||
<view class="line-clamp-2 w-full text-xs text-gray-600 leading-relaxed">
|
||||
<rich-text :nodes="item.description ?? item.content" />
|
||||
</view>
|
||||
<view class="h-1px w-full bg-gray-50" />
|
||||
<view class="flex items-center justify-between gap-x-4">
|
||||
<view class="rounded-md bg-gray-900 px-2 py-1 text-[11px] text-white">
|
||||
来自{{ item.typeCN }}
|
||||
</view>
|
||||
<text class="text-xs text-gray-600">{{ item.releaseTime }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view v-else-if="loading" class="h-[56vh] flex items-center justify-center">
|
||||
<wd-loading text="搜索中..." color="#1A1A1A" :size="52" />
|
||||
</view>
|
||||
<view v-else-if="error" class="h-[56vh] flex items-center justify-center">
|
||||
<wd-empty tip="搜索失败">
|
||||
<template #image>
|
||||
<wd-icon name="close-circle" color="#1A1A1A" :size="52" />
|
||||
</template>
|
||||
<template #bottom>
|
||||
<view
|
||||
class="flex items-center justify-center rounded-lg bg-gray-900 px-3 py-2"
|
||||
@click="handleSearch()"
|
||||
>
|
||||
<wd-icon name="search-line" color="#ffffff" :size="16" />
|
||||
<text class="pl-1 text-xs text-white">点击重试</text>
|
||||
</view>
|
||||
</template>
|
||||
</wd-empty>
|
||||
</view>
|
||||
<view v-else class="h-[56vh] flex items-center justify-center">
|
||||
<wd-empty tip="暂无搜索结果">
|
||||
<template #image>
|
||||
<wd-icon name="empty" color="#1A1A1A" :size="52" />
|
||||
</template>
|
||||
</wd-empty>
|
||||
</view>
|
||||
</view>
|
||||
</wd-transition>
|
||||
</view>
|
||||
</template>
|
||||
@@ -0,0 +1,53 @@
|
||||
<script setup lang="ts">
|
||||
import type { CSSProperties } from 'vue'
|
||||
|
||||
interface IProps {
|
||||
autoWidth?: boolean
|
||||
autoHeight?: boolean
|
||||
}
|
||||
const props = withDefaults(defineProps<IProps>(), {
|
||||
autoWidth: false,
|
||||
autoHeight: false,
|
||||
})
|
||||
// 获取attrs
|
||||
const attrs = useAttrs()
|
||||
const realUrl = ref()
|
||||
|
||||
// 计算图片尺寸
|
||||
const imageSizes = reactive({
|
||||
width: 0,
|
||||
height: 0,
|
||||
})
|
||||
|
||||
function calculateImageSize() {
|
||||
uni.getImageInfo({
|
||||
src: attrs.url as string,
|
||||
success: (res) => {
|
||||
imageSizes.width = res.width
|
||||
imageSizes.height = res.height
|
||||
realUrl.value = attrs.url as string
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const imageStyle = computed(() => {
|
||||
const style: CSSProperties = {}
|
||||
if (!props.autoWidth) {
|
||||
style.width = `${imageSizes.width}px`
|
||||
}
|
||||
if (!props.autoHeight) {
|
||||
style.height = `${imageSizes.height}px`
|
||||
}
|
||||
return style
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
calculateImageSize()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
{{ imageStyle }}
|
||||
{{ attrs }}
|
||||
<image v-if="realUrl" v-bind="attrs" :src="realUrl" :style="[imageStyle]" />
|
||||
</template>
|
||||
@@ -0,0 +1,115 @@
|
||||
<script setup lang="ts">
|
||||
import { completeUrl } from '@/utils/url'
|
||||
import { timeFrom } from '@/utils/base/timeFrom'
|
||||
import type { ListedPostVo } from '@halo-dev/api-client'
|
||||
|
||||
interface IProps {
|
||||
post?: ListedPostVo
|
||||
type?: 'default' | 'topCover' | 'rightCover'
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<IProps>(), {
|
||||
type: 'default',
|
||||
post: undefined,
|
||||
})
|
||||
|
||||
const wrapClass = computed(() => {
|
||||
if (props.type === 'topCover') {
|
||||
return 'flex-col gap-y-4'
|
||||
}
|
||||
if (props.type === 'rightCover') {
|
||||
return 'relative h-28 p-0! border-2 border-solid border-white'
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
const coverClass = computed(() => {
|
||||
if (props.type === 'topCover') {
|
||||
return 'h-32 w-full'
|
||||
}
|
||||
if (props.type === 'rightCover') {
|
||||
return 'absolute right-0 top-0 bottom-0 w-32 rounded-lt-0 rounded-lb-0'
|
||||
}
|
||||
return 'h-22 w-22'
|
||||
})
|
||||
|
||||
const textWrapClass = computed(() => {
|
||||
if (props.type === 'topCover') {
|
||||
return ''
|
||||
}
|
||||
if (props.type === 'rightCover') {
|
||||
return 'p-4 pr-32 relative z-10'
|
||||
}
|
||||
return 'pl-4'
|
||||
})
|
||||
|
||||
const textContainerClass = computed(() => {
|
||||
if (props.type === 'topCover') {
|
||||
return 'gap-y-2'
|
||||
}
|
||||
if (props.type === 'rightCover') {
|
||||
return ''
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
const textFooterClass = computed(() => {
|
||||
if (props.type === 'topCover') {
|
||||
return 'justify-between gap-x-2'
|
||||
}
|
||||
if (props.type === 'rightCover') {
|
||||
return 'gap-x-2'
|
||||
}
|
||||
return 'gap-x-2'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view v-if="post" class="uh-shadow-xs flex justify-center overflow-hidden rounded-2xl bg-white p-4" :class="wrapClass">
|
||||
<view class="shrink-0 overflow-hidden rounded-lg" :class="coverClass">
|
||||
<image :src="completeUrl(post.spec.cover)" mode="aspectFill" class="h-full w-full" />
|
||||
<view v-if="props.type === 'rightCover'" class="absolute bottom-0 left-0 top-0 z-10 h-full w-full from-white/0 via-white/10 to-white bg-gradient-to-l" />
|
||||
</view>
|
||||
<view class="box-border flex-1" :class="textWrapClass">
|
||||
<view class="h-full flex flex-1 flex-col justify-between" :class="textContainerClass">
|
||||
<view class="line-clamp-1 text-sm text-gray-900 font-bold">
|
||||
{{ post.spec.title }}
|
||||
</view>
|
||||
<view class="line-clamp-2 text-xs text-gray-500">
|
||||
{{ post.status.excerpt }}
|
||||
</view>
|
||||
<view class="w-full flex items-center" :class="textFooterClass">
|
||||
<view class="text-[11px] text-gray-600">
|
||||
{{ timeFrom(post.spec.publishTime) }}
|
||||
</view>
|
||||
<view class="shrink-0 text-xs text-gray-600">
|
||||
·
|
||||
</view>
|
||||
<view class="line-clamp-1 text-[11px] text-gray-600">
|
||||
{{ post.categories.map(c => c.spec.displayName).join('·') }}
|
||||
</view>
|
||||
<template v-if="props.type === 'topCover'">
|
||||
<view class="shrink-0 text-xs text-gray-600">
|
||||
·
|
||||
</view>
|
||||
<view class="text-[11px] text-gray-600">
|
||||
点赞 {{ post.stats.upvote }} 次
|
||||
</view>
|
||||
</template>
|
||||
<view class="shrink-0 text-xs text-gray-600">
|
||||
·
|
||||
</view>
|
||||
<view class="text-[11px] text-gray-600">
|
||||
浏览 {{ post.stats.visit }} 次
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.uh-shadow-xs {
|
||||
box-shadow: 0 0 24rpx rgba(0, 0, 0, 0.035);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,6 @@
|
||||
<template>
|
||||
<view class="w-full pb-safe">
|
||||
<!-- 60px -->
|
||||
<view class="h-62px w-full" />
|
||||
</view>
|
||||
</template>
|
||||
@@ -0,0 +1,96 @@
|
||||
import { useAppConfig } from '../useAppConfig'
|
||||
|
||||
export const markdownConfig = {
|
||||
domain: useAppConfig().appBaseUrl.value,
|
||||
tagStyle: {
|
||||
table: `
|
||||
table-layout: fixed;
|
||||
border-collapse:collapse;
|
||||
margin-bottom: 18px;
|
||||
overflow: hidden;
|
||||
font-size: 13px;
|
||||
color: var(--routine);
|
||||
background: #f2f6fc;
|
||||
border: 1px solid #dcdcdc;
|
||||
border-radius: 4px;
|
||||
`,
|
||||
th: `
|
||||
padding: 8px;
|
||||
border-right: 1px solid var(--classE);
|
||||
border-bottom: 1px solid var(--classE);
|
||||
`,
|
||||
td: `
|
||||
padding: 8px;
|
||||
border-right: 1px solid var(--classE);
|
||||
border-bottom: 1px solid var(--classE);
|
||||
`,
|
||||
blockquote: `
|
||||
padding: 8px 15px;
|
||||
color: #606266;
|
||||
background: #f2f6fc;
|
||||
border-left: 5px solid #50bfff;
|
||||
border-radius: 4px;
|
||||
line-height: 26px;
|
||||
margin-bottom: 18px;
|
||||
`,
|
||||
ul: 'padding-left: 15px;line-height: 1.85;',
|
||||
ol: 'padding-left: 15px;line-height: 1.85;',
|
||||
li: 'margin-bottom: 12px;line-height: 1.85;',
|
||||
h1: `
|
||||
margin: 30px 0 20px;
|
||||
color: var(--main);
|
||||
line-height: 24px;
|
||||
position: relative;
|
||||
font-size:1.2em;
|
||||
`,
|
||||
h2: `
|
||||
color: var(--main);
|
||||
line-height: 24px;
|
||||
position: relative;
|
||||
margin: 22px 0 16px;
|
||||
font-size: 1.16em;
|
||||
`,
|
||||
h3: `
|
||||
color: var(--main);
|
||||
line-height: 24px;
|
||||
position: relative;
|
||||
margin: 26px 0 18px;
|
||||
font-size: 1.14em;
|
||||
`,
|
||||
h4: `
|
||||
color: var(--main);
|
||||
line-height: 24px;
|
||||
margin-bottom: 18px;
|
||||
position: relative;
|
||||
font-size: 1.12em;
|
||||
`,
|
||||
h5: `
|
||||
color: var(--main);
|
||||
line-height: 24px;
|
||||
margin-bottom: 14px;
|
||||
position: relative;
|
||||
font-size: 1.1em;
|
||||
`,
|
||||
h6: `
|
||||
color: #303133;
|
||||
line-height: 24px;
|
||||
margin-bottom: 14px;
|
||||
position: relative;
|
||||
font-size: 14px;
|
||||
`,
|
||||
p: `
|
||||
line-height: 1.65;
|
||||
margin-bottom: 14px;
|
||||
font-size: 14px;
|
||||
`,
|
||||
code: ` `,
|
||||
strong: 'font-weight: 700;color: rgb(248, 57, 41);',
|
||||
video: 'width: 100%',
|
||||
},
|
||||
containStyle: 'font-family: Optima-Regular, Optima, PingFangSC-light, PingFangTC-light, "PingFang SC", Cambria, Cochin, Georgia, Times, "Times New Roman", serif;padding:12px;font-size: 14px;color: #606266;word-spacing: 0.8px;letter-spacing: 0.8px;border-radius: 6px;background-color:#FFFFFF;',
|
||||
|
||||
loadingGif: '',
|
||||
emptyGif: '',
|
||||
}
|
||||
|
||||
export default markdownConfig
|
||||
@@ -0,0 +1,155 @@
|
||||
:root {
|
||||
--main: #303133;
|
||||
--theme: #fb6c28;
|
||||
--code-background: #e8f3ff;
|
||||
--radius-inner: 4px;
|
||||
--classA: #dcdfe6;
|
||||
--classB: #e4e7ed;
|
||||
--classC: #ebeef5;
|
||||
--classD: #f2f6fc;
|
||||
--classE: #dcdcdc;
|
||||
--classF: #333;
|
||||
--classG: #dcdcdc;
|
||||
--classH: #e9f2ff;
|
||||
--classI: #5a3713;
|
||||
--classJ: #f9e5fb;
|
||||
--classK: #e4e7ed;
|
||||
--classL: #666;
|
||||
--classM: #2d2e37;
|
||||
--quote: #50bfff;
|
||||
--code: #409eff;
|
||||
}
|
||||
.uh-markdown {
|
||||
::v-deep {
|
||||
h1::before,
|
||||
h2::before,
|
||||
h3::before,
|
||||
h4::before,
|
||||
h5::before,
|
||||
h6::before {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
content: '';
|
||||
margin-right: 6px;
|
||||
background-position: center;
|
||||
}
|
||||
h1::before {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
content: '¶';
|
||||
top: -4px;
|
||||
margin-right: 12px;
|
||||
font-size: 24px;
|
||||
color: var(--theme);
|
||||
}
|
||||
h2::before {
|
||||
top: -2px;
|
||||
left: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-size: auto 100%;
|
||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAVJJREFUWEftl7FKw1AUhv9rbmf1PRw6utkHEOoi6WPoE1ifQJ8gySKCOtRFRBKsDuIiDdZAURBFF50M2sFIzpGogVK03JA2cbh3S/i55zvfvYETgRyLj5erIKOO949FyKm5eHvvHEBV2jyruq1QDQ7n2DNXALEx+D7e2vl6lBYp76scHCzErtmCEPVhqEIAfus8BZk4wM+Zd/46tskDeI0mgLUyAdoAFsoDcM0XCDFdHoDX4FGfbRF3QANoA9qANqANaAPawD82EEWId1sAOJQWz6iO+5nGch41kDw9I3bbYOaTis21wgG4G4AuAzCwXrEoGV6V1ngMJPoPjoB+PzSIa8KBr1QdwFgA6KID7t1AgFYNC5uqxZNcbgB+eASdngHM+9LmpSzF8wFEEagbgHvXoQA3s3aegmYz8P1f4NNVME+39z5e3w4lkSMc3GXtPM1/AjYDFjDGddN5AAAAAElFTkSuQmCC);
|
||||
}
|
||||
h3::before {
|
||||
top: -3px;
|
||||
left: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-size: auto 100%;
|
||||
background-repeat: none;
|
||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAH1JREFUWEft1qENgDAQheH/JANg60DAJIgmrMFCzEEYgTFgDyRFIClp0yBf9evl8pl3RsYbp7BjNLHoMptljPiMZH3WAhKQgAQkIIGXQAXUT79cniHaNMa5draliqqojIID86nRHEtvbSqlBSQgAQlIQAISkECRAA746SC5Ad6XpiGnnOGPAAAAAElFTkSuQmCC);
|
||||
}
|
||||
h4::before {
|
||||
top: -2px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
color: var(--theme);
|
||||
background-size: auto 100%;
|
||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAndJREFUWEftVkty2kAQfT1SBLtwg6AqwzbxCQIniG8QvAmwCp8kRqvgFa5KwHjlkI3NCeIbhJwgXptUgW9AdiDQdGoITqUsjcZyuYqNZ6WSXs88vel+3YQtL9ry+Xgk8KhApALNAe8x5PPbCUrgabdsD2/e+1edEpN4FsIRps7OwT9cXKKHCLw75QIL+V0XRFIUP1dptBx3ChKkxQlw8UnOG5mqLESgdsYZy5dTAE8jg5lH3Ypd5Mlxxl8ttDgGRulcq5iYgApofgn6ILzVBQsSu5/e0OXiqnMOotdatVjsOvkPl4muQIFrp5y1hJzEBA67ZavEk07WX5EexzxM5b1SYgJrFQbBOQDt3wVSuP0qTRfjowsAr3SHODa75HrqqiKX1gdMycjAYa9stU3JCPBhKue1ExNQAY3BakSgl1HBDMykI9z+Ps0WV0eXIITKdhM3c+yUS259FrVPrBM2TlclEnSmY8+S93tV+/yvH+hxxLzv5D11paFltOLmIFD3FzIbtRMzpr2K5arn+bgzJZAWl8631rjby0zAUJI3xnTfkjQTiFEAwHW3bGWNCoCv0zlvjUukgOoJgPymzQGg3itbfVMOMKiezh30ExOIqwIAvwNHZFUVzMcdbbUonGOnsomr4P1XfiFZ/tS6GOOkW7FqJh8gwomz06ol9oE7O6GhH9zLCbfeCxqDoE3AR61sm5nA/3XUZ47pmneYCSLnAeHLCQGZaAvmH72yXdjMA6oTanHpnFfQ5tDmQ+KJ6MZ+jckXY7//k4pWYB7sQVCUccxU3a9teHKcWS7ne0wI4Rhipqv7REZkku8hvhut+CEOidvjkcDWFfgD9RMzMKE7f80AAAAASUVORK5CYII=);
|
||||
}
|
||||
h5::before {
|
||||
top: -1px;
|
||||
left: 0;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background-size: 100% 100%;
|
||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAC8klEQVRYR+3WP2gTURwH8O/vKnVRRHKXP52cBO3g4p/BqYNIhy4muajUQRBFKjQV1En6ZxJBcmlRCoJDRe3FDiJVwamLS3FwqbgIgss1l2ZQF5XeT3I1Z3NJ7u5dLlAwN9699/t97vfe7/EIO/yhHe5DD9jpCv3fFVTu856+Xzi62Y/35hj9CFPNrlUwWeRJAJfBGADwBcBNI0/PRZFdAdo4xlQLjCqKjBzogat7hZCRAgPghJGBgbFnxglAOvS7b/fLb+q+qnv5BHBCyEBAWTdnANy2IxOtWSC1qsY+1jOFwAVG+gIVvXyHQbcaKuZCxmf5iMQogXFQtEsBeO5JT6BcMu+Bcb1lUhcyqfExACUAB6JEtgUqi+U5JrrmmcyFHCjwSYtQO+tSUSFbAmXdnAdwJVASdyWLPAS2kbFA8xsHNS13EzCmlx8R6KJQcBcypfFpho3cKxRna3ADsgEo6+ZjAKMhgjZ1d2KWR2gTSyD0h4jnIB2gXDIXwciFCPZvSnMl0wwshYrJGDImaMUGKovlLBPVOrDjh8APzFx8zDkjNa7FzYoGZsKT9XEarQOHmei1aJA246cqOWW6/i2l8VMGzoWIPW/k6eq2Ja6UwCz8pw2JGZ8sS8pUz8fWau/jGp+SgLchcGBgZD1Py41NUuoI+ZloV8ZU93+ogZKzfBiWfXAPCgMJU8Y42avQdMzI4ZBfmSizocqrUeJaAmsvBZGGBCtTziXeRY1rCxRAViEhXckqK93AeQJ9kYTvlmVlqmcTdhNEtefc+9X3utVmuX+CkK6oyqtu4nwrWP8bF5IZdGYjJ79wDuMCz4D+XmhFWnZbt7ab5ltBB6mbkyAaBPFCJassuwMmNb4L4EZgXwBc4AoGTZrUeA6A9x1yK6tzzvnFDlxBv0D176kCP2TCpbbjBXCRV9DZk0VeAONCE1IQ1zWg3dlF1sFQHWQIXFeBNrLAw5BwHBZWjQl6E3SbbB8X+R4Mg/Ca0wN2WtFeBTut4B84mFI4VpekyAAAAABJRU5ErkJggg==);
|
||||
}
|
||||
h6::before {
|
||||
top: -1px;
|
||||
left: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-size: auto 100%;
|
||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAEI0lEQVRYR+3Xb2wTZRwH8G/vL22RPy5GW8fK6rJpGeFFY9RO3TRRE1HfmEAammEyjZmOSEg1RrPZaTD6xjhU/Ndlf0CZgwmD+qcgZBRIETeqY0Vcnc7pGonhRY2l3m2t5upqjq693l2vcy+8N81zz/NcPvf75fd7rjos8ku3yH34HyjK0PqlDLPzD56vMNL0VHxmpgXAoUIZXKgI3sMSpK+cNTAbylaj/9IkprnLM3+mkg8XQi4EsJ4lSL+ZNbAfVd+JG/XLcSERg3M8IAtZauBtDEkeNdN6fX9NfRqXueQiSwm00wQRMDMGw94snBJkqYBrSYIIXs/ojftqGq6IXHZRFIpkKYC2JSw9SJE663W0nthlqcMawwrJYpVCag1M48pNy1YNvLWRffGNIYSCv6Cnog7rjCtVIbUEXoG7qeqaNMj51F58dWoKPZY62JeWyUJG+ctcIplcIizWCpgTl9EIyODJn9BtceDWq/6B57tO/H4R688fFaZ7AWzWAiiJEyMDx39Ed6UDdyy7Ni/w1ekxbP95VJi/F8CRYoGycGLksaEJdFU6cPdy0zxkR/RbtE6FhPvHATQUm2JFODHSfyyCLmsd7lth/hf53q/jcE8OC+NTAG7PTKiNoCqcGPnpF+PotDrwwNXl+OC3H9A8cVqYPgPgFnFo1QCLwomRg4cvoPMGBzZHTgq3zwKwZ+ddKVATXAZhf+gdnPvuojAUqmJdrspRAtQU93pXEM+8clgwhQHU5itruUBNcTt6TsP9sl8weQC0S/VFOUBNcW/2folt2z+XhZPTZh6kSKJ79aqVRuFszRxfUm8sNbdz9xlsfekz2bhCQBvDMMM8z+ubN92Mjrb71brS+97dM4wtnk8U4SSBBEGErVarzeVywePxoHVLA1pb6lUh3+8bwZMv+BTjpIAVACa9Xq+uqakJ7e3tqpGd/WfR3Jr+81awIJS0mY0A+sLhMGw2W3qfGmTXvhAef/6gapxUBDuqqqoejUQiBvFbKUH2DHyNx54bLAqXF0jT9HBjY6Pd6/XOi7oc5K7936Dp2QNF4/IBrTqdLuLxeIi2tracRSGF/PDgKB55er8muHzATQB2m81m+P1+1NbmPoVyIfsOnUOj+2PNcPmAO1iWbeE4TmexWODz+WQhayrL4No2oCkuJ5Bl2VGO49ZmcisXObdeVSuRaq7ZZ3G10KBTqRQl3pQPGY1GEQgE4HQ6heVDAO5S1cklNmUDXQRB9KZSqXkfESaTCW63G7FYDKFQKDEyMoJoNKqfe/bbAJ7QGpcrxa8xDLOV5/k0kGXZv2ZnZ5FMJtNjmqYvURR1IpFInAcwAeD7ud/pUuDmAY1G41g8Hl9DkmScoqggx3FHAIyJMMlSQfI9V5zKagDC93dsoRFKimQx2dIWOV/U/yn6bx0WyDj8vgLOAAAAAElFTkSuQmCC);
|
||||
}
|
||||
blockquote > p {
|
||||
margin-bottom: 0 !important;
|
||||
margin-top: 0 !important;
|
||||
font-size: 0.9em !important;
|
||||
}
|
||||
code[class='md-code'],
|
||||
code:not([class]) {
|
||||
display: inline-block;
|
||||
font-size: 13px;
|
||||
color: #409eff;
|
||||
margin: 2px 5px;
|
||||
padding: 0 8px;
|
||||
white-space: normal;
|
||||
text-indent: 0;
|
||||
-webkit-user-select: auto;
|
||||
-moz-user-select: auto;
|
||||
-ms-user-select: auto;
|
||||
user-select: auto;
|
||||
vertical-align: baseline;
|
||||
word-break: break-word;
|
||||
background: #e8f3ff;
|
||||
border-radius: 4px;
|
||||
}
|
||||
code[class*='language-'] {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
// border-radius: 0 0 8px 8px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
user-select: auto;
|
||||
padding: 12px 12px 14px 18px;
|
||||
margin-bottom: 16px;
|
||||
background: #282c34;
|
||||
color: #abb2bf;
|
||||
border-radius: 4px;
|
||||
text-shadow: 0 1px rgba(0, 0, 0, 0.3);
|
||||
font-family: 'Fira Code', 'Fira Mono', Menlo, Consolas, 'DejaVu Sans Mono', monospace;
|
||||
direction: ltr;
|
||||
text-align: left;
|
||||
white-space: pre;
|
||||
word-spacing: normal;
|
||||
word-break: normal;
|
||||
line-height: 1.5;
|
||||
-moz-tab-size: 2;
|
||||
-o-tab-size: 2;
|
||||
tab-size: 2;
|
||||
-webkit-hyphens: none;
|
||||
-moz-hyphens: none;
|
||||
-ms-hyphens: none;
|
||||
hyphens: none;
|
||||
}
|
||||
table {
|
||||
td {
|
||||
padding: 8px;
|
||||
border-right: 1px solid var(--classE);
|
||||
border-bottom: 1px solid var(--classE);
|
||||
}
|
||||
thead th {
|
||||
font-weight: 500;
|
||||
background: var(--classC);
|
||||
}
|
||||
|
||||
tbody tr {
|
||||
transition: background 0.35s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export function useAppConfig() {
|
||||
const appBaseUrl = ref(import.meta.env.VITE_API_BASE_URL)
|
||||
|
||||
return {
|
||||
appBaseUrl,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
/** 全局搜索弹窗时间名称 */
|
||||
export const GLOBAL_SEARCH_EVENT_NAME = 'uh-show-global-search'
|
||||
Vendored
+7
-7
@@ -14,13 +14,13 @@ interface ImportMetaEnv {
|
||||
/** 服务端口号 */
|
||||
readonly VITE_SERVER_PORT: string
|
||||
/** 后台接口地址 */
|
||||
readonly VITE_SERVER_BASEURL: string
|
||||
/** 微信小程序开发版后台接口地址,不配置则使用 VITE_SERVER_BASEURL */
|
||||
readonly VITE_SERVER_BASEURL__WEIXIN_DEVELOP?: string
|
||||
/** 微信小程序体验版后台接口地址,不配置则使用 VITE_SERVER_BASEURL */
|
||||
readonly VITE_SERVER_BASEURL__WEIXIN_TRIAL?: string
|
||||
/** 微信小程序正式版后台接口地址,不配置则使用 VITE_SERVER_BASEURL */
|
||||
readonly VITE_SERVER_BASEURL__WEIXIN_RELEASE?: string
|
||||
readonly VITE_UNI_HALO_BASEURL: string
|
||||
/** 微信小程序开发版后台接口地址,不配置则使用 VITE_UNI_HALO_BASEURL */
|
||||
readonly VITE_UNI_HALO_BASEURL__WEIXIN_DEVELOP?: string
|
||||
/** 微信小程序体验版后台接口地址,不配置则使用 VITE_UNI_HALO_BASEURL */
|
||||
readonly VITE_UNI_HALO_BASEURL__WEIXIN_TRIAL?: string
|
||||
/** 微信小程序正式版后台接口地址,不配置则使用 VITE_UNI_HALO_BASEURL */
|
||||
readonly VITE_UNI_HALO_BASEURL__WEIXIN_RELEASE?: string
|
||||
/** H5是否需要代理 */
|
||||
readonly VITE_APP_PROXY_ENABLE: 'true' | 'false'
|
||||
/** H5是否需要代理,需要的话有个前缀 */
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import type { Ref } from 'vue'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import type { IHaloListRequestBase, IHaloListResponseBase } from '@/api/types/halo'
|
||||
|
||||
interface UseScrollOptions<T, P extends IHaloListRequestBase = { page: number, size: number }> {
|
||||
fetchData: (params: P) => Promise<IHaloListResponseBase<T>>
|
||||
params: P
|
||||
}
|
||||
|
||||
interface UseScrollReturn<T> {
|
||||
list: Ref<T[]>
|
||||
loading: Ref<boolean>
|
||||
finished: Ref<boolean>
|
||||
error: Ref<any>
|
||||
refresh: () => Promise<void>
|
||||
loadMore: () => Promise<void>
|
||||
}
|
||||
|
||||
export function useHaloScroll<T, P extends IHaloListRequestBase = { page: number, size: number }>({
|
||||
fetchData,
|
||||
params,
|
||||
}: UseScrollOptions<T, P>): UseScrollReturn<T> {
|
||||
const response = ref<IHaloListResponseBase<T>>()
|
||||
const list = ref<T[]>([]) as Ref<T[]>
|
||||
const loading = ref(false)
|
||||
const finished = ref(false)
|
||||
const error = ref<any>(null)
|
||||
|
||||
const loadData = async () => {
|
||||
if (loading.value || finished.value) {
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
response.value = await fetchData(params)
|
||||
finished.value = !response.value?.hasNext
|
||||
list.value.push(...response.value?.items || [])
|
||||
params.page++
|
||||
}
|
||||
catch (err) {
|
||||
error.value = err
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const refresh = async () => {
|
||||
params.page = 1
|
||||
finished.value = false
|
||||
list.value = []
|
||||
await loadData()
|
||||
}
|
||||
|
||||
const loadMore = async () => {
|
||||
await loadData()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
refresh()
|
||||
})
|
||||
|
||||
return {
|
||||
list,
|
||||
loading,
|
||||
finished,
|
||||
error,
|
||||
refresh,
|
||||
loadMore,
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
interface IPaginationParams {
|
||||
export interface IPaginationParams {
|
||||
page: number
|
||||
size: number
|
||||
}
|
||||
|
||||
+2
-2
@@ -9,8 +9,8 @@ import { ContentTypeEnum, ResultEnum, ShowMessage } from './tools/enum'
|
||||
|
||||
// 配置动态Tag
|
||||
export const API_DOMAINS = {
|
||||
DEFAULT: import.meta.env.VITE_SERVER_BASEURL,
|
||||
SECONDARY: import.meta.env.VITE_SERVER_BASEURL_SECONDARY,
|
||||
DEFAULT: import.meta.env.VITE_UNI_HALO_BASEURL,
|
||||
SECONDARY: import.meta.env.VITE_UNI_HALO_BASEURL_SECONDARY,
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+2
-1
@@ -2,8 +2,9 @@
|
||||
"tabbar.home": "Home",
|
||||
"tabbar.gallery": "Gallery",
|
||||
"tabbar.moments": "Moments",
|
||||
"tabbar.links": "Links",
|
||||
"tabbar.about": "About",
|
||||
"tabbar.me": "Me",
|
||||
"tabbar.mine": "Mine",
|
||||
"tabbar.i18n": "I18n",
|
||||
"i18n.title": "En Title",
|
||||
"alova.title": "Alova Request",
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
"tabbar.gallery": "相册",
|
||||
"tabbar.moments": "瞬间",
|
||||
"tabbar.about": "关于",
|
||||
"tabbar.me": "我的",
|
||||
"tabbar.links": "友链",
|
||||
"tabbar.mine": "我的",
|
||||
"tabbar.i18n": "语言",
|
||||
"i18n.title": "中文标题",
|
||||
"alova.title": "Alova 请求",
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
defineOptions({
|
||||
name: 'PageTemplate',
|
||||
})
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '页面模板',
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="min-h-screen w-full flex flex-col items-center justify-center gap-y-6 bg-page">
|
||||
页面模板页面
|
||||
</view>
|
||||
</template>
|
||||
@@ -1,4 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { getPicSumImages } from '@/utils/randomResources'
|
||||
import { queryLinks } from '@/api/halo-plugin/link'
|
||||
|
||||
defineOptions({
|
||||
name: 'Gallery',
|
||||
})
|
||||
@@ -6,12 +9,340 @@ defineOptions({
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '相册',
|
||||
navigationBarBackgroundColor: '#ffffff',
|
||||
enablePullDownRefresh: true,
|
||||
},
|
||||
})
|
||||
|
||||
interface GalleryItem {
|
||||
metadataName: string
|
||||
displayName: string
|
||||
url: string
|
||||
}
|
||||
|
||||
interface GalleryGroup {
|
||||
metadataName: string
|
||||
displayName: string
|
||||
images: GalleryItem[]
|
||||
}
|
||||
|
||||
const groupBanner = reactive({
|
||||
current: 0,
|
||||
})
|
||||
const galleryStyle = ref('list')
|
||||
const showListPanel = reactive({
|
||||
show: false,
|
||||
metadataName: '',
|
||||
})
|
||||
|
||||
// 相册分组预览
|
||||
const galleryGroups = ref<GalleryGroup[]>([
|
||||
{
|
||||
metadataName: 'photo-group-HGaGs',
|
||||
displayName: '我们的故事',
|
||||
images: [{
|
||||
metadataName: '',
|
||||
displayName: '图片名称1',
|
||||
url: getPicSumImages(800, 600),
|
||||
}, {
|
||||
metadataName: '',
|
||||
displayName: '图片名称2',
|
||||
url: getPicSumImages(800, 600),
|
||||
}, {
|
||||
metadataName: '',
|
||||
displayName: '图片名称3',
|
||||
url: getPicSumImages(800, 600),
|
||||
}, {
|
||||
metadataName: '',
|
||||
displayName: '图片名称4',
|
||||
url: getPicSumImages(800, 600),
|
||||
}],
|
||||
},
|
||||
{
|
||||
metadataName: '履行记录',
|
||||
displayName: '履行记录',
|
||||
images: [{
|
||||
metadataName: '',
|
||||
displayName: '图片名称1',
|
||||
url: getPicSumImages(800, 600),
|
||||
}, {
|
||||
metadataName: '',
|
||||
displayName: '图片名称2',
|
||||
url: getPicSumImages(800, 600),
|
||||
}, {
|
||||
metadataName: '',
|
||||
displayName: '图片名称3',
|
||||
url: getPicSumImages(800, 600),
|
||||
}],
|
||||
},
|
||||
{
|
||||
metadataName: '家庭时光',
|
||||
displayName: '家庭时光',
|
||||
images: [{
|
||||
metadataName: '',
|
||||
displayName: '图片名称1',
|
||||
url: getPicSumImages(800, 600),
|
||||
}, {
|
||||
metadataName: '',
|
||||
displayName: '图片名称2',
|
||||
url: getPicSumImages(800, 600),
|
||||
}],
|
||||
},
|
||||
{
|
||||
metadataName: '其他',
|
||||
displayName: '其他',
|
||||
images: [{
|
||||
metadataName: '',
|
||||
displayName: '图片名称1',
|
||||
url: getPicSumImages(800, 600),
|
||||
}],
|
||||
},
|
||||
])
|
||||
|
||||
// 相册图片
|
||||
const galleryItems = ref<GalleryItem[]>([
|
||||
{
|
||||
metadataName: '',
|
||||
displayName: '图片名称1',
|
||||
url: getPicSumImages(400, 400),
|
||||
},
|
||||
{
|
||||
metadataName: '',
|
||||
displayName: '图片名称2',
|
||||
url: getPicSumImages(800, 600),
|
||||
},
|
||||
{
|
||||
metadataName: '',
|
||||
displayName: '图片名称3',
|
||||
url: getPicSumImages(400, 800),
|
||||
},
|
||||
{
|
||||
metadataName: '',
|
||||
displayName: '图片名称4',
|
||||
url: getPicSumImages(1920, 1080),
|
||||
},
|
||||
{
|
||||
metadataName: '',
|
||||
displayName: '图片名称5',
|
||||
url: getPicSumImages(800, 1920),
|
||||
},
|
||||
])
|
||||
|
||||
const { loading, error, data, run } = useRequest<any, IPaginationParams>(queryLinks, { loadingToast: false, loadingToastContent: '加载中...', loadingToastMask: true })
|
||||
|
||||
const { list: links, refresh } = useListData<any>({
|
||||
params: {
|
||||
page: 1,
|
||||
size: 0,
|
||||
},
|
||||
loading: toRef(loading),
|
||||
onPullDownRefresh: async (params, updateData) => {
|
||||
await run({ ...params })
|
||||
updateData({
|
||||
error: error.value,
|
||||
hasNext: data.value?.hasNext,
|
||||
items: data.value?.items ?? [],
|
||||
})
|
||||
},
|
||||
onReachBottom: async (params, updateData) => {
|
||||
await run({ ...params })
|
||||
updateData({
|
||||
error: error.value,
|
||||
hasNext: data.value?.hasNext,
|
||||
items: data.value?.items ?? [],
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
function onGroupBannerChange(e: any) {
|
||||
groupBanner.current = e.detail.current
|
||||
}
|
||||
|
||||
function getGroupImageRotateClass(index: number) {
|
||||
if (index === 0) {
|
||||
return 'z-30 border-2 border-solid border-white uh-shadow-card group-hover:scale-95'
|
||||
}
|
||||
if (index === 1) {
|
||||
return 'z-20 -rotate-6 group-hover:-rotate-22 border-2 border-solid border-white uh-shadow-card'
|
||||
}
|
||||
if (index === 2) {
|
||||
return 'z-10 rotate-6 group-hover:rotate-22 border-2 border-solid border-white uh-shadow-card'
|
||||
}
|
||||
}
|
||||
|
||||
// -- 图片列表模式
|
||||
// 图片列表容器样式
|
||||
function getGalleryListContainerClass(total: number) {
|
||||
if (total === 1) {
|
||||
return 'h-36'
|
||||
}
|
||||
if (total === 2) {
|
||||
return 'grid-cols-2 h-32'
|
||||
}
|
||||
if (total === 3) {
|
||||
return 'grid-cols-2 grid-rows-2 h-36'
|
||||
}
|
||||
if (total === 4) {
|
||||
return 'grid-cols-2 grid-rows-2 h-42'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
// 图片项样式
|
||||
function getGalleryListItemClass(index: number, total: number) {
|
||||
if (total === 1) {
|
||||
return ''
|
||||
}
|
||||
if (total === 2) {
|
||||
return ''
|
||||
}
|
||||
if (total === 3) {
|
||||
if (index === 0) {
|
||||
return 'row-span-2'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
if (total === 4) {
|
||||
return ''
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
function handleShowListPanel(metadataName: string) {
|
||||
showListPanel.metadataName = metadataName
|
||||
showListPanel.show = true
|
||||
}
|
||||
|
||||
onLoad(() => {
|
||||
refresh()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
})
|
||||
|
||||
onPageScroll(() => { })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="min-h-screen w-full flex flex-col items-center justify-center gap-y-6 bg-page">
|
||||
相册页面
|
||||
<view class="min-h-screen w-full flex flex-col bg-page">
|
||||
<!-- 分组列表:常态显示 -->
|
||||
<view class="box-border w-full px-4 py-4">
|
||||
<!-- 顶部区域固定区域 -->
|
||||
<view class="relative w-full">
|
||||
<swiper
|
||||
autoplay circular class="h-46 w-full overflow-hidden rounded-xl" :current="groupBanner.current"
|
||||
@change="onGroupBannerChange"
|
||||
>
|
||||
<swiper-item v-for="img in galleryGroups[0].images" :key="img.metadataName">
|
||||
<image :src="img.url" class="h-full w-full object-cover" />
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
<view
|
||||
class="absolute left-4 top-4 flex flex-col items-center justify-center rounded-2xl bg-orange-500 px-2 py-1.5 text-xs text-white font-bold"
|
||||
>
|
||||
最新精选
|
||||
</view>
|
||||
<view class="absolute bottom-4 left-4 flex flex-col gap-y-1">
|
||||
<view class="text-sm text-white font-bold">
|
||||
显示图片的名称
|
||||
</view>
|
||||
<view class="text-xs text-white/50">
|
||||
显示描述/日期
|
||||
</view>
|
||||
<view class="w-full flex items-center justify-center gap-x-2">
|
||||
<image
|
||||
v-for="(item, index) in galleryGroups[0].images" :key="item.metadataName" :src="item.url"
|
||||
mode="scaleToFill"
|
||||
class="box-border h-8 w-8 border-2 border-white rounded-lg border-solid transition-all duration-300"
|
||||
:class="[
|
||||
{
|
||||
'scale-110': groupBanner.current === index,
|
||||
},
|
||||
index % 2 === 0 ? '-rotate-4' : 'rotate-4',
|
||||
]" @click="groupBanner.current = index"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分组列表区域 -->
|
||||
<view class="mt-6 w-full flex items-center justify-between">
|
||||
<text class="text-sm text-gray-900 font-bold">我的相册</text>
|
||||
<text class="text-[10px] text-gray-600">共 30 组记录</text>
|
||||
</view>
|
||||
<!-- 混合模式 -->
|
||||
<view v-if="galleryStyle === 'mix'" class="grid grid-cols-2 mt-4 w-full gap-4">
|
||||
<view
|
||||
v-for="group in galleryGroups" :key="group.metadataName"
|
||||
class="group relative box-border h-36 w-full flex items-center justify-center p-1"
|
||||
>
|
||||
<!-- 预览图区域 -->
|
||||
<view class="relative h-full w-full">
|
||||
<view
|
||||
v-for="(img, index) in group.images.slice(0, 3)" :key="img.metadataName"
|
||||
class="absolute left-0 top-0 box-border h-full w-full origin-center overflow-hidden rounded-2xl transition-all duration-300"
|
||||
:class="[getGroupImageRotateClass(index)]"
|
||||
>
|
||||
<image :src="img.url" mode="scaleToFill" class="h-full w-full" />
|
||||
<!-- 文字区域 -->
|
||||
<view
|
||||
v-if="index === 0"
|
||||
class="absolute bottom-0 left-0 right-0 z-40 w-full flex flex-col gap-y-0.5 from-transparent to-black/90 bg-gradient-to-b px-2 py-1.5"
|
||||
>
|
||||
<text class="text-xs text-white font-bold">{{ group.displayName }}</text>
|
||||
<text class="text-[8px] text-white/80">{{ group.images.length }} 张图片</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 列表卡片模式 -->
|
||||
<view v-else-if="galleryStyle === 'list'" class="mt-2 w-full flex flex-col gap-y-4">
|
||||
<view
|
||||
v-for="group in galleryGroups" :key="group.metadataName"
|
||||
class="box-border flex flex-col gap-y-2 overflow-hidden rounded-xl bg-white p-4 pt-3" @click="handleShowListPanel(group.metadataName)"
|
||||
>
|
||||
<!-- 顶部区域 -->
|
||||
<view class="flex items-center justify-between">
|
||||
<view class="flex items-center gap-x-2 text-sm font-bold">
|
||||
<view class="hidden h-2 w-2 rounded-full bg-gray-900" /> {{ group.displayName }}
|
||||
</view>
|
||||
<view class="shrink-0">
|
||||
<text class="text-[10px] text-gray-400">{{ group.images.length }} 张</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 图片列表 -->
|
||||
<view
|
||||
class="grid gap-1 overflow-hidden rounded-lg"
|
||||
:class="[getGalleryListContainerClass(group.images.slice(0, 4).length)]"
|
||||
>
|
||||
<view
|
||||
v-for="(img, index) in group.images.slice(0, 4)" :key="img.metadataName"
|
||||
class="overflow-hidden"
|
||||
:class="getGalleryListItemClass(index, group.images.slice(0, 4).length)"
|
||||
>
|
||||
<image
|
||||
:src="img.url" mode="scaleToFill"
|
||||
class="h-full w-full object-cover transition-transform duration-500 hover:scale-105"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 分组列表:无数据显示 -->
|
||||
<view class="hidden">
|
||||
无数据
|
||||
</view>
|
||||
|
||||
<!-- 相册列表:根据分组显示 -->
|
||||
<uh-gallery-panel v-if="showListPanel.show" :group-name="showListPanel.metadataName" @close="showListPanel.show = false" />
|
||||
|
||||
<!-- 底部导航占位 -->
|
||||
<view class="w-full shrink-0">
|
||||
<uh-tabbar-page-placeholder />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
<script lang="ts" setup>
|
||||
import useRequest from '@/hooks/useRequest'
|
||||
import { queryPostByName, queryPosts } from '@/api/halo-base/post'
|
||||
import { cover } from '@/utils/imageHelper'
|
||||
import { timeFrom } from '@/utils/base/timeFrom'
|
||||
import { useListData } from '@/hooks/useListData'
|
||||
import { randomImageUrl } from '@/utils/randomResources'
|
||||
import type { ListedPostVo, ListedPostVoList, PostV1alpha1PublicApiQueryPostByNameRequest, PostV1alpha1PublicApiQueryPostsRequest, PostVo } from '@halo-dev/api-client'
|
||||
|
||||
import ProfileCard from '@/components/home/uh-profile-card.vue'
|
||||
import RecommendCategory from '@/components/home/uh-recommend-category.vue'
|
||||
import PostCard from '@/components/uh-post-card/uh-post-card.vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'Home',
|
||||
})
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationStyle: 'custom',
|
||||
navigationBarTitleText: '首页',
|
||||
enablePullDownRefresh: true,
|
||||
},
|
||||
})
|
||||
|
||||
const exampleImageUrls = reactive({
|
||||
banner: 'https://fastly.picsum.photos/id/353/800/800.jpg?hmac=RaDuQ92sSXj4q5vOYna9G00J7KrBUC3eBS0slCWYZXA',
|
||||
avatar: 'https://www.xiaoxiaomo.cn/logo.jpg',
|
||||
postCover1: 'https://lh3.googleusercontent.com/aida-public/AB6AXuDg-QKAXOU1_Lv726MyeTiY2R1MAJWKzjiUy__RYoUw78YL5PctVg2sfMUCRMBybAzw_E0V0fEhir4IW2RHF7YRcxm89JFDKVMlfU_yQTijnr4o-praEXX_buxFWWxsSayLdA64X9pOmjYT5FDp9kfpW4tqZBWLfiyqhiYZnyHTCFeKZruH2auHZt4OlsOdzamOOkoib4CMGmUwTj112emdzlshRDLxGOSbThTWUOw61YirCl-RUrx1WFg2kZimP9Byq4pV7tv9YhtS',
|
||||
postCover: 'https://www.yijunzhao.cn/upload/HermesWebUIHermesStudio%E8%83%8C%E5%90%8E%E7%9A%84%E5%93%81%E7%89%8C%E5%8D%87%E7%BA%A7%E4%B8%8E%E4%BA%A7%E5%93%81%E8%BF%9B%E5%8C%96-02.png',
|
||||
})
|
||||
|
||||
const banner = reactive({
|
||||
current: 0,
|
||||
items: [
|
||||
exampleImageUrls.banner,
|
||||
exampleImageUrls.postCover1,
|
||||
],
|
||||
list: [
|
||||
{
|
||||
value: exampleImageUrls.banner,
|
||||
title: 'UNI HALO v3.0',
|
||||
desc: '新全新探索简约的艺术风格',
|
||||
time: timeFrom(new Date()),
|
||||
},
|
||||
{
|
||||
value: randomImageUrl.pc,
|
||||
title: 'UNI HALO v3.0 使用手册',
|
||||
desc: 'uni-halo 使用手册,包含安装、配置、使用等信息666666666aaaa啊啊啊啊啊啊啊啊啊啊啊',
|
||||
time: timeFrom(new Date()),
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const quickList = reactive([
|
||||
{
|
||||
pagePath: 'pages/moments/moments',
|
||||
text: '动态',
|
||||
iconType: 'uiLib',
|
||||
icon: 'gift',
|
||||
},
|
||||
{
|
||||
pagePath: 'pages-blog/about/about',
|
||||
text: '关于',
|
||||
iconType: 'uiLib',
|
||||
icon: 'image',
|
||||
},
|
||||
{
|
||||
pagePath: 'pages/me/me',
|
||||
text: '我的',
|
||||
iconType: 'uiLib',
|
||||
icon: 'user',
|
||||
},
|
||||
{
|
||||
pagePath: 'pages/category/category',
|
||||
text: '分类',
|
||||
iconType: 'uiLib',
|
||||
icon: 'image',
|
||||
},
|
||||
{
|
||||
pagePath: 'pages/category/category',
|
||||
text: '其他',
|
||||
iconType: 'uiLib',
|
||||
icon: 'image',
|
||||
},
|
||||
])
|
||||
|
||||
const { loading, error, data, run } = useRequest<ListedPostVoList, PostV1alpha1PublicApiQueryPostsRequest>(queryPosts, { loadingToast: false, loadingToastContent: '加载中...', loadingToastMask: true })
|
||||
const { loading: loadingSingle, error: errorSingle, data: dataSingle, run: runSingle } = useRequest<PostVo, PostV1alpha1PublicApiQueryPostByNameRequest>(queryPostByName)
|
||||
|
||||
const { list: postList, refresh } = useListData<ListedPostVo>({
|
||||
params: {
|
||||
page: 1,
|
||||
size: 10,
|
||||
},
|
||||
loading: toRef(loading),
|
||||
onPullDownRefresh: async (params, updateData) => {
|
||||
await run({ ...params })
|
||||
updateData({
|
||||
error: error.value,
|
||||
hasNext: data.value?.hasNext,
|
||||
items: data.value?.items ?? [],
|
||||
})
|
||||
},
|
||||
onReachBottom: async (params, updateData) => {
|
||||
await run({ ...params })
|
||||
updateData({
|
||||
error: error.value,
|
||||
hasNext: data.value?.hasNext,
|
||||
items: data.value?.items ?? [],
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
onLoad(() => {
|
||||
console.log('home onLoad')
|
||||
refresh()
|
||||
// runSingle({ name: '019eb1ca-e37c-7729-97af-e0b658aeef0f' })
|
||||
})
|
||||
|
||||
onPageScroll(() => { })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="min-h-screen bg-page2">
|
||||
<!-- 顶部banner -->
|
||||
<view v-if="false" class="relative h-72 w-full">
|
||||
<image class="h-72 w-full object-cover" :src="exampleImageUrls.banner" />
|
||||
<view class="absolute bottom-4 left-4 flex flex-col gap-y-2">
|
||||
<text class="text-xs text-white">welcome to new world</text>
|
||||
<text class="text-2xl text-white font-bold">UNI HALO V3.0 </text>
|
||||
<text class="text-xs text-white">新全新探索简约的艺术风格</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view>
|
||||
<wd-swiper
|
||||
v-model:current="banner.current" :list="banner.items" indicator-position="right" autoplay
|
||||
:height="340"
|
||||
>
|
||||
<template #default="{ index }">
|
||||
<view class="relative h-full w-full">
|
||||
<image class="h-full w-full" mode="aspectFill" :src="banner.list[index].value" />
|
||||
<!-- 渐变遮罩 from-black/0 via-black/50 to-black/90 bg-gradient-to-b -->
|
||||
<view class="absolute bottom-0 left-0 right-0 p-4 pb-16">
|
||||
<view class="relative z-20 w-5/6 flex flex-col gap-y-2">
|
||||
<text class="text-xs text-white/90">{{ banner.list[index].time }}</text>
|
||||
<text class="line-clamp-1 text-xl text-white font-bold">{{ banner.list[index].title }}</text>
|
||||
<text class="line-clamp-1 text-xs text-white/95">{{ banner.list[index].desc }}</text>
|
||||
</view>
|
||||
<view
|
||||
class="absolute bottom-0 left-0 right-0 z-10 h-3/5 from-black/0 via-page/50 to-page bg-gradient-to-b"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<template #indicator="{ current, total }">
|
||||
<view class="absolute bottom-16 right-4 text-xs text-white">
|
||||
{{ current + 1 }}/{{ total }}
|
||||
</view>
|
||||
</template>
|
||||
</wd-swiper>
|
||||
</view>
|
||||
|
||||
<!-- 作者卡片 -->
|
||||
<view class="relative z-10 box-border px-4 -mt-12">
|
||||
<profile-card />
|
||||
</view>
|
||||
|
||||
<!-- 金刚区 -->
|
||||
<view class="mt-6 px-4">
|
||||
<view class="flex items-center justify-around gap-x-4">
|
||||
<view
|
||||
v-for="item in quickList" :key="item.pagePath"
|
||||
class="w-1/5 flex flex-col items-center justify-center gap-y-1"
|
||||
>
|
||||
<view class="uh-shadow-xs flex items-center justify-center rounded-2xl bg-white p-3.5">
|
||||
<wd-icon :name="item.icon" :size="24" />
|
||||
</view>
|
||||
<text class="text-xs text-gray-800">{{ item.text }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分类推荐 -->
|
||||
<recommend-category />
|
||||
|
||||
<!-- 瞬间推荐 -->
|
||||
|
||||
<!-- 置顶文章 -->
|
||||
<view v-if="dataSingle && false" class="mt-4 box-border px-4">
|
||||
<view class="mb-2 box-border flex items-center justify-between">
|
||||
<text class="text-lg text-gray-900 font-bold">置顶文章</text>
|
||||
</view>
|
||||
<view class="box-border rounded-xl bg-white p-4">
|
||||
<image :src="cover(dataSingle.spec.cover)" mode="aspectFill" class="h-36 w-full rounded-xl" />
|
||||
<view class="box-border w-full flex flex-col gap-y-1 pt-2">
|
||||
<text class="line-clamp-1 text-sm text-gray-900 font-bold">{{ dataSingle.spec.title }}</text>
|
||||
<text class="line-clamp-2 text-xs text-gray-500">{{ dataSingle.status.excerpt }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 最新笔记 -->
|
||||
<view class="mt-4 mt-4 box-border px-4">
|
||||
<view class="mb-4 box-border flex items-center justify-between">
|
||||
<text class="text-sm text-gray-900 font-bold">最新笔记</text>
|
||||
</view>
|
||||
<view class="box-border w-full flex flex-col items-center justify-center gap-y-4 overflow-hidden">
|
||||
<view v-if="error && postList.length > 0" class="un-shadow-xs w-full rounded-2xl bg-white py-12">
|
||||
<wd-empty :icon-size="60" icon="no-result" tip="暂无数据" />
|
||||
</view>
|
||||
<template v-else>
|
||||
<post-card v-for="post in postList" :key="post.metadata.name" :post="post" type="topCover" />
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部导航占位 -->
|
||||
<uh-tabbar-page-placeholder />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.uh-shadow-xs {
|
||||
box-shadow: 0 0 24rpx rgba(0, 0, 0, 0.035);
|
||||
}
|
||||
</style>
|
||||
+7
-263
@@ -1,282 +1,26 @@
|
||||
<script lang="ts" setup>
|
||||
import useRequest from '@/hooks/useRequest'
|
||||
import { queryPostByName, queryPosts } from '@/api/halo-base/post'
|
||||
import { completeUrl } from '@/utils/url'
|
||||
import { timeFrom } from '@/utils/base/timeFrom'
|
||||
import { useListData } from '@/hooks/useListData'
|
||||
import { randomImageUrl } from '@/utils/randomResources'
|
||||
|
||||
import type { ListedPostVo, ListedPostVoList, PostV1alpha1PublicApiQueryPostByNameRequest, PostV1alpha1PublicApiQueryPostsRequest, PostVo } from '@halo-dev/api-client'
|
||||
|
||||
defineOptions({
|
||||
name: 'Home',
|
||||
name: 'Index',
|
||||
})
|
||||
|
||||
definePage({
|
||||
type: 'home',
|
||||
style: {
|
||||
navigationStyle: 'custom',
|
||||
navigationBarTitleText: '首页',
|
||||
enablePullDownRefresh: true,
|
||||
},
|
||||
})
|
||||
|
||||
const exampleImageUrls = reactive({
|
||||
banner: 'https://fastly.picsum.photos/id/353/800/800.jpg?hmac=RaDuQ92sSXj4q5vOYna9G00J7KrBUC3eBS0slCWYZXA',
|
||||
avatar: 'https://www.xiaoxiaomo.cn/logo.jpg',
|
||||
postCover1: 'https://lh3.googleusercontent.com/aida-public/AB6AXuDg-QKAXOU1_Lv726MyeTiY2R1MAJWKzjiUy__RYoUw78YL5PctVg2sfMUCRMBybAzw_E0V0fEhir4IW2RHF7YRcxm89JFDKVMlfU_yQTijnr4o-praEXX_buxFWWxsSayLdA64X9pOmjYT5FDp9kfpW4tqZBWLfiyqhiYZnyHTCFeKZruH2auHZt4OlsOdzamOOkoib4CMGmUwTj112emdzlshRDLxGOSbThTWUOw61YirCl-RUrx1WFg2kZimP9Byq4pV7tv9YhtS',
|
||||
postCover: 'https://www.yijunzhao.cn/upload/HermesWebUIHermesStudio%E8%83%8C%E5%90%8E%E7%9A%84%E5%93%81%E7%89%8C%E5%8D%87%E7%BA%A7%E4%B8%8E%E4%BA%A7%E5%93%81%E8%BF%9B%E5%8C%96-02.png',
|
||||
})
|
||||
|
||||
const banner = reactive({
|
||||
current: 0,
|
||||
items: [
|
||||
exampleImageUrls.banner,
|
||||
exampleImageUrls.postCover1,
|
||||
],
|
||||
list: [
|
||||
{
|
||||
value: exampleImageUrls.banner,
|
||||
title: 'UNI HALO v3.0',
|
||||
desc: '新全新探索简约的艺术风格',
|
||||
time: timeFrom(new Date()),
|
||||
},
|
||||
{
|
||||
value: randomImageUrl.pc,
|
||||
title: 'UNI HALO v3.0 使用手册',
|
||||
desc: 'uni-halo 使用手册,包含安装、配置、使用等信息666666666aaaa啊啊啊啊啊啊啊啊啊啊啊',
|
||||
time: timeFrom(new Date()),
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const quickList = reactive([
|
||||
{
|
||||
pagePath: 'pages/moments/moments',
|
||||
text: '动态',
|
||||
iconType: 'uiLib',
|
||||
icon: 'gift',
|
||||
},
|
||||
{
|
||||
pagePath: 'pages-blog/about/about',
|
||||
text: '关于',
|
||||
iconType: 'uiLib',
|
||||
icon: 'image',
|
||||
},
|
||||
{
|
||||
pagePath: 'pages/me/me',
|
||||
text: '我的',
|
||||
iconType: 'uiLib',
|
||||
icon: 'user',
|
||||
},
|
||||
{
|
||||
pagePath: 'pages/category/category',
|
||||
text: '分类',
|
||||
iconType: 'uiLib',
|
||||
icon: 'image',
|
||||
},
|
||||
{
|
||||
pagePath: 'pages/category/category',
|
||||
text: '其他',
|
||||
iconType: 'uiLib',
|
||||
icon: 'image',
|
||||
},
|
||||
])
|
||||
|
||||
const { loading, error, data, run } = useRequest<ListedPostVoList, PostV1alpha1PublicApiQueryPostsRequest>(queryPosts, { loadingToast: true, loadingToastContent: '加载中...', loadingToastMask: true })
|
||||
const { loading: loadingSingle, error: errorSingle, data: dataSingle, run: runSingle } = useRequest<PostVo, PostV1alpha1PublicApiQueryPostByNameRequest>(queryPostByName)
|
||||
|
||||
const { list: postList, refresh } = useListData<ListedPostVo>({
|
||||
params: {
|
||||
page: 1,
|
||||
size: 10,
|
||||
},
|
||||
loading: toRef(loading),
|
||||
onPullDownRefresh: async (params, updateData) => {
|
||||
await run({ ...params })
|
||||
updateData({
|
||||
error: error.value,
|
||||
hasNext: data.value?.hasNext,
|
||||
items: data.value?.items ?? [],
|
||||
})
|
||||
},
|
||||
onReachBottom: async (params, updateData) => {
|
||||
await run({ ...params })
|
||||
updateData({
|
||||
error: error.value,
|
||||
hasNext: data.value?.hasNext,
|
||||
items: data.value?.items ?? [],
|
||||
})
|
||||
navigationBarTitleText: '启动页',
|
||||
},
|
||||
})
|
||||
|
||||
onLoad(() => {
|
||||
console.log('home onLoad')
|
||||
refresh()
|
||||
runSingle({ name: '019eb1ca-e37c-7729-97af-e0b658aeef0f' })
|
||||
console.log('index onLoad')
|
||||
// 默认跳转首页
|
||||
// uni.switchTab({ url: '/pages/home/home' })
|
||||
uni.switchTab({ url: '/pages/gallery/gallery' })
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="min-h-screen bg-page pb-safe">
|
||||
<!-- 顶部banner -->
|
||||
<view v-if="false" class="relative h-72 w-full">
|
||||
<image class="h-72 w-full object-cover" :src="exampleImageUrls.banner" />
|
||||
<view class="absolute bottom-4 left-4 flex flex-col gap-y-2">
|
||||
<text class="text-xs text-white">welcome to new world</text>
|
||||
<text class="text-2xl text-white font-bold">UNI HALO V3.0 </text>
|
||||
<text class="text-xs text-white">新全新探索简约的艺术风格</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view>
|
||||
<wd-swiper
|
||||
v-model:current="banner.current" :list="banner.items" indicator-position="right" autoplay
|
||||
:height="290"
|
||||
>
|
||||
<template #default="{ index }">
|
||||
<view class="relative h-full w-full">
|
||||
<image class="h-full w-full object-cover" :src="banner.list[index].value" />
|
||||
<view class="absolute bottom-0 left-0 right-0 from-black/0 via-black/50 to-black/90 bg-gradient-to-b p-4">
|
||||
<view class="w-5/6 flex flex-col gap-y-2">
|
||||
<text class="text-xs text-white/90">{{ banner.list[index].time }}</text>
|
||||
<text class="line-clamp-1 text-xl text-white font-bold">{{ banner.list[index].title }}</text>
|
||||
<text class="line-clamp-1 text-xs text-white/95">{{ banner.list[index].desc }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<template #indicator="{ current, total }">
|
||||
<view class="absolute bottom-4 right-4 text-xs text-white">
|
||||
{{ current + 1 }}/{{ total }}
|
||||
</view>
|
||||
</template>
|
||||
</wd-swiper>
|
||||
</view>
|
||||
|
||||
<!-- 作者卡片 -->
|
||||
<view class="mt-4 box-border px-4">
|
||||
<view class="box-border flex flex-col items-center gap-y-2 rounded-2xl bg-white p-4 shadow-sm">
|
||||
<view class="w-full flex items-center justify-between gap-x-2">
|
||||
<image
|
||||
class="uh-shadow-xs h-11 w-11 shrink-0 border-2 border-white rounded-full border-solid"
|
||||
src="https://www.xiaoxiaomo.cn/logo.jpg" mode="scaleToFill"
|
||||
/>
|
||||
<view class="h-full flex flex-1 flex-col justify-between gap-y-1">
|
||||
<view class="text-sm text-gray-900 font-bold">
|
||||
小莫唐尼
|
||||
</view>
|
||||
<view class="line-clamp-1 text-xs text-gray-500">
|
||||
一个爱凑热闹,喜欢捣鼓前端的码农。
|
||||
</view>
|
||||
</view>
|
||||
<view class="h-full shrink-0 rounded-3xl bg-[#1A1A1A] px-4 py-2 text-xs text-white">
|
||||
联系我
|
||||
</view>
|
||||
</view>
|
||||
<view class="my-1 h-1px w-full bg-gray-100" />
|
||||
<view class="grid grid-cols-4 w-full gap-x-4">
|
||||
<view class="flex flex-col items-center justify-center gap-y-0.5">
|
||||
<text class="text-sm text-gray-900 font-bold">128</text>
|
||||
<text class="text-xs text-gray-600">文章</text>
|
||||
</view>
|
||||
<view class="flex flex-col items-center justify-center gap-y-0.5">
|
||||
<text class="text-sm text-gray-900 font-bold">999</text>
|
||||
<text class="text-xs text-gray-600">访客</text>
|
||||
</view>
|
||||
<view class="flex flex-col items-center justify-center gap-y-0.5">
|
||||
<text class="text-sm text-gray-900 font-bold">1.2k</text>
|
||||
<text class="text-xs text-gray-600">获赞</text>
|
||||
</view>
|
||||
<view class="flex flex-col items-center justify-center gap-y-0.5">
|
||||
<text class="text-sm text-gray-900 font-bold">128k</text>
|
||||
<text class="text-xs text-gray-600">收藏</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 金刚区 -->
|
||||
<view class="mt-4 px-4">
|
||||
<view class="flex items-center justify-around gap-x-4">
|
||||
<view
|
||||
v-for="item in quickList" :key="item.pagePath"
|
||||
class="w-1/5 flex flex-col items-center justify-center gap-y-1"
|
||||
>
|
||||
<view class="flex items-center justify-center rounded-3xl bg-white p-3.5 shadow-sm">
|
||||
<wd-icon :name="item.icon" :size="24" />
|
||||
</view>
|
||||
<text class="text-xs text-gray-800">{{ item.text }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分类推荐 -->
|
||||
|
||||
<!-- 瞬间推荐 -->
|
||||
|
||||
<!-- 置顶文章 -->
|
||||
<view v-if="dataSingle" class="mt-4 box-border px-4">
|
||||
<view class="mb-2 box-border flex items-center justify-between">
|
||||
<text class="text-lg text-gray-900 font-bold">置顶文章</text>
|
||||
</view>
|
||||
<view class="box-border rounded-xl bg-white p-4">
|
||||
<image :src="completeUrl(dataSingle.spec.cover)" mode="aspectFill" class="h-36 w-full rounded-xl" />
|
||||
<view class="box-border w-full flex flex-col gap-y-1 pt-2">
|
||||
<text class="line-clamp-1 text-sm text-gray-900 font-bold">{{ dataSingle.spec.title }}</text>
|
||||
<text class="line-clamp-2 text-xs text-gray-500">{{ dataSingle.status.excerpt }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 最新文章 -->
|
||||
<view class="mt-4">
|
||||
<view class="mb-2 box-border flex items-center justify-between px-4">
|
||||
<text class="text-lg text-gray-900 font-bold">最新文章</text>
|
||||
</view>
|
||||
<view class="box-border w-full flex flex-col items-center justify-center gap-y-4 overflow-hidden px-4">
|
||||
<view v-if="error && postList.length > 0" class="un-shadow-xs w-full rounded-2xl bg-white py-12">
|
||||
<wd-empty :icon-size="60" icon="no-result" tip="暂无数据" />
|
||||
</view>
|
||||
<template v-else>
|
||||
<view
|
||||
v-for="post in postList" :key="post.metadata.name"
|
||||
class="uh-shadow-xs flex justify-center overflow-hidden rounded-2xl bg-white p-4"
|
||||
>
|
||||
<image :src="completeUrl(post.spec.cover)" mode="aspectFill" class="h-22 w-22 shrink-0 rounded-lg" />
|
||||
<view class="box-border flex-1 pl-4">
|
||||
<view class="h-full flex flex-1 flex-col justify-between">
|
||||
<view class="line-clamp-1 text-sm text-gray-900 font-bold">
|
||||
{{ post.spec.title }}
|
||||
</view>
|
||||
<view class="line-clamp-2 text-xs text-gray-500">
|
||||
{{ post.status.excerpt }}
|
||||
</view>
|
||||
<view class="w-full flex items-center gap-x-4">
|
||||
<view class="text-xs text-gray-600">
|
||||
{{ timeFrom(post.spec.publishTime) }}
|
||||
</view>
|
||||
<view class="text-xs text-gray-600">
|
||||
·
|
||||
</view>
|
||||
<view class="text-xs text-gray-600">
|
||||
{{ post.categories.map(c => c.spec.displayName).join('·') }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部导航占位 -->
|
||||
<view class="h-60px w-full" />
|
||||
启动页
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.uh-shadow-xs {
|
||||
box-shadow: 0 0 24rpx rgba(0, 0, 0, 0.035);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
<script lang="ts" setup>
|
||||
import { getCurrentInstance } from 'vue'
|
||||
import useRequest from '@/hooks/useRequest'
|
||||
import { queryLinkGroups, queryLinks } from '@/api/halo-plugin/link'
|
||||
import { cover } from '@/utils/imageHelper'
|
||||
import { copy } from '@/utils/base/clipboard'
|
||||
import { getRect } from '@/utils/base/getRect'
|
||||
import { useListData } from '@/hooks/useListData'
|
||||
import type { IPaginationParams } from '@/hooks/useListData'
|
||||
import { debounce } from '@/utils/base/debounce'
|
||||
|
||||
defineOptions({
|
||||
name: 'Links',
|
||||
})
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '友情链接',
|
||||
navigationBarBackgroundColor: '#ffffff',
|
||||
enablePullDownRefresh: true,
|
||||
},
|
||||
})
|
||||
|
||||
const { loading: loadingGroups, error: errorGroups, data: dataGroups, run: runGroups } = useRequest<any>(queryLinkGroups, { loadingToast: false, loadingToastContent: '加载中...', loadingToastMask: true })
|
||||
const { loading, error, data, run } = useRequest<any, IPaginationParams>(queryLinks, { loadingToast: false, loadingToastContent: '加载中...', loadingToastMask: true })
|
||||
|
||||
const { list: links, refresh } = useListData<any>({
|
||||
params: {
|
||||
page: 1,
|
||||
size: 0,
|
||||
},
|
||||
loading: toRef(loading),
|
||||
onPullDownRefresh: async (params, updateData) => {
|
||||
await run({ ...params })
|
||||
updateData({
|
||||
error: error.value,
|
||||
hasNext: data.value?.hasNext,
|
||||
items: data.value?.items ?? [],
|
||||
})
|
||||
},
|
||||
onReachBottom: async (params, updateData) => {
|
||||
await run({ ...params })
|
||||
updateData({
|
||||
error: error.value,
|
||||
hasNext: data.value?.hasNext,
|
||||
items: data.value?.items ?? [],
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
// 分组列表,处理排序,分组显示
|
||||
interface ILinkGroupData {
|
||||
name: string
|
||||
index: string
|
||||
priority: number
|
||||
links: any[]
|
||||
}
|
||||
const linkGroups = computed(() => {
|
||||
const groupedLinks = links.value.reduce((acc, link) => {
|
||||
const groupName = link.spec.groupName
|
||||
|
||||
const group = dataGroups.value?.find(item => item.metadata.name === groupName)
|
||||
let _groupName = '未分组'
|
||||
if (group) {
|
||||
_groupName = group.spec.displayName
|
||||
}
|
||||
if (!acc[_groupName]) {
|
||||
acc[_groupName] = {
|
||||
name: _groupName,
|
||||
index: _groupName,
|
||||
priority: group?.spec.priority ?? (dataGroups.value ? dataGroups.value.length + 1 : 99),
|
||||
links: [],
|
||||
}
|
||||
}
|
||||
acc[_groupName].links.push(toRaw(link))
|
||||
return acc
|
||||
}, {} as Record<string, any[]>)
|
||||
// // 分组还需要根据 分组中的 spec.priority 字段进行排序
|
||||
return Object.values(groupedLinks)
|
||||
.map((item) => {
|
||||
return item
|
||||
})
|
||||
.sort((a: ILinkGroupData, b: ILinkGroupData) => a.priority - b.priority)
|
||||
.map((item: ILinkGroupData) => {
|
||||
item.links.sort((a, b) => a.spec.priority - b.spec.priority)
|
||||
return item
|
||||
})
|
||||
})
|
||||
|
||||
const useBottomGroupNav = ref(false)
|
||||
const allGroupRects = ref([])
|
||||
const instance = getCurrentInstance()
|
||||
|
||||
async function getAllGroupPosition() {
|
||||
await nextTick()
|
||||
allGroupRects.value = []
|
||||
linkGroups.value.forEach(async (item, index) => {
|
||||
const groupRects = await getRect({ selector: `.group-start-${index},.group-end-${index}`, instance, all: true })
|
||||
allGroupRects.value.push({
|
||||
name: item.name,
|
||||
index,
|
||||
active: false,
|
||||
startRect: groupRects[0],
|
||||
endRect: groupRects[1],
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function scrollToGroup(item: any) {
|
||||
const startRect = item.startRect
|
||||
if (startRect) {
|
||||
uni.pageScrollTo({
|
||||
scrollTop: startRect.top - 12,
|
||||
duration: 300,
|
||||
})
|
||||
uni.showToast({
|
||||
title: item.name,
|
||||
icon: 'none',
|
||||
duration: 600,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleActiveNavGroup = debounce((scrollTop: number) => {
|
||||
allGroupRects.value.forEach((item) => {
|
||||
if (scrollTop >= item.startRect.top - 24 && scrollTop <= item.endRect.top + 60) {
|
||||
item.active = true
|
||||
}
|
||||
else {
|
||||
item.active = false
|
||||
}
|
||||
})
|
||||
}, 30)
|
||||
|
||||
watch(() => linkGroups.value, async (newVal) => {
|
||||
if (newVal.length !== 0) {
|
||||
await getAllGroupPosition()
|
||||
}
|
||||
})
|
||||
|
||||
onPageScroll(({ scrollTop }) => {
|
||||
handleActiveNavGroup(scrollTop)
|
||||
})
|
||||
|
||||
onLoad(async () => {
|
||||
await runGroups()
|
||||
await refresh()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="min-h-screen w-full flex flex-col bg-page2">
|
||||
<!-- <view>
|
||||
<view>分组数据</view>
|
||||
<view>{{ dataGroups }}</view>
|
||||
</view> -->
|
||||
|
||||
<!-- 分组列表 -->
|
||||
<view v-if="linkGroups.length !== 0" class="box-border w-full flex flex-col gap-y-4 overflow-hidden p-4 pb-0">
|
||||
<template v-for="(item, index) in linkGroups" :key="item.name">
|
||||
<view
|
||||
v-if="linkGroups.length !== 1" class="flex items-center justify-between text-sm"
|
||||
:class="[`group-start-${index}`]"
|
||||
>
|
||||
<text class="font-bold">{{ item.name }}</text>
|
||||
<text class="text-xs text-gray-600"> {{ item.links.length }} 条记录 </text>
|
||||
</view>
|
||||
|
||||
<!-- 分组卡片 -->
|
||||
<view
|
||||
v-for="(link, linkIndex) in item.links" :key="link.metadata.name"
|
||||
class="uh-shadow-card box-border w-full flex flex-col items-center justify-center gap-y-2 overflow-hidden border-2 border-white rounded-xl border-solid bg-white p-4"
|
||||
:class="[
|
||||
linkIndex === item.links.length - 1 ? `group-end-${index}` : '',
|
||||
]"
|
||||
>
|
||||
<!-- 顶部信息 -->
|
||||
<view class="box-border w-full flex items-center justify-between gap-x-3">
|
||||
<view
|
||||
class="uh-shadow-md box-border h-10 w-10 shrink-0 overflow-hidden border-2 border-white rounded-xl border-solid bg-white"
|
||||
>
|
||||
<image :src="cover(link.spec.logo)" mode="aspectFill" class="h-full w-full" />
|
||||
</view>
|
||||
<view class="flex flex-1 flex-col gap-y-0.5">
|
||||
<view class="line-clamp-1 text-sm text-gray-900 font-bold">
|
||||
{{ link.spec.displayName }}
|
||||
</view>
|
||||
<view class="line-clamp-1 text-xs text-gray-400">
|
||||
{{ link.spec.url }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="shrink-0">
|
||||
<view
|
||||
class="uh-shadow-md rounded-3xl bg-[#1A1A1A] px-4 py-1.5 text-xs text-white"
|
||||
@click="copy(link.spec.url, true)"
|
||||
>
|
||||
复制
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 底部信息 -->
|
||||
<view class="line-clamp-2 w-full text-xs text-gray-500 leading-relaxed">
|
||||
{{ link.spec.description }}
|
||||
<!-- {{ '你好,我是 UNI HALO v3.0 新版本,开源免费 Halo 博客小程序,支持多端编译。' }} -->
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
<view v-else class="w-full flex flex-1 items-center justify-center">
|
||||
<wd-empty icon="no-content" tip="暂无友链">
|
||||
<template #image>
|
||||
<wd-icon name="empty" color="#1A1A1A" :size="52" />
|
||||
</template>
|
||||
</wd-empty>
|
||||
</view>
|
||||
|
||||
<!-- 导航 -->
|
||||
<view
|
||||
v-if="!useBottomGroupNav && allGroupRects.length !== 0" class="fixed right-2 top-1/2 z-50" :style="{
|
||||
transform: 'translateY(-50%)',
|
||||
}"
|
||||
>
|
||||
<view class="uh-shadow-md h-full flex flex-col items-center justify-center gap-y-1 rounded-full bg-white p-1">
|
||||
<view
|
||||
v-for="(item) in allGroupRects" :key="item.index"
|
||||
class="flex flex-col items-center justify-center rounded-full px-1 py-1.5 text-gray-600"
|
||||
:class="{ 'bg-gray-900 text-white py-1.5!': item.active }" @click="scrollToGroup(item)"
|
||||
>
|
||||
<text v-if="item.active" class="text-xs">
|
||||
#
|
||||
</text>
|
||||
<text class="text-[10px] -mt-1">
|
||||
{{ item.name.slice(0, 1) }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view
|
||||
v-if="useBottomGroupNav && allGroupRects.length !== 0"
|
||||
class="fixed bottom-0 left-4 right-4 z-50 box-border pb-4"
|
||||
>
|
||||
<view class="uh-shadow-md box-border w-full flex-1 overflow-hidden rounded-full bg-white p-2">
|
||||
<scroll-view :scroll-x="true" class="uh-shadow-md w-full whitespace-nowrap rounded-full">
|
||||
<view
|
||||
v-for="(item) in allGroupRects" :key="item.index"
|
||||
class="mr-2 inline-block border border-gray-900 rounded-2xl border-solid bg-gray-900 px-3 py-1.5 text-[11px] text-white"
|
||||
@click="scrollToGroup(item.name)"
|
||||
>
|
||||
{{ item.name }}
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
<!-- 底部导航占位 -->
|
||||
<uh-tabbar-page-placeholder />
|
||||
</view>
|
||||
|
||||
<!-- 底部导航占位 -->
|
||||
<view
|
||||
class="w-full shrink-0" :class="{
|
||||
'pb-10': useBottomGroupNav,
|
||||
}"
|
||||
>
|
||||
<uh-tabbar-page-placeholder />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.uh-shadow-xs {
|
||||
box-shadow: 0 0 24rpx rgba(0, 0, 0, 0.035);
|
||||
}
|
||||
|
||||
.uh-shadow-md {
|
||||
box-shadow: 0 0 24rpx rgba(0, 0, 0, 0.075);
|
||||
}
|
||||
|
||||
.uh-shadow-card {
|
||||
box-shadow: 0 16rpx 60rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
</style>
|
||||
@@ -1,79 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { LOGIN_PAGE } from '@/router/config'
|
||||
import { useUserStore } from '@/store'
|
||||
import { useTokenStore } from '@/store/token'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '我的',
|
||||
},
|
||||
})
|
||||
|
||||
const userStore = useUserStore()
|
||||
const tokenStore = useTokenStore()
|
||||
// 使用storeToRefs解构userInfo
|
||||
const { userInfo } = storeToRefs(userStore)
|
||||
|
||||
// 微信小程序下登录
|
||||
async function handleLogin() {
|
||||
// #ifdef MP-WEIXIN
|
||||
// 微信登录
|
||||
await tokenStore.wxLogin()
|
||||
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
uni.navigateTo({
|
||||
url: `${LOGIN_PAGE}`,
|
||||
})
|
||||
// #endif
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要退出登录吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 清空用户信息
|
||||
useTokenStore().logout()
|
||||
// 执行退出登录逻辑
|
||||
uni.showToast({
|
||||
title: '退出登录成功',
|
||||
icon: 'success',
|
||||
})
|
||||
// #ifdef MP-WEIXIN
|
||||
// 微信小程序,去首页
|
||||
// uni.reLaunch({ url: '/pages/index/index' })
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
// 非微信小程序,去登录页
|
||||
// uni.navigateTo({ url: LOGIN_PAGE })
|
||||
// #endif
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="min-h-screen w-full flex flex-col items-center justify-center gap-y-6 bg-page">
|
||||
<view class="mt-3 break-all px-3 text-center text-green-500">
|
||||
{{ userInfo.username ? '已登录' : '未登录' }}
|
||||
</view>
|
||||
<view class="mt-3 break-all px-3">
|
||||
{{ JSON.stringify(userInfo, null, 2) }}
|
||||
</view>
|
||||
|
||||
<view class="mt-[60vh] px-3">
|
||||
<view class="m-auto w-160px text-center">
|
||||
<button v-if="tokenStore.hasLogin" type="warn" class="w-full" @click="handleLogout">
|
||||
退出登录
|
||||
</button>
|
||||
<button v-else type="primary" class="w-full" @click="handleLogin">
|
||||
登录
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -0,0 +1,141 @@
|
||||
<script lang="ts" setup>
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { LOGIN_PAGE } from '@/router/config'
|
||||
import { useUserStore } from '@/store'
|
||||
import { useTokenStore } from '@/store/token'
|
||||
import i18n, { t } from '@/locale/index'
|
||||
import { setTabbarItem } from '@/tabbar/i18n'
|
||||
|
||||
defineOptions({
|
||||
name: 'Mine',
|
||||
})
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '我的',
|
||||
navigationBarBackgroundColor: '#ffffff',
|
||||
enablePullDownRefresh: true,
|
||||
},
|
||||
})
|
||||
|
||||
const userStore = useUserStore()
|
||||
const tokenStore = useTokenStore()
|
||||
// 使用storeToRefs解构userInfo
|
||||
const { userInfo } = storeToRefs(userStore)
|
||||
|
||||
// 微信小程序下登录
|
||||
async function handleLogin() {
|
||||
// #ifdef MP-WEIXIN
|
||||
// 微信登录
|
||||
await tokenStore.wxLogin()
|
||||
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
uni.navigateTo({
|
||||
url: `${LOGIN_PAGE}`,
|
||||
})
|
||||
// #endif
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确定要退出登录吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 清空用户信息
|
||||
useTokenStore().logout()
|
||||
// 执行退出登录逻辑
|
||||
uni.showToast({
|
||||
title: '退出登录成功',
|
||||
icon: 'success',
|
||||
})
|
||||
// #ifdef MP-WEIXIN
|
||||
// 微信小程序,去首页
|
||||
// uni.reLaunch({ url: '/pages/index/index' })
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
// 非微信小程序,去登录页
|
||||
// uni.navigateTo({ url: LOGIN_PAGE })
|
||||
// #endif
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const current = ref(uni.getLocale())
|
||||
const languages = [
|
||||
{
|
||||
value: 'zh-Hans',
|
||||
name: '中文',
|
||||
checked: 'true',
|
||||
},
|
||||
{
|
||||
value: 'en',
|
||||
name: '英文',
|
||||
},
|
||||
]
|
||||
|
||||
function radioChange(evt) {
|
||||
// console.log(evt)
|
||||
current.value = evt.detail.value
|
||||
// 下面2句缺一不可!!!
|
||||
uni.setLocale(evt.detail.value)
|
||||
i18n.global.locale = evt.detail.value
|
||||
|
||||
// 底部tabbar需要重新设置一下
|
||||
setTabbarItem()
|
||||
// 本页的标题也需要重新设置一下
|
||||
uni.setNavigationBarTitle({
|
||||
title: t('i18n.title'),
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="box-border min-h-screen w-full flex flex-col items-center justify-center gap-y-6 bg-page2 px-4">
|
||||
<view class="mt-3 break-all px-3 text-center text-green-500">
|
||||
{{ userInfo.username ? '已登录' : '未登录' }}
|
||||
</view>
|
||||
<view class="mt-3 break-all px-3">
|
||||
{{ JSON.stringify(userInfo, null, 2) }}
|
||||
</view>
|
||||
|
||||
<!-- 切换语言 -->
|
||||
<view class="mt-6 w-full flex flex-col items-center justify-center">
|
||||
<view class="mb-2 text-gray-900 font-bold">
|
||||
切换语言
|
||||
</view>
|
||||
<view class="w-full flex items-center justify-center gap-4">
|
||||
<radio-group class="flex flex-col items-center justify-center gap-2" @change="radioChange">
|
||||
<label
|
||||
v-for="item in languages"
|
||||
:key="item.value"
|
||||
class="flex items-center gap-x-2"
|
||||
>
|
||||
<view>
|
||||
<radio :value="item.value" :checked="item.value === current" />
|
||||
</view>
|
||||
<view>{{ item.name }}</view>
|
||||
</label>
|
||||
</radio-group>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mt-6">
|
||||
<view class="m-auto w-160px text-center">
|
||||
<button v-if="tokenStore.hasLogin" type="warn" class="w-full rounded-xl text-xs" @click="handleLogout">
|
||||
退出登录
|
||||
</button>
|
||||
<button v-else class="w-full rounded-xl bg-gray-900 py-2.5 text-xs text-white" @click="handleLogin">
|
||||
立即登录
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部导航占位 -->
|
||||
<view class="w-full shrink-0">
|
||||
<uh-tabbar-page-placeholder />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -1,4 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
import { cover } from '@/utils/imageHelper'
|
||||
import { copy } from '@/utils/base/clipboard'
|
||||
import { queryMoments } from '@/api/halo-plugin/moment'
|
||||
import { timeFrom } from '@/utils/base/timeFrom'
|
||||
import { timeFormat } from '@/utils/base/timeFormat'
|
||||
|
||||
defineOptions({
|
||||
name: 'Moments',
|
||||
})
|
||||
@@ -6,12 +12,145 @@ defineOptions({
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '瞬间',
|
||||
navigationBarBackgroundColor: '#ffffff',
|
||||
enablePullDownRefresh: true,
|
||||
},
|
||||
})
|
||||
|
||||
const { loading, error, data, run } = useRequest<any, IPaginationParams>(queryMoments, { loadingToast: false, loadingToastContent: '加载中...', loadingToastMask: true })
|
||||
|
||||
const { list: moments, refresh } = useListData<any>({
|
||||
params: {
|
||||
page: 1,
|
||||
size: 10,
|
||||
},
|
||||
loading: toRef(loading),
|
||||
onPullDownRefresh: async (params, updateData) => {
|
||||
await run({ ...params })
|
||||
updateData({
|
||||
error: error.value,
|
||||
hasNext: data.value?.hasNext,
|
||||
items: data.value?.items ?? [],
|
||||
})
|
||||
},
|
||||
onReachBottom: async (params, updateData) => {
|
||||
await run({ ...params })
|
||||
updateData({
|
||||
error: error.value,
|
||||
hasNext: data.value?.hasNext,
|
||||
items: data.value?.items ?? [],
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
onLoad(() => {
|
||||
refresh()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
})
|
||||
|
||||
onPageScroll(() => { })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="min-h-screen w-full flex flex-col items-center justify-center gap-y-6 bg-page">
|
||||
瞬间页面
|
||||
<view class="min-h-screen w-full flex flex-col bg-page2">
|
||||
<!-- 瞬间列表 -->
|
||||
<view v-if="moments.length !== 0" class="box-border w-full flex flex-col gap-y-4 overflow-hidden p-4 pb-0">
|
||||
<view v-for="moment in moments" :key="moment.metadata.name" class="flex">
|
||||
<!-- 左边区域 -->
|
||||
<view class="relative h-auto w-4 flex shrink-0 items-center justify-center">
|
||||
<view class="uh-shadow-md h-full w-2px bg-white" />
|
||||
<view
|
||||
class="uh-shadow-md absolute left-1/2 top-0 box-border h-4 w-4 border-2 border-white rounded-full border-solid bg-[#1A1A1A]"
|
||||
:style="{
|
||||
transform: 'translateX(-50%)',
|
||||
}"
|
||||
/>
|
||||
</view>
|
||||
<!-- 瞬间卡片 -->
|
||||
<view class="box-border flex-1 pl-2">
|
||||
<view class="mb-2 text-sm text-gray-900 font-semibold">
|
||||
{{ timeFormat(moment.spec.releaseTime) }}
|
||||
</view>
|
||||
<view
|
||||
class="uh-shadow-card box-border w-full flex flex-1 flex-col items-center justify-center gap-y-2 overflow-hidden border-2 border-white rounded-xl border-solid bg-white p-4"
|
||||
>
|
||||
<!-- 用户信息 -->
|
||||
<view class="box-border w-full flex items-center justify-between gap-x-4">
|
||||
<view
|
||||
class="uh-shadow-md box-border h-8 w-8 shrink-0 overflow-hidden border-2 border-white rounded-xl border-solid bg-white"
|
||||
>
|
||||
<image :src="cover(moment.owner.avatar)" mode="aspectFill" class="h-full w-full" />
|
||||
</view>
|
||||
<view class="flex flex-1 flex-col gap-y-1">
|
||||
<view class="text-sm text-gray-900 font-bold">
|
||||
{{ moment.owner.displayName }}
|
||||
</view>
|
||||
<view v-if="false" class="text-xs text-gray-400">
|
||||
{{ timeFrom(moment.spec.releaseTime) }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="shrink-0">
|
||||
<view
|
||||
class="uh-shadow-md rounded-3xl bg-[#1A1A1A] px-4 py-1.5 text-xs text-white"
|
||||
>
|
||||
详情
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 瞬间内容 -->
|
||||
<view class="line-clamp-6 w-full text-xs text-gray-900 leading-relaxed">
|
||||
<rich-text :nodes="moment.spec.content.raw" />
|
||||
</view>
|
||||
|
||||
<!-- 瞬间附件 -->
|
||||
<view class="w-full">
|
||||
<!-- 如果有附件 -->
|
||||
</view>
|
||||
<!-- 分割线 -->
|
||||
<view class="h-1px w-full bg-gray-50" />
|
||||
<!-- 底部信息:标签、时间、点赞数、评论数 -->
|
||||
<view class="w-full flex items-center justify-between gap-x-2 text-xs text-gray-500 leading-relaxed">
|
||||
<template v-if="moment.spec.tags.length !== 0">
|
||||
<view class="shrink-0">
|
||||
<view
|
||||
v-for="tag in moment.spec.tags" :key="tag"
|
||||
class="text-xs text-gray-600"
|
||||
>
|
||||
{{ tag }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="shrink-0 text-xs text-gray-600">
|
||||
·
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<view class="text-[11px] text-gray-600">
|
||||
点赞 {{ moment.stats.upvote }} 次
|
||||
</view>
|
||||
<view class="shrink-0 text-xs text-gray-600">
|
||||
·
|
||||
</view>
|
||||
<view class="text-[11px] text-gray-600">
|
||||
评论 {{ moment.stats.totalComment }} 次
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="w-full flex flex-1 items-center justify-center">
|
||||
<wd-empty icon="no-content" tip="暂无瞬间">
|
||||
<template #image>
|
||||
<wd-icon name="empty" color="#1A1A1A" :size="52" />
|
||||
</template>
|
||||
</wd-empty>
|
||||
</view>
|
||||
|
||||
<!-- 底部导航占位 -->
|
||||
<view class="w-full shrink-0">
|
||||
<uh-tabbar-page-placeholder />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { queryUniHaloPluginConfigs } from '@/api/halo-plugin/uni-halo'
|
||||
import type { IUniHaloConfig } from '@/api/halo-plugin/uni-halo'
|
||||
import { deepMerge } from '@/utils/base/deepMerge'
|
||||
|
||||
const defaultConfig: IUniHaloConfig = {
|
||||
baseURL: import.meta.env.VITE_UNI_HALO_BASEURL,
|
||||
}
|
||||
|
||||
const ConfigsStoreKey = 'UNI_HALO_CONFIG'
|
||||
|
||||
/**
|
||||
* uni-halo 配置状态管理
|
||||
*/
|
||||
export const useUniHaloConfigStore = defineStore('uniHaloConfig', () => {
|
||||
/**
|
||||
* 插件配置
|
||||
*/
|
||||
const config = ref<IUniHaloConfig | undefined>(deepMerge({
|
||||
...(uni.getStorageSync(ConfigsStoreKey) ?? {} as IUniHaloConfig),
|
||||
}, defaultConfig))
|
||||
|
||||
/**
|
||||
* 获取插件配置
|
||||
*/
|
||||
async function init() {
|
||||
try {
|
||||
const res: IUniHaloConfig = await queryUniHaloPluginConfigs()
|
||||
config.value = deepMerge(config.value, res ?? {})
|
||||
uni.setStorageSync(ConfigsStoreKey, config.value)
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
config,
|
||||
init,
|
||||
}
|
||||
}, {
|
||||
persist: true,
|
||||
})
|
||||
@@ -1,5 +1,6 @@
|
||||
// 测试用的 iconfont,可生效
|
||||
@import './iconfont.css';
|
||||
@import './uh-styles.css';
|
||||
|
||||
.test {
|
||||
// 可以通过 @apply 多个样式封装整体样式
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
.uh-shadow-xs {
|
||||
box-shadow: 0 0 24rpx rgba(0, 0, 0, 0.035);
|
||||
}
|
||||
|
||||
.uh-shadow-md {
|
||||
box-shadow: 0 0 24rpx rgba(0, 0, 0, 0.075);
|
||||
}
|
||||
|
||||
.uh-shadow-card {
|
||||
box-shadow: 0 16rpx 60rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.uh-backdrop-blur {
|
||||
backdrop-filter: blur(4rpx);
|
||||
}
|
||||
|
||||
.uh-backdrop-blur-xs {
|
||||
backdrop-filter: blur(8rpx);
|
||||
}
|
||||
|
||||
.uh-backdrop-blur-sm {
|
||||
backdrop-filter: blur(12rpx);
|
||||
}
|
||||
|
||||
.uh-backdrop-blur-md {
|
||||
backdrop-filter: blur(24rpx);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<script lang="ts" setup>
|
||||
import { queryLinks } from '@/api/halo-plugin/link'
|
||||
|
||||
defineOptions({
|
||||
name: 'PageTemplate',
|
||||
})
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '页面模板',
|
||||
navigationBarBackgroundColor: '#ffffff',
|
||||
enablePullDownRefresh: true,
|
||||
},
|
||||
})
|
||||
|
||||
const { loading, error, data, run } = useRequest<any, IPaginationParams>(queryLinks, { loadingToast: false, loadingToastContent: '加载中...', loadingToastMask: true })
|
||||
|
||||
const { list: links, refresh } = useListData<any>({
|
||||
params: {
|
||||
page: 1,
|
||||
size: 0,
|
||||
},
|
||||
loading: toRef(loading),
|
||||
onPullDownRefresh: async (params, updateData) => {
|
||||
await run({ ...params })
|
||||
updateData({
|
||||
error: error.value,
|
||||
hasNext: data.value?.hasNext,
|
||||
items: data.value?.items ?? [],
|
||||
})
|
||||
},
|
||||
onReachBottom: async (params, updateData) => {
|
||||
await run({ ...params })
|
||||
updateData({
|
||||
error: error.value,
|
||||
hasNext: data.value?.hasNext,
|
||||
items: data.value?.items ?? [],
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
onLoad(() => {
|
||||
refresh()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
})
|
||||
|
||||
onPageScroll(() => {})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="min-h-screen w-full flex flex-col items-center justify-center gap-y-6 bg-page">
|
||||
页面模板页面
|
||||
</view>
|
||||
</template>
|
||||
+4
@@ -1,4 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import { markdownConfig } from '@/config/markdown/config'
|
||||
|
||||
defineOptions({
|
||||
name: 'PostDetail',
|
||||
})
|
||||
@@ -6,6 +8,8 @@ defineOptions({
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '文章详情',
|
||||
navigationBarBackgroundColor: '#ffffff',
|
||||
enablePullDownRefresh: true,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
+14
-13
@@ -31,22 +31,19 @@ function isActiveByIndex(index: number) {
|
||||
return tabbarStore.curIdx === index
|
||||
}
|
||||
|
||||
let timer: ReturnType<typeof setTimeout> | null = null
|
||||
const isActive = ref(true)
|
||||
const isActive = ref(false)
|
||||
|
||||
function activeBgColor() {
|
||||
clearTimeout(timer)
|
||||
isActive.value = false
|
||||
timer = setTimeout(() => {
|
||||
timer = null
|
||||
isActive.value = true
|
||||
}, 0)
|
||||
function handleSetActive() {
|
||||
if (tabbarStore.curIdx !== props.index) {
|
||||
return
|
||||
}
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
isActive.value = true
|
||||
}, 100)
|
||||
})
|
||||
}
|
||||
|
||||
watch(() => tabbarStore.curIdx, () => {
|
||||
activeBgColor()
|
||||
})
|
||||
|
||||
function handleClick(index: number) {
|
||||
// 点击原来的不做操作
|
||||
if (index === tabbarStore.curIdx) {
|
||||
@@ -66,6 +63,10 @@ function handleClick(index: number) {
|
||||
uni.navigateTo({ url })
|
||||
}
|
||||
}
|
||||
|
||||
onShow(() => {
|
||||
handleSetActive()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
+14
-17
@@ -32,8 +32,8 @@ export const nativeTabbarList: NativeTabBarItem[] = [
|
||||
{
|
||||
iconPath: 'static/tabbar/personal.png',
|
||||
selectedIconPath: 'static/tabbar/personalHL.png',
|
||||
pagePath: 'pages/me/me',
|
||||
text: '%tabbar.me%',
|
||||
pagePath: 'pages/mine/mine',
|
||||
text: '%tabbar.mine%',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -42,10 +42,9 @@ export const nativeTabbarList: NativeTabBarItem[] = [
|
||||
export const customTabbarList: CustomTabBarItem[] = [
|
||||
{
|
||||
text: '%tabbar.home%',
|
||||
pagePath: 'pages/index/index',
|
||||
iconType: 'unocss',
|
||||
icon: 'i-carbon-home',
|
||||
// badge: 'dot',
|
||||
pagePath: 'pages/home/home',
|
||||
iconType: 'uiLib',
|
||||
icon: 'home',
|
||||
},
|
||||
{
|
||||
pagePath: 'pages/gallery/gallery',
|
||||
@@ -57,19 +56,17 @@ export const customTabbarList: CustomTabBarItem[] = [
|
||||
pagePath: 'pages/moments/moments',
|
||||
text: '%tabbar.moments%',
|
||||
iconType: 'uiLib',
|
||||
icon: 'gift',
|
||||
icon: 'send',
|
||||
},
|
||||
// {
|
||||
// pagePath: 'pages-blog/about/about',
|
||||
// text: '%tabbar.about%',
|
||||
// iconType: 'unocss',
|
||||
// icon: 'i-carbon-menu',
|
||||
// badge: 10,
|
||||
// roles: ['admin'],
|
||||
// },
|
||||
{
|
||||
pagePath: 'pages/me/me',
|
||||
text: '%tabbar.me%',
|
||||
pagePath: 'pages/links/links',
|
||||
text: '%tabbar.links%',
|
||||
iconType: 'uiLib',
|
||||
icon: 'link',
|
||||
},
|
||||
{
|
||||
pagePath: 'pages/mine/mine',
|
||||
text: '%tabbar.mine%',
|
||||
iconType: 'uiLib',
|
||||
icon: 'user',
|
||||
},
|
||||
|
||||
+16
-6
@@ -5,6 +5,7 @@ import { setTabbarItem } from './i18n'
|
||||
import { tabbarList, tabbarStore } from './store'
|
||||
import { sleep } from '@/utils/common'
|
||||
import { debounce } from '@/utils/base/debounce'
|
||||
import { GLOBAL_SEARCH_EVENT_NAME } from '@/constants/events'
|
||||
import TabbarItem from './TabbarItem.vue'
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
@@ -58,6 +59,7 @@ onMounted(() => {
|
||||
|
||||
const backTopActive = reactive({
|
||||
show: false,
|
||||
thresholdValue: 300,
|
||||
beforeScrollTop: 0,
|
||||
currentScrollTop: 0,
|
||||
})
|
||||
@@ -117,9 +119,9 @@ const triggerPageScrollDebounceFn = debounce((scrollTop: number) => {
|
||||
backTopActive.beforeScrollTop = backTopActive.currentScrollTop
|
||||
setTimeout(() => {
|
||||
backTopActive.currentScrollTop = scrollTop
|
||||
backTopActive.show = backTopActive.currentScrollTop > backTopActive.beforeScrollTop
|
||||
}, 60)
|
||||
}, 60)
|
||||
backTopActive.show = scrollTop > backTopActive.thresholdValue && backTopActive.currentScrollTop > backTopActive.beforeScrollTop
|
||||
}, 30)
|
||||
}, 30)
|
||||
|
||||
onPageScroll(({ scrollTop }) => {
|
||||
triggerPageScrollDebounceFn(scrollTop)
|
||||
@@ -131,13 +133,17 @@ function handleScrollToTop() {
|
||||
duration: 300,
|
||||
})
|
||||
}
|
||||
|
||||
function handleShowGlobalSearch() {
|
||||
uni.$emit(GLOBAL_SEARCH_EVENT_NAME)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view v-if="customTabbarEnable" :class="{ 'h-54px pb-safe': !tabBarConfig.useFloat }">
|
||||
<view class="fixed bottom-6 left-4 right-4 z-100 flex items-center justify-between gap-x-4" @touchmove.stop.prevent>
|
||||
<view
|
||||
class="uh-shadow-sm relative box-border h-54px flex flex-1 items-center overflow-hidden border border-white rounded-full border-solid bg-white/95 p-1 backdrop-blur-[3px]"
|
||||
class="uh-shadow-sm uh-backdrop-blur relative box-border h-54px flex flex-1 items-center overflow-hidden border border-white rounded-full border-solid bg-white/95 p-1"
|
||||
>
|
||||
<TabbarItem
|
||||
v-for="(item, index) in tabbarList" :key="index" :item="item" :count="tabbarList.length"
|
||||
@@ -155,10 +161,10 @@ function handleScrollToTop() {
|
||||
</view>
|
||||
<!-- 右侧搜索框或者菜单 -->
|
||||
<view
|
||||
class="uh-shadow-sm box-border h-54px w-54px flex items-center justify-center border border-white rounded-full border-solid bg-white/95 text-2xl backdrop-blur-[3px]"
|
||||
class="uh-shadow-sm uh-backdrop-blur box-border h-54px w-54px flex items-center justify-center border border-white rounded-full border-solid bg-white/95 text-2xl"
|
||||
>
|
||||
<wd-icon v-if="backTopActive.show" name="arrow-up" :size="22" @click="handleScrollToTop" />
|
||||
<wd-icon v-else name="search-line" :size="22" />
|
||||
<wd-icon v-else name="search-line" :size="22" @click="handleShowGlobalSearch()" />
|
||||
</view>
|
||||
|
||||
<view v-if="!tabBarConfig.useFloat" class="pb-safe" />
|
||||
@@ -170,4 +176,8 @@ function handleScrollToTop() {
|
||||
.uh-shadow-sm {
|
||||
box-shadow: 0 0 24rpx rgba(0, 0, 0, 0.075);
|
||||
}
|
||||
|
||||
.uh-backdrop-blur {
|
||||
backdrop-filter: blur(3px);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -11,7 +11,7 @@ export function copy(content: string, showToast: boolean = false) {
|
||||
if (showToast) {
|
||||
uni.showToast({
|
||||
title: '复制成功',
|
||||
icon: 'success',
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 数字单位格式化 k / w / m / b
|
||||
* @param num 数字/数字字符串
|
||||
* @param decimal 保留小数位数,默认 1
|
||||
* @returns 格式化结果
|
||||
*/
|
||||
export function formatNumberUnit(num: number | string, decimal = 1): string {
|
||||
const n = Number(num)
|
||||
if (Number.isNaN(n))
|
||||
return '0'
|
||||
|
||||
// 单位映射:阈值 / 除数 / 单位
|
||||
const units = [
|
||||
{ threshold: 1e9, divisor: 1e9, unit: 'b' },
|
||||
{ threshold: 1e6, divisor: 1e6, unit: 'm' },
|
||||
{ threshold: 1e4, divisor: 1e4, unit: 'w' },
|
||||
{ threshold: 1e3, divisor: 1e3, unit: 'k' },
|
||||
]
|
||||
|
||||
for (const item of units) {
|
||||
if (n >= item.threshold) {
|
||||
const val = n / item.divisor
|
||||
const fixedVal = val.toFixed(decimal)
|
||||
// 去掉末尾 .0
|
||||
return fixedVal.replace(/\.0$/, '') + item.unit
|
||||
}
|
||||
}
|
||||
|
||||
// 小于 1000 直接返回
|
||||
return String(n)
|
||||
}
|
||||
@@ -1,14 +1,23 @@
|
||||
import { getCurrentInstance } from 'vue'
|
||||
import type { ComponentInternalInstance } from 'vue'
|
||||
|
||||
interface IOptions {
|
||||
/** 选择器 */
|
||||
selector: string
|
||||
/** 实例 */
|
||||
instance?: ComponentInternalInstance
|
||||
/** 是否获取所有匹配元素 */
|
||||
all?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取元素的位置信息
|
||||
* @param {any} selector 选择器
|
||||
* @param {boolean} all 是否获取所有匹配元素
|
||||
* @returns {Promise<any>} 返回一个 Promise,解析为元素的位置信息
|
||||
*/
|
||||
|
||||
import { getCurrentInstance } from 'vue'
|
||||
|
||||
export function getRect(selector: any, _instance: any = null, all: boolean = false): Promise<any> {
|
||||
const instance = _instance || getCurrentInstance()
|
||||
export function getRect(options: IOptions): Promise<any> {
|
||||
const { selector, instance = getCurrentInstance(), all = false } = options
|
||||
return new Promise((resolve) => {
|
||||
const query = uni.createSelectorQuery()
|
||||
.in(instance?.proxy)
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
/**
|
||||
* 验证URL格式
|
||||
*/
|
||||
function url(value: string): boolean {
|
||||
const urlRegex = /^(?:https?:)?\/\/(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}(?::\d+)?(?:\/\S*)?$/
|
||||
return urlRegex.test(value.trim())
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证电子邮箱格式
|
||||
*/
|
||||
@@ -223,6 +231,7 @@ function string(value: any) {
|
||||
}
|
||||
|
||||
export default {
|
||||
url,
|
||||
email,
|
||||
mobile,
|
||||
date,
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { completeUrl } from '@/utils/url'
|
||||
import { getRandomUrl } from '@/utils/randomResources'
|
||||
|
||||
/**
|
||||
* 图片封面处理
|
||||
* @param cover 图片封面
|
||||
* @returns 处理后的图片封面
|
||||
*/
|
||||
export function cover(cover: string) {
|
||||
if (cover.trim()) {
|
||||
return completeUrl(cover)
|
||||
}
|
||||
return getRandomUrl()
|
||||
}
|
||||
+7
-7
@@ -138,12 +138,12 @@ export function isCurrentPageTabbar() {
|
||||
*/
|
||||
export function getEnvBaseUrl() {
|
||||
// 请求基准地址
|
||||
let baseUrl = import.meta.env.VITE_SERVER_BASEURL
|
||||
let baseUrl = import.meta.env.VITE_UNI_HALO_BASEURL
|
||||
|
||||
// # 有些同学可能需要在微信小程序里面根据 develop、trial、release 分别设置上传地址,参考代码如下。
|
||||
const VITE_SERVER_BASEURL__WEIXIN_DEVELOP = 'https://ukw0y1.laf.run'
|
||||
const VITE_SERVER_BASEURL__WEIXIN_TRIAL = 'https://ukw0y1.laf.run'
|
||||
const VITE_SERVER_BASEURL__WEIXIN_RELEASE = 'https://ukw0y1.laf.run'
|
||||
const VITE_UNI_HALO_BASEURL__WEIXIN_DEVELOP = 'https://ukw0y1.laf.run'
|
||||
const VITE_UNI_HALO_BASEURL__WEIXIN_TRIAL = 'https://ukw0y1.laf.run'
|
||||
const VITE_UNI_HALO_BASEURL__WEIXIN_RELEASE = 'https://ukw0y1.laf.run'
|
||||
|
||||
// 微信小程序端环境区分
|
||||
if (isMpWeixin) {
|
||||
@@ -153,13 +153,13 @@ export function getEnvBaseUrl() {
|
||||
|
||||
switch (envVersion) {
|
||||
case 'develop':
|
||||
baseUrl = VITE_SERVER_BASEURL__WEIXIN_DEVELOP || baseUrl
|
||||
baseUrl = VITE_UNI_HALO_BASEURL__WEIXIN_DEVELOP || baseUrl
|
||||
break
|
||||
case 'trial':
|
||||
baseUrl = VITE_SERVER_BASEURL__WEIXIN_TRIAL || baseUrl
|
||||
baseUrl = VITE_UNI_HALO_BASEURL__WEIXIN_TRIAL || baseUrl
|
||||
break
|
||||
case 'release':
|
||||
baseUrl = VITE_SERVER_BASEURL__WEIXIN_RELEASE || baseUrl
|
||||
baseUrl = VITE_UNI_HALO_BASEURL__WEIXIN_RELEASE || baseUrl
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { sleep } from '@/utils/common'
|
||||
|
||||
export const randomImageUrl = {
|
||||
ycy: 'https://t.alcy.cc/ycy',
|
||||
pc: 'https://t.alcy.cc/pc',
|
||||
@@ -7,13 +9,25 @@ export const randomImageUrl = {
|
||||
xsot: 'https://api.xsot.cn/bing?jump=true',
|
||||
}
|
||||
|
||||
type randomUrlKeys = keyof typeof randomImageUrl
|
||||
|
||||
export const randomVideosUrl = {
|
||||
acg: 'https://t.alcy.cc/acg',
|
||||
}
|
||||
|
||||
export function getPicsum(width: number = 800, height?: number) {
|
||||
export function getPicSumImages(width: number = 800, height?: number) {
|
||||
const t = `${Date.now()}+${Math.random()}`
|
||||
if (!height) {
|
||||
return `https://picsum.photos/${width}`
|
||||
return `https://picsum.photos/${width}?t=${t}`
|
||||
}
|
||||
return `https://picsum.photos/${width}/${height}`
|
||||
return `https://picsum.photos/${width}/${height}?t=${t}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取随机图片URL
|
||||
* @param type 图片类型 默认pc
|
||||
* @returns 随机图片URL
|
||||
*/
|
||||
export function getRandomUrl(type?: randomUrlKeys) {
|
||||
return `${randomImageUrl[type || 'pc' as randomUrlKeys]}?t=${Date.now()}+${Math.random()}`
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
*/
|
||||
export const uploadFileUrl = {
|
||||
/** 用户头像上传地址 */
|
||||
USER_AVATAR: `${import.meta.env.VITE_SERVER_BASEURL}/user/avatar`,
|
||||
USER_AVATAR: `${import.meta.env.VITE_UNI_HALO_BASEURL}/user/avatar`,
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+14
-11
@@ -1,13 +1,16 @@
|
||||
const HALO_BASEURL = import.meta.env.VITE_SERVER_BASEURL
|
||||
import test from '@/utils/base/test'
|
||||
|
||||
const UNI_HALO_BASEURL = import.meta.env.VITE_UNI_HALO_BASEURL
|
||||
|
||||
/**
|
||||
* Complete the URL with the base URL
|
||||
* @param url The URL to be completed
|
||||
* @returns The completed URL
|
||||
*/
|
||||
export function completeUrl (url: string): string {
|
||||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||
return url
|
||||
}
|
||||
return HALO_BASEURL + url
|
||||
}
|
||||
* 补全URL
|
||||
* @description 如果URL以http://或https://开头,则直接返回URL;否则,在URL前添加HALO_BASEURL
|
||||
* @param url URL字符串
|
||||
* @returns 补全后的URL字符串
|
||||
*/
|
||||
export function completeUrl(url: string): string {
|
||||
if (test.url(url)) {
|
||||
return url
|
||||
}
|
||||
return UNI_HALO_BASEURL + url
|
||||
}
|
||||
|
||||
@@ -97,12 +97,17 @@ export default defineConfig({
|
||||
/** 主题色,用法如: text-primary */
|
||||
primary: 'var(--wot-color-theme,#0957DE)',
|
||||
page: '#F8F8F8',
|
||||
page2: '#F9FAFB',
|
||||
},
|
||||
fontSize: {
|
||||
/** 提供更小号的字体,用法如:text-2xs */
|
||||
'2xs': ['20rpx', '28rpx'],
|
||||
'3xs': ['18rpx', '26rpx'],
|
||||
},
|
||||
shadow: {
|
||||
xs: '0 0 24px rgba(0, 0, 0, 0.075)',
|
||||
card: '0 16rpx 60rpx rgba(0, 0, 0, 0.04)',
|
||||
},
|
||||
},
|
||||
// windows 系统会报错:[plugin:unocss:transformers:pre] Cannot overwrite a zero-length range - use append Left or prependRight instead.
|
||||
// 去掉下面的就正常了
|
||||
|
||||
+3
-4
@@ -57,7 +57,7 @@ export default defineConfig(({ command, mode }) => {
|
||||
const localEnv = loadEnv(mode, envDir, '')
|
||||
const {
|
||||
VITE_APP_PORT,
|
||||
VITE_SERVER_BASEURL,
|
||||
VITE_UNI_HALO_BASEURL,
|
||||
VITE_APP_TITLE,
|
||||
VITE_DELETE_CONSOLE,
|
||||
VITE_APP_PUBLIC_BASE,
|
||||
@@ -87,8 +87,7 @@ export default defineConfig(({ command, mode }) => {
|
||||
exclude: ['**/components/**/**.*', '**/sections/**/**.*'],
|
||||
// pages 目录为 src/pages,分包目录不能配置在pages目录下!!
|
||||
// 是个数组,可以配置多个,但是不能为pages里面的目录!!
|
||||
// "src/pages-demo" 是unibest demo 预留的,方便后续插入demo示例
|
||||
subPackages: ['src/pages-demo', 'src/pages-blog'],
|
||||
subPackages: ['src/subpkg-demo', 'src/subpkg-blog'],
|
||||
dts: 'src/types/uni-pages.d.ts',
|
||||
}),
|
||||
// UniOptimization 插件需要 page.json 文件,故应在 UniPages 插件之后执行
|
||||
@@ -191,7 +190,7 @@ export default defineConfig(({ command, mode }) => {
|
||||
proxy: JSON.parse(VITE_APP_PROXY_ENABLE)
|
||||
? {
|
||||
[VITE_APP_PROXY_PREFIX]: {
|
||||
target: VITE_SERVER_BASEURL,
|
||||
target: VITE_UNI_HALO_BASEURL,
|
||||
changeOrigin: true,
|
||||
// 后端有/api前缀则不做处理,没有则需要去掉
|
||||
rewrite: path =>
|
||||
|
||||
Reference in New Issue
Block a user