This commit is contained in:
oneao committed 2025-09-26 17:31:55 +08:00
1 parent a2c6812ed0
commit f431b7ead6
17 files changed
+728 -39

No files matched your search

@@ -24,6 +24,20 @@ const props = defineProps({
},
viewname: {
type: String,
required: true,
},
searchcondition: {
type: String,
required: false,
},
userid: {
type: String,
required: false,
},
searchType: {
type: String as PropType<'default' | 'ex' | 'single'>,
default: 'default',
required: false,
},
})
@@ -1,6 +1,8 @@
<script setup lang="ts">
import type { PropType } from 'vue'
import { useIndeterminate } from 'sard-uniapp'
import { computed, ref } from 'vue'
import { getAutocompleteDataApi, getAutocompleteDataExApi, getAutocompleteDataSingleApi } from '@/http/service'
const props = defineProps({
visible: {
@@ -11,10 +13,6 @@ const props = defineProps({
type: String as PropType<'radio' | 'multiple'>,
default: 'radio',
},
label: {
value: String,
default: '',
},
value: {
type: null as unknown as PropType<any | any[]>,
default: undefined,
@@ -30,8 +28,21 @@ const props = defineProps({
viewname: {
type: String,
},
searchcondition: {
type: String,
required: false,
},
userid: {
type: String,
required: false,
},
searchType: {
type: String as PropType<'default' | 'ex' | 'single'>,
default: 'default',
required: false,
},
})
const emit = defineEmits(['update:visible', 'update:value'])
const emit = defineEmits(['update:visible', 'update:value', 'change'])
const _visible = computed({
get: () => props.visible,
@@ -43,34 +54,35 @@ const _visible = computed({
const _loading = ref(false)
const searchValue = ref('')
const _pagation = ref({
const pagination = ref({
current: 1,
pageSize: 10,
total: 20,
total: 0,
})
const isPrevDisabled = computed(() => _pagation.value.current <= 1)
const isPrevDisabled = computed(() => pagination.value.current <= 1)
const isNextDisabled = computed(() =>
_pagation.value.current >= Math.ceil(_pagation.value.total / _pagation.value.pageSize),
pagination.value.current >= Math.ceil(pagination.value.total / pagination.value.pageSize),
)
// 内部 value,双向绑定 props.value
const _value = computed({
get: () => {
// 如果是多选,保证返回数组
return props.type === 'multiple'
? (Array.isArray(props.value) ? [...props.value] : [])
: props.value
if (props.type === 'multiple') {
// 只在不是数组时才兜底成 []
return Array.isArray(props.value) ? props.value : []
}
return props.value
},
set: (val) => {
emit('update:value', val)
},
})
const _options = ref<any[]>([])
const options = ref<any[]>([])
function setOptions(val: any[]) {
_options.value = val.map(item => ({
options.value = val.map(item => ({
label: item[props.labelField] ?? '',
value: item[props.valueField] ?? undefined,
disabled: !!item.disabled,
@@ -80,7 +92,7 @@ function setOptions(val: any[]) {
const { allChecked, isIndeterminate, onAllChange } = useIndeterminate(
_value,
_options,
options,
)
function select(option, toggle) {
@@ -88,6 +100,14 @@ function select(option, toggle) {
toggle(option.value)
if (props.type === 'radio') {
_visible.value = false
// 查找对应的 label
const selectedOption = options.value.find(
o => o.value === option.value,
)
// 触发事件或者更新内部 _label
emit('change', { selectedOption })
}
}
}
@@ -95,19 +115,95 @@ function select(option, toggle) {
// 显示数量
const count = computed(() => {
if (props.type === 'multiple') {
return `${(_value.value).length || 0} / ${_options.value.length}`
return `${(_value.value).length || 0} / ${options.value.length}`
}
else {
return _value.value != null ? `1 / ${_options.value.length}` : `0 / ${_options.value.length}`
return _value.value != null ? `1 / ${options.value.length}` : `0 / ${options.value.length}`
}
})
function handleAfterEnter() {
// _loading.value = true
getData()
}
function handlePagationChange(page: number) {
_pagation.value.current = page
pagination.value.current = page
getData()
}
function handleSearch() {
pagination.value.current = 1
pagination.value.total = 0
pagination.value.pageSize = 10
getData()
}
function getData() {
if (props.searchType === 'ex') {
getAutocompleteDataEx()
}
else if (props.searchType === 'single') {
getAutocompleteDataSingle()
}
else {
getAutocompleteData()
}
}
function getAutocompleteData() {
_loading.value = true
getAutocompleteDataApi({
q: searchValue.value,
page: pagination.value.current,
limit: pagination.value.pageSize,
viewname: props.viewname,
field: props.labelField,
}).then((res) => {
pagination.value.total = res.data.total
setOptions(res.data.rows)
}).finally(() => {
_loading.value = false
})
}
function getAutocompleteDataEx() {
_loading.value = true
getAutocompleteDataExApi({
q: searchValue.value,
page: pagination.value.current,
limit: pagination.value.pageSize,
viewname: props.viewname,
field: props.labelField,
userid: props.userid,
searchcondition: props.searchcondition,
}).then((res) => {
pagination.value.total = res.data.total
setOptions(res.data.rows)
}).finally(() => {
_loading.value = false
})
}
function getAutocompleteDataSingle() {
_loading.value = true
getAutocompleteDataSingleApi({
q: searchValue.value,
page: pagination.value.current,
limit: pagination.value.pageSize,
viewname: props.viewname,
field: props.labelField,
userid: props.userid,
}).then((res) => {
pagination.value.total = res.data.total
setOptions(res.data.rows)
}).finally(() => {
_loading.value = false
})
}
</script>
@@ -137,6 +233,7 @@ function handlePagationChange(page: number) {
v-model="searchValue"
placeholder="请输入搜索的值"
clearable
@confirm="handleSearch"
>
<template #prepend>
<sar-icon family="sari" name="search" />
@@ -154,7 +251,7 @@ function handlePagationChange(page: number) {
:throttle="false"
>
<sar-empty
v-if="_options.length === 0"
v-if="options.length === 0"
:root-style="{ height: '100%' }"
/>
@@ -166,7 +263,7 @@ function handlePagationChange(page: number) {
<template #custom="{ toggle }">
<sar-list inlaid>
<sar-list-item
v-for="option in _options"
v-for="option in options"
:key="option.value"
:title="option.label"
@click="select(option, toggle)"
@@ -191,7 +288,7 @@ function handlePagationChange(page: number) {
<template #custom="{ toggle }">
<sar-list inlaid>
<sar-list-item
v-for="option in _options"
v-for="option in options"
:key="option.value"
:title="option.label"
:hover="!option.disabled"
@@ -213,9 +310,9 @@ function handlePagationChange(page: number) {
</view>
<view class="sar-popout__footer">
<sar-pagination
:current="_pagation.current"
:total="_pagation.total"
:page-size="_pagation.pageSize"
:current="pagination.current"
:total="pagination.total"
:page-size="pagination.pageSize"
type="simple"
root-class="picker-select__pagination-root"
@change="handlePagationChange"
@@ -0,0 +1,117 @@
import type { CustomRequestOptions } from './interceptor'
import { toast } from '@/utils/toast'
import { httpInterceptor } from './interceptor'
export function http<T>(options: CustomRequestOptions): Promise<IResData<T>> {
return httpInternal<T>(options)
}
async function httpInternal<T>(options: CustomRequestOptions): Promise<IResData<T>> {
try {
await httpInterceptor.invoke(options)
}
catch (e) {
console.log('异常')
return Promise.reject(e)
}
return new Promise<IResData<T>>((resolve, reject) => {
uni.request({
...options,
dataType: 'json',
// #ifndef MP-WEIXIN
responseType: 'json',
// #endif
success(res) {
if (res.statusCode >= 200 && res.statusCode < 300) {
const resData = res.data as IResData<T>
if (resData.code === 200) {
try {
resData.data = JSON.parse(resData.data as string)
}
catch {}
resolve(resData)
}
else if (resData.code === 600) {
toast.error(resData.message || '当前登录已过期')
uni.navigateTo({ url: '/pages-sub/login/index' })
reject(res)
}
else {
toast.error(resData.message || '请求错误')
reject(res)
}
}
else {
if (!options.hideErrorToast) {
toast.error((res.data as IResData<T>).message || '请求错误')
}
reject(res)
}
},
fail(err) {
toast.error('网络错误,换个网络试试')
reject(err)
},
complete() {
console.log('complete')
},
})
})
}
/**
* GET 请求
* @param url 后台地址
* @param query 请求query参数
* @returns
*/
export function httpGet<T>(url: string, query?: Record<string, any>) {
return http<T>({
url,
query,
method: 'GET',
})
}
/**
* POST 请求
* @param url 后台地址
* @param data 请求body参数
* @param query 请求query参数,post请求也支持query,很多微信接口都需要
* @returns
*/
export function httpPostForm<T>(
url: string,
data?: Record<string, any>,
query?: Record<string, any>,
) {
return http<T>({
url,
query,
data,
method: 'POST',
header: {
'Content-Type': 'application/x-www-form-urlencoded',
},
})
}
export function httpPostJson<T>(
url: string,
data?: Record<string, any>,
query?: Record<string, any>,
) {
return http<T>({
url,
query,
data,
method: 'POST',
header: {
'Content-Type': 'application/json',
},
})
}
http.get = httpGet
http.postForm = httpPostForm
http.postJson = httpPostJson
@@ -0,0 +1,85 @@
import { useUserStore } from '@/store'
import { joinUrl } from '@/utils/common'
import { getDbConfigs } from '@/utils/request'
import { toast } from '@/utils/toast'
import { stringifyQuery } from './queryString'
export type CustomRequestOptions = UniApp.RequestOptions & {
query?: Record<string, any>
/** 出错时是否隐藏错误提示 */
hideErrorToast?: boolean
} & IUniUploadFileOptions // 添加uni.uploadFile参数类型
export const httpInterceptor = {
async invoke(options: CustomRequestOptions) {
// 1. 处理 query 参数
if (options.query) {
const queryStr = stringifyQuery(options.query)
options.url += options.url.includes('?') ? `&${queryStr}` : `?${queryStr}`
}
// 2. 获取用户信息
const userStore = useUserStore()
const userInfo = userStore.userInfo as IUserInfo
const { token, userid, orgid } = userInfo || {}
const isLogin = options.url.includes('login')
// 3. 获取 db 配置
const configMap = await getDbConfigs()
if (!configMap) {
toast.error('获取机构配置失败,请联系管理人员!')
if (!isLogin) {
userStore.reset()
uni.navigateTo({ url: '/pages-sub/login/index' })
}
return Promise.reject(new Error('机构码无效'))
}
console.log(configMap)
// 4. 解析 baseUrl
let baseUrl = ''
if (isLogin) {
const orgidFromData = (options.data as AnyObject)?.orgid || ''
baseUrl = configMap[orgidFromData] || ''
}
else {
baseUrl = configMap[orgid || ''] || ''
}
if (!baseUrl) {
toast.error('机构码有误,请重新登陆!')
if (!isLogin) {
userStore.reset()
uni.navigateTo({ url: '/pages-sub/login/index' })
}
return Promise.reject(new Error('机构码无效'))
}
// 5. 设置请求头
if (!isLogin) {
options.header = {
...options.header,
Authorization: `Bearer ${token}`,
userid: encodeURIComponent(userid),
orgid,
}
}
// 6. 拼接完整 URL、设置超时与平台信息
options.url = joinUrl(baseUrl, options.url)
options.timeout = 20000
options.header = {
...options.header,
}
},
}
export const requestInterceptor = {
install() {
// 拦截 request 请求
// uni.addInterceptor('request', httpInterceptor)
// 拦截 uploadFile 文件上传
// uni.addInterceptor('uploadFile', httpInterceptor)
},
}
@@ -0,0 +1,29 @@
/**
* 将对象序列化为URL查询字符串,用于替代第三方的 qs 库,节省宝贵的体积
* 支持基本类型值和数组,不支持嵌套对象
* @param obj 要序列化的对象
* @returns 序列化后的查询字符串
*/
export function stringifyQuery(obj: Record<string, any>): string {
if (!obj || typeof obj !== 'object' || Array.isArray(obj))
return ''
return Object.entries(obj)
.filter(([_, value]) => value !== undefined && value !== null)
.map(([key, value]) => {
// 对键进行编码
const encodedKey = encodeURIComponent(key)
// 处理数组类型
if (Array.isArray(value)) {
return value
.filter(item => item !== undefined && item !== null)
.map(item => `${encodedKey}=${encodeURIComponent(item)}`)
.join('&')
}
// 处理基本类型
return `${encodedKey}=${encodeURIComponent(value)}`
})
.join('&')
}
@@ -0,0 +1,39 @@
import { httpPostForm } from './index'
interface getAutocompleteDataParams {
q: string
page: number
limit: number
viewname: string
field: string
}
export function getAutocompleteDataApi(params: getAutocompleteDataParams) {
return httpPostForm<any>('/getAutocompleteData1.jsp', params)
}
interface getAutocompleteDataExParams {
q: string
page: number
limit: number
viewname: string
field: string
userid?: string
searchcondition: string
}
export function getAutocompleteDataExApi(params: getAutocompleteDataExParams) {
return httpPostForm<any>('/getAutocompleteDataEx1.jsp', params)
}
interface getAutocompleteDataSingleParams {
q: string
page: number
limit: number
viewname: string
field: string
userid?: string
}
export function getAutocompleteDataSingleApi(params: getAutocompleteDataSingleParams) {
return httpPostForm<any>('/getAutocompleteDataSingle.jsp', params)
}
@@ -1,9 +1,11 @@
import { createSSRApp } from 'vue'
import App from './App.vue'
import store from './store'
import '@/style/index.scss'
export function createApp() {
const app = createSSRApp(App)
app.use(store)
return {
app,
@@ -0,0 +1,88 @@
<script lang="ts" setup>
</script>
<template>
<view class="g3-container">
Container
</view>
</template>
<style lang="scss" scoped>
.g3-container {
display: flex;
flex-direction: column;
gap: 1.5rem;
padding: 2em;
margin: 0 auto;
font-family: 'Inter', sans-serif; /* 设置字体为Inter */
}
.g3-header {
display: flex;
flex-direction: column;
gap: 0.5rem;
.g3-header-title {
font-size: 24px;
font-weight: bold;
color: #575dfb;
}
.g3-header-prompt {
font-size: 16px;
color: #666; /* 提示文字颜色稍微调淡 */
}
}
.g3-content {
width: 100%; /* 宽度调整为100%,以适应容器大小 */
.g3-content-form {
display: flex;
flex-direction: column;
gap: 1.5rem;
.g3-content-form-item {
display: flex;
flex-direction: column;
gap: 0.5rem;
.label {
font-size: 14px;
font-weight: 500;
}
::v-deep .input {
display: flex;
align-items: center;
padding: 0.3rem;
border: 2px solid #0498e1 !important;
border-radius: 6px;
&::after {
display: none;
}
.wd-input__icon {
font-size: 20px;
color: #575dfb;
}
}
}
}
}
.g3-footer {
display: flex;
flex-direction: column;
gap: 0.5rem;
width: 100%; /* 宽度调整为100% */
text-align: center;
::v-deep .login-button {
height: 3rem;
font-size: 1rem;
font-weight: bold;
border-radius: 6px;
.wd-button__text {
display: flex;
gap: 0.5rem;
align-items: center;
justify-content: center;
}
}
.register {
font-size: 14px;
color: #666;
}
}
</style>
@@ -16,10 +16,12 @@ function show() {
</sar-list>
<sar-form label-width="240rpx">
<sar-form-item label="Activity name">
<picker-select-input />
</sar-form-item>
<sar-form-item label="Activity name">
<picker-select-input />
<picker-select-input
viewname="b_user"
label-field="b_name"
value-field="b_id"
search-type="ex"
/>
</sar-form-item>
</sar-form>
</template>
@@ -0,0 +1,16 @@
import { createPinia } from 'pinia'
import { createPersistedState } from 'pinia-plugin-persistedstate' // 数据持久化
const store = createPinia()
store.use(
createPersistedState({
storage: {
getItem: uni.getStorageSync,
setItem: uni.setStorageSync,
},
}),
)
export default store
export * from './user'
@@ -0,0 +1,44 @@
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import { toast } from '@/utils/toast'
const initState = { token: '', orgid: '', userid: '' }
export const useUserStore = defineStore(
'user',
() => {
const userInfo = ref<IUserInfo>({ ...initState })
const setUserInfo = (val: IUserInfo) => {
userInfo.value = val
}
const reset = () => {
userInfo.value = { orgid: userInfo.value.orgid, userid: userInfo.value.userid, token: '' }
}
const isLogined = computed(
() => !!(userInfo.value.token && userInfo.value.orgid && userInfo.value.userid),
)
function validateLogin(): boolean {
if (!isLogined.value) {
toast.error('尚未登陆或登录已过期')
uni.navigateTo({ url: '/pages-sub/login/index' })
return false
}
return true
}
return {
userInfo,
setUserInfo,
reset,
isLogined,
validateLogin,
}
},
{
persist: true,
},
)
@@ -7,3 +7,7 @@
height: auto !important;
}
}
uni-toast {
z-index: 9999 !important; /* 根据实际情况进行调整 */
}
+4 -9
View File
@@ -3,7 +3,7 @@
declare global {
interface IResData<T> {
code: number
msg: string
message: string
data: T
}
@@ -17,16 +17,11 @@ declare global {
}
interface IUserInfo {
nickname?: string
avatar?: string
orgid?: string // 机构码
userid?: string // 主键id,登录账号
/** 微信的 openid,非微信没有这个字段 */
openid?: string
}
interface IUserToken {
token: string
refreshToken?: string
refreshExpire?: number
token?: string
}
}
@@ -0,0 +1,38 @@
import { toast } from './toast'
export function joinUrl(base: string, path: string): string {
if (!base)
return path || ''
if (!path)
return base || ''
const baseEndsWithSlash = base.endsWith('/')
const pathStartsWithSlash = path.startsWith('/')
if (baseEndsWithSlash && pathStartsWithSlash) {
// 都有斜杠,去掉 base 末尾那个
return base.slice(0, -1) + path
}
else if (!baseEndsWithSlash && !pathStartsWithSlash) {
// 都没有斜杠,补一个
return `${base}/${path}`
}
else {
// 只有一个斜杠,直接拼接
return base + path
}
}
export function checkRequiredParams(options: Record<string, any>, keys: string[]): boolean {
for (const key of keys) {
const value = options[key]
if (!value || value === 'undefined' || value === 'null') {
toast.error(`参数 "${key}" 有误!`)
setTimeout(() => {
uni.navigateBack()
}, 1000)
return false
}
}
return true
}
@@ -0,0 +1,26 @@
/*
* @Author: 菲鸽
* @Date: 2024-03-28 19:13:55
* @Last Modified by: 菲鸽
* @Last Modified time: 2024-03-28 19:24:55
*/
export const platform = __UNI_PLATFORM__
export const isH5 = __UNI_PLATFORM__ === 'h5'
export const isApp = __UNI_PLATFORM__ === 'app'
export const isMp = __UNI_PLATFORM__.startsWith('mp-')
export const isMpWeixin = __UNI_PLATFORM__.startsWith('mp-weixin')
export const isMpAplipay = __UNI_PLATFORM__.startsWith('mp-alipay')
export const isMpToutiao = __UNI_PLATFORM__.startsWith('mp-toutiao')
export const isHarmony = __UNI_PLATFORM__.startsWith('app-harmony')
const PLATFORM = {
platform,
isH5,
isApp,
isMp,
isMpWeixin,
isMpAplipay,
isMpToutiao,
isHarmony,
}
export default PLATFORM
@@ -0,0 +1,28 @@
export function getServerBaseUrl(): string {
const origin = window.location.origin
const publicBase = import.meta.env.VITE_APP_PUBLIC_BASE || '/'
// 拼接时去掉 publicBase 开头和结尾的斜杠(只保留中间路径)
const cleanBase = publicBase.replace(/^\/+|\/+$/g, '')
return cleanBase ? `${origin}/${cleanBase}` : origin
}
export async function getDbConfigs(): Promise<Record<string, any> | null> {
const serverBaseUrl = getServerBaseUrl()
try {
const response = await fetch(`${serverBaseUrl}/static/db.json`)
if (!response.ok) {
console.error('Failed to fetch db.json:', response.status)
return null
}
const data: Record<string, any> = await response.json()
return data
}
catch (error) {
console.error('Error fetching db.json:', error)
return null
}
}
@@ -0,0 +1,65 @@
/**
* toast 弹窗组件
* 支持 success/error/warning/info 四种状态
* 可配置 duration, position 等参数
*/
type ToastType = 'success' | 'error' | 'warning' | 'info'
interface ToastOptions {
type?: ToastType
duration?: number
position?: 'top' | 'middle' | 'bottom'
icon?: 'success' | 'error' | 'none' | 'loading' | 'fail' | 'exception'
message: string
}
export function showToast(options: ToastOptions | string) {
const defaultOptions: ToastOptions = {
type: 'info',
duration: 2000,
position: 'middle',
message: '',
}
const mergedOptions
= typeof options === 'string'
? { ...defaultOptions, message: options }
: { ...defaultOptions, ...options }
// 映射position到uniapp支持的格式
const positionMap: Record<ToastOptions['position'], 'top' | 'bottom' | 'center'> = {
top: 'top',
middle: 'center',
bottom: 'bottom',
}
// 映射图标类型
const iconMap: Record<
ToastType,
'success' | 'error' | 'none' | 'loading' | 'fail' | 'exception'
> = {
success: 'success',
error: 'error',
warning: 'fail',
info: 'none',
}
// 调用uni.showToast显示提示
uni.showToast({
title: mergedOptions.message,
duration: mergedOptions.duration,
position: positionMap[mergedOptions.position],
icon: mergedOptions.icon || iconMap[mergedOptions.type],
mask: true,
})
}
export const toast = {
success: (message: string, options?: Omit<ToastOptions, 'type'>) =>
showToast({ ...options, type: 'success', message }),
error: (message: string, options?: Omit<ToastOptions, 'type'>) =>
showToast({ ...options, type: 'error', message }),
warning: (message: string, options?: Omit<ToastOptions, 'type'>) =>
showToast({ ...options, type: 'warning', message }),
info: (message: string, options?: Omit<ToastOptions, 'type'>) =>
showToast({ ...options, type: 'info', message }),
}